// DramaScope — feature components: social login, library status, reviews, share, ads, notifications, premium, AI quick picks
// v=20260527-ds138
const { useState, useEffect, useRef, useMemo } = React;

// ── ICONS (lightweight Lucide-style SVGs) ─────────────────────────────────────
const Icon = ({ name, size=16, color='currentColor', stroke=2 }) => {
  const paths = {
    sparkles: 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3zM19 14l.7 2.1L22 17l-2.3.9L19 20l-.7-2.1L16 17l2.3-.9L19 14zM5 14l.7 2.1L8 17l-2.3.9L5 20l-.7-2.1L2 17l2.3-.9L5 14z',
    bookmark: 'M6 3h12v18l-6-4-6 4V3z',
    share:    'M16 6l-4-4-4 4M12 2v14M5 12v8a2 2 0 002 2h10a2 2 0 002-2v-8',
    bell:     'M6 8a6 6 0 0112 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 004 0',
    crown:    'M2 18h20l-2-10-5 4-5-7-5 7-5-4-2 10z',
    flame:    'M12 22c5 0 8-3 8-8 0-4-3-7-4-9 0 3-3 5-5 5-1 2-3 4-3 7s2 5 4 5z',
    eye:      'M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM12 9a3 3 0 100 6 3 3 0 000-6z',
    eyeOff:   'M2 2l20 20M6.7 6.7C4.1 8.4 2 12 2 12s4 7 10 7c2 0 3.8-.6 5.3-1.5M9.5 9.5a3 3 0 004 4',
    star:     'M12 2l3 7 7 .5-5.5 4.5 2 7L12 17l-6.5 4 2-7L2 9.5 9 9z',
    heart:    'M12 21s-7-4.5-9.5-9C1 8.5 3 5 6.5 5 9 5 11 7 12 8c1-1 3-3 5.5-3 3.5 0 5.5 3.5 4 7C19 16.5 12 21 12 21z',
    msg:      'M3 8a3 3 0 013-3h12a3 3 0 013 3v8a3 3 0 01-3 3H9l-4 4v-4H6a3 3 0 01-3-3V8z',
    users:    'M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM22 21v-2a4 4 0 00-3-3.9M16 3.1A4 4 0 0116 11',
    play:     'M5 3l14 9-14 9V3z',
    check:    'M5 12l5 5 9-11',
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" style={{ display:'inline-block', verticalAlign:'middle' }}>
      <path d={paths[name] || paths.star}/>
    </svg>
  );
};

// ── SOCIAL LOGIN BUTTONS ──────────────────────────────────────────────────────
const SOCIAL_PROVIDERS = [
  { id:'google',   label:'Continue with Google',   bg:'#fff',     fg:'#111',   icon:(<svg width="16" height="16" viewBox="0 0 48 48"><path fill="#FFC107" d="M43.6 20.1H42V20H24v8h11.3c-1.6 4.5-6 7.7-11.3 7.7-6.6 0-12-5.4-12-12s5.4-12 12-12c3 0 5.8 1.1 7.9 3l5.7-5.7C34.5 6.1 29.5 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.3-.1-2.6-.4-3.9z"/><path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.6 16 19 13 24 13c3 0 5.8 1.1 7.9 3l5.7-5.7C34.5 6.1 29.5 4 24 4 16.3 4 9.7 8.3 6.3 14.7z"/><path fill="#4CAF50" d="M24 44c5.4 0 10.3-2.1 14-5.4l-6.5-5.5c-2 1.5-4.6 2.4-7.5 2.4-5.3 0-9.7-3.2-11.3-7.7l-6.5 5C9.4 39.6 16.1 44 24 44z"/><path fill="#1976D2" d="M43.6 20.1H42V20H24v8h11.3c-.8 2.2-2.2 4.2-4.1 5.6l6.5 5.5C42.8 35 44 29.9 44 24c0-1.3-.1-2.6-.4-3.9z"/></svg>) },
  { id:'telegram', label:'Continue with Telegram', bg:'#229ED9',  fg:'#fff',   icon:(<svg width="16" height="16" viewBox="0 0 24 24" fill="#fff"><path d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0zm5.43 8.16-1.81 8.55c-.14.61-.5.76-1.01.47l-2.79-2.06-1.35 1.3c-.15.15-.27.27-.56.27l.2-2.84 5.16-4.66c.22-.2-.05-.31-.34-.11l-6.38 4.02-2.75-.86c-.6-.19-.61-.6.13-.89l10.74-4.14c.5-.18.94.12.78.95z"/></svg>) },
  { id:'facebook', label:'Continue with Facebook', bg:'#1877F2',  fg:'#fff',   icon:(<svg width="16" height="16" viewBox="0 0 24 24" fill="#fff"><path d="M22 12.06C22 6.5 17.5 2 12 2S2 6.5 2 12.06c0 5 3.66 9.13 8.44 9.88V14.9H7.9v-2.84h2.54v-2.16c0-2.5 1.5-3.9 3.78-3.9 1.1 0 2.24.2 2.24.2v2.46h-1.27c-1.25 0-1.64.77-1.64 1.56v1.84h2.78l-.44 2.84h-2.34V22c4.78-.75 8.45-4.88 8.45-9.94z"/></svg>) },
];

function SocialLoginButtons({ onSelect }) {
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:8, marginBottom:18 }}>
      {SOCIAL_PROVIDERS.map(p => (
        <button key={p.id} onClick={()=>onSelect(p)} type="button" style={{
          width:'100%', padding:'10px 14px', borderRadius:8,
          background:p.bg, color:p.fg, border: p.bg==='#fff' ? '1px solid #d0d0d0' : 'none',
          fontWeight:600, fontSize:13.5, display:'flex', alignItems:'center', justifyContent:'center', gap:10,
          transition:'transform 0.1s, filter 0.1s'
        }}
        onMouseEnter={e=>e.currentTarget.style.filter='brightness(1.08)'}
        onMouseLeave={e=>e.currentTarget.style.filter='none'}
        >
          {p.icon}
          <span>{p.label}</span>
        </button>
      ))}
    </div>
  );
}

// ── LIBRARY STATUS ────────────────────────────────────────────────────────────
// Дефолт (fallback) — реальные label/color приходят из taxonomy.json через __dsLibraryStatuses
// Палитра: tiffany #0abab5 + небесно-голубой
// «favorite» убран — есть отдельная кнопка ♥ FavoriteDramaButton рядом с библиотекой
// Цвета статусов библиотеки — единый источник правды для всех мест:
// квадратный значок у названия дорамы, бейджи на карточках, ViewersSummary.
// iconColor — цвет иконки ВНУТРИ цветного бейджа (контраст с background).
const LIBRARY_STATUSES_FALLBACK = [
  // Цвета: ярко-голубой watching / зелёный completed (по ТЗ Marina май 2026 —
  // раньше оба были бирюзово-зелёные и сливались).
  { id:'watching',  label:'Watching',     color:'#1e9eff', iconColor:'#ffffff' },  // ярко-голубой фон, белая иконка
  { id:'plan',      label:'Plan to Watch',color:'#7bc3ff', iconColor:'#0a1226' },  // светло-голубой фон, тёмная иконка
  { id:'completed', label:'Completed',    color:'#22c55e', iconColor:'#ffffff' },  // зелёный фон, белая иконка
  { id:'rewatch',   label:'Rewatch',      color:'#8a98a8', iconColor:'#0a1226' },  // серый фон, тёмная иконка
  { id:'dropped',   label:'Dropped',      color:'#e05858', iconColor:'#ffffff' },  // красный фон, белая иконка
];
// Обычный массив (mutable). При загрузке taxonomy.json index.html заменит содержимое через splice.
const LIBRARY_STATUSES = [...LIBRARY_STATUSES_FALLBACK];
if (typeof window !== 'undefined') {
  window.__dsLibStatusesArr = LIBRARY_STATUSES;
  // Если taxonomy уже загружена раньше (race) — сразу подставим
  if (Array.isArray(window.__dsLibraryStatuses) && window.__dsLibraryStatuses.length) {
    LIBRARY_STATUSES.splice(0, LIBRARY_STATUSES.length, ...window.__dsLibraryStatuses);
  }
}
// Иконка "открытая книга + плюс" — для дефолтной кнопки «Добавить в библиотеку».
// Две страницы развёрнутой книги с корешком по центру, маленький плюс справа.
function BookPlusIcon({ size = 20 }) {
  return (
    <svg width={size * 1.25} height={size} viewBox="0 0 30 24" fill="none" stroke="currentColor"
      strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"
      style={{ display: 'inline-block' }}>
      {/* Левая страница */}
      <path d="M2 4h6a3 3 0 0 1 3 3v13a2 2 0 0 0-2-2H2z"/>
      {/* Правая страница */}
      <path d="M19 4h-6a3 3 0 0 0-3 3v13a2 2 0 0 1 2-2h7z"/>
      {/* Плюс справа */}
      <line x1="25" y1="8"  x2="25" y2="14"/>
      <line x1="22" y1="11" x2="28" y2="11"/>
    </svg>
  );
}

// Тонко-линейные SVG иконки в стиле проекта (currentColor)
const LIB_ICONS = {
  watching:  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline-block', verticalAlign:'-0.15em'}}><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>,
  plan:      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline-block', verticalAlign:'-0.15em'}}><path d="M6 3h12v18l-6-4-6 4z"/></svg>,
  completed: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline-block', verticalAlign:'-0.15em'}}><circle cx="12" cy="12" r="9"/><polyline points="8 12 11 15 16 9"/></svg>,
  favorite:  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline-block', verticalAlign:'-0.15em'}}><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>,
  rewatch:   <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline-block', verticalAlign:'-0.15em'}}><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>,
  dropped:   <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline-block', verticalAlign:'-0.15em'}}><circle cx="12" cy="12" r="9"/><line x1="6" y1="6" x2="18" y2="18"/></svg>,
};

function LibraryStatusButton({ item, fullWidth, square = false }) {
  const { user, openSignUp } = window.useAuth();
  const { t, lang } = window.useI18n();
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const storageKey = `ds_library_${user?.email||'guest'}`;
  const [library, setLibrary] = useState(()=>{ try { return JSON.parse(localStorage.getItem(storageKey))||[]; } catch { return []; }});
  const entry = library.find(x=>x.id===item.id);
  const status = entry?.status;
  const curStatus = LIBRARY_STATUSES.find(s=>s.id===status);
  const isRu = lang === 'ru';

  useEffect(()=>{
    const h = (e)=>{ if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return ()=>document.removeEventListener('mousedown', h);
  },[]);

  // Реактивно обновляем local library при изменении из других мест (например DramaCard)
  useEffect(() => {
    const refresh = () => {
      try { setLibrary(JSON.parse(localStorage.getItem(storageKey)) || []); } catch {}
    };
    window.addEventListener('ds-library-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-library-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);

  const setStatus = (newStatus) => {
    if (!user) { openSignUp(); return; }
    let next;
    if (newStatus === null) {
      next = library.filter(x=>x.id!==item.id);
    } else {
      const data = { id:item.id, slug:item.slug, title_en:item.title_en, title_ru:item.title_ru, title_original:item.title_original, posterPath:item.posterPath, firstAirDate:item.firstAirDate, voteAverage:item.voteAverage, originCountry:item.originCountry, status:newStatus, addedAt:new Date().toISOString() };
      next = entry ? library.map(x=>x.id===item.id?{...x, ...data}:x) : [...library, data];
    }
    setLibrary(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
    window.dispatchEvent(new CustomEvent('ds-library-changed'));
    setOpen(false);
  };

  if (!user) {
    if (square) {
      return (
        <button onClick={openSignUp}
          title={isRu ? '🔒 Войдите, чтобы добавить в библиотеку' : '🔒 Sign in to add to library'}
          style={{
            width: 40, height: 40, borderRadius: '50%',
            background: 'linear-gradient(135deg, #1487cb 0%, #4FA1DC 100%)',
            color: '#ffffff', border: '1px solid #1487cb',
            boxShadow: '0 4px 14px rgba(20, 135, 203, 0.32)',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', padding: 0
          }}>
          <BookPlusIcon size={22}/>
        </button>
      );
    }
    return (
      <button onClick={openSignUp} style={{
        padding:'10px 18px', borderRadius:8, fontSize:13, fontWeight:700,
        background:'linear-gradient(135deg, #1487cb 0%, #4FA1DC 100%)', color:'#ffffff',
        border:'1px solid #1487cb',
        boxShadow:'0 4px 18px rgba(20, 135, 203, 0.32)',
        display:'flex', alignItems:'center', gap:6, width:fullWidth?'100%':'auto'
      }}>🔒 {t('Add to Watchlist')||'Add to Watchlist'}</button>
    );
  }

  // ── Квадратный режим (square): красивый 40×40 с иконкой статуса ──
  if (square) {
    return (
      <div ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
        <button onClick={() => setOpen(v => !v)}
          className="ds-libstatus-square"
          title={curStatus
            ? (isRu ? `Статус: ${t(curStatus.label)} — клик чтобы поменять` : `Status: ${t(curStatus.label)} — click to change`)
            : (isRu ? 'Добавить в библиотеку' : 'Add to Library')}
          style={{
            width: 40, height: 40, borderRadius: '50%',
            background: curStatus ? (curStatus.color || '#1487cb') : 'linear-gradient(135deg, #1487cb 0%, #4FA1DC 100%)',
            border: curStatus ? `2px solid ${curStatus.color}` : '1px solid #1487cb',
            color: curStatus ? (curStatus.iconColor || '#ffffff') : '#ffffff',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', padding: 0,
            boxShadow: curStatus ? `0 2px 10px ${curStatus.color}55` : '0 4px 14px rgba(20, 135, 203, 0.32)',
            transition: 'all 0.15s'
          }}>
          {curStatus
            ? <span style={{ display: 'inline-flex', transform: 'scale(0.9)' }}>{LIB_ICONS[curStatus.id]}</span>
            : <BookPlusIcon size={22}/>}
        </button>
        {open && (
          <div style={{
            position: 'absolute', top: 'calc(100% + 6px)', left: 0, zIndex: 60,
            background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 10,
            padding: 6, minWidth: 200, boxShadow: '0 8px 28px rgba(0,0,0,0.55)'
          }}>
            {LIBRARY_STATUSES.filter(s => s.id !== 'favorite').map(s => (
              <button key={s.id} onClick={() => setStatus(s.id)} style={{
                width: '100%', padding: '8px 12px', borderRadius: 6, fontSize: 13, textAlign: 'left',
                display: 'flex', alignItems: 'center', gap: 8,
                color: status === s.id ? s.color : 'var(--text2)',
                background: status === s.id ? `${s.color}18` : 'transparent', marginBottom: 2
              }}
              onMouseEnter={e => { if (status !== s.id) e.currentTarget.style.background = 'rgba(74,158,255,0.06)'; }}
              onMouseLeave={e => { if (status !== s.id) e.currentTarget.style.background = 'transparent'; }}>
                {/* Иконка ВСЕГДА своего цвета — даже если статус ещё не выбран */}
                <span style={{ display: 'inline-flex', alignItems: 'center', color: s.color }}>{LIB_ICONS[s.id]}</span>
                <span style={{ fontWeight: status === s.id ? 700 : 500 }}>{t(s.label)}</span>
              </button>
            ))}
            {status && (
              <>
                <div style={{ height: 1, background: 'rgba(74,158,255,0.08)', margin: '4px 8px' }} />
                <button onClick={() => setStatus(null)} style={{
                  width: '100%', padding: '8px 12px', borderRadius: 6, fontSize: 13, textAlign: 'left',
                  color: 'var(--red)'
                }}>{isRu ? 'Убрать из библиотеки' : 'Remove from Library'}</button>
              </>
            )}
          </div>
        )}
      </div>
    );
  }

  return (
    <div ref={ref} style={{ position:'relative', display:fullWidth?'block':'inline-block', width:fullWidth?'100%':'auto' }}>
      <button onClick={()=>setOpen(v=>!v)} style={{
        padding:'10px 18px', borderRadius:8, fontSize:13, fontWeight:700,
        background: curStatus ? `${curStatus.color}22` : 'linear-gradient(135deg, #1487cb 0%, #4FA1DC 100%)',
        color: curStatus ? curStatus.color : '#ffffff',
        border: curStatus ? `1px solid ${curStatus.color}55` : '1px solid #1487cb',
        boxShadow: curStatus ? 'none' : '0 4px 18px rgba(20, 135, 203, 0.32)',
        display:'flex', alignItems:'center', gap:8, width:fullWidth?'100%':'auto',
        justifyContent: fullWidth ? 'space-between' : 'flex-start'
      }}>
        <span style={{ display:'flex', alignItems:'center', gap:6 }}>
          <span style={{ display:'inline-flex', alignItems:'center' }}>{curStatus ? LIB_ICONS[curStatus.id] : <BookPlusIcon size={18}/>}</span>
          <span>{curStatus ? t(curStatus.label) : t('Add to Library')}</span>
        </span>
        <span style={{ fontSize:9 }}>▾</span>
      </button>
      {open && (
        <div style={{
          position:'absolute', top:'calc(100% + 6px)', left:0, zIndex:50,
          background:'var(--bg2)', border:'1px solid var(--border)', borderRadius:10,
          padding:6, minWidth:200, boxShadow:'0 8px 28px rgba(0,0,0,0.55)'
        }}>
          {LIBRARY_STATUSES.filter(s => s.id !== 'favorite').map(s => (
            <button key={s.id} onClick={()=>setStatus(s.id)} style={{
              width:'100%', padding:'8px 12px', borderRadius:6, fontSize:13, textAlign:'left',
              display:'flex', alignItems:'center', gap:8, color: status===s.id ? s.color : 'var(--text2)',
              background: status===s.id ? `${s.color}18` : 'transparent', marginBottom:2
            }}
            onMouseEnter={e=>{ if(status!==s.id) e.currentTarget.style.background='rgba(74,158,255,0.06)'; }}
            onMouseLeave={e=>{ if(status!==s.id) e.currentTarget.style.background='transparent'; }}
            >
              <span style={{ display:'inline-flex', alignItems:'center' }}>{LIB_ICONS[s.id]}</span>
              <span style={{ fontWeight:status===s.id?700:500 }}>{t(s.label)}</span>
            </button>
          ))}
          {status && (
            <>
              <div style={{ height:1, background:'rgba(74,158,255,0.08)', margin:'4px 8px' }}/>
              <button onClick={()=>setStatus(null)} style={{
                width:'100%', padding:'8px 12px', borderRadius:6, fontSize:13, textAlign:'left',
                color:'var(--red)'
              }}>{t('Remove from Library')}</button>
            </>
          )}
        </div>
      )}
    </div>
  );
}

// ── RECOMMEND BLOCK ───────────────────────────────────────────────────────────
const RECOMMEND_REASONS = ['Chemistry','Emotional story','Green flag male lead','Amazing OST','Beautiful cinematography','Addictive','Comfort drama','Strong acting','Beautiful romance','Great ending'];

function RecommendBlock({ dramaId }) {
  const { t } = (window.useI18n ? window.useI18n() : { t: (s) => s });
  const { user, openSignIn } = window.useAuth();
  const storageKey = `ds_recommend_${dramaId}_${user?.email||'guest'}`;
  const aggregateKey = `ds_recommend_agg_${dramaId}`;
  const [data, setData] = useState(()=>{ try{ return JSON.parse(localStorage.getItem(storageKey))||{ recommended:false, reasons:[] }; }catch{ return { recommended:false, reasons:[] }; } });
  const [showReasons, setShowReasons] = useState(false);
  const agg = JSON.parse(localStorage.getItem(aggregateKey) || '{"yes":0,"no":0,"total":0}');
  const percent = agg.total > 0 ? Math.round((agg.yes / agg.total) * 100) : Math.floor(Math.random()*30+60);

  const toggleRecommend = () => {
    if (!user) { openSignIn(); return; }
    const next = { ...data, recommended: !data.recommended };
    setData(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
    // simulate aggregate update
    const a = JSON.parse(localStorage.getItem(aggregateKey)||'{"yes":0,"no":0,"total":0}');
    if (next.recommended) { a.yes += 1; a.total += 1; }
    else { a.yes = Math.max(0, a.yes - 1); a.total = Math.max(0, a.total - 1); }
    localStorage.setItem(aggregateKey, JSON.stringify(a));
    if (next.recommended) setShowReasons(true);
    // Sync на сервер: PUT /api/ratings/drama/:id/recommendation { recommended: bool }
    // — отдельный endpoint, не затирает другие dimensions (overall/ost/etc).
    // Это даёт рекомендации в публичном профиле /u/:id (Sprint B, июнь 2026).
    try {
      const api = window.__dsApi;
      if (api?.fetch) {
        api.fetch(`/api/ratings/drama/${dramaId}/recommendation`, {
          method: 'PUT',
          body: JSON.stringify({ recommended: next.recommended }),
        }).catch(() => {});
      }
    } catch {}
  };

  const toggleReason = (r) => {
    const reasons = data.reasons.includes(r) ? data.reasons.filter(x=>x!==r) : [...data.reasons, r];
    const next = { ...data, reasons };
    setData(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
  };

  return (
    <div className="ds-drama-panel" style={{ background:'var(--bg2)', border:'none', borderRadius:12, padding:22, marginTop:16 }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14, flexWrap:'wrap', gap:8 }}>
        <h3 style={{ fontSize:16, fontWeight:700 }}>{t('Recommend this drama')}</h3>
        <span style={{ fontSize:12, color:'#1487cb', fontWeight:600, display:'inline-flex', alignItems:'center', gap:6 }}>
          <span style={{
            display:'inline-flex', alignItems:'center', justifyContent:'center',
            width:16, height:16, borderRadius:'50%', background:'#1487cb', color:'#fff'
          }}>
            <Icon name="check" size={10}/>
          </span>
          {t('Recommended by')} {percent}% {t('of users')}
        </span>
      </div>
      <button onClick={toggleRecommend} style={{
        padding:'10px 18px', borderRadius:999, fontSize:13, fontWeight:700,
        background: data.recommended ? 'rgba(20,135,203,0.12)' : 'var(--bg3)',
        color: data.recommended ? '#1487cb' : 'var(--text2)',
        border: `1px solid ${data.recommended ? 'rgba(20,135,203,0.4)' : 'rgba(74,158,255,0.1)'}`,
        display:'flex', alignItems:'center', gap:8
      }}>
        <Icon name="check" size={14}/>
        {data.recommended ? t("You recommend this drama") : t("I recommend this drama")}
      </button>

      {data.recommended && (
        <div style={{ marginTop:16 }}>
          <div style={{ fontSize:12, color:'var(--text3)', marginBottom:10, fontWeight:600 }}>{t('Why do you recommend it? (pick all that apply)')}</div>
          <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
            {RECOMMEND_REASONS.map(r => {
              const active = data.reasons.includes(r);
              return (
                <button key={r} onClick={()=>toggleReason(r)} style={{
                  padding:'6px 12px', borderRadius:999, fontSize:11.5, fontWeight:400,
                  background: active ? '#1487cb' : 'var(--bg3)',
                  color: active ? '#ffffff' : 'var(--text2)',
                  border:`1px solid ${active?'#1487cb':'rgba(74,158,255,0.1)'}`,
                  transition:'all 0.15s'
                }}>{t(r)}</button>
              );
            })}
          </div>
          <div style={{ marginTop:14, padding:'10px 14px', background:'rgba(20,135,203,0.06)', borderRadius:8, borderLeft:'3px solid #1487cb' }}>
            <div style={{ fontSize:11, color:'var(--text3)', marginBottom:3 }}>{t('Public summary')}</div>
            <div style={{ fontSize:12, color:'var(--text)' }}>{t('Most recommended for:')} <strong>{t('Chemistry')}, {t('Emotional impact')}, {t('Visuals')}, {t('OST')}, {t('Comfort')}.</strong></div>
          </div>
        </div>
      )}
    </div>
  );
}

// ── REVIEWS SECTION ───────────────────────────────────────────────────────────
const REVIEW_REACTIONS = ['❤️','😂','😭','🔥','🛐','👏','🤯','😡'];

// Универсальная секция отзывов. Принимает либо dramaId, либо actorId.
// storage-ключ: legacy `ds_reviews_<dramaId>` для дорам (обратная совместимость),
// `ds_reviews_actor_<actorId>` для актёров.
function ReviewsSection({ dramaId, actorId }) {
  const { t } = (window.useI18n ? window.useI18n() : { t: (s) => s });
  const { user, openSignIn } = window.useAuth();
  const storageKey = dramaId
    ? `ds_reviews_${dramaId}`
    : `ds_reviews_actor_${actorId}`;
  const [reviews, setReviews] = useState(()=>{ try { return JSON.parse(localStorage.getItem(storageKey))||[]; } catch { return []; }});
  const [showForm, setShowForm] = useState(false);
  const [text, setText] = useState('');
  const [isSpoiler, setIsSpoiler] = useState(false);

  const save = (next) => { setReviews(next); localStorage.setItem(storageKey, JSON.stringify(next)); };

  const post = () => {
    if (!user || !text.trim()) return;
    const review = { id:Date.now(), userEmail:user.email, userName:user.name, text:text.trim(), spoiler:isSpoiler, date:'just now', reactions:{}, replies:[] };
    save([review, ...reviews]);
    setText(''); setIsSpoiler(false); setShowForm(false);
  };

  const react = (reviewId, emoji) => {
    if (!user) { openSignIn(); return; }
    save(reviews.map(r => {
      if (r.id!==reviewId) return r;
      const rxs = { ...(r.reactions||{}) };
      const list = rxs[emoji] || [];
      rxs[emoji] = list.includes(user.email) ? list.filter(e=>e!==user.email) : [...list, user.email];
      return { ...r, reactions: rxs };
    }));
  };

  const reply = (reviewId, replyText) => {
    if (!user || !replyText.trim()) return;
    save(reviews.map(r => r.id===reviewId ? { ...r, replies:[...(r.replies||[]), { id:Date.now(), userEmail:user.email, userName:user.name, text:replyText.trim(), date:'just now', reactions:{} }] } : r));
  };

  const removeReview = (reviewId) => save(reviews.filter(r=>r.id!==reviewId));

  return (
    <div style={{ background:'var(--bg2)', border:'1px solid var(--border)', borderRadius:12, padding:22, marginTop:16 }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:16, flexWrap:'wrap', gap:8 }}>
        <h3 style={{ fontSize:16, fontWeight:700 }}>{t('Reviews')} ({reviews.length})</h3>
        {user ? (
          <button onClick={()=>setShowForm(v=>!v)} style={{
            padding:'8px 16px', borderRadius:8, background:'var(--accent)', color:'#fff', fontWeight:700, fontSize:12.5
          }}>{showForm ? t('Cancel') : t('+ Write Review')}</button>
        ) : (
          <button onClick={openSignIn} style={{
            padding:'8px 16px', borderRadius:8, background:'var(--bg3)', color:'var(--text2)', fontWeight:600, fontSize:12.5,
            border:'1px solid rgba(74,158,255,0.15)'
          }}>🔒 {t('Sign in to write a review')}</button>
        )}
      </div>

      {showForm && user && (
        <div style={{ background:'var(--bg3)', border:'1px solid var(--border)', borderRadius:10, padding:16, marginBottom:18 }}>
          <textarea value={text} onChange={e=>setText(e.target.value)} placeholder={t('Share your thoughts on this drama...')} rows={4}
            style={{ width:'100%', padding:'10px 12px', borderRadius:8, background:'var(--bg)', border:'1px solid rgba(74,158,255,0.1)', color:'var(--text)', fontSize:13, outline:'none', resize:'vertical', marginBottom:10 }}/>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', flexWrap:'wrap', gap:8 }}>
            <label style={{ display:'flex', alignItems:'center', gap:6, fontSize:12, color:'var(--text2)', cursor:'pointer' }}>
              <input type="checkbox" checked={isSpoiler} onChange={e=>setIsSpoiler(e.target.checked)}/>
              <Icon name="eyeOff" size={13}/>
              <span>{t('Contains spoilers')}</span>
            </label>
            <button onClick={post} disabled={!text.trim()} style={{
              padding:'8px 22px', borderRadius:8, background:'var(--accent)', color:'#fff', fontWeight:700, fontSize:13, opacity:!text.trim()?0.5:1
            }}>{t('Post Review')}</button>
          </div>
        </div>
      )}

      {reviews.length === 0 ? (
        <div style={{ textAlign:'center', padding:'24px 0', color:'var(--text3)', fontSize:13 }}>{t('No reviews yet. Be the first to share your thoughts!')}</div>
      ) : (
        <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
          {reviews.map(r => <ReviewItem key={r.id} review={r} onReact={react} onReply={reply} onRemove={removeReview} currentUser={user}/>)}
        </div>
      )}
    </div>
  );
}

function ReviewItem({ review:r, onReact, onReply, onRemove, currentUser }) {
  const [revealed, setRevealed] = useState(!r.spoiler);
  const [replyOpen, setReplyOpen] = useState(false);
  const [replyText, setReplyText] = useState('');
  const isOwn = currentUser?.email === r.userEmail;

  return (
    <div style={{ background:'var(--bg3)', border:'1px solid rgba(74,158,255,0.07)', borderRadius:10, padding:16 }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:8 }}>
        <div style={{ display:'flex', alignItems:'center', gap:8 }}>
          <div style={{ width:30, height:30, borderRadius:'50%', background:'linear-gradient(135deg, var(--accent), var(--purple))', color:'#fff', fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', fontSize:12 }}>
            {(r.userName||'?').charAt(0).toUpperCase()}
          </div>
          <div>
            <div style={{ fontSize:12.5, fontWeight:700, color:'var(--accent2)' }}>{r.userName}</div>
            <div style={{ fontSize:10, color:'var(--text3)' }}>{r.date}</div>
          </div>
        </div>
        {isOwn && (
          <button onClick={()=>{ if(window.confirm('Delete this review?')) onRemove(r.id); }} style={{ fontSize:11, color:'var(--text3)' }}>Delete</button>
        )}
      </div>

      {r.spoiler && !revealed ? (
        <div style={{ background:'rgba(255,107,107,0.08)', border:'1px dashed rgba(255,107,107,0.3)', borderRadius:8, padding:14, textAlign:'center' }}>
          <div style={{ fontSize:12, color:'var(--red)', marginBottom:8, display:'flex', alignItems:'center', justifyContent:'center', gap:6 }}>
            <Icon name="eyeOff" size={13}/> Careful — this review contains spoilers
          </div>
          <button onClick={()=>setRevealed(true)} style={{ fontSize:12, color:'var(--accent)', fontWeight:600, padding:'5px 14px', borderRadius:6, background:'rgba(74,158,255,0.1)' }}>
            <Icon name="eye" size={11}/> Show Spoiler
          </button>
        </div>
      ) : (
        <>
          {r.spoiler && (
            <div style={{ fontSize:10, color:'var(--red)', fontWeight:600, marginBottom:8, display:'flex', alignItems:'center', gap:4 }}>
              <Icon name="eyeOff" size={11}/> SPOILER
            </div>
          )}
          <p style={{ fontSize:13.5, lineHeight:1.7, color:'var(--text)', marginBottom:10 }}>{r.text}</p>
        </>
      )}

      <div style={{ display:'flex', gap:4, flexWrap:'wrap', marginTop:10 }}>
        {REVIEW_REACTIONS.map(emoji => {
          const list = (r.reactions||{})[emoji] || [];
          const active = currentUser && list.includes(currentUser.email);
          return (
            <button key={emoji} onClick={()=>onReact(r.id, emoji)} style={{
              padding:'3px 9px', borderRadius:14, fontSize:13,
              background: active ? 'rgba(74,158,255,0.15)' : 'transparent',
              border:`1px solid ${active?'var(--accent)':'rgba(74,158,255,0.08)'}`,
              display:'flex', alignItems:'center', gap:4
            }}>
              <span>{emoji}</span>
              {list.length>0 && <span style={{ fontSize:11, color:'var(--text3)', fontWeight:600 }}>{list.length}</span>}
            </button>
          );
        })}
        {currentUser && (
          <button onClick={()=>setReplyOpen(v=>!v)} style={{
            padding:'3px 9px', borderRadius:14, fontSize:11, color:'var(--text3)', marginLeft:'auto'
          }}>Reply</button>
        )}
      </div>

      {replyOpen && currentUser && (
        <div style={{ marginTop:10, display:'flex', gap:6 }}>
          <input value={replyText} onChange={e=>setReplyText(e.target.value)} placeholder="Write a reply..."
            style={{ flex:1, padding:'7px 11px', borderRadius:7, background:'var(--bg)', border:'1px solid rgba(74,158,255,0.1)', color:'var(--text)', fontSize:12, outline:'none' }}/>
          <button onClick={()=>{ onReply(r.id, replyText); setReplyText(''); setReplyOpen(false); }} disabled={!replyText.trim()} style={{
            padding:'7px 14px', borderRadius:7, background:'var(--accent)', color:'#fff', fontSize:12, fontWeight:600, opacity:!replyText.trim()?0.5:1
          }}>Reply</button>
        </div>
      )}

      {(r.replies||[]).length > 0 && (
        <div style={{ marginTop:12, paddingLeft:14, borderLeft:'2px solid rgba(74,158,255,0.15)', display:'flex', flexDirection:'column', gap:8 }}>
          {r.replies.map(rep => (
            <div key={rep.id} style={{ background:'rgba(74,158,255,0.05)', borderRadius:8, padding:'10px 12px' }}>
              <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:4 }}>
                <span style={{ fontSize:11.5, fontWeight:700, color:'var(--accent2)' }}>{rep.userName}</span>
                <span style={{ fontSize:10, color:'var(--text3)' }}>· {rep.date}</span>
              </div>
              <p style={{ fontSize:12.5, lineHeight:1.6, color:'var(--text)' }}>{rep.text}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── SHARE MENU ────────────────────────────────────────────────────────────────
const SHARE_PLATFORMS = [
  { id:'copy',     label:'Copy Link',         color:'#4a9eff' },
  { id:'x',        label:'Share on X',        color:'#000' },
  { id:'facebook', label:'Share on Facebook', color:'#1877F2' },
  { id:'telegram', label:'Share on Telegram', color:'#0088CC' },
  { id:'reddit',   label:'Share on Reddit',   color:'#FF4500' },
  { id:'whatsapp', label:'Share on WhatsApp', color:'#25D366' },
  { id:'pinterest',label:'Share on Pinterest',color:'#E60023' },
];

function ShareMenu({ title='Check this out on MaoDrama', url='', compact = false }) {
  const { t, lang } = (window.useI18n ? window.useI18n() : { t: (s) => s, lang: 'en' });
  const [open, setOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const ref = useRef(null);
  const shareUrl = url || window.location.href;
  const isRu = lang === 'ru';
  const shareTooltip = isRu
    ? (copied ? 'Ссылка скопирована!' : 'Рассказать друзьям — отправить ссылку в соцсети или скопировать.')
    : (copied ? 'Link copied!' : 'Share with friends — send link to social media or copy.');

  useEffect(()=>{
    const h = (e)=>{ if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return ()=>document.removeEventListener('mousedown', h);
  },[]);

  const share = (platform) => {
    const encoded = encodeURIComponent(shareUrl);
    const encTitle = encodeURIComponent(title);
    const urls = {
      x: `https://twitter.com/intent/tweet?url=${encoded}&text=${encTitle}`,
      facebook: `https://www.facebook.com/sharer/sharer.php?u=${encoded}`,
      telegram: `https://t.me/share/url?url=${encoded}&text=${encTitle}`,
      reddit:   `https://reddit.com/submit?url=${encoded}&title=${encTitle}`,
      whatsapp: `https://wa.me/?text=${encTitle}%20${encoded}`,
      pinterest:`https://pinterest.com/pin/create/button/?url=${encoded}&description=${encTitle}`,
    };
    if (platform === 'copy') {
      navigator.clipboard?.writeText(shareUrl);
      setCopied(true);
      setTimeout(()=>setCopied(false), 1500);
    } else if (urls[platform]) {
      window.open(urls[platform], '_blank', 'noopener,noreferrer,width=600,height=600');
    }
    setOpen(false);
  };

  const ShareTip = window.DSHoverTip || (({ children }) => children);
  return (
    <div ref={ref} style={{ position:'relative', display:'inline-flex', alignItems: 'center', verticalAlign: 'middle' }}>
      <ShareTip tip={open ? null : shareTooltip}>
        <button onClick={()=>setOpen(v=>!v)}
          className={compact ? 'ds-action-icon-btn ds-action-share' : ''}
          aria-label="Share"
          style={compact ? {
            width: 36, height: 36, borderRadius: '50%',
            color: '#1487cb',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            padding: 0, cursor: 'pointer'
          } : {
            padding:'10px 18px', borderRadius:8, fontSize:13, fontWeight:700,
            background:'rgba(159, 233, 224, 0.15)', color:'#9fe9e0', border:'1px solid rgba(159, 233, 224, 0.45)',
            display:'flex', alignItems:'center', gap:8, cursor: 'pointer'
          }}>
          <Icon name="share" size={compact ? 16 : 14}/>
          {!compact && <span>{copied ? t('Copied!') : t('Share')}</span>}
        </button>
      </ShareTip>
      {open && (
        <div className="ds-share-dropdown" style={{
          position:'absolute', top:'calc(100% + 6px)', right:0, zIndex:50,
          background:'var(--bg2)', border:'1px solid var(--border)', borderRadius:10,
          padding:6, minWidth:200, boxShadow:'0 8px 28px rgba(0,0,0,0.55)'
        }}>
          {SHARE_PLATFORMS.map(p => (
            <button key={p.id} onClick={()=>share(p.id)} style={{
              width:'100%', padding:'8px 12px', borderRadius:6, fontSize:13, textAlign:'left',
              color:'var(--text2)', display:'flex', alignItems:'center', gap:8, marginBottom:1
            }}
            onMouseEnter={e=>e.currentTarget.style.background='rgba(74,158,255,0.06)'}
            onMouseLeave={e=>e.currentTarget.style.background='transparent'}
            >
              <span style={{ width:6, height:6, borderRadius:'50%', background:p.color, flexShrink:0 }}/>
              <span>{p.label}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ── AD SLOT ───────────────────────────────────────────────────────────────────
function AdSlot({ size='leaderboard', label='Advertisement', style={} }) {
  const sizes = {
    leaderboard: { width:'min(728px,100%)', height:90,  desc:'728×90' },
    billboard:   { width:'min(970px,100%)', height:250, desc:'970×250' },
    mpu:         { width:300, height:250, desc:'300×250' },
    halfpage:    { width:300, height:600, desc:'300×600' },
    mobile:      { width:'min(320px,100%)', height:50,  desc:'320×50'  },
  };
  const s = sizes[size]||sizes.leaderboard;
  return (
    <div style={{ display:'flex', justifyContent:'center', margin:'20px 0', ...style }}>
      <div style={{
        width:s.width, height:s.height, maxWidth:'100%',
        background:'repeating-linear-gradient(45deg, rgba(74,158,255,0.03), rgba(74,158,255,0.03) 8px, rgba(74,158,255,0.06) 8px, rgba(74,158,255,0.06) 16px)',
        border:'1px dashed rgba(74,158,255,0.2)', borderRadius:8,
        display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:4
      }}>
        <span style={{ fontSize:10, color:'var(--text3)', textTransform:'uppercase', letterSpacing:'1.2px', fontWeight:700 }}>{label}</span>
        <span style={{ fontSize:10, color:'var(--text3)', opacity:0.6 }}>{s.desc}</span>
      </div>
    </div>
  );
}

// ── NOTIFICATIONS ─────────────────────────────────────────────────────────────
// Реальные уведомления из D1 (через worker /api/me/notifications).
// На mount грузим список + unread-count. После открытия колокольчика —
// помечаем всё как read автоматически. Polling каждые 60 сек на фоне.

// Форматирует ISO timestamp в человеческое «2 ч назад» / «вчера» / «12 мая».
function formatRelativeTime(iso, isRu) {
  if (!iso) return '';
  // SQLite даёт без Z — нормализуем к UTC
  const t = new Date(iso.includes('Z') || iso.includes('+') ? iso : iso + 'Z');
  const diffSec = Math.round((Date.now() - t.getTime()) / 1000);
  if (diffSec < 60) return isRu ? 'только что' : 'just now';
  if (diffSec < 3600) {
    const m = Math.round(diffSec / 60);
    return isRu ? `${m} мин назад` : `${m}m ago`;
  }
  if (diffSec < 86400) {
    const h = Math.round(diffSec / 3600);
    return isRu ? `${h} ч назад` : `${h}h ago`;
  }
  if (diffSec < 86400 * 7) {
    const d = Math.round(diffSec / 86400);
    return isRu ? `${d} дн назад` : `${d}d ago`;
  }
  return t.toLocaleDateString(isRu ? 'ru-RU' : 'en-US', { day: '2-digit', month: 'short' });
}

function NotificationBell() {
  const { user } = window.useAuth();
  const { lang } = (window.useI18n ? window.useI18n() : { lang: 'ru' });
  const isRu = lang === 'ru';
  const api = window.__dsApi;
  const [open, setOpen] = useState(false);
  const [items, setItems] = useState([]);
  const [unread, setUnread] = useState(0);
  const [loading, setLoading] = useState(false);
  const ref = useRef(null);

  // Загрузка списка — вызывается при mount, при открытии, и периодически.
  const loadList = useCallback(async () => {
    if (!user || !api) return;
    setLoading(true);
    try {
      const data = await api.fetch('/api/me/notifications?limit=20');
      setItems(data.items || []);
      setUnread(data.unread_count || 0);
    } catch (e) {
      console.warn('[notifications] load failed:', e);
    } finally {
      setLoading(false);
    }
  }, [user, api]);

  // Лёгкий polling — только unread_count для bell-badge (без всего списка).
  const pollUnread = useCallback(async () => {
    if (!user || !api) return;
    try {
      const data = await api.fetch('/api/me/notifications/unread-count');
      setUnread(data.unread_count || 0);
    } catch {}
  }, [user, api]);

  // Публикуем unread-count в window — чтобы AvatarMenu и другие компоненты
  // могли его читать без отдельного запроса к API.
  useEffect(() => {
    window.__dsUnreadNotifs = unread;
  }, [unread]);

  // На mount — полная загрузка. Затем polling каждые 60 сек.
  useEffect(() => {
    if (!user) { setItems([]); setUnread(0); return; }
    loadList();
    const id = setInterval(pollUnread, 60_000);
    return () => clearInterval(id);
  }, [user, loadList, pollUnread]);

  // Outside click — закрываем dropdown
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  if (!user) return null;

  const markAllRead = async () => {
    if (unread === 0) return;
    // Оптимистично — сразу обнуляем счётчик и помечаем все items
    setUnread(0);
    setItems(prev => prev.map(n => ({ ...n, read: true })));
    try { await api.fetch('/api/me/notifications/read-all', { method: 'POST' }); }
    catch (e) { console.warn('[notifications] mark-all-read failed:', e); }
  };

  const handleClick = async (notif) => {
    // Помечаем как read (оптимистично)
    if (!notif.read) {
      setItems(prev => prev.map(n => n.id === notif.id ? { ...n, read: true } : n));
      setUnread(u => Math.max(0, u - 1));
      try { await api.fetch(`/api/me/notifications/${notif.id}/read`, { method: 'POST' }); }
      catch {}
    }
    // Навигация по url (если есть)
    if (notif.url) {
      if (notif.url.startsWith('#/')) {
        window.location.hash = notif.url;
      } else if (notif.url.startsWith('http')) {
        window.open(notif.url, '_blank');
      }
    }
    setOpen(false);
  };

  const handleDelete = async (e, notif) => {
    e.stopPropagation();
    // Оптимистично убираем из списка
    setItems(prev => prev.filter(n => n.id !== notif.id));
    if (!notif.read) setUnread(u => Math.max(0, u - 1));
    try { await api.fetch(`/api/me/notifications/${notif.id}`, { method: 'DELETE' }); }
    catch (e) { console.warn('[notifications] delete failed:', e); loadList(); }
  };

  const toggle = () => {
    const next = !open;
    setOpen(next);
    if (next) loadList();  // refresh при открытии
  };

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button onClick={toggle} style={{
        padding: '6px 8px', position: 'relative', color: 'var(--text2)', borderRadius: 7,
        border: '1px solid transparent', display: 'flex', alignItems: 'center'
      }}
      onMouseEnter={e => e.currentTarget.style.background = 'rgba(74,158,255,0.07)'}
      onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
      aria-label={isRu ? 'Уведомления' : 'Notifications'}
      >
        <svg width="20" height="20" 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>
        {unread > 0 && (
          <span className="ds-notif-badge" style={{
            position: 'absolute', top: 2, right: 2, minWidth: 14, height: 14, borderRadius: 7,
            background: 'var(--red)', color: '#fff', fontSize: 9, fontWeight: 700,
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 3px'
          }}>{unread > 99 ? '99+' : unread}</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: 10, width: 340, maxHeight: 480, overflowY: 'auto', boxShadow: '0 12px 36px rgba(0,0,0,0.65)'
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px 10px', borderBottom: '1px solid rgba(74,158,255,0.08)', marginBottom: 6 }}>
            <span style={{ fontSize: 13, fontWeight: 700 }}>
              {isRu ? 'Уведомления' : 'Notifications'}
            </span>
            {unread > 0 && (
              <button onClick={markAllRead} style={{ fontSize: 11, color: 'var(--accent)', background: 'transparent', border: 'none', cursor: 'pointer' }}>
                {isRu ? 'Отметить все' : 'Mark all read'}
              </button>
            )}
          </div>
          {loading && items.length === 0 && (
            <div style={{ padding: '24px 12px', textAlign: 'center', fontSize: 12, color: 'var(--text3)' }}>
              {isRu ? 'Загружаем…' : 'Loading…'}
            </div>
          )}
          {!loading && items.length === 0 && (
            <div style={{ padding: '32px 12px', textAlign: 'center', fontSize: 12, color: 'var(--text3)' }}>
              <div style={{ fontSize: 32, marginBottom: 8 }}>📭</div>
              {isRu ? 'Пока пусто' : 'No notifications yet'}
            </div>
          )}
          {items.map(n => (
            <div key={n.id} onClick={() => handleClick(n)} style={{
              padding: '9px 10px', borderRadius: 7, marginBottom: 2, cursor: 'pointer',
              background: n.read ? 'transparent' : 'rgba(74,158,255,0.06)',
              display: 'flex', gap: 10, alignItems: 'flex-start',
              position: 'relative'
            }}
            onMouseEnter={e => e.currentTarget.style.background = n.read ? 'rgba(74,158,255,0.04)' : 'rgba(74,158,255,0.10)'}
            onMouseLeave={e => e.currentTarget.style.background = n.read ? 'transparent' : 'rgba(74,158,255,0.06)'}
            >
              <span style={{ fontSize: 18, flexShrink: 0, lineHeight: 1.1 }}>{n.icon || '🔔'}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12.5, color: 'var(--text)', lineHeight: 1.4, fontWeight: n.read ? 400 : 600 }}>{n.title}</div>
                {n.body && (
                  <div style={{ fontSize: 11.5, color: 'var(--text2)', lineHeight: 1.4, marginTop: 2 }}>{n.body}</div>
                )}
                <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 3 }}>{formatRelativeTime(n.created_at, isRu)}</div>
              </div>
              <button onClick={(e) => handleDelete(e, n)} title={isRu ? 'Скрыть' : 'Dismiss'} style={{
                position: 'absolute', top: 6, right: 6, width: 18, height: 18, borderRadius: 3,
                background: 'transparent', color: 'var(--text3)', border: 'none', cursor: 'pointer',
                fontSize: 14, lineHeight: 1, padding: 0, opacity: 0.5
              }}
              onMouseEnter={e => e.currentTarget.style.opacity = 1}
              onMouseLeave={e => e.currentTarget.style.opacity = 0.5}
              >×</button>
              {!n.read && <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--accent)', position: 'absolute', top: 12, right: 28 }} />}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── MESSAGES INBOX (envelope icon, same style as bell) ────────────────────────
// Пока ЛС между пользователями не реализованы → пустой массив, badge не показывается.
// Когда сделаем DM-фичу — заменим на fetch из /api/messages.
const SAMPLE_MESSAGES = [];

function MessagesIcon() {
  const { user } = window.useAuth();
  const [open, setOpen] = useState(false);
  const [messages] = useState(SAMPLE_MESSAGES);
  const ref = useRef(null);
  const unread = messages.filter(m => !m.read).length;

  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  if (!user) return null;

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button onClick={() => setOpen(v => !v)} style={{
        padding: '6px 8px', position: 'relative', color: 'var(--text2)', borderRadius: 7,
        border: '1px solid transparent', display: 'flex', alignItems: 'center'
      }}
        onMouseEnter={e => e.currentTarget.style.background = 'rgba(74,158,255,0.07)'}
        onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
        aria-label="Сообщения"
      >
        <svg width="20" height="20" 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>
        {unread > 0 && (
          <span className="ds-notif-badge" style={{
            position: 'absolute', top: 2, right: 2, minWidth: 14, height: 14, borderRadius: 7,
            background: 'var(--red)', color: '#fff', fontSize: 9, fontWeight: 700,
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 3px'
          }}>{unread}</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: 10, width: 320, maxHeight: 420, overflowY: 'auto', boxShadow: '0 12px 36px rgba(0,0,0,0.65)'
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px 10px', borderBottom: '1px solid rgba(74,158,255,0.08)', marginBottom: 6 }}>
            <span style={{ fontSize: 13, fontWeight: 700 }}>Сообщения</span>
          </div>
          {messages.length === 0 ? (
            <div style={{ padding: '20px 10px', textAlign: 'center', color: 'var(--text3)', fontSize: 12 }}>Пока пусто</div>
          ) : messages.map(m => (
            <div key={m.id} style={{
              padding: '9px 10px', borderRadius: 7, marginBottom: 2, cursor: 'pointer',
              background: m.read ? 'transparent' : 'rgba(74,158,255,0.06)',
              display: 'flex', gap: 10, alignItems: 'flex-start'
            }}>
              <span style={{ fontSize: 18, flexShrink: 0 }}>{m.emoji}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 2 }}>{m.from}</div>
                <div style={{ fontSize: 12.5, color: 'var(--text)', lineHeight: 1.4 }}>{m.text}</div>
                <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 3 }}>{m.time}</div>
              </div>
              {!m.read && <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--accent)', flexShrink: 0, marginTop: 6 }} />}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── RICH TEXT (paragraphs + markdown: heading, bold, italic, quote, list, hr) ──
// Принимает plain text + markdown. Парсит без dangerouslySetInnerHTML.
function RichText({ text, className, style, gallery }) {
  if (!text) return null;
  const paragraphs = String(text).split(/\n\s*\n/).map(p => p.replace(/^\s+|\s+$/g, '')).filter(Boolean);
  const galleryArr = Array.isArray(gallery) ? gallery : [];

  const renderInline = (s) => {
    // Шаг 1: разрезаем по **bold**
    const parts = [];
    const reBold = /\*\*([^*]+)\*\*/g;
    let lastIdx = 0, m;
    while ((m = reBold.exec(s)) !== null) {
      if (m.index > lastIdx) parts.push({ kind: 'text', val: s.slice(lastIdx, m.index) });
      parts.push({ kind: 'bold', val: m[1] });
      lastIdx = m.index + m[0].length;
    }
    if (lastIdx < s.length) parts.push({ kind: 'text', val: s.slice(lastIdx) });
    // Шаг 2: внутри text-частей *italic*
    const out = [];
    parts.forEach((p, i) => {
      if (p.kind === 'bold') { out.push(<strong key={`b${i}`}>{p.val}</strong>); return; }
      const reItalic = /\*([^*\n]+)\*/g;
      let lastI = 0, mi;
      while ((mi = reItalic.exec(p.val)) !== null) {
        if (mi.index > lastI) out.push(p.val.slice(lastI, mi.index));
        out.push(<em key={`i${i}-${mi.index}`}>{mi[1]}</em>);
        lastI = mi.index + mi[0].length;
      }
      if (lastI < p.val.length) out.push(p.val.slice(lastI));
    });
    return out;
  };

  // Парсит суффикс {color} в конце строки заголовка:
  //   "Заголовок {#1487cb}"  → text: "Заголовок", color: "#1487cb"
  //   "Заголовок {accent}"   → text: "Заголовок", color: "var(--accent)"
  //   "Заголовок"            → text: "Заголовок", color: null
  // Поддерживаемые named-цвета: accent, accent2, gold, red, green, purple
  // (тема-aware через CSS-переменные).
  const NAMED_COLOR = { accent: 'var(--accent)', accent2: 'var(--accent2)', gold: 'var(--gold)', red: 'var(--red)', green: 'var(--green)', purple: 'var(--purple)' };
  const parseHeadingColor = (s) => {
    const m = s.match(/^(.*?)\s*\{(#[0-9a-fA-F]{3,8}|[a-z]+)\}\s*$/);
    if (!m) return { text: s, color: null };
    const raw = m[2];
    const color = raw.startsWith('#') ? raw : (NAMED_COLOR[raw.toLowerCase()] || raw);
    return { text: m[1], color };
  };

  // Извлекает YouTube ID из разных форматов URL.
  const extractYtId = (s) => {
    s = s.trim();
    if (/^[\w-]{11}$/.test(s)) return s;
    let m = s.match(/youtu\.be\/([\w-]{11})/);
    if (m) return m[1];
    m = s.match(/[?&]v=([\w-]{11})/);
    if (m) return m[1];
    m = s.match(/youtube\.com\/(?:embed|shorts|v)\/([\w-]{11})/);
    if (m) return m[1];
    return null;
  };

  // Рендер блока (параграф / heading / quote / list / hr / photo / image / video)
  const renderBlock = (block, idx) => {
    // Photo shortcode: [photo:N] на отдельной строке — картинка из gallery[N-1]
    const photoMatch = block.match(/^\[photo:(\d+)\]$/);
    if (photoMatch) {
      const n = parseInt(photoMatch[1], 10) - 1;
      const src = galleryArr[n];
      if (!src) return null;
      const url = src.startsWith('http') ? src : `./assets/${src}`;
      return (
        <img key={idx} src={url} alt="" loading="lazy"
          style={{ display: 'block', width: '100%', maxWidth: 700, margin: '1.2em auto', borderRadius: 10, border: '1px solid var(--border)' }} />
      );
    }
    // Inline image: [img:URL] — встроенная картинка по URL (R2 или внешний)
    const imgMatch = block.match(/^\[img:([^\]]+)\]$/);
    if (imgMatch) {
      const url = imgMatch[1].trim();
      return (
        <img key={idx} src={url} alt="" loading="lazy"
          style={{ display: 'block', width: '100%', maxWidth: 700, margin: '1.2em auto', borderRadius: 10, border: '1px solid var(--border)' }}
          onError={e => e.currentTarget.style.opacity = 0.2} />
      );
    }
    // YouTube embed: [yt:VIDEOID] или [yt:URL]
    const ytMatch = block.match(/^\[yt:([^\]]+)\]$/);
    if (ytMatch) {
      const ytId = extractYtId(ytMatch[1]);
      if (!ytId) return null;
      return (
        <div key={idx} style={{ position: 'relative', paddingBottom: '56.25%', height: 0, maxWidth: 700, margin: '1.2em auto', borderRadius: 10, overflow: 'hidden', background: '#000' }}>
          <iframe src={`https://www.youtube.com/embed/${ytId}?rel=0`} title="Video" allow="accelerometer; autoplay; encrypted-media; picture-in-picture" allowFullScreen
            style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 0 }} />
        </div>
      );
    }
    // Horizontal rule
    if (/^---+$/.test(block)) {
      return <hr key={idx} style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '1.4em 0' }} />;
    }
    // Heading 3
    if (block.startsWith('### ')) {
      const { text, color } = parseHeadingColor(block.slice(4));
      return <h3 key={idx} style={{ fontSize: '1.05em', fontWeight: 800, margin: '1.2em 0 0.5em', color: color || 'var(--text)' }}>{renderInline(text)}</h3>;
    }
    // Heading 2
    if (block.startsWith('## ')) {
      const { text, color } = parseHeadingColor(block.slice(3));
      return <h2 key={idx} style={{ fontSize: '1.2em', fontWeight: 800, margin: '1.3em 0 0.6em', color: color || 'var(--text)' }}>{renderInline(text)}</h2>;
    }
    // Heading 1 (новое)
    if (block.startsWith('# ')) {
      const { text, color } = parseHeadingColor(block.slice(2));
      return <h1 key={idx} style={{ fontSize: '1.5em', fontWeight: 800, margin: '1.4em 0 0.7em', color: color || 'var(--text)' }}>{renderInline(text)}</h1>;
    }
    const lines = block.split(/\n/).map(l => l.replace(/\s+$/, ''));
    // Blockquote — все строки начинаются с '>'
    if (lines.every(l => /^>\s?/.test(l))) {
      const inner = lines.map(l => l.replace(/^>\s?/, '')).join('\n');
      return (
        <blockquote key={idx} style={{
          margin: '1em 0', padding: '8px 16px',
          borderLeft: '3px solid var(--accent2, #5ed7c6)',
          background: 'rgba(94,215,198,0.06)',
          color: 'var(--text)', fontStyle: 'italic'
        }}>{renderInline(inner)}</blockquote>
      );
    }
    // Unordered list — все строки '- ' или '• '
    if (lines.every(l => /^[-•]\s+/.test(l))) {
      return (
        <ul key={idx} style={{ margin: '0.9em 0', paddingLeft: '1.5em', lineHeight: 1.7 }}>
          {lines.map((l, i) => <li key={i}>{renderInline(l.replace(/^[-•]\s+/, ''))}</li>)}
        </ul>
      );
    }
    // Ordered list — все строки 'N. '
    if (lines.every(l => /^\d+\.\s+/.test(l))) {
      return (
        <ol key={idx} style={{ margin: '0.9em 0', paddingLeft: '1.5em', lineHeight: 1.7 }}>
          {lines.map((l, i) => <li key={i}>{renderInline(l.replace(/^\d+\.\s+/, ''))}</li>)}
        </ol>
      );
    }
    // Обычный параграф
    return (
      <p key={idx} style={{ whiteSpace: 'pre-wrap', margin: '0.85em 0' }}>{renderInline(block)}</p>
    );
  };

  return (
    <div className={className} style={style}>
      {paragraphs.map(renderBlock)}
    </div>
  );
}

// ── RELEASE NOTIFY BUTTON ─────────────────────────────────────────────────────
// Подписка пользователя на уведомление о релизе дорамы (статус Ожидается / Приостановлено)
function ReleaseNotifyButton({ drama, compact = false }) {
  const { user, openSignIn } = window.useAuth();
  const { t } = window.useI18n();
  const st = (drama.status || '').toLowerCase();
  const eligible = ['ожидается', 'upcoming', 'expected', 'planned',
                    'приостановлено', 'on hold', 'paused', 'suspended'].includes(st);
  const storageKey = `ds_release_notify_${user?.email || 'guest'}`;
  const readSubs = () => { try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; } };
  const [subs, setSubs] = useState(readSubs);
  if (!eligible) return null;
  const isSubscribed = !!subs[drama.id];
  const toggle = (e) => {
    if (e) e.stopPropagation();
    if (!user) { openSignIn(); return; }
    const next = { ...subs };
    if (isSubscribed) {
      delete next[drama.id];
    } else {
      next[drama.id] = {
        dramaId: drama.id,
        title: drama.title_ru || drama.title_en,
        status_when_subscribed: drama.status,
        subscribed_at: new Date().toISOString()
      };
    }
    localStorage.setItem(storageKey, JSON.stringify(next));
    setSubs(next);
    window.dispatchEvent(new CustomEvent('ds-release-notify-changed', { detail: { dramaId: drama.id, subscribed: !isSubscribed } }));
  };
  // Иконки в едином стиле проекта (тонкие линии, currentColor)
  const BellIcon = ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
      <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>
  );
  const CheckIcon = ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
      <polyline points="5 12 10 17 19 7" />
    </svg>
  );
  // compact = карточки в каталоге (sky-blue стиль, оригинальный, НЕ ТРОГАТЬ).
  // full   = большая кнопка на странице дорамы (85% белая для контраста на hero).
  if (compact) {
    return (
      <button onClick={toggle} className={'ds-notify-compact' + (isSubscribed ? ' is-sub' : '')} style={{
        display: 'inline-flex', alignItems: 'center', gap: 5,
        padding: '4px 8px', borderRadius: 6,
        fontSize: 10, fontWeight: 700,
        background: isSubscribed ? 'rgba(61,214,140,0.16)' : 'rgba(74,158,255,0.10)',
        color: isSubscribed ? '#3dd68c' : '#7bc3ff',
        border: `1px solid ${isSubscribed ? 'rgba(61,214,140,0.4)' : 'rgba(74,158,255,0.3)'}`,
        cursor: 'pointer', width: '100%', justifyContent: 'center',
        transition: 'all 0.15s'
      }} title={!user ? t('Sign in to get a release alert') : ''}>
        {isSubscribed ? <CheckIcon size={12} /> : <BellIcon size={12} />}
        <span>{isSubscribed ? t('Subscribed') : t('Notify me')}</span>
      </button>
    );
  }
  return (
    <button onClick={toggle} style={{
      display: 'inline-flex', alignItems: 'center', gap: 8,
      padding: '9px 16px', borderRadius: 8,
      fontSize: 13, fontWeight: 500,
      background: isSubscribed ? 'rgba(61,214,140,0.85)' : 'rgba(255,255,255,0.85)',
      color: isSubscribed ? '#ffffff' : '#1e6fd9',
      border: 'none',
      cursor: 'pointer', transition: 'all 0.15s'
    }}
      onMouseEnter={(e) => e.currentTarget.style.filter = 'brightness(0.97)'}
      onMouseLeave={(e) => e.currentTarget.style.filter = 'none'}
      title={!user ? t('Sign in to get a release alert') : ''}>
      {isSubscribed ? <CheckIcon size={16} /> : <BellIcon size={16} />}
      <span>{isSubscribed ? t("We'll notify you about the release") : t('Notify me about the release')}</span>
    </button>
  );
}

// ── SETTINGS GEAR (header icon → opens profile/settings page) ─────────────────
function SettingsGear({ onClick }) {
  const { user } = window.useAuth();
  if (!user) return null;
  return (
    <button onClick={onClick} aria-label="Настройки" style={{
      padding: '6px 8px', color: 'var(--text2)', borderRadius: 7,
      border: '1px solid transparent', display: 'flex', alignItems: 'center', cursor: 'pointer'
    }}
      onMouseEnter={e => e.currentTarget.style.background = 'rgba(74,158,255,0.07)'}
      onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
    >
      <svg width="20" height="20" 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>
    </button>
  );
}

// ── MOOD CHIPS (for AI Picks page) ────────────────────────────────────────────
const MOOD_OPTIONS = [
  { emoji:'😭', label:'Cry'                  },
  { emoji:'❤️', label:'Fall in love'         },
  { emoji:'🔥', label:'Chemistry'            },
  { emoji:'🥺', label:'Comfort'              },
  { emoji:'⚔️', label:'Epic wuxia'           },
  { emoji:'🚩', label:'Toxic romance'        },
  { emoji:'✨', label:'Happy ending'         },
  { emoji:'🌸', label:'Green flag male lead' },
  { emoji:'🎵', label:'Emotional OST'        },
  { emoji:'💪', label:'Strong female lead'   },
];

// Маппинг emoji → имя линейной иконки (для MoodChips)
const MOOD_EMOJI_TO_ICON = {
  '😭': 'heartBroken',
  '❤️': 'heartOutline',
  '🔥': 'flame',
  '🥺': 'heartOutline',
  '⚔️': 'crown',
  '🚩': 'flag',
  '✨': 'sparkles',
  '🌸': 'leaf',
  '🎵': 'starShine',
  '💪': 'handsUp',
};

// Тонко-линейные SVG иконки (используем те же что в DRAMA_AI_ACTIONS_FALLBACK)
const MOOD_LINE_ICONS = {
  heartBroken: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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>,
  heartOutline: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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>,
  flame: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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>,
  crown: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><path d="M3 18l2-11 5 6 2-9 2 9 5-6 2 11z"/><path d="M3 21h18"/></svg>,
  flag: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><path d="M5 21V4"/><path d="M5 4h12l-2 4 2 4H5"/></svg>,
  sparkles: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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>,
  leaf: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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>,
  starShine: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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>,
  handsUp: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><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 MoodChips({ value, onChange }) {
  const { t } = (window.useI18n ? window.useI18n() : { t: (s) => s });
  const options = (window.__dsAiMoods && window.__dsAiMoods.length) ? window.__dsAiMoods : MOOD_OPTIONS;
  // Поддержка множественного выбора: value — это строка с label'ами через запятую
  const selected = (value || '').split(',').map(s => s.trim()).filter(Boolean);
  const isSelected = (label) => selected.includes(label);
  const toggle = (label) => {
    const next = isSelected(label)
      ? selected.filter(s => s !== label)
      : [...selected, label];
    onChange(next.join(', '));
  };
  return (
    <div className="ds-mood-chips" style={{ display:'flex', flexWrap:'wrap', gap:6, marginBottom:14 }}>
      {options.map(m => {
        const active = isSelected(m.label);
        const iconName = MOOD_EMOJI_TO_ICON[m.emoji] || 'sparkles';
        const IconSvg = MOOD_LINE_ICONS[iconName];
        return (
          <button key={m.label} onClick={()=>toggle(m.label)} className="ds-mood-chip" style={{
            padding:'5px 11px', borderRadius:16, fontSize:11.5, fontWeight:500,
            background: active ? 'rgba(192,132,252,0.2)' : 'var(--bg3)',
            color: active ? 'var(--purple)' : 'var(--text2)',
            border:`1px solid ${active?'var(--purple)':'rgba(74,158,255,0.1)'}`,
            display:'flex', alignItems:'center', gap:5, transition:'all 0.15s', lineHeight:1.2
          }}>
            {IconSvg && <span style={{ display:'inline-flex', color: active ? 'var(--purple)' : 'var(--text3)' }}>{IconSvg}</span>}
            <span>{t(m.label)}</span>
          </button>
        );
      })}
    </div>
  );
}

// ── DRAMA AI QUICK BUTTONS ────────────────────────────────────────────────────
// Fallback используется только если taxonomy.json не загрузился
const DRAMA_AI_ACTIONS_FALLBACK = [
  { id:'similar',   label:'Similar Dramas',          icon:'sparkles' },
  { id:'samevibe',  label:'Same Vibe',               icon:'moon' },
  { id:'morelike',  label:'More Like This',          icon:'starShine' },
  { id:'chemistry', label:'Better Chemistry',        icon:'heartOutline' },
  { id:'damage',    label:'Same Emotional Damage',   icon:'heartBroken' },
  { id:'comfort',   label:'More Comfort Dramas',     icon:'heartOutline' },
  { id:'greenflag', label:'More Green Flag Romance', icon:'leaf' },
];

// Тонко-линейные SVG в стиле проекта (синхронизировано с LineIcons в ds-auth.jsx)
const MAO_LINE_ICONS = {
  sparkles: <svg width="14" height="14" 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="14" height="14" 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="14" height="14" 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>,
  heartOutline: <svg width="14" height="14" 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>,
  heartBroken: <svg width="14" height="14" 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>,
  leaf: <svg width="14" height="14" 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>,
  flame: <svg width="14" height="14" 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="14" height="14" 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>,
  crown: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M3 18l2-11 5 6 2-9 2 9 5-6 2 11z"/><path d="M3 21h18"/></svg>,
};

function DramaAIButtons({ onAction, dramaTitle, bare = false }) {
  const { t } = (window.useI18n ? window.useI18n() : { t: (s) => s });
  const { user, openSignIn } = (window.useAuth ? window.useAuth() : { user: null, openSignIn: () => {} });
  const actions = (window.__dsMaoPicks && window.__dsMaoPicks.length) ? window.__dsMaoPicks : DRAMA_AI_ACTIONS_FALLBACK;
  const handleClick = (a, ev) => {
    if (ev && ev.stopPropagation) ev.stopPropagation();
    if (onAction) { onAction(a); return; }
    if (!user) { openSignIn(); return; }
    // Открыть Mao Picks с предзаполненным запросом
    window.__dsAiPreset = {
      mood: dramaTitle ? `${a.label} — like "${dramaTitle}"` : a.label,
      favorite: dramaTitle || ''
    };
    window.__dsGoPage && window.__dsGoPage('ai');
  };
  const tagsRow = (
    <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
      {actions.map(a => (
        <button key={a.id} onClick={(ev) => handleClick(a, ev)} style={{
          padding:'8px 16px', borderRadius:999, fontSize:12, fontWeight:600,
          display:'flex', alignItems:'center', gap:6,
          cursor: 'pointer', opacity: user ? 1 : 0.7
        }}>
          {a.icon && MAO_LINE_ICONS[a.icon] && (
            <span style={{ display:'inline-flex' }}>{MAO_LINE_ICONS[a.icon]}</span>
          )}
          <span>{t(a.label)}</span>
        </button>
      ))}
    </div>
  );
  // bare=true → только теги, без обёрточного карда и без заголовка «Mao Recommendations».
  // Используется при встраивании в другой Mao-блок (например, ds-mao-drama-promo на стр. дорамы).
  if (bare) return tagsRow;
  return (
    <div className="ds-ai-card" style={{ padding:20, marginTop:16, marginBottom:16, position:'relative' }}>
      {window.MaoSoonBadge && <window.MaoSoonBadge top={10} right={10} />}
      <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:12 }}>
        <Icon name="sparkles" size={16} color="#c9b6ff"/>
        <h3 style={{ fontSize:14, fontWeight:700 }}>{t('Mao Recommendations')}</h3>
      </div>
      {tagsRow}
    </div>
  );
}

// ── PREMIUM PAGE ──────────────────────────────────────────────────────────────
function PremiumPage() {
  const tiers = [
    { id:'free', name:'Free', price:'€0', period:'forever', highlight:false, features:[
      ['✓','Basic browsing'],['✓','Ratings & reactions'],['✓','News feed'],['✓','Limited Mao Picks (5/day)'],['✓','Watchlist'],['×','Ads shown'],['×','Premium badge'],['×','Advanced Mao'],
    ]},
    { id:'premium', name:'Premium', price:'€4.99', period:'/month · €39/year', highlight:true, features:[
      ['✓','Everything in Free'],['✓','No ads'],['✓','Premium badge'],['✓','Unlimited Mao recommendations'],['✓','Advanced taste profile'],['✓','Unlimited collections'],['✓','Exclusive rankings'],['✓','Priority support'],
    ]},
    { id:'vip', name:'VIP+', price:'€9.99', period:'/month', highlight:false, features:[
      ['✓','Everything in Premium'],['✓','VIP discussions'],['✓','Deeper AI recommendations'],['✓','Profile customization'],['✓','Early access to features'],['✓','VIP-only badges'],['✓','Beta features access'],['✓','Direct creator contact'],
    ]},
  ];

  return (
    <div className="page-enter" style={{ maxWidth:1200, margin:'0 auto', padding:'90px 24px 48px' }}>
      <div style={{ textAlign:'center', marginBottom:36 }}>
        <Icon name="crown" size={36} color="#f5c518"/>
        <h1 style={{ fontSize:32, fontWeight:800, marginTop:10, marginBottom:8 }}>Upgrade Your C-Drama Experience</h1>
        <p style={{ fontSize:14, color:'var(--text2)', maxWidth:520, margin:'0 auto', lineHeight:1.7 }}>
          Go ad-free, unlock advanced AI recommendations, and join the VIP fandom community.
        </p>
      </div>

      <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit,minmax(260px,1fr))', gap:18, marginBottom:32 }}>
        {tiers.map(t => (
          <div key={t.id} style={{
            background: t.highlight ? 'linear-gradient(135deg, rgba(74,158,255,0.1), rgba(192,132,252,0.08))' : 'var(--bg2)',
            border: t.highlight ? '1.5px solid var(--accent)' : '1px solid var(--border)',
            borderRadius:14, padding:24, position:'relative'
          }}>
            {t.highlight && (
              <div style={{ position:'absolute', top:-10, left:'50%', transform:'translateX(-50%)', background:'var(--accent)', color:'#fff', fontSize:10, fontWeight:800, padding:'4px 12px', borderRadius:12, letterSpacing:'0.5px' }}>MOST POPULAR</div>
            )}
            <h3 style={{ fontSize:18, fontWeight:800, marginBottom:4 }}>{t.name}</h3>
            <div style={{ display:'flex', alignItems:'baseline', gap:6, marginBottom:18 }}>
              <span style={{ fontSize:30, fontWeight:800, color: t.highlight?'var(--accent)':'var(--text)' }}>{t.price}</span>
              <span style={{ fontSize:12, color:'var(--text3)' }}>{t.period}</span>
            </div>
            <button style={{
              width:'100%', padding:'11px', borderRadius:8, marginBottom:18,
              background: t.highlight ? 'var(--accent)' : 'var(--bg3)',
              color: t.highlight ? '#fff' : 'var(--text)', fontWeight:700, fontSize:13,
              border: t.highlight ? 'none' : '1px solid rgba(74,158,255,0.15)'
            }}>
              {t.id==='free' ? 'Current Plan' : t.id==='vip' ? 'Go VIP+' : 'Upgrade to Premium'}
            </button>
            <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
              {t.features.map(([icon, label], i) => (
                <div key={i} style={{ display:'flex', alignItems:'center', gap:8, fontSize:12.5 }}>
                  <span style={{ width:16, color: icon==='✓' ? 'var(--green)' : 'var(--text3)', fontWeight:700 }}>{icon}</span>
                  <span style={{ color: icon==='✓' ? 'var(--text2)' : 'var(--text3)' }}>{label}</span>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>

      <div style={{ textAlign:'center', fontSize:12, color:'var(--text3)' }}>
        Cancel anytime · Secure payment · Premium support
      </div>
    </div>
  );
}

// ── FAVORITE DRAMA MARK ───────────────────────────────────────────────────────
// Красное сердце, появляется ПОСЛЕ названия дорамы везде, где она показывается,
// если она в списке любимых текущего пользователя.
// Зеркало FavoritePersonMark — читает ds_fav_dramas_<email|guest>.
function FavoriteDramaMark({ dramaId, size = 14, style }) {
  const { user } = window.useAuth();
  const key = `ds_fav_dramas_${user?.email || 'guest'}`;
  const check = () => {
    if (!dramaId) return false;
    try { return JSON.parse(localStorage.getItem(key) || '[]').some(x => x.id === dramaId); } catch { return false; }
  };
  const [isFav, setIsFav] = useState(check);
  useEffect(() => {
    const refresh = () => setIsFav(check());
    refresh();
    window.addEventListener('ds-fav-dramas-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-fav-dramas-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [dramaId, user?.email]);
  if (!isFav) return null;
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center',
      verticalAlign: 'middle', marginLeft: 6,
      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: '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 DRAMA ────────────────────────────────────────────────────────────
// Кнопка-сердце для дорамы: сохраняет дораму в список «любимые дорамы» юзера.
// Хранится в localStorage.ds_fav_dramas_<email>, видна на профиле юзера и т. п.
// Это ОТДЕЛЬНЫЙ от LibraryStatusButton быстрый toggle — может сосуществовать
// со статусом «Просмотрено», «Смотрю» и пр.
function FavoriteDramaButton({ item, compact = false }) {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const storageKey = `ds_fav_dramas_${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-dramas-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-fav-dramas-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);
  const isFav = list.some(x => x.id === item.id);
  const toggle = (e) => {
    e.preventDefault(); e.stopPropagation();
    if (!user) { openSignUp(); return; }
    const next = isFav
      ? list.filter(x => x.id !== item.id)
      : [...list, {
          id: item.id,
          title_en: item.title_en,
          title_ru: item.title_ru,
          posterPath: item.posterPath,
          slug: item.slug,
          firstAirDate: item.firstAirDate,
          addedAt: new Date().toISOString()
        }];
    setList(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
    window.dispatchEvent(new CustomEvent('ds-fav-dramas-changed'));
  };
  const isRu = lang === 'ru';
  const tooltip = !user
    ? (isRu ? 'Войдите, чтобы добавить в любимые' : 'Sign in to favorite')
    : isFav
      ? (isRu ? 'Убрать из любимых' : 'Remove from favorites')
      : (isRu ? 'Добавить в любимые' : 'Add to favorites');
  // Резолвим DSHoverTip на момент рендера (а не модуля) — на случай порядка загрузки скриптов
  const TipWrap = ({ children }) => {
    const Tip = window.DSHoverTip;
    return Tip ? <Tip tip={tooltip}>{children}</Tip> : children;
  };
  // ── Compact-вариант: маленький кружок с сердечком на постере.
  //    Размер 22×22, фон и обводка по теме через .ds-fav-heart-btn
  //    (day=белый круг+чёрная обводка, night=тёмный круг+белая обводка, active=красное без обводки). ──
  if (compact) {
    return (
      <TipWrap>
        <button onClick={toggle}
          className="ds-fav-heart-btn"
          data-active={isFav ? '1' : '0'}
          aria-label={tooltip}
          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>
      </TipWrap>
    );
  }
  // ── Обычный (большой) вариант — для action-bar рядом с библиотечной кнопкой ──
  return (
    <TipWrap>
      <button onClick={toggle}
        className="ds-action-icon-btn ds-action-fav"
        data-active={isFav ? '1' : '0'}
        aria-label={tooltip}
        style={{
          width: 40, height: 40, borderRadius: '50%',
          color: isFav ? '#ff3b5e' : '#ffffff',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          cursor: 'pointer', padding: 0,
          transition: 'all 0.15s', flexShrink: 0
        }}>
        <svg width="20" height="20" 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>
    </TipWrap>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// USER LISTS — пользовательские плейлисты дорам с произвольными названиями.
// Данные: localStorage.ds_user_lists_<email|guest> = [{id, name, dramaIds[], createdAt}]
// Используется в Sidebar на главной и на странице профиля.
// ─────────────────────────────────────────────────────────────────────────────

const LISTS_KEY_PREFIX = 'ds_user_lists_';

// Хелперы для работы со списками
function _readUserLists(email) {
  const key = LISTS_KEY_PREFIX + (email || 'guest');
  try { return JSON.parse(localStorage.getItem(key)) || []; } catch { return []; }
}
function _writeUserLists(email, lists) {
  const key = LISTS_KEY_PREFIX + (email || 'guest');
  localStorage.setItem(key, JSON.stringify(lists));
  window.dispatchEvent(new CustomEvent('ds-userlists-changed'));
}

// ── LIBRARY BLOCK ─────────────────────────────────────────────────────────────
// Аккордеон по статусам. По дефолту все секции свёрнуты — видны только заголовки
// со счётчиками. Клик по секции → раскрывается список первых 10 дорам с постерами.
// «Далее» подгружает следующие 10. Слушает 'ds-library-changed', реактивно.
function LibraryBlock() {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const storageKey = `ds_library_${user?.email || 'guest'}`;
  const [library, setLibrary] = useState(() => {
    try { return JSON.parse(localStorage.getItem(storageKey)) || []; } catch { return []; }
  });
  // Какие секции раскрыты
  const [openIds, setOpenIds] = useState({});
  // Поиск по библиотеке
  const [q, setQ] = useState('');
  // Lazy-load dramas.json чтобы достать slug по id (для постеров, если в библиотеке
  // ещё лежат старые записи без slug).
  const [dramasMap, setDramasMap] = useState(window.__dsDramasMapById || null);
  useEffect(() => {
    if (dramasMap) return;
    if (window.__dsDramasMapById) { setDramasMap(window.__dsDramasMapById); return; }
    fetch('./data/dramas.json?t=' + Date.now())
      .then(r => r.json())
      .then(arr => {
        const m = {};
        for (const d of (arr || [])) m[d.id] = d;
        window.__dsDramasMapById = m;
        setDramasMap(m);
      })
      .catch(() => {});
  }, []);
  useEffect(() => {
    const refresh = () => {
      try { setLibrary(JSON.parse(localStorage.getItem(storageKey)) || []); } catch {}
    };
    refresh();
    window.addEventListener('ds-library-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-library-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);

  // Privacy: если юзер включил «Скрыть Брошено» в кабинете → фильтруем секцию.
  // Чтение из window.__dsUserPrivacy (выставляется ProfilePage после GET /api/me/profile).
  const hideDropped = !!(window.__dsUserPrivacy && window.__dsUserPrivacy.hide_dropped_status);
  const STATUSES_RAW = (window.__dsLibStatusesArr && window.__dsLibStatusesArr.length)
    ? window.__dsLibStatusesArr
    : LIBRARY_STATUSES_FALLBACK;
  const STATUSES = hideDropped ? STATUSES_RAW.filter(s => s.id !== 'dropped') : STATUSES_RAW;
  const filtered = library.filter(x => x.status && (!hideDropped || x.status !== 'dropped'));
  // Фильтр по поиску — по любому из названий
  const qLow = q.trim().toLowerCase();
  const matchSearch = (d) => {
    if (!qLow) return true;
    return [d.title_en, d.title_ru, d.title_original, d.title_pinyin]
      .filter(Boolean).some(t => t.toLowerCase().includes(qLow));
  };
  const grouped = STATUSES.filter(s => s.id !== 'favorite').map(s => ({
    s,
    items: filtered.filter(x => x.status === s.id && matchSearch(x))
  }));
  // При активном поиске автоматически раскрываем все секции с совпадениями
  const isSearching = qLow.length > 0;

  const toggleSection = (id) => {
    setOpenIds(o => ({ ...o, [id]: !o[id] }));
  };
  const open = (drama) => { if (window.__dsSelectItem) window.__dsSelectItem(drama.id); };
  // Удаление из библиотеки прямо из drawer'а (через крестик у строки).
  // Не открывает дораму (см. stopPropagation на onClick кнопки). Защитим
  // от случайного клика — простой confirm() с названием.
  const removeFromLibrary = (drama) => {
    const title = (isRu && drama.title_ru) ? drama.title_ru : (drama.title_en || drama.title_ru || drama.title_original || drama.slug || drama.id);
    const msg = isRu
      ? `Удалить «${title}» из библиотеки?`
      : `Remove "${title}" from your library?`;
    if (!window.confirm(msg)) return;
    // Адресный DELETE на сервер — обходит refusePushIfEmpty в ds-sync,
    // который иначе ABORT'ит push когда после удаления localCount=0 и
    // восстанавливает запись с сервера (баг "последняя дорама не удаляется").
    const did = drama.drama_id != null ? drama.drama_id : drama.id;
    if (window.__dsApi?.fetch && did != null) {
      window.__dsApi.fetch(`/api/library/${did}`, { method: 'DELETE' })
        .catch(() => { /* network fail — локально всё равно убираем, sync дотолкает при следующем заходе */ });
    }
    const next = library.filter(x => {
      const xid = x.drama_id != null ? x.drama_id : x.id;
      return String(xid) !== String(did);
    });
    setLibrary(next);
    try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch {}
    // Без detail.reason — ds-sync дёрнет pushLibrary, который из-за refusePushIfEmpty
    // не сможет навредить (мы уже сделали явный DELETE выше).
    window.dispatchEvent(new CustomEvent('ds-library-changed'));
  };

  // Постер — берём posterPath через adapter.imageUrl (поддерживает и R2 URL,
  // и legacy slug-based local пути). Источник: сама запись, потом map по id,
  // потом fallback slug+.webp. Раньше тут был только slug-fallback и для дорам
  // с R2-постерами библиотека не показывала картинку (хотя detail-страница да).
  const posterUrl = (drama) => {
    const fromMap = dramasMap ? (dramasMap[Number(drama.id)] || dramasMap[String(drama.id)]) : null;
    const fromCache = (window.__dsLibTitleCache && (window.__dsLibTitleCache[Number(drama.id)] || window.__dsLibTitleCache[String(drama.id)])) || null;
    const merged = { ...(fromMap || {}), ...(fromCache || {}), ...drama };
    // 1) Если есть posterPath (R2 URL или относительный) — отдаём через adapter.imageUrl
    if (merged.posterPath && window.adapter?.imageUrl) {
      const url = window.adapter.imageUrl(merged.posterPath, 'w185');
      if (url) return url;
    }
    // 2) Fallback на slug+.webp в локальной папке (legacy путь)
    if (merged.slug) return `./assets/posters/w185/${merged.slug}.webp`;
    return null;
  };

  return (
    <div className="ds-home-side-card" style={{
      background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 18,
      boxShadow: '0 4px 14px rgba(20,135,203,0.12)'
    }}>
      <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
        <span>{isRu ? 'Моя библиотека' : 'My library'}</span>
        <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{filtered.length}</span>
      </h3>
      {user && filtered.length > 0 && (
        <div style={{ position: 'relative', marginBottom: 10 }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
            strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"
            style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
              color: 'var(--text3)', pointerEvents: 'none' }}>
            <circle cx="11" cy="11" r="7"/>
            <line x1="20" y1="20" x2="16.65" y2="16.65"/>
          </svg>
          <input value={q} onChange={(e) => setQ(e.target.value)}
            placeholder={isRu ? 'Поиск по библиотеке…' : 'Search library…'}
            style={{
              width: '100%', padding: '7px 28px 7px 30px', fontSize: 12,
              background: 'var(--bg3)',
              border: '1px solid rgba(20,135,203,0.22)',
              borderRadius: 999, color: 'var(--text)', outline: 'none',
              boxSizing: 'border-box'
            }}
            onFocus={(e) => e.currentTarget.style.borderColor = 'rgba(20,135,203,0.55)'}
            onBlur={(e) => e.currentTarget.style.borderColor = 'rgba(20,135,203,0.22)'} />
          {q && (
            <button onClick={() => setQ('')}
              aria-label="Clear"
              style={{
                position: 'absolute', right: 6, top: '50%', transform: 'translateY(-50%)',
                width: 20, height: 20, border: 'none', background: 'transparent',
                color: 'var(--text3)', cursor: 'pointer', fontSize: 16, lineHeight: 1, padding: 0
              }}>×</button>
          )}
        </div>
      )}
      {!user && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu ? 'Войди, чтобы вести библиотеку.' : 'Sign in to save dramas to your library.'}
          <button onClick={openSignUp} style={{ marginTop: 6, padding: '5px 10px', fontSize: 11, fontWeight: 700,
            background: '#1487cb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', display: 'block' }}>
            {isRu ? 'Войти' : 'Sign in'}
          </button>
        </div>
      )}
      {user && filtered.length === 0 && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu
            ? 'Библиотека пуста. Нажми «+» на постере дорамы, чтобы добавить.'
            : 'Library is empty. Tap "+" on a drama poster to add.'}
        </div>
      )}
      {/* При поиске показываем сообщение "ничего не найдено", если все секции пустые */}
      {user && isSearching && grouped.every(g => g.items.length === 0) && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '10px 4px', lineHeight: 1.5 }}>
          {isRu ? `Ничего не найдено по запросу «${q}».` : `No matches for "${q}".`}
        </div>
      )}
      {user && grouped.map(({ s, items }) => {
        // При поиске пустые секции скрываем
        if (isSearching && items.length === 0) return null;
        // При активном поиске автоматически раскрываем секцию с совпадениями
        const isOpen = isSearching ? true : !!openIds[s.id];
        return (
          <div key={s.id} style={{ borderTop: '1px solid rgba(140,180,235,0.08)' }}>
            <button onClick={() => toggleSection(s.id)}
              style={{
                width: '100%', padding: '10px 4px', background: 'transparent', border: 'none', cursor: 'pointer',
                display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left'
              }}>
              <span style={{ display: 'inline-flex', color: s.color, transform: 'scale(0.95)', flexShrink: 0 }}>
                {LIB_ICONS[s.id]}
              </span>
              <span style={{ flex: 1, fontSize: 12.5, fontWeight: 400, color: 'var(--text)' }}>
                {isRu ? (RU_STATUS_LABEL[s.id] || s.label) : s.label}
              </span>
              <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{items.length}</span>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
                style={{ color: 'var(--text3)', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
                <polyline points="6 9 12 15 18 9"/>
              </svg>
            </button>
            {isOpen && items.length > 0 && (
              <div style={{
                display: 'flex', flexDirection: 'column', gap: 6, paddingBottom: 10,
                maxHeight: 480, overflowY: 'auto',
                /* Не передаём скролл наружу — на drawer и страницу */
                overscrollBehavior: 'contain',
                WebkitOverflowScrolling: 'touch',
                /* подсветка скроллбара sky-blue */
                scrollbarColor: 'rgba(20,135,203,0.45) transparent',
                scrollbarWidth: 'thin'
              }}>
                {items.map(d => {
                  // Hydrate title/year/slug из dramasMap. Если ни map, ни cache — lazy fetch
                  // через adapter.getDrama. Если и тот вернул null (дорама не в D1) —
                  // помечаем как orphan и показываем "Не найдено в каталоге".
                  // Type coercion для id — в библиотеке может быть string, в map — number.
                  const idNum = Number(d.id);
                  const idKey = String(d.id);
                  const fromMap = (dramasMap && (dramasMap[idNum] || dramasMap[idKey])) || null;
                  const fromCache = (window.__dsLibTitleCache && (window.__dsLibTitleCache[idNum] || window.__dsLibTitleCache[idKey])) || null;
                  const isOrphan = fromCache && fromCache._orphan;
                  const full = { ...(fromMap || {}), ...(fromCache || {}), ...d };
                  const hasRealTitle = !!(full.title_ru || full.title_en || full.title_original);
                  const title = isOrphan
                    ? (isRu ? 'Не найдено в каталоге' : 'Not in catalog')
                    : (hasRealTitle
                        ? ((isRu && full.title_ru) ? full.title_ru : (full.title_en || full.title_ru || full.title_original))
                        : (full.slug || `#${d.id}`));
                  const year = (full.firstAirDate || '').slice(0, 4);
                  const poster = posterUrl(full);
                  // Async lazy-hydrate через adapter.fetchDrama (правильный метод — getDrama
                  // его alias). Передаём числовой id чтобы Map.get в adapter не промахнулся.
                  if (!fromMap && !fromCache && window.adapter?.fetchDrama) {
                    window.__dsLibTitleCache = window.__dsLibTitleCache || {};
                    if (!window.__dsLibTitleInflight) window.__dsLibTitleInflight = {};
                    if (!window.__dsLibTitleInflight[idKey]) {
                      window.__dsLibTitleInflight[idKey] = true;
                      window.adapter.fetchDrama(idNum).then(drama => {
                        if (drama && (drama.title_ru || drama.title_en || drama.title_original)) {
                          window.__dsLibTitleCache[idKey] = {
                            title_en: drama.title_en, title_ru: drama.title_ru,
                            title_original: drama.title_original, slug: drama.slug,
                            firstAirDate: drama.firstAirDate, posterPath: drama.posterPath,
                          };
                        } else {
                          // Дорама не в D1 — помечаем как orphan.
                          window.__dsLibTitleCache[idKey] = { _orphan: true };
                        }
                        window.dispatchEvent(new CustomEvent('ds-library-changed'));
                      }).catch(() => {
                        window.__dsLibTitleCache[idKey] = { _orphan: true };
                        window.dispatchEvent(new CustomEvent('ds-library-changed'));
                      });
                    }
                  }
                  return (
                    <div key={d.id} onClick={() => isOrphan ? null : open(d)} style={{
                      display: 'flex', gap: 8, alignItems: 'center', cursor: isOrphan ? 'default' : 'pointer',
                      padding: '5px 6px', borderRadius: 6, transition: 'background 0.12s',
                      opacity: isOrphan ? 0.55 : 1
                    }}
                    onMouseEnter={(e) => { if (!isOrphan) e.currentTarget.style.background = 'rgba(20,135,203,0.08)'; }}
                    onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                      <div style={{ width: 36, height: 50, borderRadius: 4, overflow: 'hidden',
                        background: 'rgba(140,180,235,0.10)', flexShrink: 0,
                        display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                        {poster
                          ? <img src={poster} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                              onError={(e) => e.currentTarget.style.opacity = 0.2} />
                          : isOrphan
                            ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca8b8" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
                            : null}
                      </div>
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 12, fontWeight: isOrphan ? 500 : 600, fontStyle: isOrphan ? 'italic' : 'normal', lineHeight: 1.3,
                          color: isOrphan ? 'var(--text3)' : 'var(--text)',
                          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
                        <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 1 }}>
                          {isOrphan ? `id ${d.id}` : year}
                        </div>
                      </div>
                      {/* Крестик «удалить из библиотеки» — стоп клик не открывает дораму */}
                      <button
                        onClick={(e) => { e.stopPropagation(); removeFromLibrary(d); }}
                        title={isRu ? 'Удалить из библиотеки' : 'Remove from library'}
                        aria-label={isRu ? 'Удалить из библиотеки' : 'Remove from library'}
                        style={{
                          flexShrink: 0,
                          width: 22, height: 22, borderRadius: '50%',
                          background: 'transparent', border: 'none',
                          color: 'var(--text3)', cursor: 'pointer',
                          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                          padding: 0, opacity: 0.55, transition: 'opacity 0.15s, background 0.15s, color 0.15s',
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; e.currentTarget.style.background = 'rgba(224,88,88,0.18)'; e.currentTarget.style.color = '#e05858'; }}
                        onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.55; e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text3)'; }}>
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                          <line x1="18" y1="6" x2="6" y2="18"/>
                          <line x1="6" y1="6" x2="18" y2="18"/>
                        </svg>
                      </button>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

// Русские лейблы для статусов (fallback'у LIBRARY_STATUSES_FALLBACK имена английские)
const RU_STATUS_LABEL = {
  watching: 'Смотрю',
  plan: 'В планах',
  completed: 'Просмотрено',
  rewatch: 'Пересматриваю',
  dropped: 'Брошено',
  favorite: 'Любимое'
};

// ── FAVORITES BLOCK ───────────────────────────────────────────────────────────
// Аккордеон в drawer'е библиотеки: «Любимые дорамы» (с сердечком на постере)
// + «Любимые актёры» (с сердечком на фото).
// Источники:
//   ds_fav_dramas_<email>  — событие ds-fav-dramas-changed
//   ds_fav_people_<email>  — событие ds-fav-people-changed
function FavoritesBlock() {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const dramasKey = `ds_fav_dramas_${user?.email || 'guest'}`;
  const peopleKey = `ds_fav_people_${user?.email || 'guest'}`;
  const factsKey  = `ds_fav_facts_${user?.email || 'guest'}`;
  const ostsKey   = `ds_fav_drama_osts_${user?.email || 'guest'}`;
  const [favDramas, setFavDramas] = useState(() => { try { return JSON.parse(localStorage.getItem(dramasKey)) || []; } catch { return []; } });
  const [favPeople, setFavPeople] = useState(() => { try { return JSON.parse(localStorage.getItem(peopleKey)) || []; } catch { return []; } });
  const [favFactIds, setFavFactIds] = useState(() => { try { return JSON.parse(localStorage.getItem(factsKey)) || []; } catch { return []; } });
  const [favFactsData, setFavFactsData] = useState([]);  // полные карточки с сервера
  const [favOstIds, setFavOstIds] = useState(() => { try { return JSON.parse(localStorage.getItem(ostsKey)) || []; } catch { return []; } });
  // Какие секции раскрыты
  const [openIds, setOpenIds] = useState({});
  // Реактивная подписка — клик на heart где угодно обновит этот блок мгновенно.
  useEffect(() => {
    const refreshDramas = () => { try { setFavDramas(JSON.parse(localStorage.getItem(dramasKey)) || []); } catch { setFavDramas([]); } };
    const refreshPeople = () => { try { setFavPeople(JSON.parse(localStorage.getItem(peopleKey)) || []); } catch { setFavPeople([]); } };
    const refreshFacts  = () => { try { setFavFactIds(JSON.parse(localStorage.getItem(factsKey)) || []); } catch { setFavFactIds([]); } };
    const refreshOsts   = () => { try { setFavOstIds(JSON.parse(localStorage.getItem(ostsKey)) || []); } catch { setFavOstIds([]); } };
    const refreshAll = () => { refreshDramas(); refreshPeople(); refreshFacts(); refreshOsts(); };
    refreshAll();
    window.addEventListener('ds-fav-dramas-changed', refreshDramas);
    window.addEventListener('ds-fav-people-changed', refreshPeople);
    window.addEventListener('ds-fav-facts-changed', refreshFacts);
    window.addEventListener('ds-fav-drama-osts-changed', refreshOsts);
    window.addEventListener('storage', refreshAll);
    return () => {
      window.removeEventListener('ds-fav-dramas-changed', refreshDramas);
      window.removeEventListener('ds-fav-people-changed', refreshPeople);
      window.removeEventListener('ds-fav-facts-changed', refreshFacts);
      window.removeEventListener('ds-fav-drama-osts-changed', refreshOsts);
      window.removeEventListener('storage', refreshAll);
    };
  }, [dramasKey, peopleKey, factsKey, ostsKey]);

  // Lazy-загрузка карточек фактов по сохранённым ids (когда раздел раскрывают)
  useEffect(() => {
    if (!favFactIds.length) { setFavFactsData([]); return; }
    const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
    if (!apiBase) return;
    fetch(`${apiBase}/api/facts/by-ids?ids=${favFactIds.slice(0, 100).join(',')}`)
      .then(r => r.ok ? r.json() : null)
      .then(j => { if (j?.items) setFavFactsData(j.items); })
      .catch(() => {});
  }, [favFactIds.join(',')]);

  const toggleSection = (id) => setOpenIds(o => ({ ...o, [id]: !o[id] }));
  const openDrama = (d) => { if (window.__dsSelectItem) window.__dsSelectItem(d.id); };
  const openActor = (p) => { if (window.__dsSelectActor) window.__dsSelectActor(p.id); };

  // Фильтр-санитар: в localStorage иногда оседают "пустые" записи (только id,
  // без name/photoUrl) — старые favorites c одного раза, когда heart жали до
  // полной загрузки данных. Не показываем их — рисуют пустые кружки.
  const _isUsefulFav = (x) => x && x.id != null && (x.name || x.name_ru || x.title || x.title_ru || x.title_en || x.photoUrl || x.posterPath);
  const favDramasView = (favDramas || []).filter(_isUsefulFav);
  const favPeopleView = (favPeople || []).filter(_isUsefulFav);
  const favFactsView  = (favFactsData || []).filter(f => f && f.id);
  // OST-фавы — список id дорам. Карточки берём из adapter cache (там есть постер+название).
  const favOstView = (favOstIds || []).map(id => {
    const map = window.adapter?._dramasById;
    if (!map) return { id, _stub: true };
    const d = map.get(Number(id)) || map.get(String(id));
    return d || { id, _stub: true };
  }).filter(x => x);
  const totalCount = favDramasView.length + favPeopleView.length + favFactIds.length + favOstIds.length;

  // Иконка сердца — общая для обеих секций.
  const heartIcon = (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="1" strokeLinejoin="round" style={{ display: 'block' }}>
      <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>
  );

  const heartColor = '#ff3b5e';

  // Постер дорамы. Берём актуальный posterPath из кэша адаптера (там R2-URL для
  // дорам без локального постера), затем сохранённый favourite, и резолвим через
  // imageUrl (он пропускает полные http-URL как есть). Локальный slug — fallback.
  const dramaPosterUrl = (d) => {
    const map = window.adapter && window.adapter._dramasById;
    const fresh = map ? (map.get(Number(d.id)) || map.get(String(d.id))) : null;
    const pp = (fresh && fresh.posterPath) || d.posterPath;
    if (pp && window.adapter && window.adapter.imageUrl) {
      const u = window.adapter.imageUrl(pp, 'w185');
      if (u) return u;
    }
    return d.slug ? `./assets/posters/w185/${d.slug}.webp` : null;
  };

  // Фото актёра. Берём актуальный profilePath из кэша адаптера (там свежее фото,
  // в т.ч. недавно добавленное в R2), резолвим через profileImageUrl. Сохранённый
  // в избранном photoUrl — fallback (бывает устаревшим/пустым).
  const personPhotoUrl = (p) => {
    const map = window.adapter && window.adapter._peopleById;
    const fresh = map ? (map.get(Number(p.id)) || map.get(String(p.id))) : null;
    const pp = fresh && (fresh.profilePath || fresh.profile_path);
    if (pp && window.adapter && window.adapter.profileImageUrl) {
      const u = window.adapter.profileImageUrl(pp);
      if (u) return u;
    }
    return p.photoUrl || null;
  };

  return (
    <div className="ds-home-side-card" style={{
      background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 18,
      boxShadow: '0 4px 14px rgba(20,135,203,0.12)'
    }}>
      <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
        <span style={{ display: 'inline-flex', color: heartColor }}>{heartIcon}</span>
        <span>{isRu ? 'Любимое' : 'Favorites'}</span>
        <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{totalCount}</span>
      </h3>
      {!user && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu ? 'Войди, чтобы сохранять любимые.' : 'Sign in to save favorites.'}
          <button onClick={openSignUp} style={{ marginTop: 6, padding: '5px 10px', fontSize: 11, fontWeight: 700,
            background: '#1487cb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', display: 'block' }}>
            {isRu ? 'Войти' : 'Sign in'}
          </button>
        </div>
      )}

      {/* ── Секция: Любимые дорамы ── */}
      {user && (
        <div style={{ borderTop: '1px solid rgba(140,180,235,0.08)' }}>
          <button onClick={() => toggleSection('dramas')}
            style={{
              width: '100%', padding: '10px 4px', background: 'transparent', border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left'
            }}>
            <span style={{ flex: 1, fontSize: 12.5, fontWeight: 400, color: 'var(--text)' }}>
              {isRu ? 'Любимые дорамы' : 'Favorite dramas'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{favDramasView.length}</span>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
              style={{ color: 'var(--text3)', transform: openIds.dramas ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {openIds.dramas && (
            favDramasView.length > 0 ? (
              <div style={{
                display: 'flex', flexDirection: 'column', gap: 6, paddingBottom: 10,
                maxHeight: 480, overflowY: 'auto',
                overscrollBehavior: 'contain', WebkitOverflowScrolling: 'touch',
                scrollbarColor: 'rgba(20,135,203,0.45) transparent', scrollbarWidth: 'thin'
              }}>
                {favDramasView.map(d => {
                  const title = (isRu && d.title_ru) ? d.title_ru : (d.title_en || d.title_ru || '');
                  const year = (d.firstAirDate || '').slice(0, 4);
                  const poster = dramaPosterUrl(d);
                  return (
                    <div key={d.id} onClick={() => openDrama(d)} style={{
                      display: 'flex', gap: 8, alignItems: 'center', cursor: 'pointer',
                      padding: '5px 6px', borderRadius: 6, transition: 'background 0.12s'
                    }}
                    onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(20,135,203,0.08)'}
                    onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                      <div style={{ width: 36, height: 50, borderRadius: 4, overflow: 'hidden',
                        background: 'rgba(140,180,235,0.10)', flexShrink: 0 }}>
                        {poster && <img src={poster} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                          onError={(e) => e.currentTarget.style.opacity = 0.2} />}
                      </div>
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 12, fontWeight: 600, lineHeight: 1.3,
                          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
                        <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 1 }}>{year}</div>
                      </div>
                    </div>
                  );
                })}
              </div>
            ) : (
              <div style={{ fontSize: 11, color: 'var(--text3)', padding: '4px 6px 10px', lineHeight: 1.5 }}>
                {isRu ? 'Пока нет. Жми сердечко на постере дорамы.' : 'No favorites yet. Tap the heart on a drama poster.'}
              </div>
            )
          )}
        </div>
      )}

      {/* ── Секция: Любимые актёры ── */}
      {user && (
        <div style={{ borderTop: '1px solid rgba(140,180,235,0.08)' }}>
          <button onClick={() => toggleSection('people')}
            style={{
              width: '100%', padding: '10px 4px', background: 'transparent', border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left'
            }}>
            <span style={{ flex: 1, fontSize: 12.5, fontWeight: 400, color: 'var(--text)' }}>
              {isRu ? 'Любимые актёры' : 'Favorite actors'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{favPeopleView.length}</span>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
              style={{ color: 'var(--text3)', transform: openIds.people ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {openIds.people && (
            favPeopleView.length > 0 ? (
              <div style={{
                display: 'flex', flexDirection: 'column', gap: 6, paddingBottom: 10,
                maxHeight: 480, overflowY: 'auto',
                overscrollBehavior: 'contain', WebkitOverflowScrolling: 'touch',
                scrollbarColor: 'rgba(20,135,203,0.45) transparent', scrollbarWidth: 'thin'
              }}>
                {favPeopleView.map(p => (
                  <div key={p.id} onClick={() => openActor(p)} style={{
                    display: 'flex', gap: 8, alignItems: 'center', cursor: 'pointer',
                    padding: '5px 6px', borderRadius: 6, transition: 'background 0.12s'
                  }}
                  onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(20,135,203,0.08)'}
                  onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                    <div style={{ width: 36, height: 50, borderRadius: '50%', overflow: 'hidden',
                      background: 'rgba(140,180,235,0.10)', flexShrink: 0,
                      width: 40, height: 40 /* круглый аватар вместо постера */
                    }}>
                      {(() => { const ph = personPhotoUrl(p); return ph ? <img src={ph} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                        onError={(e) => e.currentTarget.style.opacity = 0.2} /> : null; })()}
                    </div>
                    <div style={{ minWidth: 0, flex: 1 }}>
                      <div style={{ fontSize: 12, fontWeight: 600, lineHeight: 1.3,
                        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</div>
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <div style={{ fontSize: 11, color: 'var(--text3)', padding: '4px 6px 10px', lineHeight: 1.5 }}>
                {isRu ? 'Пока нет. Жми сердечко на фото актёра.' : 'No favorites yet. Tap the heart on an actor photo.'}
              </div>
            )
          )}
        </div>
      )}

      {/* ── Секция: Избранные факты ── */}
      {user && (
        <div style={{ borderTop: '1px solid rgba(140,180,235,0.08)' }}>
          <button onClick={() => toggleSection('facts')}
            style={{
              width: '100%', padding: '10px 4px', background: 'transparent', border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left'
            }}>
            <span style={{ flex: 1, fontSize: 12.5, fontWeight: 400, color: 'var(--text)' }}>
              {isRu ? 'Избранные факты' : 'Saved facts'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{favFactIds.length}</span>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
              style={{ color: 'var(--text3)', transform: openIds.facts ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {openIds.facts && (
            favFactsView.length > 0 ? (
              <div style={{
                display: 'flex', flexDirection: 'column', gap: 8, paddingBottom: 10,
                maxHeight: 480, overflowY: 'auto',
                overscrollBehavior: 'contain', WebkitOverflowScrolling: 'touch',
                scrollbarColor: 'rgba(10,186,181,0.45) transparent', scrollbarWidth: 'thin'
              }}>
                {favFactsView.map(f => {
                  const text = isRu ? (f.text_ru || f.text_en) : (f.text_en || f.text_ru);
                  const goToArchive = () => { window.location.hash = '#/facts'; };
                  return (
                    <div key={f.id} onClick={goToArchive} style={{
                      display: 'flex', gap: 8, alignItems: 'flex-start', cursor: 'pointer',
                      padding: '6px', borderRadius: 8, transition: 'background 0.12s',
                    }}
                    onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(10,186,181,0.06)'}
                    onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                      {f.image_url ? (
                        <div style={{ width: 36, height: 36, borderRadius: 6, overflow: 'hidden',
                          background: 'rgba(140,180,235,0.10)', flexShrink: 0 }}>
                          <img src={f.image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'top' }} />
                        </div>
                      ) : (
                        <div style={{ width: 36, height: 36, borderRadius: 6, flexShrink: 0,
                          background: 'rgba(10,186,181,0.10)', color: '#0abab5',
                          display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                            <circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/>
                          </svg>
                        </div>
                      )}
                      <div style={{ minWidth: 0, flex: 1, fontSize: 11.5, color: 'var(--text)', lineHeight: 1.35,
                        display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
                        {text}
                      </div>
                    </div>
                  );
                })}
              </div>
            ) : (
              <div style={{ fontSize: 11, color: 'var(--text3)', padding: '4px 6px 10px', lineHeight: 1.5 }}>
                {isRu ? 'Пока пусто. Нажми закладку на карточке факта.' : 'Empty. Tap the bookmark on a fact card.'}
              </div>
            )
          )}
        </div>
      )}

      {/* ── Секция: Любимые саундтреки ── */}
      {user && (
        <div style={{ borderTop: '1px solid rgba(140,180,235,0.08)' }}>
          <button onClick={() => toggleSection('osts')}
            style={{
              width: '100%', padding: '10px 4px', background: 'transparent', border: 'none', cursor: 'pointer',
              display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left'
            }}>
            <span style={{ flex: 1, fontSize: 12.5, fontWeight: 400, color: 'var(--text)' }}>
              {isRu ? 'Любимые саундтреки' : 'Saved soundtracks'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{favOstIds.length}</span>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
              style={{ color: 'var(--text3)', transform: openIds.osts ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {openIds.osts && (
            favOstIds.length > 0 ? (
              <div style={{
                display: 'flex', flexDirection: 'column', gap: 6, paddingBottom: 10,
                maxHeight: 480, overflowY: 'auto',
                overscrollBehavior: 'contain', WebkitOverflowScrolling: 'touch',
                scrollbarColor: 'rgba(10,186,181,0.45) transparent', scrollbarWidth: 'thin'
              }}>
                {favOstView.map(d => {
                  const title = isRu ? (d.title_ru || d.title_en) : (d.title_en || d.title_ru);
                  const posterUrl = d.slug ? `./assets/posters/w185/${d.slug}.webp` : null;
                  const openOstTab = () => {
                    // ?tab=soundtrack — DSDetailPage откроет таб «Саундтрек» сразу
                    window.location.hash = `#/drama/${d.id}?tab=soundtrack`;
                  };
                  return (
                    <div key={d.id} onClick={openOstTab} style={{
                      display: 'flex', gap: 8, alignItems: 'center', cursor: 'pointer',
                      padding: '5px 6px', borderRadius: 6, transition: 'background 0.12s'
                    }}
                    onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(10,186,181,0.08)'}
                    onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                      <div style={{ width: 32, height: 48, borderRadius: 4, overflow: 'hidden',
                        background: 'rgba(140,180,235,0.10)', flexShrink: 0 }}>
                        {posterUrl ? <img src={posterUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                          onError={(e) => e.currentTarget.style.opacity = 0.2} />
                          : <div style={{ width: '100%', height: '100%', background: 'rgba(10,186,181,0.10)', color: '#0abab5',
                              display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                                <path d="M9 17V5l12-2v12"/><circle cx="6" cy="17" r="3"/><circle cx="18" cy="15" r="3"/>
                              </svg>
                            </div>}
                      </div>
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 12, fontWeight: 600, lineHeight: 1.3,
                          whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                          {title || `#${d.id}`}
                        </div>
                        <div style={{ fontSize: 10, color: '#0abab5', marginTop: 2 }}>
                          {isRu ? 'Саундтрек' : 'Soundtrack'} →
                        </div>
                      </div>
                    </div>
                  );
                })}
              </div>
            ) : (
              <div style={{ fontSize: 11, color: 'var(--text3)', padding: '4px 6px 10px', lineHeight: 1.5 }}>
                {isRu ? 'Пока пусто. Жми закладку рядом с заголовком «Саундтрек».' : 'Empty. Tap the bookmark next to soundtrack title.'}
              </div>
            )
          )}
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// SUBSCRIPTIONS BLOCK — «Мои подписки» в slide-out drawer.
// Показывает актёров, на которых юзер подписан (FollowButton), а под каждым —
// новые новости и новые дорамы. Красный бейдж сверху = общее число непрочитанных.
//
// Источники:
//   - ds_follows_<email>        — список подписок [{id, name, name_ru, photoUrl, subscribedAt}]
//   - data/news.json            — глобальный список новостей; ищем актёра в text/title/excerpt/actorIds
//   - window.adapter.getAllDramas() — дорамы; ищем актёра в cast[].personId
//   - ds_subs_seen_<email>      — карта "прочитанного": { [actorId]: { news: {id:true}, dramas: {id:true} } }
//
// Что считается "новым":
//   - Новость:  news.date >= subscribedAt И ещё не помечена прочитанной
//   - Дорама:   drama._syncedAt >= subscribedAt (или firstAirDate >= subscribedAt-год) И не прочитана
// ─────────────────────────────────────────────────────────────────────────────

const SUBS_SEEN_KEY = (email) => `ds_subs_seen_${email || 'guest'}`;

function _dsReadSubsSeen(email) {
  try { return JSON.parse(localStorage.getItem(SUBS_SEEN_KEY(email))) || {}; } catch { return {}; }
}
function _dsWriteSubsSeen(email, obj) {
  try { localStorage.setItem(SUBS_SEEN_KEY(email), JSON.stringify(obj)); } catch {}
}
// Помечаем элемент прочитанным (kind = 'news' | 'dramas')
function _dsMarkSubsSeen(email, actorId, kind, itemId) {
  if (!actorId || !itemId) return;
  const seen = _dsReadSubsSeen(email);
  if (!seen[actorId]) seen[actorId] = { news: {}, dramas: {} };
  if (!seen[actorId][kind]) seen[actorId][kind] = {};
  seen[actorId][kind][itemId] = true;
  _dsWriteSubsSeen(email, seen);
  window.dispatchEvent(new CustomEvent('ds-subs-seen-changed'));
}
// Помечаем все элементы актёра прочитанными (когда юзер раскрывает секцию)
function _dsMarkActorAllSeen(email, actorId, newsIds, dramaIds) {
  if (!actorId) return;
  const seen = _dsReadSubsSeen(email);
  if (!seen[actorId]) seen[actorId] = { news: {}, dramas: {} };
  for (const id of newsIds || []) seen[actorId].news[id] = true;
  for (const id of dramaIds || []) seen[actorId].dramas[id] = true;
  _dsWriteSubsSeen(email, seen);
  window.dispatchEvent(new CustomEvent('ds-subs-seen-changed'));
}

// Текстовый поиск имени актёра в новости (fallback когда нет actorIds).
function _dsNewsMentionsActor(news, actor) {
  if (!news || !actor) return false;
  // Явная привязка из админки (related_actor_ids) — высший приоритет.
  if (Array.isArray(news.related_actor_ids) && news.related_actor_ids.length) {
    return news.related_actor_ids.map(Number).includes(Number(actor.id));
  }
  // Если у новости есть явный список actorIds (legacy) — используем его.
  if (Array.isArray(news.actorIds) && news.actorIds.length) {
    return news.actorIds.includes(actor.id);
  }
  // Иначе ищем имя в title + excerpt + content (раньше content игнорили —
  // тогда упускали много валидных матчей; ложные срабатывания редки если
  // имя >= 3 символов и не слишком общее).
  const hay = ((news.title || '') + ' ' + (news.excerpt || '') + ' ' + (news.content || '')).toLowerCase();
  const names = [actor.name, actor.name_ru, actor.name_en, actor.originalName]
    .filter(Boolean).map(s => String(s).toLowerCase());
  return names.some(n => n.length >= 3 && hay.includes(n));
}

function SubscriptionsBlock() {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const followsKey = `ds_follows_${user?.email || 'guest'}`;

  const [follows, setFollows] = useState(() => { try { return JSON.parse(localStorage.getItem(followsKey)) || []; } catch { return []; } });
  const [seen, setSeen] = useState(() => _dsReadSubsSeen(user?.email));
  const [news, setNews] = useState(() => (Array.isArray(window.__dsNews) ? window.__dsNews : []));
  const [allDramas, setAllDramas] = useState(() => {
    try { return (window.adapter && typeof window.adapter.getAllDramas === 'function') ? window.adapter.getAllDramas() : []; }
    catch { return []; }
  });
  const [openActorId, setOpenActorId] = useState(null); // только один аккордеон раскрыт

  // Реактивная подписка на изменения follows + seen + news.
  useEffect(() => {
    const refreshFollows = () => { try { setFollows(JSON.parse(localStorage.getItem(followsKey)) || []); } catch { setFollows([]); } };
    const refreshSeen = () => setSeen(_dsReadSubsSeen(user?.email));
    // Initial refresh — на случай если SubscriptionsBlock смонтирован до login
    // (тогда follows прочитан из ds_follows_guest, а не из ds_follows_<email>).
    refreshFollows();
    refreshSeen();
    window.addEventListener('ds-follows-changed', refreshFollows);
    window.addEventListener('ds-subs-seen-changed', refreshSeen);
    window.addEventListener('storage', refreshFollows);
    return () => {
      window.removeEventListener('ds-follows-changed', refreshFollows);
      window.removeEventListener('ds-subs-seen-changed', refreshSeen);
      window.removeEventListener('storage', refreshFollows);
    };
  }, [followsKey, user?.email]);

  // Подгружаем news.json (если ещё не загружен)
  useEffect(() => {
    if (Array.isArray(window.__dsNews) && window.__dsNews.length) {
      setNews(window.__dsNews);
      return;
    }
    const apiBase = (window.__dsApi && window.__dsApi.base) || '';
    const url = apiBase ? `${apiBase}/api/news?limit=100` : './data/news.json?t=' + Date.now();
    fetch(url)
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        const arr = Array.isArray(data) ? data : (data && Array.isArray(data.items) ? data.items : null);
        if (Array.isArray(arr)) {
          const sorted = [...arr].sort((a, b) => ((b.date || b.published_at) || '').localeCompare((a.date || a.published_at) || ''));
          window.__dsNews = sorted;
          setNews(sorted);
        }
      })
      .catch(() => {});
  }, []);

  // Если adapter ещё не загрузил dramas — ждём и пробуем снова.
  useEffect(() => {
    if (allDramas.length) return;
    const tryLoad = () => {
      try {
        const arr = (window.adapter && typeof window.adapter.getAllDramas === 'function') ? window.adapter.getAllDramas() : [];
        if (arr.length) setAllDramas(arr);
      } catch {}
    };
    const id = setInterval(tryLoad, 600);
    return () => clearInterval(id);
  }, [allDramas.length]);

  // Для каждого подписанного актёра — список новых новостей и новых дорам (НЕ помеченных прочитанными).
  // Возвращает {actorId: {newsItems, dramaItems, unreadCount}}.
  const perActor = useMemo(() => {
    const map = {};
    const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
    const weekAgo = Date.now() - WEEK_MS;
    for (const f of follows) {
      const subAt = f.subscribedAt ? new Date(f.subscribedAt).getTime() : 0;
      const seenForActor = (seen[f.id] || { news: {}, dramas: {} });
      // — Новости —
      // Видимы если: новость с упоминанием актёра + вышла после подписки + НЕ старше недели.
      // Прочитанные ОСТАЮТСЯ в списке (без бейджа), но мутно — чтобы юзер не терял контекст,
      // и через неделю всё равно пропадают. По ТЗ Marina: «не пропадать сразу, через неделю
      // или после прочтения» — трактуем как «не должно пропадать в момент клика».
      const matchedNews = (news || []).filter(n => {
        if (!_dsNewsMentionsActor(n, f)) return false;
        const nd = n.date || n.published_at;
        if (!nd) return false;
        const newsTime = new Date(String(nd).replace(' ', 'T')).getTime();
        if (newsTime < subAt) return false;
        if (newsTime < weekAgo) return false; // старше 7 дней — скрываем
        return true;
      });
      // — Дорамы —
      // "Новая дорама" = строго этого года или позже (анонсы будущих). Старое не считаем.
      // Matching: сравниваем personId через String() (id может быть number или string в разных
      // источниках) + fallback по имени актёра — на случай если cast-запись есть, но без personId.
      const currentYear = new Date().getFullYear();
      const fid = String(f.id);
      const fnames = [f.name, f.name_ru, f.name_en].filter(Boolean).map(s => String(s).trim().toLowerCase());
      const matchedDramas = (allDramas || []).filter(d => {
        if (!Array.isArray(d.cast)) return false;
        const hasActor = d.cast.some(c => {
          if (!c) return false;
          if (c.personId != null && String(c.personId) === fid) return true;
          // Fallback по имени (если personId не заполнен в cast-записи).
          const cnames = [c.name, c.name_ru, c.name_en, c.character].filter(Boolean).map(s => String(s).trim().toLowerCase());
          return fnames.some(n => cnames.includes(n));
        });
        if (!hasActor) return false;
        if (!d.firstAirDate) return false;
        const yr = parseInt(String(d.firstAirDate).slice(0, 4), 10);
        if (isNaN(yr)) return false;
        return yr >= currentYear;
      });
      const unreadNews = matchedNews.filter(n => !seenForActor.news?.[n.id]).length;
      const unreadDramas = matchedDramas.filter(d => !seenForActor.dramas?.[d.id]).length;
      map[f.id] = {
        newsItems: matchedNews,
        dramaItems: matchedDramas,
        unreadCount: unreadNews + unreadDramas
      };
    }
    return map;
  }, [follows, news, allDramas, seen]);

  const totalUnread = follows.reduce((sum, f) => sum + ((perActor[f.id]?.unreadCount) || 0), 0);
  const totalCount = follows.length;

  const toggleActor = (actor) => {
    if (openActorId === actor.id) {
      setOpenActorId(null);
    } else {
      setOpenActorId(actor.id);
      // НЕ помечаем сразу всё прочитанным — юзер должен увидеть список и
      // кликнуть на конкретный пункт. Иначе после фильтра newsItems
      // (matchedNews отбрасывает прочитанные) секция тут же опустеет.
    }
  };

  const openActor = (actor) => { if (window.__dsSelectActor) window.__dsSelectActor(actor.id); };
  const openNews = (newsItem) => { if (window.__dsOpenNews) window.__dsOpenNews(newsItem.id); else if (window.__dsGoPage) window.__dsGoPage('news'); };
  const openDrama = (drama) => { if (window.__dsSelectItem) window.__dsSelectItem(drama.id); };

  // ── UI ──
  const bellIcon = (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
      <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" />
    </svg>
  );

  // Маленький красный кружок-бейдж с числом
  const Badge = ({ n, size = 16 }) => (
    n > 0 ? (
      <span style={{
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        minWidth: size, height: size, padding: '0 5px',
        borderRadius: 999, background: '#e05858', color: '#fff',
        fontSize: Math.max(9, size - 6), fontWeight: 700, lineHeight: 1,
        boxShadow: '0 2px 6px rgba(224,88,88,0.4)'
      }}>{n}</span>
    ) : null
  );

  return (
    <div className="ds-home-side-card" style={{
      background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 18,
      boxShadow: '0 4px 14px rgba(20,135,203,0.12)'
    }}>
      <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, display: 'flex', alignItems: 'center', gap: 8 }}>
        <Badge n={totalUnread} />
        <span style={{ display: 'inline-flex', color: '#7bc3ff' }}>{bellIcon}</span>
        <span>{isRu ? 'Мои подписки' : 'My Subscriptions'}</span>
        <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500, marginLeft: 'auto' }}>{totalCount}</span>
      </h3>
      {!user && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu ? 'Войди, чтобы подписываться на актёров.' : 'Sign in to subscribe to actors.'}
          <button onClick={openSignUp} style={{ marginTop: 6, padding: '5px 10px', fontSize: 11, fontWeight: 700,
            background: '#1487cb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', display: 'block' }}>
            {isRu ? 'Войти' : 'Sign in'}
          </button>
        </div>
      )}
      {user && follows.length === 0 && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '4px 0 6px', lineHeight: 1.5 }}>
          {isRu
            ? 'Подпишитесь на актёров — кнопка «+ Подписаться» на странице актёра. Сюда будут падать новости и новые дорамы с ними.'
            : 'Subscribe to actors via the "+ Subscribe" button on an actor page. News and new dramas with them will appear here.'}
        </div>
      )}

      {/* Список подписок */}
      {user && follows.length > 0 && (
        <div style={{ borderTop: '1px solid rgba(140,180,235,0.08)' }}>
          {follows.map((f) => {
            const data = perActor[f.id] || { newsItems: [], dramaItems: [], unreadCount: 0 };
            const isOpen = openActorId === f.id;
            const name = (isRu && f.name_ru) ? f.name_ru : (f.name || '—');
            return (
              <div key={f.id} style={{ borderBottom: '1px solid rgba(140,180,235,0.06)' }}>
                <button onClick={() => toggleActor(f)}
                  style={{
                    width: '100%', padding: '8px 4px', background: 'transparent', border: 'none', cursor: 'pointer',
                    display: 'flex', alignItems: 'center', gap: 8, textAlign: 'left'
                  }}>
                  {/* Аватар актёра */}
                  <div style={{
                    width: 28, height: 28, borderRadius: '50%', overflow: 'hidden',
                    background: 'rgba(140,180,235,0.10)', flexShrink: 0,
                    display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14
                  }}>
                    {f.photoUrl ? <img src={f.photoUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} onError={(e) => { e.currentTarget.style.display = 'none'; }} /> : '👤'}
                  </div>
                  <span onClick={(e) => { e.stopPropagation(); openActor(f); }}
                    style={{ flex: 1, fontSize: 12.5, fontWeight: 500, color: 'var(--text)',
                      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {name}
                  </span>
                  <Badge n={data.unreadCount} size={14} />
                  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                    strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
                    style={{ color: 'var(--text3)', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}>
                    <polyline points="6 9 12 15 18 9"/>
                  </svg>
                </button>
                {isOpen && (
                  <div style={{ padding: '2px 6px 10px 36px', display: 'flex', flexDirection: 'column', gap: 4 }}>
                    {data.newsItems.length === 0 && data.dramaItems.length === 0 && (
                      <div style={{ fontSize: 11, color: 'var(--text3)', padding: '4px 0', lineHeight: 1.4 }}>
                        {isRu ? 'Свежих новостей и проектов нет.' : 'No new news or projects yet.'}
                      </div>
                    )}
                    {/* Новости */}
                    {data.newsItems.map(n => {
                      const isSeen = !!(seen[f.id]?.news?.[n.id]);
                      return (
                      <a key={'n' + n.id}
                        onClick={(e) => { e.preventDefault(); _dsMarkSubsSeen(user?.email, f.id, 'news', n.id); openNews(n); }}
                        href="#" style={{
                          display: 'flex', gap: 4, alignItems: 'baseline',
                          color: 'var(--text2)', fontSize: 11.5, lineHeight: 1.35,
                          textDecoration: 'none', cursor: 'pointer',
                          padding: '2px 0',
                          opacity: isSeen ? 0.55 : 1
                        }}>
                        <span style={{ color: isSeen ? 'var(--text3)' : '#e05858', fontSize: 8, marginTop: 2, flexShrink: 0 }}>●</span>
                        <span style={{ flex: 1, minWidth: 0 }}>
                          <span style={{ color: 'var(--text3)', fontSize: 10, marginRight: 4 }}>
                            {isRu ? 'Новость' : 'News'}:
                          </span>
                          {n.title}
                        </span>
                      </a>
                      );
                    })}
                    {/* Дорамы */}
                    {data.dramaItems.map(d => {
                      const title = (isRu && d.title_ru) ? d.title_ru : (d.title_en || d.title_ru || '—');
                      const year = (String(d.firstAirDate || '').slice(0, 4)) || '';
                      const isSeen = !!(seen[f.id]?.dramas?.[d.id]);
                      return (
                        <a key={'d' + d.id}
                          onClick={(e) => { e.preventDefault(); _dsMarkSubsSeen(user?.email, f.id, 'dramas', d.id); openDrama(d); }}
                          href="#" style={{
                            display: 'flex', gap: 4, alignItems: 'baseline',
                            color: 'var(--text2)', fontSize: 11.5, lineHeight: 1.35,
                            textDecoration: 'none', cursor: 'pointer',
                            padding: '2px 0',
                            opacity: isSeen ? 0.55 : 1
                          }}>
                          <span style={{ color: '#9fcbe9', fontSize: 8, marginTop: 2, flexShrink: 0 }}>▶</span>
                          <span style={{ flex: 1, minWidth: 0 }}>
                            <span style={{ color: 'var(--text3)', fontSize: 10, marginRight: 4 }}>
                              {isRu ? 'Новая дорама' : 'New drama'}:
                            </span>
                            {title}{year && <span style={{ color: 'var(--text3)' }}> · {year}</span>}
                          </span>
                        </a>
                      );
                    })}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ── SAVED NEWS / TOPICS — общие helpers и компоненты блоков для drawer ──────
const SAVED_NEWS_KEY = (email) => `ds_saved_news_${email || 'guest'}`;
const SAVED_TOPICS_KEY = (email) => `ds_saved_topics_${email || 'guest'}`;

function _dsReadArr(key) {
  try { return JSON.parse(localStorage.getItem(key)) || []; } catch { return []; }
}
function _dsWriteArr(key, arr) {
  localStorage.setItem(key, JSON.stringify(arr));
}

// Public helpers — для интеграции из других мест (например NewsCard добавит закладку)
if (typeof window !== 'undefined') {
  window.__dsToggleSavedNews = function(email, news) {
    const key = SAVED_NEWS_KEY(email);
    const arr = _dsReadArr(key);
    const idx = arr.findIndex(x => x.id === news.id);
    if (idx >= 0) arr.splice(idx, 1);
    else arr.unshift({ ...news, savedAt: new Date().toISOString() });
    _dsWriteArr(key, arr);
    window.dispatchEvent(new CustomEvent('ds-saved-news-changed'));
    return idx < 0; // true если только что добавлено
  };
  window.__dsIsNewsSaved = function(email, newsId) {
    return _dsReadArr(SAVED_NEWS_KEY(email)).some(x => x.id === newsId);
  };
  window.__dsToggleSavedTopic = function(email, topic) {
    const key = SAVED_TOPICS_KEY(email);
    const arr = _dsReadArr(key);
    const idx = arr.findIndex(x => x.id === topic.id);
    if (idx >= 0) arr.splice(idx, 1);
    else arr.unshift({ ...topic, savedAt: new Date().toISOString() });
    _dsWriteArr(key, arr);
    window.dispatchEvent(new CustomEvent('ds-saved-topics-changed'));
    return idx < 0;
  };
  window.__dsIsTopicSaved = function(email, topicId) {
    return _dsReadArr(SAVED_TOPICS_KEY(email)).some(x => x.id === topicId);
  };
}

// Сохранённые новости — блок в drawer
function SavedNewsBlock() {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const storageKey = SAVED_NEWS_KEY(user?.email);
  const [list, setList] = useState(() => _dsReadArr(storageKey));
  const [q, setQ] = useState('');
  useEffect(() => {
    const refresh = () => setList(_dsReadArr(storageKey));
    refresh();
    window.addEventListener('ds-saved-news-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-saved-news-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);
  const qLow = q.trim().toLowerCase();
  const filtered = qLow
    ? list.filter(n => (n.title || '').toLowerCase().includes(qLow) || (n.excerpt || '').toLowerCase().includes(qLow))
    : list;
  const remove = (id, e) => {
    e.stopPropagation();
    const next = list.filter(x => x.id !== id);
    _dsWriteArr(storageKey, next);
    setList(next);
    window.dispatchEvent(new CustomEvent('ds-saved-news-changed'));
  };
  return (
    <div className="ds-home-side-card" style={{
      background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 18,
      boxShadow: '0 4px 14px rgba(20,135,203,0.12)'
    }}>
      <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
        <span>{isRu ? 'Сохранённые новости' : 'Saved news'}</span>
        <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{list.length}</span>
      </h3>
      {!user && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu ? 'Войди, чтобы сохранять новости.' : 'Sign in to save news.'}
          <button onClick={openSignUp} style={{ marginTop: 6, padding: '5px 10px', fontSize: 11, fontWeight: 700,
            background: '#1487cb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', display: 'block' }}>
            {isRu ? 'Войти' : 'Sign in'}
          </button>
        </div>
      )}
      {user && list.length === 0 && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu
            ? 'Нет сохранённых новостей. Нажми на закладку рядом с заголовком новости.'
            : 'No saved news yet. Tap the bookmark next to a news headline.'}
        </div>
      )}
      {user && list.length > 0 && (
        <>
          <div style={{ position: 'relative', marginBottom: 10 }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"
              style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
                color: 'var(--text3)', pointerEvents: 'none' }}>
              <circle cx="11" cy="11" r="7"/>
              <line x1="20" y1="20" x2="16.65" y2="16.65"/>
            </svg>
            <input value={q} onChange={(e) => setQ(e.target.value)}
              placeholder={isRu ? 'Поиск по новостям…' : 'Search news…'}
              style={{
                width: '100%', padding: '7px 28px 7px 30px', fontSize: 12,
                background: 'var(--bg3)', border: '1px solid rgba(20,135,203,0.22)',
                borderRadius: 999, color: 'var(--text)', outline: 'none', boxSizing: 'border-box'
              }} />
            {q && (
              <button onClick={() => setQ('')} aria-label="Clear"
                style={{ position: 'absolute', right: 6, top: '50%', transform: 'translateY(-50%)',
                  width: 20, height: 20, border: 'none', background: 'transparent',
                  color: 'var(--text3)', cursor: 'pointer', fontSize: 16, lineHeight: 1, padding: 0 }}>×</button>
            )}
          </div>
          <div style={{
            display: 'flex', flexDirection: 'column', gap: 8,
            maxHeight: 480, overflowY: 'auto',
            overscrollBehavior: 'contain',
            WebkitOverflowScrolling: 'touch',
            scrollbarColor: 'rgba(20,135,203,0.45) transparent', scrollbarWidth: 'thin'
          }}>
            {filtered.map(n => (
              <div key={n.id} onClick={() => window.__dsOpenNews && window.__dsOpenNews(n.id)}
                style={{
                  display: 'flex', gap: 8, alignItems: 'flex-start',
                  padding: 6, borderRadius: 6, cursor: 'pointer', position: 'relative',
                  transition: 'background 0.12s'
                }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(20,135,203,0.08)'}
                onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                {n.coverImage && (
                  <div style={{ width: 48, height: 36, borderRadius: 4, overflow: 'hidden', flexShrink: 0, background: 'rgba(140,180,235,0.10)' }}>
                    <img src={n.coverImage} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                      onError={(e) => e.currentTarget.style.opacity = 0.2} />
                  </div>
                )}
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ fontSize: 12, fontWeight: 600, lineHeight: 1.3, color: 'var(--text)' }}>
                    {n.title || (isRu ? '(без заголовка)' : '(untitled)')}
                  </div>
                  {n.date && (
                    <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 2 }}>{n.date}</div>
                  )}
                </div>
                <button onClick={(e) => remove(n.id, e)} aria-label="Remove"
                  title={isRu ? 'Удалить из сохранённых' : 'Remove'}
                  style={{ width: 20, height: 20, padding: 0, background: 'transparent',
                    border: 'none', color: 'var(--text3)', cursor: 'pointer', fontSize: 14, lineHeight: 1, flexShrink: 0 }}>×</button>
              </div>
            ))}
            {filtered.length === 0 && qLow && (
              <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 4px' }}>
                {isRu ? `Ничего не найдено по «${q}».` : `No matches for "${q}".`}
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

// Сохранённые обсуждения — блок в drawer
function SavedTopicsBlock() {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const storageKey = SAVED_TOPICS_KEY(user?.email);
  const [list, setList] = useState(() => _dsReadArr(storageKey));
  const [q, setQ] = useState('');
  useEffect(() => {
    const refresh = () => setList(_dsReadArr(storageKey));
    refresh();
    window.addEventListener('ds-saved-topics-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-saved-topics-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);
  const qLow = q.trim().toLowerCase();
  const filtered = qLow
    ? list.filter(t => (t.text || '').toLowerCase().includes(qLow) || (t.drama || '').toLowerCase().includes(qLow))
    : list;
  const remove = (id, e) => {
    e.stopPropagation();
    const next = list.filter(x => x.id !== id);
    _dsWriteArr(storageKey, next);
    setList(next);
    window.dispatchEvent(new CustomEvent('ds-saved-topics-changed'));
  };
  return (
    <div className="ds-home-side-card" style={{
      background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 18,
      boxShadow: '0 4px 14px rgba(20,135,203,0.12)'
    }}>
      <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
        <span>{isRu ? 'Сохранённые обсуждения' : 'Saved discussions'}</span>
        <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{list.length}</span>
      </h3>
      {!user && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu ? 'Войди, чтобы сохранять обсуждения.' : 'Sign in to save discussions.'}
          <button onClick={openSignUp} style={{ marginTop: 6, padding: '5px 10px', fontSize: 11, fontWeight: 700,
            background: '#1487cb', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', display: 'block' }}>
            {isRu ? 'Войти' : 'Sign in'}
          </button>
        </div>
      )}
      {user && list.length === 0 && (
        <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 0', lineHeight: 1.5 }}>
          {isRu
            ? 'Нет сохранённых обсуждений. Нажми на закладку рядом с темой.'
            : 'No saved discussions yet. Tap the bookmark next to a topic.'}
        </div>
      )}
      {user && list.length > 0 && (
        <>
          <div style={{ position: 'relative', marginBottom: 10 }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"
              style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
                color: 'var(--text3)', pointerEvents: 'none' }}>
              <circle cx="11" cy="11" r="7"/>
              <line x1="20" y1="20" x2="16.65" y2="16.65"/>
            </svg>
            <input value={q} onChange={(e) => setQ(e.target.value)}
              placeholder={isRu ? 'Поиск по обсуждениям…' : 'Search discussions…'}
              style={{
                width: '100%', padding: '7px 28px 7px 30px', fontSize: 12,
                background: 'var(--bg3)', border: '1px solid rgba(20,135,203,0.22)',
                borderRadius: 999, color: 'var(--text)', outline: 'none', boxSizing: 'border-box'
              }} />
            {q && (
              <button onClick={() => setQ('')} aria-label="Clear"
                style={{ position: 'absolute', right: 6, top: '50%', transform: 'translateY(-50%)',
                  width: 20, height: 20, border: 'none', background: 'transparent',
                  color: 'var(--text3)', cursor: 'pointer', fontSize: 16, lineHeight: 1, padding: 0 }}>×</button>
            )}
          </div>
          <div style={{
            display: 'flex', flexDirection: 'column', gap: 8,
            maxHeight: 480, overflowY: 'auto',
            overscrollBehavior: 'contain',
            WebkitOverflowScrolling: 'touch',
            scrollbarColor: 'rgba(20,135,203,0.45) transparent', scrollbarWidth: 'thin'
          }}>
            {filtered.map(tp => (
              <div key={tp.id} onClick={() => window.__dsGoPage && window.__dsGoPage('community')}
                style={{
                  display: 'flex', gap: 8, alignItems: 'flex-start',
                  padding: 6, borderRadius: 6, cursor: 'pointer',
                  transition: 'background 0.12s'
                }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(20,135,203,0.08)'}
                onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                {tp.emoji && <span style={{ fontSize: 16, flexShrink: 0 }}>{tp.emoji}</span>}
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ fontSize: 12, fontWeight: 600, lineHeight: 1.35 }}>{tp.text}</div>
                  {(tp.replies != null || tp.lastPost) && (
                    <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 2 }}>
                      {tp.replies != null ? `${tp.replies} ${isRu ? 'ответов' : 'replies'}` : ''}
                      {tp.replies != null && tp.lastPost ? ' · ' : ''}
                      {tp.lastPost || ''}
                    </div>
                  )}
                </div>
                <button onClick={(e) => remove(tp.id, e)} aria-label="Remove"
                  title={isRu ? 'Удалить из сохранённых' : 'Remove'}
                  style={{ width: 20, height: 20, padding: 0, background: 'transparent',
                    border: 'none', color: 'var(--text3)', cursor: 'pointer', fontSize: 14, lineHeight: 1, flexShrink: 0 }}>×</button>
              </div>
            ))}
            {filtered.length === 0 && qLow && (
              <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 4px' }}>
                {isRu ? `Ничего не найдено по «${q}».` : `No matches for "${q}".`}
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

function UserListsBlock({ onSelectList, compact = false }) {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const [lists, setLists] = useState(() => _readUserLists(user?.email));
  const [creating, setCreating] = useState(false);
  const [newName, setNewName] = useState('');
  const isRu = lang === 'ru';

  // Реактивно обновляем при изменении (например, после добавления дорамы в список)
  useEffect(() => {
    const refresh = () => setLists(_readUserLists(user?.email));
    refresh();
    window.addEventListener('ds-userlists-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-userlists-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [user?.email]);

  // Подгружаем dramas.json лениво — для превью-обложек (первый постер из списка)
  const [dramasMap, setDramasMap] = useState(window.__dsDramasMapById || null);
  useEffect(() => {
    if (dramasMap) return;
    if (window.__dsDramasMapById) { setDramasMap(window.__dsDramasMapById); return; }
    fetch('./data/dramas.json?t=' + Date.now())
      .then(r => r.json())
      .then(arr => {
        const m = {};
        for (const d of (arr || [])) m[d.id] = d;
        window.__dsDramasMapById = m;
        setDramasMap(m);
      })
      .catch(() => {});
  }, []);

  const createList = () => {
    if (!user) { openSignUp(); return; }
    const name = newName.trim();
    if (!name) return;
    const next = [...lists, {
      id: Date.now(),
      name,
      dramaIds: [],
      createdAt: new Date().toISOString()
    }];
    _writeUserLists(user.email, next);
    setLists(next);
    setNewName('');
    setCreating(false);
  };

  const removeList = (listId) => {
    if (!confirm(isRu ? 'Удалить плейлист?' : 'Delete this list?')) return;
    const next = lists.filter(l => l.id !== listId);
    _writeUserLists(user?.email, next);
    setLists(next);
  };

  // Отформатировать «8 месяцев назад»
  const ago = (iso) => {
    const ms = Date.now() - new Date(iso).getTime();
    const days = Math.floor(ms / 86400000);
    if (days < 1) return isRu ? 'сегодня' : 'today';
    if (days < 30) return isRu ? `${days} дн. назад` : `${days}d ago`;
    const months = Math.floor(days / 30);
    if (months < 12) return isRu ? `${months} мес. назад` : `${months}mo ago`;
    const years = Math.floor(months / 12);
    return isRu ? `${years} ${years === 1 ? 'год' : years < 5 ? 'года' : 'лет'} назад` : `${years}y ago`;
  };

  // Получить URL обложки для списка — первый постер первой дорамы
  const coverUrl = (list) => {
    if (!list.dramaIds?.length || !dramasMap) return null;
    for (const id of list.dramaIds) {
      const d = dramasMap[id];
      if (d?.slug) return `./assets/posters/w185/${d.slug}.webp`;
    }
    return null;
  };

  return (
    <div className="ds-home-side-card" style={{
      background: 'var(--bg2)',
      border: 'none',
      borderRadius: 12,
      padding: compact ? 14 : 18,
      boxShadow: '0 4px 14px rgba(20,135,203,0.12)'
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
        <h3 style={{ fontSize: 13, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
          <span>{isRu ? 'Мои плейлисты' : 'My lists'}</span>
          <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>{lists.length}</span>
        </h3>
        {!creating && (
          <button onClick={() => { if (!user) { openSignUp(); return; } setCreating(true); }}
            style={{ fontSize: 11, color: 'var(--accent)', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
            <span style={{ fontSize: 14, lineHeight: 1 }}>+</span>
            {isRu ? 'Создать' : 'Create'}
          </button>
        )}
      </div>

      {creating && (
        <div style={{ display: 'flex', gap: 6, marginBottom: 12 }}>
          <input value={newName} onChange={(e) => setNewName(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') createList(); if (e.key === 'Escape') { setCreating(false); setNewName(''); } }}
            placeholder={isRu ? 'Название плейлиста...' : 'List name...'}
            autoFocus
            style={{
              flex: 1, padding: '7px 10px', fontSize: 12,
              background: 'var(--bg3)', border: '1px solid rgba(74,158,255,0.20)',
              borderRadius: 6, color: 'var(--text)', outline: 'none'
            }} />
          <button onClick={createList} disabled={!newName.trim()} style={{
            padding: '6px 12px', borderRadius: 6, fontSize: 11, fontWeight: 700,
            background: newName.trim() ? 'var(--accent)' : 'rgba(140,180,235,0.10)',
            color: newName.trim() ? '#fff' : 'var(--text3)',
            cursor: newName.trim() ? 'pointer' : 'default', border: 'none'
          }}>✓</button>
          <button onClick={() => { setCreating(false); setNewName(''); }} style={{
            padding: '6px 10px', borderRadius: 6, fontSize: 11,
            background: 'transparent', color: 'var(--text3)', cursor: 'pointer'
          }}>✕</button>
        </div>
      )}

      {lists.length === 0 && !creating && (
        <div style={{ fontSize: 11, color: 'var(--text3)', textAlign: 'center', padding: '14px 6px', lineHeight: 1.5 }}>
          {isRu
            ? 'Пока нет плейлистов. Создайте свой первый — например, «Любимые исторические» или «На пересмотр».'
            : 'No lists yet. Create your first — e.g. "Favorite Historicals" or "To Rewatch".'}
        </div>
      )}

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {lists.map(list => {
          const cover = coverUrl(list);
          return (
            <div key={list.id}
              onClick={() => onSelectList && onSelectList(list)}
              style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: 8, borderRadius: 8,
                border: '1px solid rgba(140,180,235,0.10)',
                background: 'rgba(140,180,235,0.03)',
                cursor: onSelectList ? 'pointer' : 'default',
                transition: 'background 0.15s'
              }}
              onMouseEnter={(e) => { if (onSelectList) e.currentTarget.style.background = 'rgba(140,180,235,0.08)'; }}
              onMouseLeave={(e) => { if (onSelectList) e.currentTarget.style.background = 'rgba(140,180,235,0.03)'; }}>
              <div style={{
                width: 44, height: 44, borderRadius: 6, overflow: 'hidden',
                background: 'var(--bg3)', flexShrink: 0,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                fontSize: 18
              }}>
                {cover
                  ? <img src={cover} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} onError={(e) => { e.currentTarget.style.display = 'none'; }} />
                  : <span style={{ color: 'var(--text3)' }}>📑</span>}
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {list.name}
                </div>
                <div style={{ fontSize: 10, color: 'var(--text3)', marginTop: 2 }}>
                  {ago(list.createdAt)} · {list.dramaIds.length} {isRu ? 'дорам' : 'titles'}
                </div>
              </div>
              <button onClick={(e) => { e.stopPropagation(); removeList(list.id); }}
                title={isRu ? 'Удалить' : 'Delete'}
                style={{
                  width: 22, height: 22, borderRadius: 4,
                  background: 'transparent', border: 'none',
                  color: 'var(--text3)', fontSize: 14, lineHeight: 1, cursor: 'pointer',
                  opacity: 0.5
                }}
                onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; e.currentTarget.style.color = '#e05858'; }}
                onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.5; e.currentTarget.style.color = 'var(--text3)'; }}>×</button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── ADD TO LIST MENU ─────────────────────────────────────────────────────────
// Кнопка-плюс на странице дорамы. Открывает дропдаун со списком плейлистов.
// Можно создать новый плейлист прямо отсюда.
function AddToListButton({ dramaId }) {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const [open, setOpen] = useState(false);
  const [newName, setNewName] = useState('');
  const ref = useRef(null);

  const [lists, setLists] = useState(() => _readUserLists(user?.email));
  useEffect(() => {
    const refresh = () => setLists(_readUserLists(user?.email));
    window.addEventListener('ds-userlists-changed', refresh);
    return () => window.removeEventListener('ds-userlists-changed', refresh);
  }, [user?.email]);

  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  const toggleInList = (listId) => {
    if (!user) { openSignUp(); return; }
    const next = lists.map(l => {
      if (l.id !== listId) return l;
      const has = l.dramaIds.includes(dramaId);
      return { ...l, dramaIds: has ? l.dramaIds.filter(x => x !== dramaId) : [...l.dramaIds, dramaId] };
    });
    _writeUserLists(user.email, next);
    setLists(next);
  };

  const createAndAdd = () => {
    if (!user) { openSignUp(); return; }
    const name = newName.trim();
    if (!name) return;
    const next = [...lists, { id: Date.now(), name, dramaIds: [dramaId], createdAt: new Date().toISOString() }];
    _writeUserLists(user.email, next);
    setLists(next);
    setNewName('');
  };

  const TipWrap = ({ children }) => {
    const Tip = window.DSHoverTip;
    const tip = isRu ? 'Добавить в свой плейлист' : 'Add to your list';
    return Tip ? <Tip tip={tip}>{children}</Tip> : children;
  };

  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
      <TipWrap>
        <button onClick={() => { if (!user) { openSignUp(); return; } setOpen(v => !v); }}
          className="ds-action-icon-btn ds-action-list"
          aria-label="Add to list"
          title={isRu ? 'Добавить в плейлист' : 'Add to list'}
          style={{
            width: 40, height: 40, borderRadius: '50%',
            color: '#1487cb',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', padding: 0
          }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
            <line x1="8" y1="6" x2="21" y2="6" />
            <line x1="8" y1="12" x2="21" y2="12" />
            <line x1="8" y1="18" x2="21" y2="18" />
            <circle cx="4" cy="6" r="1.4" />
            <circle cx="4" cy="12" r="1.4" />
            <circle cx="4" cy="18" r="1.4" />
          </svg>
        </button>
      </TipWrap>
      {open && (
        <div className="ds-list-dropdown" style={{
          position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 60,
          background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 10,
          padding: 8, minWidth: 240, maxHeight: 380, overflowY: 'auto',
          boxShadow: '0 10px 30px rgba(0,0,0,0.55)'
        }}>
          <div style={{ fontSize: 10, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.5px', fontWeight: 700, padding: '4px 8px 8px' }}>
            {isRu ? 'В плейлист' : 'Add to list'}
          </div>
          {lists.length === 0 && (
            <div style={{ fontSize: 11, color: 'var(--text3)', padding: '6px 10px 10px', lineHeight: 1.5 }}>
              {isRu ? 'У вас пока нет плейлистов. Создайте первый ниже ↓' : 'No lists yet. Create one below ↓'}
            </div>
          )}
          {lists.map(list => {
            const has = list.dramaIds.includes(dramaId);
            return (
              <button key={list.id} onClick={() => toggleInList(list.id)} style={{
                width: '100%', padding: '7px 10px', borderRadius: 6, fontSize: 12,
                textAlign: 'left', display: 'flex', alignItems: 'center', gap: 8,
                background: has ? 'rgba(10,186,181,0.12)' : 'transparent',
                color: has ? '#0abab5' : 'var(--text)',
                border: 'none', cursor: 'pointer', marginBottom: 2
              }}
              onMouseEnter={e => { if (!has) e.currentTarget.style.background = 'rgba(74,158,255,0.06)'; }}
              onMouseLeave={e => { if (!has) e.currentTarget.style.background = 'transparent'; }}>
                <span style={{ fontSize: 13, width: 16 }}>{has ? '✓' : '○'}</span>
                <span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: has ? 700 : 500 }}>{list.name}</span>
                <span style={{ fontSize: 10, color: 'var(--text3)' }}>{list.dramaIds.length}</span>
              </button>
            );
          })}
          <div style={{ height: 1, background: 'rgba(140,180,235,0.10)', margin: '6px 4px' }} />
          <div style={{ display: 'flex', gap: 5, padding: 4 }}>
            <input value={newName} onChange={(e) => setNewName(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter') createAndAdd(); }}
              placeholder={isRu ? 'Новый плейлист…' : 'New list…'}
              style={{
                flex: 1, padding: '6px 8px', fontSize: 12,
                background: 'var(--bg3)', border: '1px solid rgba(74,158,255,0.18)',
                borderRadius: 5, color: 'var(--text)', outline: 'none'
              }} />
            <button onClick={createAndAdd} disabled={!newName.trim()} style={{
              padding: '6px 10px', borderRadius: 5, fontSize: 12, fontWeight: 700,
              background: newName.trim() ? 'var(--accent)' : 'rgba(140,180,235,0.08)',
              color: newName.trim() ? '#fff' : 'var(--text3)',
              cursor: newName.trim() ? 'pointer' : 'default', border: 'none'
            }}>+</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// EDIT MODE — глобальный переключатель + редакторы под разные страницы.
// Состояние хранится в window.__dsEditMode (true/false); смена диспатчит
// 'ds-editmode-changed', все слушатели подхватывают.
// Эндпоинты сохранения: upload-server (http://localhost:8765), который должен
// быть запущен (Запустить админку.command поднимает его).
// ─────────────────────────────────────────────────────────────────────────────

const UPLOAD_BASE = 'http://localhost:8765';

// ADMIN-GATE: edit-mode карандашик показывается ТОЛЬКО на локалке (localhost / LAN).
// На проде (pages.dev / custom domain) toggle скрыт — никто не увидит и не сможет
// случайно включить edit-режим. Все ✎-кнопки и DSDramaEditor читают
// window.__dsEditMode — которое по дефолту false; без toggle'а его не включить из UI.
// (Сознательный админ может вручную из console: `window.__dsEditMode = true` — это
// аварийный bypass и фича, не баг.)
function _isAdminHostname() {
  if (typeof window === 'undefined') return false;
  const h = window.location.hostname;
  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;
}

// sessionStorage-ключ для admin edit-mode. Marina, 1 июня 2026: попап
// раньше закрывался когда родитель пересоздавал компонент (SWR refetch,
// save-response → setState) — useState(false) сбрасывался. Теперь храним
// в sessionStorage чтобы переживало любые рендеры/ремоунты. Закрытие ТОЛЬКО
// через кнопку × или Esc — никаких автоматических.
const _DS_EDIT_KEY = 'ds_admin_edit_mode';
function _readEditMode() {
  try { return sessionStorage.getItem(_DS_EDIT_KEY) === '1'; } catch { return false; }
}
function _writeEditMode(v) {
  try { sessionStorage.setItem(_DS_EDIT_KEY, v ? '1' : '0'); } catch {}
}

function DSEditModeToggle() {
  // Раньше гейт был по hostname (localhost only) — теперь по user.is_admin,
  // чтобы Marina могла редактировать прямо на проде через инлайн-попап.
  // На public-сайте (не-админам) кнопка не рендерится.
  //
  // ВАЖНО: все хуки (useState, useEffect, useContext) ДОЛЖНЫ вызываться
  // до любого early-return — иначе React видит разное кол-во хуков между
  // рендерами (когда user меняется null↔object) и падает с Rules of Hooks
  // violation. Поэтому считаем хуки сначала, гейт — последним.
  const { user } = (window.useAuth ? window.useAuth() : { user: null });
  // Initial из sessionStorage — переживёт ремоунт компонента.
  const [on, setOn] = useState(_readEditMode);
  const { t } = (window.useI18n ? window.useI18n() : { t: (s) => s });
  // Синхронизируем window.__dsEditMode с реальным state (другие компоненты читают).
  if (typeof window !== 'undefined') window.__dsEditMode = on;
  // Слушаем смену edit-режима из других мест (например кнопка × в попапе).
  useEffect(() => {
    const h = (e) => { const v = !!e.detail; _writeEditMode(v); setOn(v); };
    window.addEventListener('ds-editmode-changed', h);
    return () => window.removeEventListener('ds-editmode-changed', h);
  }, []);
  if (!user?.is_admin) return null;
  const toggle = () => {
    const next = !on;
    window.__dsEditMode = next;
    _writeEditMode(next);
    setOn(next);
    window.dispatchEvent(new CustomEvent('ds-editmode-changed', { detail: next }));
  };
  return (
    <button onClick={toggle}
      title={on ? (t('Edit mode: ON') || 'Edit mode: ON') : (t('Edit mode: OFF') || 'Edit mode: OFF')}
      aria-label="Toggle edit mode"
      style={{
        position: 'fixed', bottom: 24, right: 24, zIndex: 500,
        width: 52, height: 52, borderRadius: '50%',
        background: on ? '#3dd68c' : 'rgba(8,12,26,0.92)',
        color: on ? '#0a1226' : '#fff',
        border: `2px solid ${on ? '#3dd68c' : 'rgba(140,180,235,0.32)'}`,
        fontSize: 22, lineHeight: 1, cursor: 'pointer', padding: 0,
        boxShadow: '0 6px 20px rgba(0,0,0,0.5)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        transition: 'all 0.2s'
      }}>
      {on ? '✓' : '✎'}
    </button>
  );
}

// Селектор страны для фильтра /actors. Пишет в wd_citizenship как
// [{qid, code, label}] чтобы совпадало с форматом Wikidata-данных.
// QID = идентификатор в Wikidata, label берём в формате принятом в наших
// импортах (русское/официальное) — это совпадает с тем что приходило
// из wd-pipeline и значит filter на /actors сматчит без переделок.
const _COUNTRY_OPTIONS = [
  { code: 'CN', qid: 'Q148',  flag: '🇨🇳', labelRu: 'Китай',         labelEn: 'China' },
  { code: 'KR', qid: 'Q884',  flag: '🇰🇷', labelRu: 'Южная Корея',   labelEn: 'South Korea' },
  { code: 'JP', qid: 'Q17',   flag: '🇯🇵', labelRu: 'Япония',         labelEn: 'Japan' },
  { code: 'TH', qid: 'Q869',  flag: '🇹🇭', labelRu: 'Тайланд',        labelEn: 'Thailand' },
  { code: 'TW', qid: 'Q865',  flag: '🇹🇼', labelRu: 'Тайвань',        labelEn: 'Taiwan' },
  { code: 'HK', qid: 'Q8646', flag: '🇭🇰', labelRu: 'Гонконг',        labelEn: 'Hong Kong' },
  { code: 'SG', qid: 'Q334',  flag: '🇸🇬', labelRu: 'Сингапур',       labelEn: 'Singapore' },
  { code: 'PH', qid: 'Q928',  flag: '🇵🇭', labelRu: 'Филиппины',      labelEn: 'Philippines' },
  { code: 'VN', qid: 'Q881',  flag: '🇻🇳', labelRu: 'Вьетнам',        labelEn: 'Vietnam' },
  { code: 'MY', qid: 'Q833',  flag: '🇲🇾', labelRu: 'Малайзия',       labelEn: 'Malaysia' },
  { code: 'ID', qid: 'Q252',  flag: '🇮🇩', labelRu: 'Индонезия',      labelEn: 'Indonesia' },
  { code: 'US', qid: 'Q30',   flag: '🇺🇸', labelRu: 'США',            labelEn: 'USA' },
];
function _ActorCitizenshipSelector({ actor, onSave }) {
  const current = Array.isArray(actor.wd_citizenship) ? actor.wd_citizenship : [];
  const currentCode = current[0]?.code || '';
  const [status, setStatus] = useState('');
  // Локаль из document.documentElement.lang (фронт ставит ru/en).
  const isRu = (typeof document !== 'undefined'
    ? (document.documentElement.lang || 'ru')
    : 'ru').startsWith('ru');
  const onChange = async (code) => {
    if (!code) {
      setStatus('saving');
      try { await onSave([]); setStatus('saved'); setTimeout(() => setStatus(''), 1500); }
      catch (e) { setStatus('error'); console.error(e); }
      return;
    }
    const opt = _COUNTRY_OPTIONS.find(o => o.code === code);
    if (!opt) return;
    setStatus('saving');
    try {
      // Сохраняем РУССКИЙ label — таким приходит из wd-pipeline и так
      // ожидает filter на /actors. Не трогать без миграции данных.
      await onSave([{ qid: opt.qid, code: opt.code, label: opt.labelRu }]);
      setStatus('saved');
      setTimeout(() => setStatus(''), 1500);
    } catch (e) { setStatus('error'); console.error(e); }
  };
  const currentOpt = _COUNTRY_OPTIONS.find(o => o.code === currentCode);
  return (
    <div style={{ marginBottom: 10 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
        <span style={{ fontSize: 11, color: '#8ba0c8', fontWeight: 600 }}>
          {isRu ? 'Страна (для фильтра «Актёры»)' : 'Country (for /actors filter)'}
        </span>
        <span style={{ fontSize: 10, color: status === 'saved' ? '#5ed7c6' : status === 'error' ? '#e05858' : '#6f88b3' }}>
          {status === 'saved' ? (isRu ? '✓ сохранено' : '✓ saved')
            : status === 'error' ? (isRu ? '✗ ошибка' : '✗ error')
            : status === 'saving' ? '…' : ''}
        </span>
      </div>
      <div style={{ position: 'relative' }}>
        {/* Префикс-флаг текущей страны (видим через background-эмодзи слева) */}
        {currentOpt && (
          <div style={{
            position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
            fontSize: 16, pointerEvents: 'none', lineHeight: 1,
          }}>{currentOpt.flag}</div>
        )}
        <select value={currentCode} onChange={(e) => onChange(e.target.value)}
          style={{
            width: '100%', padding: currentOpt ? '8px 12px 8px 34px' : '8px 12px',
            fontSize: 13,
            background: 'rgba(20,28,52,0.6)', border: '1px solid rgba(140,180,235,0.22)',
            borderRadius: 6, color: '#e8efff', outline: 'none', cursor: 'pointer',
            appearance: 'none', WebkitAppearance: 'none',
          }}>
          <option value="">— {isRu ? 'не задана' : 'not set'} —</option>
          {_COUNTRY_OPTIONS.map(o => (
            <option key={o.code} value={o.code}>
              {o.flag} {isRu ? o.labelRu : o.labelEn}
            </option>
          ))}
        </select>
      </div>
      {current.length > 1 && (
        <div style={{ fontSize: 10, color: '#8ba0c8', marginTop: 4 }}>
          {isRu ? 'Доп. гражданства: ' : 'Additional: '}
          {current.slice(1).map(c => {
            const o = _COUNTRY_OPTIONS.find(x => x.code === c.code);
            return o ? `${o.flag} ${isRu ? o.labelRu : o.labelEn}` : (c.label || c.code);
          }).join(', ')}
        </div>
      )}
      <div style={{ fontSize: 10, color: '#6f88b3', marginTop: 4, lineHeight: 1.4 }}>
        {isRu
          ? 'Влияет на фильтр «Актёры» в навигации. После сохранения изменения видны через 1–2 минуты (кеш каталога).'
          : 'Affects the Actors filter in navigation. Changes visible in 1–2 minutes (catalog cache).'}
      </div>
    </div>
  );
}

// ── Глобальный pending-save tracker ──────────────────────────────────────
// Все автосохраняющие поля в редакторах регистрируют свои pending-сохранения
// в этом наборе. DSDramaEditor / DSActorEditor читают его, чтобы:
//  1. Показывать «N сохраняется…» в шапке.
//  2. Блокировать закрытие × до завершения (или предупреждать).
//  3. beforeunload prompt при закрытии вкладки с несохранённым.
if (typeof window !== 'undefined' && !window.__dsPendingSaves) {
  window.__dsPendingSaves = new Set();
  window.__dsSaveErrors = new Map(); // key -> last error msg
  const emit = () => window.dispatchEvent(new CustomEvent('ds-pending-saves-changed', {
    detail: { count: window.__dsPendingSaves.size, errors: window.__dsSaveErrors.size }
  }));
  window.__dsRegisterPendingSave = (key) => { window.__dsPendingSaves.add(key); window.__dsSaveErrors.delete(key); emit(); };
  window.__dsResolvePendingSave = (key) => { window.__dsPendingSaves.delete(key); window.__dsSaveErrors.delete(key); emit(); };
  window.__dsRejectPendingSave = (key, err) => {
    window.__dsPendingSaves.delete(key);
    window.__dsSaveErrors.set(key, String(err && err.message || err || 'unknown'));
    emit();
  };
  // beforeunload — предупредить если есть pending или ошибки
  window.addEventListener('beforeunload', (e) => {
    if (window.__dsPendingSaves.size > 0 || window.__dsSaveErrors.size > 0) {
      e.preventDefault();
      e.returnValue = 'Есть несохранённые изменения. Закрыть страницу?';
      return e.returnValue;
    }
  });
}

// Универсальное поле с дебаунсом сохранения. Используется внутри DSDramaEditor.
//
// Семантика (после фикса 30 мая 2026):
//  - 2000мс debounce на автосохранение при печати (даём время заполнить поле).
//  - При unmount компонента — НЕМЕДЛЕННЫЙ flush pending-таймера (не теряем данные).
//  - При onBlur поля — flush сразу (не дожидаясь 450мс).
//  - Регистрация в window.__dsPendingSaves чтобы шапка показывала статус
//    и × мог предупредить «есть несохранённое».
//  - localStorage draft `ds_field_draft_<saveKey>` — каждое изменение моментально
//    дублируется локально; чистится после успешного save с сервера.
//  - Ошибка фетча: console.error + статус 'error' + остаётся в __dsSaveErrors,
//    чтобы было видно в шапке редактора.
function _EditField({ label, value, onSave, type = 'text', rows, placeholder, saveKey }) {
  const [val, setVal] = useState(value ?? '');
  const [status, setStatus] = useState(''); // '', 'saving', 'saved', 'error'
  const [errMsg, setErrMsg] = useState('');
  const timerRef = useRef(null);
  const pendingValRef = useRef(null); // последнее введённое значение, ждущее flush
  const onSaveRef = useRef(onSave);
  useEffect(() => { onSaveRef.current = onSave; }, [onSave]);
  // Stable key чтобы tracker умел различать поля
  const keyRef = useRef(saveKey || `field_${Math.random().toString(36).slice(2)}`);

  useEffect(() => { setVal(value ?? ''); }, [value]);

  // Ядро отправки. Принимает значение для отправки. Не зависит от состояния поля,
  // чтобы оставаться валидным после unmount (вызывается из useEffect cleanup).
  const doSave = async (toSave) => {
    const key = keyRef.current;
    if (typeof window !== 'undefined') window.__dsRegisterPendingSave?.(key);
    try {
      await onSaveRef.current(toSave);
      setStatus('saved'); setErrMsg('');
      // Чистим draft из localStorage — сервер принял
      try { localStorage.removeItem(`ds_field_draft_${key}`); } catch {}
      if (typeof window !== 'undefined') window.__dsResolvePendingSave?.(key);
      setTimeout(() => setStatus(s => s === 'saved' ? '' : s), 1500);
    } catch (e) {
      console.error('[_EditField save failed]', label || key, e);
      setStatus('error'); setErrMsg(String(e && e.message || e || 'unknown'));
      if (typeof window !== 'undefined') window.__dsRejectPendingSave?.(key, e);
    } finally {
      pendingValRef.current = null;
    }
  };

  // flush — отправить pending значение сразу (используется в onBlur, при Save-кнопке,
  // и в unmount-cleanup).
  const flush = () => {
    if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
    if (pendingValRef.current !== null) {
      const v = pendingValRef.current;
      doSave(v); // огонь-и-забудь — внутри регистрируется в __dsPendingSaves
    }
  };

  // КЛЮЧЕВОЙ ФИКС: cleanup-effect, который flush'ит на unmount.
  // Без него — debounce-таймер успешно «отменялся» неявно (через clearTimeout
  // в следующем handle), но при unmount таймер НЕ flush'ился и user терял данные.
  useEffect(() => {
    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        if (pendingValRef.current !== null) {
          // Отправляем pending немедленно. Даже если компонент unmount'ится,
          // promise отработает и зарегистрируется в __dsPendingSaves.
          doSave(pendingValRef.current);
        }
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handle = (newVal) => {
    setVal(newVal);
    setStatus('saving');
    pendingValRef.current = newVal;
    // Дублируем в localStorage немедленно (на случай краха браузера)
    try { localStorage.setItem(`ds_field_draft_${keyRef.current}`, JSON.stringify({ v: newVal, t: Date.now() })); } catch {}
    clearTimeout(timerRef.current);
    // 2000мс — даём время спокойно заполнить поле; при переходе на другое поле
    // (onBlur) или закрытии — сохраняется сразу, данные не теряются.
    timerRef.current = setTimeout(() => { timerRef.current = null; doSave(newVal); }, 2000);
  };

  // onBlur — мгновенный flush. Даёт UX-предсказуемость: ушёл из поля → сохранилось.
  const handleBlur = () => { flush(); };

  const fieldStyle = {
    width: '100%', padding: '8px 10px',
    background: '#131b35',
    border: `1px solid ${status === 'error' ? '#e05858' : '#2a3458'}`,
    borderRadius: 6,
    color: '#e8efff', fontSize: 13, fontFamily: 'inherit', outline: 'none'
  };
  return (
    <div style={{ marginBottom: 14 }}>
      <label style={{ display:'flex', justifyContent:'space-between', alignItems:'center', fontSize: 10, fontWeight: 700, color: '#8ba0c8', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 4 }}>
        <span>{label}</span>
        <span title={status === 'error' ? errMsg : ''} style={{
          fontSize: 9, fontWeight: 600,
          color: status === 'saved' ? '#5ed7c6' : status === 'error' ? '#e05858' : status === 'saving' ? '#f0c674' : 'transparent',
          transition: 'color 0.2s'
        }}>{status === 'saved' ? '✓ saved' : status === 'error' ? `✗ ${errMsg.slice(0, 40)}` : status === 'saving' ? '…' : ''}</span>
      </label>
      {type === 'textarea'
        ? <textarea value={val} onChange={(e) => handle(e.target.value)} onBlur={handleBlur} rows={rows || 4} placeholder={placeholder} style={{ ...fieldStyle, resize: 'vertical', minHeight: 80, lineHeight: 1.5 }} />
        : <input type={type} value={val} onChange={(e) => handle(type === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value)} onBlur={handleBlur} placeholder={placeholder} style={fieldStyle} />
      }
    </div>
  );
}

// _EditSelect — выпадающий список с автосохранением (как _EditField, но select).
// Используется для фиксированных enum-полей (статус дорамы и т.п.).
// После фикса 30 мая 2026: регистрация в __dsPendingSaves + console.error +
// видимая ошибка под лейблом.
function _EditSelect({ label, value, onSave, options, saveKey }) {
  const [val, setVal] = useState(value ?? '');
  const [status, setStatus] = useState('');
  const [errMsg, setErrMsg] = useState('');
  const keyRef = useRef(saveKey || `select_${Math.random().toString(36).slice(2)}`);
  useEffect(() => { setVal(value ?? ''); }, [value]);
  const handle = async (newVal) => {
    setVal(newVal);
    setStatus('saving');
    const key = keyRef.current;
    if (typeof window !== 'undefined') window.__dsRegisterPendingSave?.(key);
    try {
      await onSave(newVal);
      setStatus('saved'); setErrMsg('');
      if (typeof window !== 'undefined') window.__dsResolvePendingSave?.(key);
      setTimeout(() => setStatus(s => s === 'saved' ? '' : s), 1500);
    } catch (e) {
      console.error('[_EditSelect save failed]', label || key, e);
      setStatus('error'); setErrMsg(String(e && e.message || e || 'unknown'));
      if (typeof window !== 'undefined') window.__dsRejectPendingSave?.(key, e);
    }
  };
  return (
    <div style={{ marginBottom: 14 }}>
      <label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 10, fontWeight: 700, color: '#8ba0c8', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 4 }}>
        <span>{label}</span>
        <span title={status === 'error' ? errMsg : ''} style={{
          fontSize: 9, fontWeight: 600,
          color: status === 'saved' ? '#5ed7c6' : status === 'error' ? '#e05858' : status === 'saving' ? '#f0c674' : 'transparent',
          transition: 'color 0.2s'
        }}>{status === 'saved' ? '✓ saved' : status === 'error' ? `✗ ${errMsg.slice(0, 40)}` : status === 'saving' ? '…' : ''}</span>
      </label>
      <select value={val} onChange={(e) => handle(e.target.value)} style={{
        width: '100%', padding: '8px 10px',
        background: '#131b35',
        border: `1px solid ${status === 'error' ? '#e05858' : '#2a3458'}`,
        borderRadius: 6,
        color: '#e8efff', fontSize: 13, fontFamily: 'inherit', outline: 'none', cursor: 'pointer'
      }}>
        <option value="">— не указан —</option>
        {(options || []).map(o => <option key={o} value={o}>{o}</option>)}
      </select>
    </div>
  );
}

// ── СОВМЕСТНЫЕ ХЕЛПЕРЫ для drag-drop / paste картинок ─────────────────────
// Парсит URL из drop/paste-данных (поддерживает text/uri-list, plain, html).
function _extractImageUrlFromDt(dt) {
  if (!dt) return '';
  // Собираем ВСЕ кандидаты из всех источников. Браузеры кладут в drop-event
  // несколько вариантов URL: ссылку на страницу, ссылку на картинку, иногда
  // и то и другое. Если просто взять первый text/uri-list — для картинки,
  // обёрнутой в <a href="page.html">, получим page.html (HTML, не картинку).
  const candidates = [];
  // 1) text/html — самое надёжное: <img src=...> ВСЕГДА указывает на саму
  //    картинку, даже если картинка обёрнута в ссылку.
  try {
    const html = dt.getData('text/html') || '';
    // Может быть несколько <img>, берём все
    const re = /<img[^>]+src=["']([^"']+)["']/gi;
    let m; while ((m = re.exec(html))) candidates.push(m[1]);
  } catch {}
  // 2) text/uri-list — может быть страница ИЛИ картинка
  try {
    const list = (dt.getData('text/uri-list') || '').split(/\r?\n/);
    for (const u of list) {
      const t = (u || '').trim();
      if (t && !t.startsWith('#')) candidates.push(t);
    }
  } catch {}
  // 3) text/plain — фолбэк
  try {
    const plain = (dt.getData('text/plain') || '').trim().split(/\r?\n/)[0];
    if (plain) candidates.push(plain);
  } catch {}

  // Нормализуем: только http(s), уберём дубли
  const seen = new Set();
  const urls = candidates
    .map(u => (u || '').trim())
    .filter(u => /^https?:\/\//i.test(u))
    .filter(u => { if (seen.has(u)) return false; seen.add(u); return true; });

  if (urls.length === 0) return '';
  // Предпочитаем URL, который ВЫГЛЯДИТ как картинка: расширение .jpg/.png/...
  // ИЛИ путь содержит /image/, /img/, /poster/, /photo/, /media/
  const isImageUrl = (u) => {
    if (/\.(jpe?g|png|webp|gif|avif|svg)([?#].*)?$/i.test(u)) return true;
    if (/\/(image|img|imag|poster|photo|media|cdn|static)/i.test(u)) return true;
    return false;
  };
  return urls.find(isImageUrl) || urls[0];
}

// Хук: ловит paste на document, передаёт File или URL в обработчики.
// enabled — позволяет включать только когда зона активна / попап открыт.
// Игнорирует paste внутри input/textarea (там работает обычная вставка текста).
function _useImagePaste({ onFile, onUrl, enabled = true }) {
  useEffect(() => {
    if (!enabled) return;
    const handler = (e) => {
      const tgt = e.target;
      if (tgt && tgt.tagName && /^(INPUT|TEXTAREA|SELECT)$/.test(tgt.tagName)) return;
      if (tgt && tgt.isContentEditable) return;
      const cd = e.clipboardData;
      if (!cd) return;
      // 1) Файлы (скриншоты, copy image из проводника)
      const files = [...(cd.files || [])].filter(f => f.type && f.type.startsWith('image/'));
      if (files.length) { e.preventDefault(); for (const f of files) onFile && onFile(f); return; }
      // 2) clipboardData.items с kind=file (copy image из браузера/мессенджера)
      for (const it of (cd.items || [])) {
        if (it.kind === 'file' && it.type && it.type.startsWith('image/')) {
          const f = it.getAsFile();
          if (f) { e.preventDefault(); onFile && onFile(f); return; }
        }
      }
      // 3) URL картинки из text/plain или text/uri-list
      const txt = cd.getData('text/plain') || cd.getData('text/uri-list') || '';
      const url = (txt || '').split(/\r?\n/)[0].trim();
      if (url && /^https?:\/\//i.test(url)) {
        // Эвристика: если выглядит как ссылка на картинку — грузим.
        // Иначе всё равно пробуем (worker сам отвергнет non-image content-type).
        e.preventDefault();
        onUrl && onUrl(url);
      }
    };
    document.addEventListener('paste', handler, true);
    return () => document.removeEventListener('paste', handler, true);
  }, [enabled, onFile, onUrl]);
}

// Блокирует браузерный default при drop'е файла мимо drop-zone'ы
// (иначе браузер открывает картинку в новой вкладке и теряется контекст).
const _popupDragGuard = {
  onDragOver: (e) => { e.preventDefault(); },
  onDrop: (e) => { e.preventDefault(); },
};

function DSDramaEditor({ show, onUpdated }) {
  // Marina, 1 июня 2026: попап «захлопывается во время ввода» — это происходит
  // когда DSDetailPage перерендеривается из SWR-refetch'a или после save, и
  // если DSDramaEditor ремоунтится (или ключ компонента меняется), useState(false)
  // сбрасывает editMode. Теперь читаем из sessionStorage — переживёт что угодно.
  const [editMode, setEditMode] = useState(_readEditMode);
  const panelRef = useRef(null);
  // Глобальный счётчик pending / errored сохранений — чтобы юзер ВИДЕЛ что
  // что-то ещё пишется или упало.
  const [pendingCount, setPendingCount] = useState(0);
  const [errorCount, setErrorCount] = useState(0);
  useEffect(() => {
    const h = (e) => { const v = !!e.detail; _writeEditMode(v); setEditMode(v); };
    window.addEventListener('ds-editmode-changed', h);
    return () => window.removeEventListener('ds-editmode-changed', h);
  }, []);
  useEffect(() => {
    const h = (e) => {
      setPendingCount(e?.detail?.count || 0);
      setErrorCount(e?.detail?.errors || 0);
    };
    window.addEventListener('ds-pending-saves-changed', h);
    // На случай если редактор открыли когда уже что-то pending:
    if (typeof window !== 'undefined' && window.__dsPendingSaves) {
      setPendingCount(window.__dsPendingSaves.size);
      setErrorCount(window.__dsSaveErrors?.size || 0);
    }
    return () => window.removeEventListener('ds-pending-saves-changed', h);
  }, []);
  // При открытии панели — сбросить scrollTop в 0 (Chrome иногда сохраняет
  // позицию прокрутки внутреннего контейнера после refresh, что обрезает
  // шапку панели).
  useEffect(() => {
    if (editMode && panelRef.current) {
      panelRef.current.scrollTop = 0;
    }
  }, [editMode]);
  if (!editMode || !show?.id) return null;

  // Маппинг camelCase (как в JSON dramas.json) → snake_case (как в D1).
  // Worker /api/admin/dramas/:id принимает snake_case согласно schema 0011.
  const KEY_MAP = {
    firstAirDate: 'first_air_date',
    episodeCount: 'episode_count',
    voteAverage:  'vote_average',
    voteCount:    'vote_count',
    posterPath:   'poster_path',
    originCountry: 'origin_country',
    keywordIds:   'keyword_ids',
    externalIds:  'external_ids',
    cast:         'cast_list',   // DSEditorCast шлёт updateField('cast', …)
  };
  const toSnake = (k) => KEY_MAP[k] || k;

  const updateField = async (key, value) => {
    const api = window.__dsApi;
    if (!api) throw new Error('API not configured');
    const dbKey = toSnake(key);
    const body = { [dbKey]: value };
    await api.fetch(`/api/admin/dramas/${show.id}`, {
      method: 'PUT',
      body: JSON.stringify(body),
    });
    // Optimistic update — обновляем локально в обоих case'ах (camelCase
    // для JSON-совместимости + snake_case для D1-совместимости).
    show[key] = value;
    show[dbKey] = value;
    // КРИТИЧНО: обновить кеш adapter, иначе после закрытия попапа DSDetailPage
    // покажет старые данные (Marina, 1 июня 2026 — баг с "невидимыми сохранениями").
    if (window.adapter && window.adapter.updateDramaInCache) {
      window.adapter.updateDramaInCache({ id: show.id, [key]: value });
    }
    if (onUpdated) onUpdated(show);
  };

  // Списки (жанры, networks, страны, alt_titles) — текстовое поле с запятыми → массив
  const commaListAsString = (arr, key) => (arr || []).map(x => typeof x === 'string' ? x : (x[key] || '')).filter(Boolean).join(', ');
  const updateGenres = async (str) => {
    // Сохраняем как массив объектов с .name (как и приходит из TMDB)
    const names = str.split(',').map(s => s.trim()).filter(Boolean);
    // Стараемся сохранить id из существующих если совпадает name
    const existing = (show.genres || []);
    const arr = names.map((n, i) => {
      const ex = existing.find(g => (g.name || '').toLowerCase() === n.toLowerCase());
      return ex || { id: -(i + 1), name: n };
    });
    return updateField('genres', arr);
  };
  // networks — сервер хранит массив строк? проверим эндпоинт
  // В upload-server `/drama/update` сейчас принимает: title_en, title_ru, ..., genres (object array), cast, crew.
  // networks НЕ в whitelist'е сервера. Чтобы не молча терять — пометим, что для networks нужен отдельный endpoint
  // позже. Пока — сохраняем только то, что сервер реально принимает.

  return (
    <div {..._popupDragGuard} ref={panelRef} style={{
      position: 'fixed', top: 76, right: 12, bottom: 12, width: 360,
      background: 'rgba(10,18,38,0.97)', border: '1px solid rgba(140,180,235,0.28)',
      borderRadius: 12, zIndex: 450, padding: 18, overflowY: 'auto',
      boxShadow: '-8px 8px 32px rgba(0,0,0,0.6)',
      backdropFilter: 'blur(10px)'
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14, paddingBottom: 10, borderBottom: '1px solid rgba(140,180,235,0.18)' }}>
        <div>
          <div style={{ fontSize: 10, color: '#8ba0c8', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700 }}>Edit drama</div>
          <div style={{ fontSize: 13, fontWeight: 700, color: '#e8efff', marginTop: 2 }}>id: {show.id} · slug: {show.slug}</div>
        </div>
        <button onClick={() => {
          // Защита от потери данных: если есть pending или errors — confirm
          if (pendingCount > 0) {
            const ok = confirm(`Сохраняется ${pendingCount} полей… Подожди ещё пару секунд. Закрыть всё равно?`);
            if (!ok) return;
          } else if (errorCount > 0) {
            const ok = confirm(`Есть ${errorCount} НЕсохранённых полей с ошибкой. Закрыть всё равно (потеряешь изменения)?`);
            if (!ok) return;
          }
          window.__dsEditMode = false;
          window.dispatchEvent(new CustomEvent('ds-editmode-changed', { detail: false }));
        }} title="Close edit mode" style={{
          width: 28, height: 28, borderRadius: 6, background: 'rgba(140,180,235,0.10)', border: 'none',
          color: '#e8efff', fontSize: 16, cursor: 'pointer'
        }}>×</button>
      </div>

      {/* Status banner — Marina ВИДИТ что что-то сохраняется или упало.
          Раньше тихий debounce без видимого статуса приводил к тому, что
          она закрывала ×, не дождавшись save, и теряла данные. */}
      {(pendingCount > 0 || errorCount > 0) && (
        <div style={{
          marginBottom: 12, padding: '8px 12px', borderRadius: 8,
          background: errorCount > 0 ? 'rgba(196,69,69,0.12)' : 'rgba(240,198,116,0.12)',
          border: `1px solid ${errorCount > 0 ? 'rgba(196,69,69,0.4)' : 'rgba(240,198,116,0.4)'}`,
          fontSize: 12, color: errorCount > 0 ? '#e05858' : '#f0c674', fontWeight: 600
        }}>
          {errorCount > 0
            ? `⚠ ${errorCount} полей не сохранилось — проверь консоль (F12)`
            : `… Сохраняем ${pendingCount} ${pendingCount === 1 ? 'поле' : 'поля'}, не закрывай попап`
          }
        </div>
      )}
      {pendingCount === 0 && errorCount === 0 && (
        <div style={{
          marginBottom: 12, padding: '6px 12px', borderRadius: 8,
          background: 'rgba(94,215,198,0.08)',
          border: '1px solid rgba(94,215,198,0.25)',
          fontSize: 11, color: '#5ed7c6', fontWeight: 600
        }}>
          ✓ Все изменения сохранены. Закрытие × безопасно.
        </div>
      )}

      {/* Постер — drag-and-drop из папки или с других сайтов */}
      <_DSEditorPoster show={show} />

      <_EditField label="Title English" value={show.title_en} onSave={(v) => updateField('title_en', v)} />
      <_EditField label="Title Русский" value={show.title_ru} onSave={(v) => updateField('title_ru', v)} />
      <_EditField label="Original (CJK)" value={show.title_original} onSave={(v) => updateField('title_original', v)} placeholder="灼灼风流" />
      <_EditField label="Pinyin" value={show.title_pinyin} onSave={(v) => updateField('title_pinyin', v)} />

      <_EditField label="Overview English" type="textarea" rows={5} value={show.overview_en} onSave={(v) => updateField('overview_en', v)} />
      <_EditField label="Overview Русский" type="textarea" rows={5} value={show.overview_ru} onSave={(v) => updateField('overview_ru', v)} />

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <_EditField label="Дата релиза" value={show.firstAirDate} onSave={(v) => updateField('firstAirDate', v)} placeholder="2026-05-01" />
        <_EditSelect label="Статус" value={show.status} onSave={(v) => updateField('status', v)}
          options={['Анонсировано', 'Ожидается', 'Выходит с...', 'Выходит', 'Завершено', 'Приостановлено', 'Отменено']} />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <_EditField label="Episode Count" type="number" value={show.episodeCount} onSave={(v) => updateField('episodeCount', v)} />
        <_EditField label="Vote Average" type="number" value={show.voteAverage} onSave={(v) => updateField('voteAverage', v)} />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <_EditField label="Длительность серии (мин)" type="number" value={show.runtime} onSave={(v) => updateField('runtime', v)} placeholder="45" />
      </div>

      <_EditField label="Genres (через запятую)"
        value={(show.genres || []).map(g => g.name).join(', ')}
        onSave={updateGenres}
        placeholder="Romance, Historical, Drama" />

      <_EditField label="Канал / Networks (через запятую)"
        value={(show.networks || []).map(n => typeof n === 'string' ? n : (n.name || '')).filter(Boolean).join(', ')}
        onSave={(v) => {
          const names = v.split(',').map(s => s.trim()).filter(Boolean);
          // Сохраняем порядок и id из существующих если name совпадает
          const existing = show.networks || [];
          const arr = names.map((n, i) => {
            const ex = existing.find(x => (x.name || '').toLowerCase() === n.toLowerCase());
            return ex || { id: -(i + 1), name: n };
          });
          return updateField('networks', arr);
        }}
        placeholder="Tencent Video, iQiyi, Hunan TV" />

      <_EditField label="Alt titles (через запятую)"
        value={(show.alt_titles || []).join(', ')}
        onSave={(v) => updateField('alt_titles', v.split(',').map(s => s.trim()).filter(Boolean))} />

      <_EditField label="Origin Country (коды через запятую)"
        value={(show.originCountry || []).join(', ')}
        onSave={(v) => updateField('originCountry', v.split(',').map(s => s.trim()).filter(Boolean))}
        placeholder="CN, KR, JP" />

      {/* ── ФОТО — 3 раздела (постеры-альт, кадры, доп. изображения) ── */}
      <_DSEditorPhotos show={show} />

      {/* ── ТРЕЙЛЕРЫ ── */}
      <_DSEditorTrailers show={show} updateField={updateField} />

      {/* ── САУНДТРЕК (YouTube / Spotify / Apple Music) ── */}
      <_DSEditorOst show={show} />

      {/* ── СВЯЗАННЫЕ ПРОИЗВЕДЕНИЯ ── */}
      <_DSEditorRelated show={show} updateField={updateField} />

      {/* ── CAST — актёры с именем персонажа и чекбоксом «главная роль» ── */}
      <_DSEditorCast show={show} updateField={updateField} />

      {/* ── CREW — режиссёры/сценаристы/продюсеры и т.д. с должностью ── */}
      <_DSEditorCrew show={show} updateField={updateField} />

      <div style={{ fontSize: 10, color: '#6f88b3', marginTop: 18, paddingTop: 12, borderTop: '1px solid rgba(140,180,235,0.10)', lineHeight: 1.5 }}>
        Изменения автосохраняются в D1 через <code style={{ color: '#5ed7c6' }}>/api/admin/dramas/{show.id}</code>.<br/>
        ⚠️ Public сайт пока читает из <code>data/dramas.json</code>, поэтому правки видны только в админ-режиме.
        Полное переключение reading'а на D1 — в следующей фазе.<br/>
        Все секции сохраняются в D1 + R2 (фото / постер → R2-bucket, остальное → JSON-поля).
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Хелперы DSDramaEditor: секции, фото, трейлеры, связанные произведения.
// ─────────────────────────────────────────────────────────────────────────────

// ── DSActorEditor — popup-редактор актёра (аналог DSDramaEditor) ────────
// Открывается на странице актёра когда window.__dsEditMode === true.
// Поля редактирования сохраняются автоматически через PUT /api/admin/actors/:id.
// Фото актёра загружается в R2 (общий bucket AVATARS, namespace p/<id>/).
function DSActorEditor({ actor, onUpdated }) {
  // ВСЕ хуки сначала — до любого early-return. Иначе при смене editMode/actor
  // количество вызванных хуков меняется и React падает с Rules of Hooks.
  // Marina, 1 июня 2026: initial из sessionStorage чтобы попап не схлопывался
  // при ремоунте/перерендере родителя (после save / SWR refetch).
  const [editMode, setEditMode] = useState(_readEditMode);
  const [photoUploading, setPhotoUploading] = useState(false);
  const [dragActive, setDragActive] = useState(false);
  // Refs для upload-callback'ов — нужны чтобы вызвать _useImagePaste ДО
  // early-return (он содержит useEffect и должен вызываться в каждом рендере),
  // а сами функции uploadPhotoFile/uploadPhotoFromUrl определяются ниже.
  const uploadFileRef = useRef(null);
  const uploadUrlRef = useRef(null);
  useEffect(() => {
    const h = (e) => { const v = !!e.detail; _writeEditMode(v); setEditMode(v); };
    window.addEventListener('ds-editmode-changed', h);
    return () => window.removeEventListener('ds-editmode-changed', h);
  }, []);
  // Paste из буфера — ВЫЗЫВАЕТСЯ ВСЕГДА, активность управляется enabled.
  _useImagePaste({
    onFile: (f) => uploadFileRef.current && uploadFileRef.current(f),
    onUrl:  (u) => uploadUrlRef.current  && uploadUrlRef.current(u),
    enabled: editMode && !!actor?.id,
  });
  if (!editMode || !actor?.id) return null;

  // Маппинг camelCase (как в JSON / actor object) → snake_case (D1 schema).
  const KEY_MAP = {
    originalName:     'original_name',
    alsoKnownAs:      'also_known_as',
    placeOfBirth:     'place_of_birth',
    profilePath:      'profile_path',
  };
  const toSnake = (k) => KEY_MAP[k] || k;

  const updateField = async (key, value) => {
    const api = window.__dsApi;
    if (!api) throw new Error('API not configured');
    const dbKey = toSnake(key);
    await api.fetch(`/api/admin/actors/${actor.id}`, {
      method: 'PUT',
      body: JSON.stringify({ [dbKey]: value }),
    });
    // Optimistic local update
    actor[key] = value;
    actor[dbKey] = value;
    if (onUpdated) onUpdated(actor);
  };

  // Общий upload handler — вызывается и из <input type="file">, и из drop-зоны.
  const uploadPhotoFile = async (f) => {
    if (!f) return;
    if (!/^image\/(jpeg|jpg|png|webp)$/i.test(f.type)) {
      alert('Неподдерживаемый формат. JPG, PNG или WebP.');
      return;
    }
    if (f.size > 5 * 1024 * 1024) { alert('Файл слишком большой (макс 5 MB)'); return; }
    setPhotoUploading(true);
    try {
      const fd = new FormData();
      fd.append('file', f);
      const data = await window.__dsApi.fetch(`/api/admin/actors/${actor.id}/photo`, {
        method: 'POST', body: fd,
      });
      actor.profilePath = data.url;
      actor.profile_path = data.url;
      if (onUpdated) onUpdated(actor);
    } catch (err) {
      alert('Ошибка загрузки: ' + err.message);
    } finally {
      setPhotoUploading(false);
    }
  };

  const onPhotoFile = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    e.target.value = '';  // позволит выбрать тот же файл повторно
    uploadPhotoFile(f);
  };

  // Скачать картинку по URL через worker (т.к. чужие CDN блочат CORS).
  const uploadPhotoFromUrl = async (url) => {
    setPhotoUploading(true);
    try {
      const data = await window.__dsApi.fetch(`/api/admin/actors/${actor.id}/photo-from-url`, {
        method: 'POST', body: JSON.stringify({ url }),
      });
      actor.profilePath = data.url;
      actor.profile_path = data.url;
      if (onUpdated) onUpdated(actor);
    } catch (err) {
      alert('Ошибка: ' + err.message);
    } finally {
      setPhotoUploading(false);
    }
  };

  // Прокидываем callbacks в refs, которые читает _useImagePaste (вызывается
  // выше, до early-return). Перезаписываются на каждом рендере — это норма
  // для ref-сценария «вызвать хук рано, callback определить позже».
  uploadFileRef.current = uploadPhotoFile;
  uploadUrlRef.current = uploadPhotoFromUrl;

  // Drag-and-drop handlers. dragActive declared at top (rules of hooks).
  const onDragOver = (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(true); };
  const onDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); };
  const onDrop = async (e) => {
    e.preventDefault(); e.stopPropagation();
    setDragActive(false);
    const dt = e.dataTransfer;
    if (!dt) return;

    // Сценарий 1: перетащили локальный файл (из папки)
    const f = dt.files?.[0];
    if (f) { uploadPhotoFile(f); return; }

    // Сценарий 2: перетащили картинку из другой вкладки браузера —
    // прилетает URL в text/uri-list или text/plain или HTML с <img src=...>.
    // Пробуем найти URL разными способами.
    let url = '';
    try { url = dt.getData('text/uri-list') || ''; } catch {}
    if (!url) { try { url = dt.getData('text/plain') || ''; } catch {} }
    if (!url) {
      // Парсим src из HTML фрагмента
      let html = '';
      try { html = dt.getData('text/html') || ''; } catch {}
      const m = html.match(/<img[^>]+src=["']([^"']+)["']/i);
      if (m) url = m[1];
    }
    // Берём первую строку (text/uri-list может быть многострочным)
    url = url.split(/\r?\n/)[0].trim();
    if (url && /^https?:\/\//.test(url)) {
      uploadPhotoFromUrl(url);
    } else {
      alert('Не удалось определить файл или URL картинки');
    }
  };

  const currentPhoto = actor.profilePath
    ? (actor.profilePath.startsWith('http') ? actor.profilePath
       : `./assets/profiles/${actor.profilePath}`)
    : null;

  return (
    <div {..._popupDragGuard} style={{
      position: 'fixed', top: 76, right: 12, bottom: 12, width: 360,
      background: 'rgba(10,18,38,0.97)', border: '1px solid rgba(140,180,235,0.28)',
      borderRadius: 12, zIndex: 450, padding: 18, overflowY: 'auto',
      boxShadow: '-8px 8px 32px rgba(0,0,0,0.6)',
      backdropFilter: 'blur(10px)'
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14, paddingBottom: 10, borderBottom: '1px solid rgba(140,180,235,0.18)' }}>
        <div>
          <div style={{ fontSize: 10, color: '#8ba0c8', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700 }}>Edit actor</div>
          <div style={{ fontSize: 13, fontWeight: 700, color: '#e8efff', marginTop: 2 }}>id: {actor.id} · slug: {actor.slug}</div>
        </div>
        <button onClick={() => {
          window.__dsEditMode = false;
          window.dispatchEvent(new CustomEvent('ds-editmode-changed', { detail: false }));
        }} title="Close edit mode" style={{
          width: 28, height: 28, borderRadius: 6, background: 'rgba(140,180,235,0.10)', border: 'none',
          color: '#e8efff', fontSize: 16, cursor: 'pointer'
        }}>×</button>
      </div>

      {/* Фото актёра + drag-and-drop upload */}
      <div style={{ marginBottom: 14 }}>
        <label style={{ fontSize: 10, fontWeight: 700, color: '#8ba0c8', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 6, display: 'block' }}>Фото</label>
        <div
          onDragOver={onDragOver}
          onDragEnter={onDragOver}
          onDragLeave={onDragLeave}
          onDrop={onDrop}
          style={{
            display: 'flex', gap: 10, alignItems: 'flex-start',
            padding: 10, borderRadius: 8,
            background: dragActive ? 'rgba(74,158,255,0.15)' : '#131b35',
            border: `2px dashed ${dragActive ? '#7bc3ff' : '#2a3458'}`,
            transition: 'all 0.15s', position: 'relative'
          }}>
          {currentPhoto ? (
            <img src={currentPhoto} alt="" style={{ width: 80, height: 100, objectFit: 'cover', borderRadius: 6, background: '#0a1226', flexShrink: 0 }} onError={e => e.currentTarget.style.opacity = 0.2} />
          ) : (
            <div style={{ width: 80, height: 100, background: '#0a1226', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#5a6788', fontSize: 12, flexShrink: 0 }}>нет</div>
          )}
          <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
            <label style={{
              padding: '8px 12px', borderRadius: 6, background: '#1f2a4f', color: '#e8efff',
              fontSize: 11.5, cursor: photoUploading ? 'wait' : 'pointer', opacity: photoUploading ? 0.6 : 1,
              border: '1px solid #2a3458', display: 'inline-flex', alignItems: 'center', gap: 6,
              alignSelf: 'flex-start'
            }}>
              {photoUploading ? '⏳ Загрузка…' : '📷 Выбрать файл'}
              <input type="file" accept="image/jpeg,image/png,image/webp" onChange={onPhotoFile} disabled={photoUploading} style={{ display: 'none' }} />
            </label>
            <div style={{ fontSize: 11, color: dragActive ? '#7bc3ff' : '#8ba0c8', lineHeight: 1.4 }}>
              {dragActive ? '👇 Отпусти файл здесь' : '…перетащи файл или Cmd+V из буфера'}
            </div>
          </div>
        </div>
        <p style={{ fontSize: 10, color: '#5a6788', marginTop: 6 }}>JPG/PNG/WebP, до 5 MB. R2 + CDN-кэш 1 год.</p>
      </div>

      {/* Имена */}
      <_EditField label="Name (EN)" value={actor.name} onSave={(v) => updateField('name', v)} />
      <_EditField label="Имя (RU)" value={actor.name_ru} onSave={(v) => updateField('name_ru', v)} />
      <_EditField label="原 / Original (CJK)" value={actor.originalName || actor.original_name} onSave={(v) => updateField('originalName', v)} placeholder="王一博" />
      <_EditField label="Alt names (через запятую)"
        value={(actor.alsoKnownAs || []).join(', ')}
        onSave={(v) => updateField('alsoKnownAs', v.split(',').map(s => s.trim()).filter(Boolean))} />

      {/* Биография */}
      <_EditField label="Биография (RU)" type="textarea" rows={6} value={actor.bio_ru} onSave={(v) => updateField('bio_ru', v)} />
      <_EditField label="Biography (EN)" type="textarea" rows={6} value={actor.biography} onSave={(v) => updateField('biography', v)} />

      {/* Личное */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <_EditField label="День рождения" value={actor.birthday} onSave={(v) => updateField('birthday', v)} placeholder="1997-08-05" />
        <_EditField label="Место рождения" value={actor.placeOfBirth || actor.place_of_birth} onSave={(v) => updateField('placeOfBirth', v)} />
      </div>

      {/* Страна (для фильтра на /actors) — пишет в wd_citizenship */}
      <_ActorCitizenshipSelector actor={actor} onSave={(arr) => updateField('wd_citizenship', arr)} />
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <_EditField label="Рост (см)" type="number" value={actor.height_cm} onSave={(v) => updateField('height_cm', v)} />
        <_EditField label="Вес (кг)" type="number" value={actor.weight_kg} onSave={(v) => updateField('weight_kg', v)} />
      </div>

      {/* Соцсети */}
      <_EditField label="Instagram (handle, без @)" value={actor.social_instagram} onSave={(v) => updateField('social_instagram', v)} placeholder="yibo.w_85" />

      {/* Профессии и образование (JSON-массивы как CSV) */}
      <_EditField label="Профессии (через запятую)"
        value={(actor.occupation || []).join(', ')}
        onSave={(v) => updateField('occupation', v.split(',').map(s => s.trim()).filter(Boolean))}
        placeholder="Актёр, Певец, Танцор" />
      <_EditField label="Образование (через запятую)"
        value={(actor.education || []).join(', ')}
        onSave={(v) => updateField('education', v.split(',').map(s => s.trim()).filter(Boolean))} />

      {/* Галерея — фото для вкладки «Медиа» на странице актёра */}
      <_DSActorGalleryEditor actor={actor} onUpdated={onUpdated} />

      {/* Фильмография — привязка к дорамам */}
      <_DSActorDramaBinder actor={actor} />

      <div style={{ fontSize: 10, color: '#6f88b3', marginTop: 18, paddingTop: 12, borderTop: '1px solid rgba(140,180,235,0.10)', lineHeight: 1.5 }}>
        Изменения автосохраняются в D1 через <code style={{ color: '#5ed7c6' }}>/api/admin/actors/{actor.id}</code>.<br/>
      </div>
    </div>
  );
}

// ── ACTOR GALLERY EDITOR ─────────────────────────────────────────────────
// Drop-zone для загрузки фото в галерею актёра (вкладка «Медиа»).
// Поддерживает: локальные файлы, drag URL с других сайтов, удаление.
// Использует /api/admin/actors/:id/gallery (multipart) и /gallery-from-url.
function _DSActorGalleryEditor({ actor, onUpdated }) {
  const initial = Array.isArray(actor.gallery) ? actor.gallery : [];
  const [items, setItems] = useState(initial);
  const [status, setStatus] = useState('');
  const [err, setErr] = useState('');
  const [dragOver, setDragOver] = useState(false);
  const inputRef = useRef(null);
  const dragDepthRef = useRef(0);

  const setLocal = (next) => {
    setItems(next);
    actor.gallery = next;
    if (onUpdated) onUpdated(actor);
  };

  const upload = async (file) => {
    setStatus('saving'); setErr('');
    try {
      const fd = new FormData();
      fd.append('file', file);
      const j = await window.__dsApi.fetch(`/api/admin/actors/${actor.id}/gallery`, { method: 'POST', body: fd });
      setLocal(j.items || [...items, j.url]);
      setStatus('saved'); setTimeout(() => setStatus(''), 1500);
    } catch (e) {
      setErr(e.message); setStatus('error');
    }
  };

  const uploadUrl = async (url) => {
    setStatus('saving'); setErr('');
    try {
      const j = await window.__dsApi.fetch(`/api/admin/actors/${actor.id}/gallery-from-url`, {
        method: 'POST', body: JSON.stringify({ url })
      });
      setLocal(j.items || [...items, j.url]);
      setStatus('saved'); setTimeout(() => setStatus(''), 1500);
    } catch (e) {
      setErr(e.message); setStatus('error');
    }
  };

  const del = async (item) => {
    const url = typeof item === 'string' ? item : (item?.url || '');
    if (!url) return;
    if (!confirm('Удалить фото из галереи?')) return;
    setStatus('saving'); setErr('');
    try {
      const j = await window.__dsApi.fetch(`/api/admin/actors/${actor.id}/gallery`, {
        method: 'DELETE', body: JSON.stringify({ url })
      });
      setLocal(j.items || items.filter(x => (typeof x === 'string' ? x : x?.url) !== url));
      setStatus('saved'); setTimeout(() => setStatus(''), 1500);
    } catch (e) {
      setErr(e.message); setStatus('error');
    }
  };

  // Используем общий хелпер. Возвращает 1 URL (предпочитает картинку);
  // оборачиваем в массив для совместимости с существующим for-of.
  const extractUrls = (dt) => {
    const u = _extractImageUrlFromDt(dt);
    return u ? [u] : [];
  };

  const handleDrop = async (e) => {
    e.preventDefault(); e.stopPropagation();
    setDragOver(false);
    dragDepthRef.current = 0;
    const dt = e.dataTransfer;
    if (!dt) return;
    const fileList = [...(dt.files || [])].filter(f => f.type.startsWith('image/'));
    if (fileList.length) {
      for (const f of fileList) await upload(f);
      return;
    }
    const urls = extractUrls(dt);
    for (const u of urls) await uploadUrl(u);
  };

  // Для рендера превью преобразуем legacy-формат (имя файла) → полный путь.
  const previewSrc = (item) => {
    const u = typeof item === 'string' ? item : (item?.url || '');
    if (!u) return '';
    if (/^https?:\/\//i.test(u)) return u;
    return `./assets/profiles/gallery/${actor.slug}/${u}`;
  };

  return (
    <div
      style={{
        marginTop: 14, marginBottom: 14, paddingTop: 12,
        borderTop: '1px solid rgba(140,180,235,0.18)',
      }}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
        <span style={{ fontSize: 10, color: '#8ba0c8', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
          Галерея (Медиа) · {items.length}
        </span>
        <span style={{ fontSize: 9, color: status === 'saved' ? '#5ed7c6' : status === 'error' ? '#e05858' : '#6f88b3' }}>
          {status === 'saved' ? '✓ saved' : status === 'error' ? '✗ error' : status === 'saving' ? '…' : dragOver ? 'drop here' : ''}
        </span>
      </div>
      {err && (
        <div style={{ marginBottom: 6, padding: '6px 8px', background: 'rgba(196,69,69,0.12)', border: '1px solid rgba(196,69,69,0.30)', borderRadius: 6, fontSize: 10, color: '#ffb0a8', lineHeight: 1.4, wordBreak: 'break-word' }}>
          {err}
        </div>
      )}
      <div
        style={{
          padding: dragOver ? 8 : 4,
          borderRadius: 8,
          background: dragOver ? 'rgba(94,215,198,0.10)' : 'transparent',
          border: dragOver ? '2px dashed #5ed7c6' : '2px dashed rgba(94,215,198,0.18)',
          transition: 'background 0.15s, border-color 0.15s, padding 0.15s',
          display: 'flex', flexWrap: 'wrap', gap: 4,
        }}
        onDragEnter={(e) => { e.preventDefault(); e.stopPropagation(); dragDepthRef.current += 1; setDragOver(true); }}
        onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; }}
        onDragLeave={(e) => {
          e.preventDefault(); e.stopPropagation();
          dragDepthRef.current -= 1;
          if (dragDepthRef.current <= 0) { dragDepthRef.current = 0; setDragOver(false); }
        }}
        onDrop={handleDrop}
      >
        {items.map((item, i) => {
          const src = previewSrc(item);
          if (!src) return null;
          return (
            <div key={src + i} style={{ position: 'relative', width: 70, height: 92, borderRadius: 4, overflow: 'hidden', background: '#1a2348' }}>
              <img src={src} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                onError={e => e.currentTarget.style.opacity = 0.2} />
              <button onClick={() => del(item)} title="Удалить" style={{
                position: 'absolute', top: 2, right: 2, width: 18, height: 18, borderRadius: '50%',
                background: 'rgba(196,69,69,0.9)', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 11, lineHeight: 1, padding: 0
              }}>×</button>
            </div>
          );
        })}
        <button onClick={() => inputRef.current?.click()}
          title="Кликни или перетащи файлы / URL"
          style={{
            width: 70, height: 92, borderRadius: 4,
            background: 'rgba(94,215,198,0.10)', border: '1px dashed rgba(94,215,198,0.40)',
            color: '#5ed7c6', cursor: 'pointer', fontSize: 24, fontWeight: 300, padding: 0
          }}>+</button>
        <input ref={inputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
          onChange={async (e) => {
            for (const f of [...e.target.files]) await upload(f);
            e.target.value = '';
          }} />
      </div>
      <p style={{ fontSize: 10, color: '#5a6788', marginTop: 6 }}>
        JPG/PNG/WebP/GIF, до 5 MB. Перетащи файл/картинку или нажми «+».
      </p>
    </div>
  );
}

// ── DRAMA BINDER для актёра ──────────────────────────────────────────
// Показывает фильмографию (где actor уже есть в cast_list) + поиск для
// добавления новой дорамы с указанием роли. Обновляет cast_list дорамы
// через PUT /api/admin/dramas/:id.
function _DSActorDramaBinder({ actor }) {
  const [dramaPool, setDramaPool] = useState([]);
  const [q, setQ] = useState('');
  const [open, setOpen] = useState(false);
  const [picked, setPicked] = useState(null);  // выбранная дорама из дропдауна
  const [characterName, setCharacterName] = useState('');
  const [isMain, setIsMain] = useState(false);
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState('');
  const [tick, setTick] = useState(0);  // форсим перерисовку при добавлении/удалении
  const wrapRef = useRef(null);

  useEffect(() => {
    if (window.adapter?.getAllDramas) {
      const list = window.adapter.getAllDramas();
      if (Array.isArray(list)) setDramaPool(list);
    }
  }, []);
  useEffect(() => {
    const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  // Фильмография — все дорамы где actor в cast_list
  const filmography = dramaPool
    .filter(d => Array.isArray(d.cast) && d.cast.some(c => c.personId === actor.id))
    .map(d => {
      const role = d.cast.find(c => c.personId === actor.id);
      return { drama: d, role };
    })
    .sort((a, b) => (b.drama.firstAirDate || '').localeCompare(a.drama.firstAirDate || ''));

  const matches = q.trim().length === 0 ? [] : dramaPool.filter(d => {
    // Не показываем уже привязанные
    if (Array.isArray(d.cast) && d.cast.some(c => c.personId === actor.id)) return false;
    const qq = q.toLowerCase();
    return ((d.title_en || '').toLowerCase().includes(qq) ||
            (d.title_ru || '').toLowerCase().includes(qq) ||
            (d.title_original || '').includes(q));
  }).slice(0, 12);

  const bind = async () => {
    if (!picked) return;
    setSaving(true); setErr('');
    try {
      const existing = Array.isArray(picked.cast) ? picked.cast : [];
      // Не добавляем если уже есть
      if (existing.some(c => c.personId === actor.id)) {
        setErr('Этот актёр уже привязан к дораме');
        setSaving(false);
        return;
      }
      const next = [...existing, {
        personId: actor.id,
        characterName: characterName.trim() || '',
        main: !!isMain,
        order: existing.length,
      }];
      await window.__dsApi.fetch(`/api/admin/dramas/${picked.id}`, {
        method: 'PUT',
        body: JSON.stringify({ cast_list: next })
      });
      // Локально обновляем кэш adapter'а чтобы фильмография сразу показала
      picked.cast = next;
      setQ(''); setPicked(null); setCharacterName(''); setIsMain(false); setOpen(false);
      setTick(t => t + 1);
    } catch (e) {
      setErr(e.message);
    } finally { setSaving(false); }
  };

  const unbind = async (drama) => {
    if (!confirm(`Удалить из «${drama.title_ru || drama.title_en}»?`)) return;
    try {
      const next = (drama.cast || []).filter(c => c.personId !== actor.id);
      await window.__dsApi.fetch(`/api/admin/dramas/${drama.id}`, {
        method: 'PUT',
        body: JSON.stringify({ cast_list: next })
      });
      drama.cast = next;
      setTick(t => t + 1);
    } catch (e) { alert('Ошибка: ' + e.message); }
  };

  return (
    <div style={{ marginTop: 18, paddingTop: 14, borderTop: '1px solid rgba(140,180,235,0.12)' }}>
      <div style={{ fontSize: 11, color: '#5ed7c6', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '1px', marginBottom: 10 }}>
        Фильмография · {filmography.length}
      </div>

      {/* Текущие дорамы */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
        {filmography.map(({ drama, role }) => {
          const posterUrl = drama.posterPath ? window.adapter.imageUrl(drama.posterPath, 'w185') : null;
          const title = drama.title_ru || drama.title_en || `#${drama.id}`;
          const year = (drama.firstAirDate || '').slice(0, 4);
          return (
            <div key={drama.id} style={{
              display: 'flex', gap: 8, alignItems: 'center',
              background: 'rgba(20,28,52,0.5)', padding: '6px 8px', borderRadius: 6
            }}>
              {posterUrl
                ? <img src={posterUrl} alt="" title="Открыть страницу дорамы"
                    onClick={() => { window.location.hash = `#/drama/${drama.id}`; }}
                    style={{ width: 30, height: 44, objectFit: 'cover', borderRadius: 3, background: '#1a2348', flexShrink: 0, cursor: 'pointer' }}
                    onError={e => e.currentTarget.style.opacity = 0.2} />
                : <div onClick={() => { window.location.hash = `#/drama/${drama.id}`; }}
                    style={{ width: 30, height: 44, background: '#1a2348', borderRadius: 3, flexShrink: 0, cursor: 'pointer' }} />}
              <div onClick={() => { window.location.hash = `#/drama/${drama.id}`; }}
                title="Открыть страницу дорамы"
                style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}>
                <div style={{ fontSize: 12, fontWeight: 600, color: '#e8efff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {title} {year && <span style={{ color: '#8ba0c8', fontWeight: 400 }}>· {year}</span>}
                </div>
                {(role?.characterName || role?.main) && (
                  <div style={{ fontSize: 10, color: '#8ba0c8' }}>
                    {role.main && <span style={{ color: '#5ed7c6', marginRight: 4 }}>★ главная</span>}
                    {role.characterName}
                  </div>
                )}
              </div>
              <button onClick={() => unbind(drama)} title="Удалить" style={{
                padding: '2px 7px', background: 'rgba(196,69,69,0.20)', color: '#e05858',
                border: 'none', borderRadius: 4, fontSize: 11, cursor: 'pointer', flexShrink: 0
              }}>×</button>
            </div>
          );
        })}
        {filmography.length === 0 && (
          <div style={{ fontSize: 11, color: '#6f88b3', fontStyle: 'italic' }}>Дорам пока нет. Найди ниже и привяжи.</div>
        )}
      </div>

      {/* Поиск + форма привязки */}
      <div ref={wrapRef} style={{ position: 'relative' }}>
        <input
          value={q}
          onChange={(e) => { setQ(e.target.value); setOpen(true); setPicked(null); setErr(''); }}
          onFocus={() => setOpen(true)}
          placeholder="+ Привязать дораму — начни печатать название…"
          style={{
            width: '100%', padding: '8px 12px', fontSize: 13,
            background: 'rgba(20,28,52,0.6)', border: '1px solid rgba(140,180,235,0.22)',
            borderRadius: 6, color: '#e8efff', outline: 'none', boxSizing: 'border-box'
          }} />
        {open && q.trim().length > 0 && !picked && (
          <div style={{
            position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
            maxHeight: 280, overflowY: 'auto', zIndex: 10,
            background: '#0f1936', border: '1px solid rgba(94,215,198,0.45)',
            borderRadius: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.4)'
          }}>
            {matches.map(d => {
              const posterUrl = d.posterPath ? window.adapter.imageUrl(d.posterPath, 'w185') : null;
              const title = d.title_ru || d.title_en || `#${d.id}`;
              const year = (d.firstAirDate || '').slice(0, 4);
              return (
                <button key={d.id} onClick={() => { setPicked(d); setOpen(false); }}
                  style={{
                    display: 'flex', gap: 8, width: '100%', alignItems: 'center',
                    padding: '6px 10px', fontSize: 12, color: '#e8efff',
                    background: 'transparent', border: 'none', cursor: 'pointer',
                    borderBottom: '1px solid rgba(140,180,235,0.10)', textAlign: 'left'
                  }}
                  onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(94,215,198,0.10)'}
                  onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                  {posterUrl && <img src={posterUrl} style={{ width: 24, height: 36, objectFit: 'cover', borderRadius: 2, flexShrink: 0 }} />}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 600 }}>{title}</div>
                    {d.title_en && d.title_ru && <div style={{ fontSize: 10, color: '#8ba0c8' }}>{d.title_en}</div>}
                    {year && <div style={{ fontSize: 10, color: '#6f88b3' }}>{year}</div>}
                  </div>
                </button>
              );
            })}
            {matches.length === 0 && (
              <div style={{ padding: '10px 12px', fontSize: 11, color: '#6f88b3', fontStyle: 'italic' }}>Не найдено или все уже привязаны</div>
            )}
          </div>
        )}
      </div>

      {/* Inline-форма после выбора дорамы */}
      {picked && (
        <div style={{ marginTop: 8, padding: 10, background: 'rgba(94,215,198,0.08)', border: '1px solid rgba(94,215,198,0.30)', borderRadius: 6 }}>
          <div style={{ fontSize: 12, color: '#5ed7c6', marginBottom: 8 }}>
            <strong>{picked.title_ru || picked.title_en}</strong>
            <button onClick={() => { setPicked(null); setQ(''); setCharacterName(''); setIsMain(false); }}
              style={{ marginLeft: 8, fontSize: 11, color: '#8ba0c8', background: 'transparent', border: 'none', cursor: 'pointer' }}>отмена</button>
          </div>
          <input type="text" value={characterName} onChange={(e) => setCharacterName(e.target.value)}
            placeholder="Имя персонажа (опционально)"
            style={{
              width: '100%', padding: '6px 10px', fontSize: 12, marginBottom: 6,
              background: 'rgba(10,15,30,0.6)', border: '1px solid rgba(140,180,235,0.22)',
              borderRadius: 4, color: '#e8efff', outline: 'none', boxSizing: 'border-box'
            }} />
          <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#8ba0c8', cursor: 'pointer', marginBottom: 8 }}>
            <input type="checkbox" checked={isMain} onChange={(e) => setIsMain(e.target.checked)}
              style={{ accentColor: '#5ed7c6', cursor: 'pointer' }} />
            Главная роль
          </label>
          {err && <div style={{ fontSize: 11, color: '#ffb0a8', marginBottom: 6 }}>{err}</div>}
          <button onClick={bind} disabled={saving} style={{
            width: '100%', padding: '7px 12px', borderRadius: 4,
            background: '#1487cb', color: '#fff',
            border: 'none', fontSize: 12, fontWeight: 700, cursor: saving ? 'wait' : 'pointer'
          }}>{saving ? 'Сохраняем…' : '+ Привязать к фильмографии'}</button>
        </div>
      )}
    </div>
  );
}

// ── POSTER drop-zone для попапа дорамы ─────────────────────────────────────
// drag-and-drop файла + URL с других сайтов (аналог DSActorEditor.photo).
// Сохраняется в R2 через POST /api/admin/dramas/:id/poster или /poster-from-url.
function _DSEditorPoster({ show }) {
  const [uploading, setUploading] = useState(false);
  const [dragActive, setDragActive] = useState(false);
  const [bust, setBust] = useState(0);  // форсим перерисовку <img> после upload

  const currentPoster = (() => {
    const p = show.posterPath || show.poster_path;
    if (!p) return null;
    if (/^https?:\/\//i.test(p)) return p;
    return window.adapter?.imageUrl ? window.adapter.imageUrl(p, 'w500') : null;
  })();

  const uploadFile = async (f) => {
    if (!f) return;
    if (!/^image\/(jpeg|jpg|png|webp)$/i.test(f.type)) {
      alert('Только JPG, PNG или WebP.'); return;
    }
    if (f.size > 5 * 1024 * 1024) { alert('Файл > 5 MB'); return; }
    setUploading(true);
    try {
      const fd = new FormData();
      fd.append('file', f);
      const data = await window.__dsApi.fetch(`/api/admin/dramas/${show.id}/poster`, {
        method: 'POST', body: fd,
      });
      show.posterPath = data.url;
      show.poster_path = data.url;
      setBust(Date.now());
    } catch (err) {
      alert('Ошибка загрузки: ' + err.message);
    } finally {
      setUploading(false);
    }
  };

  const uploadFromUrl = async (url) => {
    setUploading(true);
    try {
      const data = await window.__dsApi.fetch(`/api/admin/dramas/${show.id}/poster-from-url`, {
        method: 'POST', body: JSON.stringify({ url }),
      });
      show.posterPath = data.url;
      show.poster_path = data.url;
      setBust(Date.now());
    } catch (err) {
      alert('Ошибка: ' + err.message);
    } finally {
      setUploading(false);
    }
  };

  const onFile = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    e.target.value = '';
    uploadFile(f);
  };

  // Ctrl/Cmd+V — вставить картинку из буфера. Активно пока попап смонтирован.
  // Скриншоты, copy-image из браузера, URL из буфера — всё работает.
  _useImagePaste({ onFile: uploadFile, onUrl: uploadFromUrl, enabled: true });

  const onDragOver = (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(true); };
  const onDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); };
  const onDrop = async (e) => {
    e.preventDefault(); e.stopPropagation();
    setDragActive(false);
    const dt = e.dataTransfer;
    if (!dt) return;
    const f = dt.files?.[0];
    if (f) { uploadFile(f); return; }
    const url = _extractImageUrlFromDt(dt);
    if (url && /^https?:\/\//.test(url)) uploadFromUrl(url);
    else alert('Не удалось определить файл или URL картинки');
  };

  return (
    <div style={{ marginBottom: 14 }}>
      <label style={{ fontSize: 10, fontWeight: 700, color: '#8ba0c8', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 6, display: 'block' }}>Постер</label>
      <div
        onDragOver={onDragOver}
        onDragEnter={onDragOver}
        onDragLeave={onDragLeave}
        onDrop={onDrop}
        style={{
          display: 'flex', gap: 10, alignItems: 'flex-start',
          padding: 10, borderRadius: 8,
          background: dragActive ? 'rgba(74,158,255,0.15)' : '#131b35',
          border: `2px dashed ${dragActive ? '#7bc3ff' : '#2a3458'}`,
          transition: 'all 0.15s', position: 'relative'
        }}>
        {currentPoster ? (
          <img src={bust ? `${currentPoster}${currentPoster.includes('?') ? '&' : '?'}b=${bust}` : currentPoster}
            alt="" style={{ width: 80, height: 116, objectFit: 'cover', borderRadius: 6, background: '#0a1226', flexShrink: 0 }}
            onError={e => e.currentTarget.style.opacity = 0.2} />
        ) : (
          <div style={{ width: 80, height: 116, background: '#0a1226', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#5a6788', fontSize: 12, flexShrink: 0 }}>нет</div>
        )}
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
          <label style={{
            padding: '8px 12px', borderRadius: 6, background: '#1f2a4f', color: '#e8efff',
            fontSize: 11.5, cursor: uploading ? 'wait' : 'pointer', opacity: uploading ? 0.6 : 1,
            border: '1px solid #2a3458', display: 'inline-flex', alignItems: 'center', gap: 6,
            alignSelf: 'flex-start'
          }}>
            {uploading ? '⏳ Загрузка…' : '🖼️ Выбрать файл'}
            <input type="file" accept="image/jpeg,image/png,image/webp" onChange={onFile} disabled={uploading} style={{ display: 'none' }} />
          </label>
          <div style={{ fontSize: 11, color: dragActive ? '#7bc3ff' : '#8ba0c8', lineHeight: 1.4 }}>
            {dragActive ? '👇 Отпусти здесь' : '…перетащи файл / URL или Cmd+V'}
          </div>
        </div>
      </div>
      <p style={{ fontSize: 10, color: '#5a6788', marginTop: 6 }}>JPG/PNG/WebP, до 5 MB. R2 + CDN-кэш 1 год. Скриншот в буфере → Ctrl+V в зону.</p>
    </div>
  );
}

function _DSEditorSection({ title, children }) {
  return (
    <div style={{
      marginTop: 18, paddingTop: 14,
      borderTop: '1px solid rgba(140,180,235,0.12)'
    }}>
      <div style={{ fontSize: 11, color: '#5ed7c6', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '1px', marginBottom: 10 }}>{title}</div>
      {children}
    </div>
  );
}

// ── ФОТО ── 3 категории, drag/drop файл / URL → R2.
// Хранятся как массивы URL'ов в JSON-полях dramas (см. _PHOTO_KINDS.field).
// Каждый элемент может быть либо строкой-URL, либо объектом {url, name?}.
const _PHOTO_KINDS = [
  { key: 'posters', label: 'Постеры (альт)',   field: 'posters_alt',    folder: 'posters-alt' },
  { key: 'stills',  label: 'Кадры',            field: 'gallery',        folder: 'gallery' },
  { key: 'extras',  label: 'Доп. изображения', field: 'gallery_extras', folder: 'gallery-extras' },
];

// Резолвер фото в попапе админки — синхронизирован с публичным DramaMediaSection.
// В БД хранится либо относительное имя файла (poster-1.webp), либо полный URL
// (R2 https://… либо /posters/…). Относительные имена нужно префиксить
// ./assets/dramas/<folder>/<slug>/<file>.
function _photoFullUrl(item, show, folder) {
  const u = _photoUrlOf(item);
  if (!u) return '';
  if (/^https?:\/\//i.test(u) || u.startsWith('/') || u.startsWith('./')) return u;
  return `./assets/dramas/${folder}/${show.slug}/${u}`;
}

function _DSEditorPhotos({ show }) {
  if (!show?.id) return null;
  return (
    <_DSEditorSection title="Фото">
      {_PHOTO_KINDS.map(k => (
        <_DSPhotoKindBlock key={k.key} show={show} kindConfig={k} />
      ))}
    </_DSEditorSection>
  );
}

// Извлекаем URL из элемента массива (поддерживаем оба формата — string и {url}).
function _photoUrlOf(item) {
  if (typeof item === 'string') return item;
  if (item && typeof item === 'object') return item.url || '';
  return '';
}

function _DSPhotoKindBlock({ show, kindConfig }) {
  const { key: kind, field, label, folder } = kindConfig;
  const [files, setFiles] = useState(show[field] || []);
  const [status, setStatus] = useState('');
  const [err, setErr] = useState('');
  const [dragOver, setDragOver] = useState(false);
  const inputRef = useRef(null);
  const dragDepthRef = useRef(0);

  const setLocal = (next) => {
    setFiles(next);
    show[field] = next;
  };

  const upload = async (file) => {
    setStatus('saving'); setErr('');
    try {
      const fd = new FormData();
      fd.append('file', file);
      fd.append('kind', kind);
      const j = await window.__dsApi.fetch(`/api/admin/dramas/${show.id}/photo`, { method: 'POST', body: fd });
      setLocal(j.items || [...files, j.url]);
      setStatus('saved'); setTimeout(() => setStatus(''), 1500);
    } catch (e) {
      setErr(e.message); setStatus('error');
    }
  };

  const uploadUrl = async (url) => {
    setStatus('saving'); setErr('');
    try {
      const j = await window.__dsApi.fetch(`/api/admin/dramas/${show.id}/photo-from-url`, {
        method: 'POST', body: JSON.stringify({ url, kind })
      });
      setLocal(j.items || [...files, j.url]);
      setStatus('saved'); setTimeout(() => setStatus(''), 1500);
    } catch (e) {
      setErr(e.message); setStatus('error');
    }
  };

  const del = async (item) => {
    const url = _photoUrlOf(item);
    if (!url) return;
    if (!confirm('Удалить фото?')) return;
    setStatus('saving'); setErr('');
    try {
      const j = await window.__dsApi.fetch(`/api/admin/dramas/${show.id}/photo`, {
        method: 'DELETE', body: JSON.stringify({ url, kind })
      });
      setLocal(j.items || files.filter(x => _photoUrlOf(x) !== url));
      setStatus('saved'); setTimeout(() => setStatus(''), 1500);
    } catch (e) {
      setErr(e.message); setStatus('error');
    }
  };

  // Используем общий хелпер. Возвращает 1 URL (предпочитает картинку);
  // оборачиваем в массив для совместимости с существующим for-of.
  const extractUrls = (dt) => {
    const u = _extractImageUrlFromDt(dt);
    return u ? [u] : [];
  };

  const handleDrop = async (e) => {
    e.preventDefault(); e.stopPropagation();
    setDragOver(false);
    dragDepthRef.current = 0;
    const dt = e.dataTransfer;
    if (!dt) return;
    const fileList = [...(dt.files || [])].filter(f => f.type.startsWith('image/'));
    if (fileList.length) {
      for (const f of fileList) await upload(f);
      return;
    }
    const urls = extractUrls(dt);
    for (const u of urls) await uploadUrl(u);
  };

  return (
    <div
      style={{
        marginBottom: 12,
        padding: dragOver ? 8 : 0,
        borderRadius: 8,
        background: dragOver ? 'rgba(94,215,198,0.10)' : 'transparent',
        border: dragOver ? '2px dashed #5ed7c6' : '2px dashed transparent',
        transition: 'background 0.15s, border-color 0.15s, padding 0.15s'
      }}
      onDragEnter={(e) => { e.preventDefault(); e.stopPropagation(); dragDepthRef.current += 1; setDragOver(true); }}
      onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; }}
      onDragLeave={(e) => {
        e.preventDefault(); e.stopPropagation();
        dragDepthRef.current -= 1;
        if (dragDepthRef.current <= 0) { dragDepthRef.current = 0; setDragOver(false); }
      }}
      onDrop={handleDrop}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
        <span style={{ fontSize: 10, color: '#8ba0c8', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{label} · {files.length}</span>
        <span style={{ fontSize: 9, color: status === 'saved' ? '#5ed7c6' : status === 'error' ? '#e05858' : '#6f88b3' }}>
          {status === 'saved' ? '✓ saved' : status === 'error' ? '✗ error' : status === 'saving' ? '…' : dragOver ? 'drop here' : ''}
        </span>
      </div>
      {err && (
        <div style={{ marginBottom: 6, padding: '6px 8px', background: 'rgba(196,69,69,0.12)', border: '1px solid rgba(196,69,69,0.30)', borderRadius: 6, fontSize: 10, color: '#ffb0a8', lineHeight: 1.4, wordBreak: 'break-word' }}>
          {err}
        </div>
      )}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
        {files.map((item, i) => {
          const u = _photoUrlOf(item);
          if (!u) return null;
          const fullUrl = _photoFullUrl(item, show, folder);
          return (
            <div key={u + i} style={{ position: 'relative', width: 56, height: 78, borderRadius: 4, overflow: 'hidden', background: '#1a2348' }}>
              <img src={fullUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                onError={e => e.currentTarget.style.opacity = 0.2} />
              <button onClick={() => del(item)} title="Удалить" style={{
                position: 'absolute', top: 2, right: 2, width: 18, height: 18, borderRadius: '50%',
                background: 'rgba(196,69,69,0.9)', color: '#fff', border: 'none', cursor: 'pointer', fontSize: 11, lineHeight: 1, padding: 0
              }}>×</button>
            </div>
          );
        })}
        <button onClick={() => inputRef.current?.click()}
          title="Кликни или перетащи файлы / URL"
          style={{
            width: 56, height: 78, borderRadius: 4,
            background: 'rgba(94,215,198,0.10)', border: '1px dashed rgba(94,215,198,0.40)',
            color: '#5ed7c6', cursor: 'pointer', fontSize: 22, fontWeight: 300, padding: 0
          }}>+</button>
        <input ref={inputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
          onChange={async (e) => {
            for (const f of [...e.target.files]) await upload(f);
            e.target.value = '';
          }} />
      </div>
    </div>
  );
}

// ── ТРЕЙЛЕРЫ ──
// Хранятся как JSON-массив в dramas.trailers. Формат: [{youtube_id, title}, ...].
// Поддерживаем добавление по любой YouTube/Bilibili-ссылке + ручному ID.
// Сохраняется через updateField('trailers', next) → PUT /api/admin/dramas/:id.
function _parseYouTubeId(input) {
  if (!input) return null;
  const s = String(input).trim();
  // Если просто 11-символьный ID — берём как есть
  if (/^[\w-]{11}$/.test(s)) return s;
  // youtu.be/XXXXX
  let m = s.match(/youtu\.be\/([\w-]{11})/);
  if (m) return m[1];
  // youtube.com/watch?v=XXXXX
  m = s.match(/[?&]v=([\w-]{11})/);
  if (m) return m[1];
  // youtube.com/embed/XXXXX или /shorts/XXXXX
  m = s.match(/youtube\.com\/(?:embed|shorts|v)\/([\w-]{11})/);
  if (m) return m[1];
  return null;
}

function _DSEditorTrailers({ show, updateField }) {
  const [trailers, setTrailers] = useState(show.trailers || []);
  const [newUrl, setNewUrl] = useState('');
  const [newTitle, setNewTitle] = useState('');
  const [status, setStatus] = useState('');
  const [err, setErr] = useState('');

  const save = async (next) => {
    setTrailers(next);
    show.trailers = next;
    try { await updateField('trailers', next); }
    catch (e) { console.error('save trailers', e); setStatus('error'); setErr(e.message); return; }
    setStatus('saved');
    setTimeout(() => setStatus(''), 1500);
  };

  const add = async () => {
    setErr('');
    const id = _parseYouTubeId(newUrl);
    if (!id) {
      setErr('Не похоже на YouTube-ссылку. Поддерживается youtu.be/X, youtube.com/watch?v=X, или просто 11-символьный ID.');
      return;
    }
    if (trailers.some(t => t.youtube_id === id)) {
      setErr('Этот трейлер уже добавлен.');
      return;
    }
    setStatus('saving');
    const next = [...trailers, { youtube_id: id, title: newTitle.trim() || null }];
    await save(next);
    setNewUrl(''); setNewTitle('');
  };

  const del = async (id) => {
    if (!confirm('Удалить трейлер?')) return;
    setStatus('saving');
    await save(trailers.filter(t => t.youtube_id !== id));
  };

  return (
    <_DSEditorSection title={`Трейлеры · ${trailers.length}`}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 10 }}>
        {trailers.map(t => (
          <div key={t.youtube_id} style={{ display: 'flex', alignItems: 'center', gap: 6, background: '#1a2348', borderRadius: 6, padding: 6 }}>
            <img src={`https://i.ytimg.com/vi/${t.youtube_id}/mqdefault.jpg`} alt=""
              onClick={() => window.open(`https://youtu.be/${t.youtube_id}`, '_blank')}
              style={{ width: 64, height: 36, objectFit: 'cover', borderRadius: 3, cursor: 'pointer', flexShrink: 0 }} />
            <span style={{ flex: 1, minWidth: 0, fontSize: 11, color: '#e8efff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.title || t.youtube_id}</span>
            <button onClick={() => del(t.youtube_id)} style={{ width: 22, height: 22, borderRadius: 4, background: 'rgba(196,69,69,0.20)', color: '#e05858', border: 'none', fontSize: 13, cursor: 'pointer', padding: 0 }}>×</button>
          </div>
        ))}
        {trailers.length === 0 && (
          <div style={{ fontSize: 11, color: '#6f88b3', fontStyle: 'italic' }}>Трейлеров пока нет</div>
        )}
      </div>
      <input value={newUrl} onChange={(e) => { setNewUrl(e.target.value); setErr(''); }} placeholder="https://youtu.be/... или ID"
        style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none', marginBottom: 6 }} />
      <input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder="Название (опционально)"
        style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none', marginBottom: 6 }} />
      {err && <div style={{ fontSize: 10.5, color: '#ffb0a8', marginBottom: 6, lineHeight: 1.3 }}>{err}</div>}
      <button onClick={add} disabled={!newUrl.trim() || status === 'saving'} style={{
        width: '100%', padding: '7px 12px', borderRadius: 6,
        background: newUrl.trim() ? 'rgba(94,215,198,0.20)' : 'rgba(140,180,235,0.05)',
        color: newUrl.trim() ? '#5ed7c6' : '#6f88b3',
        border: 'none', fontSize: 12, fontWeight: 700, cursor: newUrl.trim() ? 'pointer' : 'default'
      }}>+ Добавить трейлер {status === 'saved' && '· ✓'} {status === 'saving' && '…'}</button>
    </_DSEditorSection>
  );
}

// ── OST editor (саундтрек дорамы) ─────────────────────────────────────────
// Три ячейки: YouTube (обязательная база) / Spotify (опц.) / Apple Music (опц.).
// В каждую — paste URL → server-side parseMusicUrl → preview embed → save/delete.
// Endpoints: /api/admin/content/ost (CRUD), /api/admin/content/parse-music-url.
function _DSEditorOst({ show }) {
  const [items, setItems] = useState([]);    // существующие OST из БД
  const [loaded, setLoaded] = useState(false);
  const api = (typeof window !== 'undefined') ? window.__dsApi : null;

  const reload = async () => {
    if (!api || !show?.id) return;
    try {
      const data = await api.fetch(`/api/admin/content/ost/by-drama/${show.id}`);
      setItems(data.items || []);
    } catch (e) { console.error('[ost reload]', e); }
    finally { setLoaded(true); }
  };
  useEffect(() => { reload(); }, [show?.id]);

  const grouped = {};
  for (const it of items) grouped[it.platform] = it;
  const PLATFORMS = [
    { key: 'youtube',     label: 'YouTube',       hint: 'youtube.com/playlist?list=… или youtu.be/…  (работает у всех)' },
    { key: 'soundcloud',  label: 'SoundCloud',    hint: 'soundcloud.com/{user}/sets/{playlist} или /{track}  (играет полные треки бесплатно, доступен в РФ)' },
    { key: 'yandex',      label: 'Яндекс.Музыка', hint: 'music.yandex.ru/album/… или /users/{login}/playlists/{kind}' },
    { key: 'spotify',     label: 'Spotify',       hint: 'open.spotify.com/playlist|album|track/…' },
    { key: 'apple_music', label: 'Apple Music',   hint: 'music.apple.com/{country}/album|playlist/…' },
  ];

  return (
    <_DSEditorSection title={`Саундтрек · ${items.length}/5`}>
      {!grouped.youtube && items.length > 0 && (
        <div style={{ fontSize: 10.5, color: '#ffb0a8', marginBottom: 8, lineHeight: 1.4 }}>
          ⚠️ Нет YouTube — у юзеров без аккаунтов в стриминговых сервисах не будет дефолтного плеера.
        </div>
      )}
      {PLATFORMS.map(p => (
        <_DSEditorOstRow
          key={p.key}
          dramaId={show.id}
          platform={p.key}
          label={p.label}
          hint={p.hint}
          current={grouped[p.key] || null}
          onChanged={reload}
        />
      ))}
    </_DSEditorSection>
  );
}

function _DSEditorOstRow({ dramaId, platform, label, hint, current, onChanged }) {
  const api = (typeof window !== 'undefined') ? window.__dsApi : null;
  const [url, setUrl] = useState('');
  const [titleRu, setTitleRu] = useState(current?.title_ru || '');
  const [titleEn, setTitleEn] = useState(current?.title_en || '');
  const [artist, setArtist] = useState(current?.artist || '');
  const [status, setStatus] = useState('');
  const [err, setErr] = useState('');
  const [expanded, setExpanded] = useState(false);

  useEffect(() => {
    setTitleRu(current?.title_ru || '');
    setTitleEn(current?.title_en || '');
    setArtist(current?.artist || '');
  }, [current?.id]);

  const save = async () => {
    setErr(''); setStatus('saving');
    try {
      let body;
      if (url.trim()) {
        // Новый URL — сервер сам распарсит платформу
        body = { drama_id: dramaId, url: url.trim(), title_ru: titleRu, title_en: titleEn, artist };
      } else if (current) {
        // Без URL — только обновляем мета
        body = { title_ru: titleRu, title_en: titleEn, artist };
      } else {
        setErr('Вставь URL'); setStatus(''); return;
      }
      if (current && !url.trim()) {
        await api.fetch(`/api/admin/content/ost/${current.id}`, { method: 'PUT', body: JSON.stringify(body) });
      } else {
        const res = await api.fetch('/api/admin/content/ost', { method: 'POST', body: JSON.stringify(body) });
        // Если был POST с url, парсер на сервере вернёт правильную платформу.
        // Сервер делает UPSERT по (drama_id, platform), так что не страшно.
      }
      setUrl('');
      setStatus('saved');
      setTimeout(() => setStatus(''), 1500);
      onChanged && onChanged();
    } catch (e) {
      console.error('[ost save]', e);
      setErr(e?.message || 'Ошибка');
      setStatus('');
    }
  };

  const del = async () => {
    if (!current || !confirm(`Удалить ${label} плеер?`)) return;
    try {
      await api.fetch(`/api/admin/content/ost/${current.id}`, { method: 'DELETE' });
      onChanged && onChanged();
    } catch (e) {
      setErr(e?.message || 'Ошибка удаления');
    }
  };

  // Preview embed для существующего
  const buildEmbedUrl = (item) => {
    if (item.platform === 'spotify') {
      const kind = item.embed_kind || 'playlist';
      return `https://open.spotify.com/embed/${kind}/${item.embed_id}?utm_source=generator`;
    }
    if (item.platform === 'apple_music') {
      const kind = item.embed_kind === 'track' ? 'song' : (item.embed_kind || 'album');
      const country = item.embed_country || 'us';
      return `https://embed.music.apple.com/${country}/${kind}/_/${item.embed_id}`;
    }
    if (item.platform === 'yandex') {
      const kind = item.embed_kind || 'album';
      return `https://music.yandex.ru/iframe/#${kind}/${item.embed_id}`;
    }
    if (item.platform === 'soundcloud') {
      const fullUrl = `https://soundcloud.com/${item.embed_id}`;
      const enc = encodeURIComponent(fullUrl);
      return `https://w.soundcloud.com/player/?url=${enc}&color=%23007aff&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false&visual=false`;
    }
    if (item.embed_kind === 'playlist') {
      return `https://www.youtube-nocookie.com/embed/videoseries?list=${item.embed_id}&rel=0`;
    }
    return `https://www.youtube-nocookie.com/embed/${item.embed_id}?rel=0`;
  };

  return (
    <div style={{
      marginBottom: 10, background: '#1a2348', borderRadius: 6, padding: 8,
      border: current ? '1px solid rgba(94,215,198,0.30)' : '1px solid #2a3458',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
        <strong style={{ fontSize: 12, color: '#e8efff' }}>{label}</strong>
        {current ? (
          <span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: 'rgba(94,215,198,0.18)', color: '#5ed7c6', fontWeight: 600 }}>✓ есть</span>
        ) : (
          <span style={{ fontSize: 10, color: '#6f88b3' }}>не настроен</span>
        )}
        {current && (
          <button onClick={() => setExpanded(v => !v)} style={{
            marginLeft: 'auto', fontSize: 10, padding: '2px 8px', borderRadius: 4,
            background: 'transparent', color: '#6f88b3', border: '1px solid #2a3458', cursor: 'pointer',
          }}>{expanded ? 'Свернуть' : 'Редактировать'}</button>
        )}
      </div>

      {/* Превью если уже есть */}
      {current && !expanded && (
        <div style={{ background: '#0e1530', borderRadius: 4, padding: 6, fontSize: 11, color: '#9fb4e0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {current.embed_kind || 'embed'}: <code style={{ color: '#5ed7c6' }}>{current.embed_id}</code>
            {current.title_ru || current.title_en ? ` · ${current.title_ru || current.title_en}` : ''}
          </span>
        </div>
      )}

      {(expanded || !current) && (
        <>
          <input
            value={url}
            onChange={(e) => { setUrl(e.target.value); setErr(''); }}
            placeholder={current ? 'Новая ссылка (заменит текущую) или пусто чтобы только поменять название' : hint}
            style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 11, outline: 'none', marginBottom: 6, boxSizing: 'border-box' }}
          />
          <div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
            <input value={titleRu} onChange={(e) => setTitleRu(e.target.value)} placeholder="Название RU (опц.)" style={{ flex: 1, padding: '6px 8px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 11, outline: 'none' }} />
            <input value={titleEn} onChange={(e) => setTitleEn(e.target.value)} placeholder="EN (опц.)" style={{ flex: 1, padding: '6px 8px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 11, outline: 'none' }} />
            <input value={artist} onChange={(e) => setArtist(e.target.value)} placeholder="Артист" style={{ flex: 1, padding: '6px 8px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 11, outline: 'none' }} />
          </div>
          {err && <div style={{ fontSize: 10.5, color: '#ffb0a8', marginBottom: 6, lineHeight: 1.3 }}>{err}</div>}
          <div style={{ display: 'flex', gap: 6 }}>
            <button onClick={save} disabled={(!url.trim() && !current) || status === 'saving'} style={{
              flex: 1, padding: '6px 10px', borderRadius: 6,
              background: (url.trim() || current) ? 'rgba(94,215,198,0.20)' : 'rgba(140,180,235,0.05)',
              color: (url.trim() || current) ? '#5ed7c6' : '#6f88b3',
              border: 'none', fontSize: 11, fontWeight: 700, cursor: (url.trim() || current) ? 'pointer' : 'default'
            }}>{current ? 'Сохранить' : '+ Добавить'} {status === 'saved' && '· ✓'} {status === 'saving' && '…'}</button>
            {current && (
              <button onClick={del} style={{
                padding: '6px 12px', borderRadius: 6, background: 'rgba(196,69,69,0.20)',
                color: '#e05858', border: 'none', fontSize: 11, fontWeight: 700, cursor: 'pointer',
              }}>Удалить</button>
            )}
          </div>
        </>
      )}

      {/* Иконка-превью embed при expanded */}
      {current && expanded && (
        <div style={{ marginTop: 8, background: '#000', borderRadius: 4, overflow: 'hidden' }}>
          {current.platform === 'youtube' ? (
            <div style={{ position: 'relative', paddingBottom: '56.25%', height: 0 }}>
              <iframe src={buildEmbedUrl(current)} title="preview" allowFullScreen style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 0 }} />
            </div>
          ) : (
            <iframe
              src={buildEmbedUrl(current)}
              title="preview"
              style={{ width: '100%', height: (current.platform === 'apple_music' || current.platform === 'yandex' || (current.platform === 'soundcloud' && current.embed_kind === 'playlist')) ? 200 : 160, border: 0, background: '#fff' }}
            />
          )}
        </div>
      )}
    </div>
  );
}

// ── PEOPLE-SEARCH общая инфра для cast/crew редактора ───────────────────────
// Загружаем people.json один раз и кэшируем в модуле; useState + промис
// гарантируют что параллельные открытия редактора шарят один и тот же запрос.
let _PEOPLE_CACHE = null;
let _PEOPLE_PROMISE = null;
function _loadPeople() {
  if (_PEOPLE_CACHE) return Promise.resolve(_PEOPLE_CACHE);
  if (_PEOPLE_PROMISE) return _PEOPLE_PROMISE;
  // Prod: тянем АКТЁРОВ ВСЕХ из D1 через /api/actors?limit=20000. Без явного
  // limit endpoint возвращает 50-100 — поиск в редакторе cast не находил большинство
  // актёров (д) и при добавлении показывал "#id" вместо имени (е) потому что
  // personById не находил их в кэше. Fallback на static JSON для локалки.
  const apiBase = window.__dsApi?.base || '';
  const url = apiBase ? `${apiBase}/api/actors?limit=20000` : `./data/people.json?t=${Date.now()}`;
  _PEOPLE_PROMISE = fetch(url)
    .then(r => r.json())
    .then(arr => { _PEOPLE_CACHE = Array.isArray(arr) ? arr : []; return _PEOPLE_CACHE; })
    .catch(() => { _PEOPLE_CACHE = []; return _PEOPLE_CACHE; });
  return _PEOPLE_PROMISE;
}
function _personPhoto(p) {
  if (!p || !p.profilePath) return '';
  // profilePath может быть полным URL (R2) или относительным путём TMDB.
  // window.adapter.profileImageUrl делает обе схемы.
  if (window.adapter?.profileImageUrl) {
    return window.adapter.profileImageUrl(p.profilePath) || '';
  }
  if (/^https?:\/\//i.test(p.profilePath)) return p.profilePath;
  return `./assets/profiles/${p.slug}.webp`;
}

// Поиск персоны по подстроке имени (en/ru/original) с дропдауном-выпадашкой.
// onPick(person) — колбэк при выборе.
// excludeIds — set/array уже добавленных, чтобы не дублировать.
// Если совпадений нет, в конце дропдауна показывается "+ Создать <q>".
function _DSPersonSearch({ excludeIds, onPick, placeholder }) {
  const [q, setQ] = useState('');
  const [people, setPeople] = useState([]);
  const [open, setOpen] = useState(false);
  const [creating, setCreating] = useState(false);
  const [createName, setCreateName] = useState('');
  const [createNameRu, setCreateNameRu] = useState('');
  const [createOriginal, setCreateOriginal] = useState('');
  const [createErr, setCreateErr] = useState('');
  const wrapRef = useRef(null);
  useEffect(() => { _loadPeople().then(setPeople); }, []);
  useEffect(() => {
    const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); setCreating(false); } };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);
  const exclude = new Set(excludeIds || []);
  const matches = q.trim().length === 0 ? [] : people.filter(p => {
    if (exclude.has(p.id)) return false;
    const qq = q.toLowerCase();
    return ((p.name || '').toLowerCase().includes(qq) ||
            (p.name_ru || '').toLowerCase().includes(qq) ||
            (p.originalName || '').toLowerCase().includes(qq));
  }).slice(0, 15);

  const startCreate = () => {
    // Угадываем язык введённого запроса: если кириллица → name_ru, иначе → name
    const isCyrillic = /[а-яё]/i.test(q);
    setCreateName(isCyrillic ? '' : q.trim());
    setCreateNameRu(isCyrillic ? q.trim() : '');
    setCreateOriginal('');
    setCreateErr('');
    setCreating(true);
    setOpen(true);  // дублируем — на случай если open был сброшен
  };
  const submitCreate = async () => {
    setCreateErr('');
    const nameEn = createName.trim();
    const nameRu = createNameRu.trim();
    const nameOrig = createOriginal.trim();
    if (!nameEn && !nameRu && !nameOrig) {
      setCreateErr('Нужно имя (английский / русский / оригинальное)');
      return;
    }
    try {
      const api = window.__dsApi;
      if (!api) throw new Error('API не подключён — перезагрузи страницу');
      // worker возвращает { ok, id, slug } — строим минимальный объект сами
      const j = await api.fetch(`/api/admin/actors`, {
        method: 'POST',
        body: JSON.stringify({
          name: nameEn,
          name_ru: nameRu,
          original_name: nameOrig,
        })
      });
      if (!j || !j.ok) throw new Error(j?.error || 'create failed');
      const newPerson = {
        id: j.id,
        slug: j.slug,
        name: nameEn || null,
        name_ru: nameRu || null,
        originalName: nameOrig || null,
        profilePath: null,
      };
      _PEOPLE_CACHE = [newPerson, ...(_PEOPLE_CACHE || people)];
      setPeople(_PEOPLE_CACHE);
      onPick(newPerson);
      setQ(''); setOpen(false); setCreating(false);
    } catch (e) {
      console.error('[create actor]', e);
      setCreateErr(e.message || 'Ошибка создания');
    }
  };

  return (
    <div ref={wrapRef} style={{ position: 'relative', marginTop: 8 }}>
      <input
        value={q}
        onChange={(e) => { setQ(e.target.value); setOpen(true); setCreating(false); }}
        onFocus={() => setOpen(true)}
        placeholder={placeholder || 'Поиск по имени…'}
        style={{
          width: '100%', padding: '8px 12px', fontSize: 13,
          background: 'rgba(20,28,52,0.6)', border: '1px solid rgba(140,180,235,0.22)',
          borderRadius: 6, color: '#e8efff', outline: 'none', boxSizing: 'border-box'
        }} />
      {open && q.trim().length > 0 && !creating && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
          maxHeight: 260, overflowY: 'auto', zIndex: 10,
          background: '#0f1936', border: '1px solid rgba(94,215,198,0.45)',
          borderRadius: 6, boxShadow: '0 8px 20px rgba(0,0,0,0.55)'
        }}>
          {matches.map(p => (
            <div key={p.id}
              onMouseDown={(e) => { e.preventDefault(); onPick(p); setQ(''); setOpen(false); }}
              style={{
                display: 'flex', gap: 8, alignItems: 'center',
                padding: '6px 10px', cursor: 'pointer'
              }}
              onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(94,215,198,0.10)'}
              onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
              <img src={_personPhoto(p)} alt="" onError={(e) => e.currentTarget.style.opacity = 0.2}
                style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', background: '#1a2348', flexShrink: 0 }} />
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 12, color: '#e8efff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {p.name_ru || p.name || `#${p.id}`}
                </div>
                <div style={{ fontSize: 10, color: '#8ba0c8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {p.name_ru && p.name ? p.name : (p.originalName || '')}
                </div>
              </div>
            </div>
          ))}
          {/* Опция "Создать <q>" — внизу дропдауна. Используем <button> + onClick
              (а не div+onMouseDown) для надёжности: button click не закрывает
              dropdown через document-mousedown listener (wrapRef.contains всё ещё true). */}
          <button type="button"
            onMouseDown={(e) => { e.preventDefault(); /* не теряем фокус инпута */ }}
            onClick={(e) => { e.preventDefault(); e.stopPropagation(); startCreate(); }}
            style={{
              width: '100%', textAlign: 'left',
              padding: '8px 10px', borderTop: matches.length > 0 ? '1px solid rgba(140,180,235,0.12)' : 'none',
              background: 'rgba(94,215,198,0.06)', cursor: 'pointer',
              fontSize: 12, color: '#5ed7c6', fontWeight: 700,
              display: 'flex', alignItems: 'center', gap: 6,
              border: 'none', borderRadius: 0, font: 'inherit'
            }}
            onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(94,215,198,0.18)'}
            onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(94,215,198,0.06)'}>
            <span style={{ fontSize: 14, lineHeight: 1 }}>+</span>
            <span>Создать «{q}» — нет в базе</span>
          </button>
        </div>
      )}
      {creating && (
        <div
          onClick={(e) => e.stopPropagation()}
          onMouseDown={(e) => e.stopPropagation()}
          style={{
            position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0, zIndex: 1000,
            background: '#0f1936', border: '2px solid #5ed7c6',
            borderRadius: 6, boxShadow: '0 8px 30px rgba(0,0,0,0.75), 0 0 0 100px rgba(0,0,0,0.4)',
            padding: 12
          }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
            <div style={{ fontSize: 11, color: '#5ed7c6', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1 }}>
              Новая персона
            </div>
            <button type="button" onClick={(e) => { e.stopPropagation(); setCreating(false); }}
              title="Закрыть"
              style={{ width: 22, height: 22, borderRadius: 4, background: 'rgba(140,180,235,0.10)',
                color: '#e8efff', fontSize: 14, lineHeight: 1, border: 'none', cursor: 'pointer', padding: 0 }}>×</button>
          </div>
          <input autoFocus value={createName} onChange={(e) => setCreateName(e.target.value)}
            placeholder="Name (English) — напр. Wang Yibo"
            style={{ width: '100%', padding: '7px 9px', fontSize: 12, marginBottom: 6,
              background: 'rgba(10,15,30,0.85)', border: '1px solid rgba(140,180,235,0.30)',
              borderRadius: 4, color: '#e8efff', outline: 'none', boxSizing: 'border-box' }} />
          <input value={createNameRu} onChange={(e) => setCreateNameRu(e.target.value)}
            placeholder="Имя (Русский) — напр. Ван Ибо"
            style={{ width: '100%', padding: '7px 9px', fontSize: 12, marginBottom: 6,
              background: 'rgba(10,15,30,0.85)', border: '1px solid rgba(140,180,235,0.30)',
              borderRadius: 4, color: '#e8efff', outline: 'none', boxSizing: 'border-box' }} />
          <input value={createOriginal} onChange={(e) => setCreateOriginal(e.target.value)}
            placeholder="Original (CJK) — напр. 王一博 — опционально"
            style={{ width: '100%', padding: '7px 9px', fontSize: 12, marginBottom: 8,
              background: 'rgba(10,15,30,0.85)', border: '1px solid rgba(140,180,235,0.30)',
              borderRadius: 4, color: '#e8efff', outline: 'none', boxSizing: 'border-box' }} />
          {createErr && (
            <div style={{ color: '#ffb0a8', background: 'rgba(196,69,69,0.15)', border: '1px solid rgba(196,69,69,0.4)',
              padding: '6px 8px', borderRadius: 4, fontSize: 11, marginBottom: 8, lineHeight: 1.4 }}>
              ⚠️ {createErr}
            </div>
          )}
          <div style={{ display: 'flex', gap: 6 }}>
            <button type="button"
              onClick={(e) => { e.stopPropagation(); submitCreate(); }}
              style={{ flex: 1, padding: '8px 10px', borderRadius: 4, fontSize: 12, fontWeight: 700,
                background: '#1487cb', color: '#fff', border: 'none', cursor: 'pointer' }}>
              ✓ Создать и добавить
            </button>
            <button type="button"
              onClick={(e) => { e.stopPropagation(); setCreating(false); }}
              style={{ padding: '8px 10px', borderRadius: 4, fontSize: 12, fontWeight: 400,
                background: 'transparent', color: '#8ba0c8', border: '1px solid rgba(140,180,235,0.22)', cursor: 'pointer' }}>
              Отмена
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// CAST editor — список актёров с именем персонажа и чекбоксом "главная роль"
function _DSEditorCast({ show, updateField }) {
  const [cast, setCast] = useState(show.cast || []);
  const [people, setPeople] = useState([]);
  useEffect(() => { _loadPeople().then(setPeople); }, []);
  const personById = (id) => people.find(p => p.id === id);
  const save = async (next) => {
    setCast(next);
    show.cast = next;
    try { await updateField('cast', next); } catch (e) { console.error('save cast', e); }
  };
  const onAdd = (person) => {
    if (cast.some(c => c.personId === person.id)) return;
    save([...cast, { personId: person.id, characterName: '', main: false, order: cast.length }]);
  };
  const onChar = (pid, characterName) => save(cast.map(c => c.personId === pid ? { ...c, characterName } : c));
  const onMain = (pid, main) => save(cast.map(c => c.personId === pid ? { ...c, main } : c));
  const onRm = (pid) => save(cast.filter(c => c.personId !== pid).map((c, i) => ({ ...c, order: i })));
  return (
    <_DSEditorSection title={`В ролях (cast) · ${cast.length}`}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        {cast.map(c => {
          const p = personById(c.personId);
          return (
            <div key={c.personId} style={{
              display: 'flex', gap: 6, alignItems: 'center',
              background: 'rgba(20,28,52,0.5)', padding: '6px 8px', borderRadius: 6
            }}>
              <img src={_personPhoto(p)} alt="" title="Открыть страницу актёра"
                onClick={() => { window.location.hash = `#/actor/${c.personId}`; }}
                onError={(e) => e.currentTarget.style.opacity = 0.2}
                style={{ width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', background: '#1a2348', flexShrink: 0, cursor: 'pointer' }} />
              <div onClick={() => { window.location.hash = `#/actor/${c.personId}`; }}
                title="Открыть страницу актёра"
                style={{ minWidth: 100, maxWidth: 130, flexShrink: 0, cursor: 'pointer' }}>
                <div style={{ fontSize: 11, fontWeight: 600, color: '#e8efff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {p?.name_ru || p?.name || `#${c.personId}`}
                </div>
                {p?.name_ru && p?.name && (
                  <div style={{ fontSize: 10, color: '#8ba0c8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</div>
                )}
              </div>
              <label title="Главная роль" style={{
                display: 'flex', alignItems: 'center', gap: 3, fontSize: 10, color: '#8ba0c8',
                cursor: 'pointer', userSelect: 'none', flexShrink: 0
              }}>
                <input type="checkbox" checked={!!c.main} onChange={(e) => onMain(c.personId, e.target.checked)}
                  style={{ accentColor: '#5ed7c6', cursor: 'pointer' }} />
                Главная
              </label>
              <input type="text" defaultValue={c.characterName || ''} placeholder="Персонаж"
                onBlur={(e) => { if (e.target.value !== (c.characterName || '')) onChar(c.personId, e.target.value); }}
                style={{
                  flex: 1, minWidth: 0, padding: '4px 8px', fontSize: 11,
                  background: 'rgba(10,15,30,0.6)', border: '1px solid transparent',
                  borderRadius: 4, color: '#e8efff', outline: 'none'
                }}
                onFocus={(e) => e.currentTarget.style.borderColor = 'rgba(94,215,198,0.45)'}
                onBlurCapture={(e) => e.currentTarget.style.borderColor = 'transparent'} />
              <button onClick={() => onRm(c.personId)} title="Удалить"
                style={{
                  padding: '2px 7px', background: 'rgba(196,69,69,0.20)', color: '#e05858',
                  border: 'none', borderRadius: 4, fontSize: 11, cursor: 'pointer', flexShrink: 0
                }}>×</button>
            </div>
          );
        })}
        {cast.length === 0 && (
          <div style={{ fontSize: 11, color: '#6f88b3', fontStyle: 'italic' }}>Актёров пока нет — добавь через поиск ниже</div>
        )}
      </div>
      <_DSPersonSearch excludeIds={cast.map(c => c.personId)} onPick={onAdd}
        placeholder="Добавить актёра — начни печатать имя…" />
    </_DSEditorSection>
  );
}

// CREW editor — режиссёры/сценаристы/продюсеры и т.д. с должностью
const _CREW_JOBS = [
  'Режиссёр', 'Сценарист', 'Продюсер', 'Исполнительный продюсер',
  'Оператор', 'Композитор', 'Художник-постановщик', 'Художник по костюмам',
  'Монтажёр', 'Звукорежиссёр', 'Хореограф', 'Каскадёр-постановщик'
];
function _DSEditorCrew({ show, updateField }) {
  const [crew, setCrew] = useState(show.crew || []);
  const [people, setPeople] = useState([]);
  useEffect(() => { _loadPeople().then(setPeople); }, []);
  const personById = (id) => people.find(p => p.id === id);
  const save = async (next) => {
    setCrew(next);
    show.crew = next;
    try { await updateField('crew', next); } catch (e) { console.error('save crew', e); }
  };
  const onAdd = (person) => {
    save([...crew, { personId: person.id, job: 'Режиссёр' }]);
  };
  const onJob = (pid, job) => save(crew.map((c, i) => (c.personId === pid && i === crew.findIndex(x => x.personId === pid && x.job === c.job) ? { ...c, job } : c)));
  const onRm = (pid, job) => save(crew.filter(c => !(c.personId === pid && c.job === job)));
  // Внутренний хелпер — обновить должность одной строки по индексу (на случай если у одного человека несколько ролей)
  const onJobAt = (idx, job) => save(crew.map((c, i) => i === idx ? { ...c, job } : c));
  const onRmAt = (idx) => save(crew.filter((_, i) => i !== idx));
  return (
    <_DSEditorSection title={`Режиссёры / Команда (crew) · ${crew.length}`}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        {crew.map((c, idx) => {
          const p = personById(c.personId);
          const isStandard = _CREW_JOBS.includes(c.job);
          return (
            <div key={`${c.personId}-${idx}`} style={{
              display: 'flex', gap: 6, alignItems: 'center',
              background: 'rgba(20,28,52,0.5)', padding: '6px 8px', borderRadius: 6
            }}>
              <img src={_personPhoto(p)} alt="" title="Открыть страницу"
                onClick={() => { window.location.hash = `#/actor/${c.personId}`; }}
                onError={(e) => e.currentTarget.style.opacity = 0.2}
                style={{ width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', background: '#1a2348', flexShrink: 0, cursor: 'pointer' }} />
              <div onClick={() => { window.location.hash = `#/actor/${c.personId}`; }}
                title="Открыть страницу"
                style={{ minWidth: 100, maxWidth: 130, flexShrink: 0, cursor: 'pointer' }}>
                <div style={{ fontSize: 11, fontWeight: 600, color: '#e8efff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {p?.name_ru || p?.name || `#${c.personId}`}
                </div>
                {p?.name_ru && p?.name && (
                  <div style={{ fontSize: 10, color: '#8ba0c8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</div>
                )}
              </div>
              <select value={isStandard ? c.job : ''} onChange={(e) => onJobAt(idx, e.target.value)}
                style={{
                  padding: '4px 6px', fontSize: 11, minWidth: 110,
                  background: 'rgba(10,15,30,0.6)', border: '1px solid transparent',
                  borderRadius: 4, color: '#e8efff', outline: 'none'
                }}>
                <option value="">— должность —</option>
                {_CREW_JOBS.map(j => <option key={j} value={j}>{j}</option>)}
                {!isStandard && c.job && <option value={c.job}>{c.job} (кастом)</option>}
              </select>
              <input type="text" defaultValue={!isStandard ? c.job : ''} placeholder="или своё…"
                onBlur={(e) => { const v = e.target.value.trim(); if (v && v !== c.job) onJobAt(idx, v); }}
                style={{
                  flex: 1, minWidth: 0, padding: '4px 8px', fontSize: 11,
                  background: 'rgba(10,15,30,0.6)', border: '1px solid transparent',
                  borderRadius: 4, color: '#e8efff', outline: 'none'
                }} />
              <button onClick={() => onRmAt(idx)} title="Удалить"
                style={{
                  padding: '2px 7px', background: 'rgba(196,69,69,0.20)', color: '#e05858',
                  border: 'none', borderRadius: 4, fontSize: 11, cursor: 'pointer', flexShrink: 0
                }}>×</button>
            </div>
          );
        })}
        {crew.length === 0 && (
          <div style={{ fontSize: 11, color: '#6f88b3', fontStyle: 'italic' }}>Команды пока нет — добавь через поиск ниже</div>
        )}
      </div>
      <_DSPersonSearch excludeIds={[] /* у одного человека может быть несколько ролей в команде */} onPick={onAdd}
        placeholder="Добавить члена команды — начни печатать имя…" />
    </_DSEditorSection>
  );
}

// ── СВЯЗАННЫЕ ПРОИЗВЕДЕНИЯ ── books / dramas / shows / concerts / movies
const _RELATED_KINDS = [
  { id: 'drama',   label: 'Дорама' },
  { id: 'movie',   label: 'Фильм' },
  { id: 'show',    label: 'Шоу' },
  { id: 'concert', label: 'Концерт' },
  { id: 'book',    label: 'Книга' },
  { id: 'other',   label: 'Другое' },
];

function _DSEditorRelated({ show, updateField }) {
  const [items, setItems] = useState(show.related_works || []);
  const [kind, setKind] = useState('drama');
  const [title, setTitle] = useState('');
  const [url, setUrl] = useState('');
  const [note, setNote] = useState('');
  const [status, setStatus] = useState('');
  const [err, setErr] = useState('');
  // Для kind='drama' — поиск по существующим дорамам
  const [dramaPool, setDramaPool] = useState([]);
  const [dramaQ, setDramaQ] = useState('');
  const [dramaOpen, setDramaOpen] = useState(false);
  const dramaWrapRef = useRef(null);

  // Грузим все дорамы один раз — для автокомплита kind='drama'
  useEffect(() => {
    if (window.adapter?.getAllDramas) {
      const list = window.adapter.getAllDramas();
      if (Array.isArray(list)) setDramaPool(list);
    }
  }, []);
  useEffect(() => {
    const h = (e) => { if (dramaWrapRef.current && !dramaWrapRef.current.contains(e.target)) setDramaOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  const save = async (next) => {
    setItems(next);
    show.related_works = next;
    try { await updateField('related_works', next); }
    catch (e) { console.error('save related_works', e); setStatus('error'); setErr(e.message); return; }
    setStatus('saved');
    setTimeout(() => setStatus(''), 1500);
  };

  const add = async () => {
    setErr('');
    if (!title.trim()) { setErr('Введи название'); return; }
    setStatus('saving');
    const newItem = {
      id: 'r' + Date.now() + Math.random().toString(36).slice(2, 6),
      kind,
      title: title.trim(),
      url: url.trim() || null,
      note: note.trim() || null,
    };
    await save([...items, newItem]);
    setTitle(''); setUrl(''); setNote(''); setDramaQ('');
  };

  // Привязать существующую дораму из БД — берём её id+slug, формируем URL
  const addExistingDrama = async (d) => {
    setErr('');
    if (items.some(it => it.kind === 'drama' && it.dramaId === d.id)) {
      setErr('Эта дорама уже добавлена'); return;
    }
    setStatus('saving');
    const newItem = {
      id: 'r' + Date.now() + Math.random().toString(36).slice(2, 6),
      kind: 'drama',
      dramaId: d.id,
      title: d.title_ru || d.title_en || d.title_original || `#${d.id}`,
      url: `#/drama/${d.id}`,  // внутренний hash-роут
      note: null,
    };
    await save([...items, newItem]);
    setDramaQ(''); setDramaOpen(false); setTitle('');
  };

  const del = async (id) => {
    if (!confirm('Удалить связанное произведение?')) return;
    setStatus('saving');
    await save(items.filter(it => it.id !== id));
  };

  const kindLabel = (k) => (_RELATED_KINDS.find(x => x.id === k) || { label: 'Другое' }).label;

  // Матчи дорам для автокомплита
  const dramaMatches = dramaQ.trim().length === 0 ? [] : dramaPool.filter(d => {
    if (d.id === show.id) return false;  // саму себя не предлагаем
    const qq = dramaQ.toLowerCase();
    return ((d.title_en || '').toLowerCase().includes(qq) ||
            (d.title_ru || '').toLowerCase().includes(qq) ||
            (d.title_original || '').includes(dramaQ));
  }).slice(0, 12);

  return (
    <_DSEditorSection title={`Связанные произведения · ${items.length}`}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 10 }}>
        {items.map(it => (
          <div key={it.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 6, background: '#1a2348', borderRadius: 6, padding: 7 }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 9, color: '#5ed7c6', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: 2 }}>{kindLabel(it.kind)}</div>
              <div style={{ fontSize: 12, fontWeight: 600, color: '#e8efff' }}>{it.title}</div>
              {it.url && <a href={it.url} target="_blank" rel="noreferrer" style={{ fontSize: 10, color: '#7bc3ff', wordBreak: 'break-all', display: 'block', marginTop: 2 }}>{it.url}</a>}
              {it.note && <div style={{ fontSize: 10, color: '#8ba0c8', marginTop: 2, lineHeight: 1.4 }}>{it.note}</div>}
            </div>
            <button onClick={() => del(it.id)} style={{ width: 22, height: 22, borderRadius: 4, background: 'rgba(196,69,69,0.20)', color: '#e05858', border: 'none', fontSize: 13, cursor: 'pointer', padding: 0, flexShrink: 0 }}>×</button>
          </div>
        ))}
      </div>
      <div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
        <select value={kind} onChange={(e) => { setKind(e.target.value); setErr(''); }}
          style={{ flex: '0 0 100px', padding: '7px 8px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none' }}>
          {_RELATED_KINDS.map(k => <option key={k.id} value={k.id}>{k.label}</option>)}
        </select>
        {kind === 'drama' ? (
          // Поиск по существующим дорамам в БД
          <div ref={dramaWrapRef} style={{ flex: 1, position: 'relative' }}>
            <input value={dramaQ} onChange={(e) => { setDramaQ(e.target.value); setDramaOpen(true); }}
              onFocus={() => setDramaOpen(true)}
              placeholder="Поиск дорамы по названию…"
              style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
            {dramaOpen && dramaQ.trim().length > 0 && (
              <div style={{
                position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
                maxHeight: 240, overflowY: 'auto', zIndex: 10,
                background: '#0f1936', border: '1px solid rgba(94,215,198,0.45)',
                borderRadius: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.4)'
              }}>
                {dramaMatches.map(d => (
                  <button key={d.id} onClick={() => addExistingDrama(d)}
                    style={{
                      display: 'block', width: '100%', textAlign: 'left',
                      padding: '7px 10px', fontSize: 12, color: '#e8efff',
                      background: 'transparent', border: 'none', cursor: 'pointer',
                      borderBottom: '1px solid rgba(140,180,235,0.10)'
                    }}
                    onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(94,215,198,0.10)'}
                    onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                    <div style={{ fontWeight: 600 }}>{d.title_ru || d.title_en || `#${d.id}`}</div>
                    {d.title_en && d.title_ru && <div style={{ fontSize: 10, color: '#8ba0c8' }}>{d.title_en}</div>}
                    {d.firstAirDate && <div style={{ fontSize: 10, color: '#6f88b3' }}>{d.firstAirDate.slice(0, 4)}</div>}
                  </button>
                ))}
                {dramaMatches.length === 0 && (
                  <div style={{ padding: '10px 12px', fontSize: 11, color: '#6f88b3', fontStyle: 'italic' }}>Не найдено. Добавь вручную ниже.</div>
                )}
              </div>
            )}
          </div>
        ) : (
          <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Название"
            style={{ flex: 1, padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none' }} />
        )}
      </div>
      {/* Ручная форма — fallback для всех kind (включая drama, если нет в БД) */}
      {kind === 'drama' && (
        <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Или название вручную (если дорамы нет в базе)"
          style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none', marginBottom: 6 }} />
      )}
      <input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="URL (опционально)"
        style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none', marginBottom: 6 }} />
      <textarea value={note} onChange={(e) => setNote(e.target.value)} placeholder="Заметка / контекст (опционально)" rows={2}
        style={{ width: '100%', padding: '7px 10px', background: '#131b35', border: '1px solid #2a3458', borderRadius: 6, color: '#e8efff', fontSize: 12, outline: 'none', marginBottom: 6, resize: 'vertical', fontFamily: 'inherit' }} />
      {err && <div style={{ fontSize: 10.5, color: '#ffb0a8', marginBottom: 6 }}>{err}</div>}
      <button onClick={add} disabled={!title.trim() || status === 'saving'} style={{
        width: '100%', padding: '7px 12px', borderRadius: 6,
        background: title.trim() ? 'rgba(94,215,198,0.20)' : 'rgba(140,180,235,0.05)',
        color: title.trim() ? '#5ed7c6' : '#6f88b3',
        border: 'none', fontSize: 12, fontWeight: 700, cursor: title.trim() ? 'pointer' : 'default'
      }}>+ Добавить вручную {status === 'saved' && '· ✓'} {status === 'saving' && '…'}</button>
    </_DSEditorSection>
  );
}

// ── COMMENTS THREAD ──────────────────────────────────────────────────────────
// Универсальный компонент комментариев. Использование:
//   <CommentsThread threadKey="news-12" />
// Хранит данные в localStorage по ключу ds_comments_<threadKey>.
// Поддерживает: написание, ответы (1 уровень nesting), emoji-реакции, edit/delete своих, emoji-picker для вставки в текст.
const CMT_REACTIONS = ['👍', '❤️', '🔥', '😂', '😢', '😮'];
const CMT_EMOJI_PICKER = [
  '😀', '😁', '😂', '🤣', '😅', '😊', '😍', '🥰', '😘', '😎',
  '🤔', '😏', '🥺', '😢', '😭', '😮', '😱', '🤯', '😡', '🤬',
  '👍', '👎', '👏', '🙌', '💪', '🤝', '🙏', '✌️', '🤞', '👌',
  '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '💔', '💯', '✨',
  '🔥', '⭐', '🌟', '💥', '🎉', '🎊', '🎁', '🌸', '🌹', '🍀',
  '☕', '🍿', '🍕', '🍔', '🍜', '🍱', '🍣', '🥟', '🍙', '🍡'
];

function _readComments(threadKey) {
  try { return JSON.parse(localStorage.getItem('ds_comments_' + threadKey)) || []; } catch { return []; }
}
function _writeComments(threadKey, list) {
  localStorage.setItem('ds_comments_' + threadKey, JSON.stringify(list));
  window.dispatchEvent(new CustomEvent('ds-comments-changed', { detail: { threadKey } }));
}

function _agoTime(iso, isRu) {
  const ms = Date.now() - new Date(iso).getTime();
  const s = Math.floor(ms / 1000);
  if (s < 60) return isRu ? 'только что' : 'just now';
  const m = Math.floor(s / 60);
  if (m < 60) return isRu ? `${m} мин` : `${m}m`;
  const h = Math.floor(m / 60);
  if (h < 24) return isRu ? `${h} ч` : `${h}h`;
  const days = Math.floor(h / 24);
  if (days < 30) return isRu ? `${days} дн` : `${days}d`;
  return new Date(iso).toLocaleDateString(isRu ? 'ru-RU' : 'en-US');
}

function _CommentForm({ onSubmit, placeholder, autoFocus, onCancel, initial = '' }) {
  const [text, setText] = useState(initial);
  const [showEmoji, setShowEmoji] = useState(false);
  const taRef = useRef(null);
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  useEffect(() => { if (autoFocus && taRef.current) taRef.current.focus(); }, [autoFocus]);

  const insertEmoji = (e) => {
    const ta = taRef.current;
    if (!ta) { setText(t => t + e); return; }
    const start = ta.selectionStart, end = ta.selectionEnd;
    const next = text.slice(0, start) + e + text.slice(end);
    setText(next);
    setTimeout(() => {
      ta.focus();
      ta.setSelectionRange(start + e.length, start + e.length);
    }, 0);
  };

  const submit = () => {
    const v = text.trim();
    if (!v) return;
    onSubmit(v);
    setText('');
    setShowEmoji(false);
  };

  return (
    <div style={{ position: 'relative' }}>
      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
        <textarea
          ref={taRef}
          value={text}
          onChange={(e) => setText(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit(); }}
          placeholder={placeholder || (isRu ? 'Написать комментарий…' : 'Write a comment…')}
          rows={2}
          style={{
            flex: 1, padding: '10px 12px', minHeight: 56, resize: 'vertical',
            background: 'var(--bg3)', border: '1px solid var(--border)', borderRadius: 10,
            color: 'var(--text)', fontSize: 13, fontFamily: 'inherit', outline: 'none', lineHeight: 1.5
          }}
        />
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          <button
            type="button"
            onClick={() => setShowEmoji(v => !v)}
            title={isRu ? 'Добавить эмодзи' : 'Add emoji'}
            style={{
              width: 36, height: 36, borderRadius: 8,
              background: showEmoji ? 'rgba(94,215,198,0.16)' : 'var(--bg3)',
              border: '1px solid var(--border)', color: 'var(--text2)',
              fontSize: 18, cursor: 'pointer', padding: 0
            }}>😊</button>
          <button
            type="button" onClick={submit} disabled={!text.trim()}
            title={isRu ? 'Отправить (Cmd+Enter)' : 'Send (Cmd+Enter)'}
            style={{
              width: 36, height: 36, borderRadius: 8,
              background: text.trim() ? 'var(--accent)' : 'var(--bg3)',
              border: 'none', color: text.trim() ? '#0a1226' : 'var(--text3)',
              fontSize: 15, fontWeight: 800, cursor: text.trim() ? 'pointer' : 'default'
            }}>→</button>
        </div>
      </div>
      {showEmoji && (
        <div style={{
          position: 'absolute', right: 0, bottom: 'calc(100% + 6px)', zIndex: 1000,
          background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 10,
          padding: 8, width: 280, maxHeight: 220, overflowY: 'auto',
          boxShadow: '0 8px 28px rgba(0,0,0,0.45)',
          display: 'grid', gridTemplateColumns: 'repeat(10, 1fr)', gap: 2
        }}>
          {CMT_EMOJI_PICKER.map((e, i) => (
            <button key={i} type="button" onClick={() => insertEmoji(e)}
              style={{
                width: '100%', aspectRatio: '1', borderRadius: 6, background: 'transparent',
                border: 'none', fontSize: 18, cursor: 'pointer', padding: 0
              }}
              onMouseEnter={(ev) => ev.currentTarget.style.background = 'rgba(140,180,235,0.10)'}
              onMouseLeave={(ev) => ev.currentTarget.style.background = 'transparent'}>{e}</button>
          ))}
        </div>
      )}
      {onCancel && (
        <button type="button" onClick={onCancel}
          style={{
            marginTop: 6, fontSize: 11, color: 'var(--text3)', background: 'transparent',
            border: 'none', cursor: 'pointer', padding: '4px 0'
          }}>{isRu ? 'Отмена' : 'Cancel'}</button>
      )}
    </div>
  );
}

function _CommentItem({ comment, threadKey, allList, onChange, user, isReply = false }) {
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const [replyOpen, setReplyOpen] = useState(false);
  const [editOpen, setEditOpen] = useState(false);
  const isOwn = user && comment.author?.email === user.email;
  const replies = isReply ? [] : allList.filter(c => c.parentId === comment.id);

  // Reaction toggle: добавить/убрать свой email из массива
  const toggleReaction = (emoji) => {
    if (!user) { window.dispatchEvent(new CustomEvent('ds-open-signup')); return; }
    const next = allList.map(c => {
      if (c.id !== comment.id) return c;
      const reactions = { ...(c.reactions || {}) };
      const arr = reactions[emoji] || [];
      reactions[emoji] = arr.includes(user.email)
        ? arr.filter(e => e !== user.email)
        : [...arr, user.email];
      if (reactions[emoji].length === 0) delete reactions[emoji];
      return { ...c, reactions };
    });
    _writeComments(threadKey, next);
    onChange(next);
  };

  const deleteComment = () => {
    if (!confirm(isRu ? 'Удалить комментарий?' : 'Delete comment?')) return;
    const next = allList.filter(c => c.id !== comment.id && c.parentId !== comment.id);
    _writeComments(threadKey, next);
    onChange(next);
  };

  const saveEdit = (newText) => {
    const next = allList.map(c => c.id === comment.id ? { ...c, text: newText, editedAt: new Date().toISOString() } : c);
    _writeComments(threadKey, next);
    onChange(next);
    setEditOpen(false);
  };

  const addReply = (text) => {
    const next = [...allList, {
      id: 'c' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
      parentId: comment.id,
      author: { email: user.email, name: user.name || user.email.split('@')[0] },
      text, createdAt: new Date().toISOString(),
      reactions: {}
    }];
    _writeComments(threadKey, next);
    onChange(next);
    setReplyOpen(false);
  };

  const initial = (comment.author?.name || comment.author?.email || '?')[0].toUpperCase();
  return (
    <div style={{
      padding: isReply ? '10px 12px' : '14px 14px',
      borderRadius: 10,
      background: isReply ? 'rgba(140,180,235,0.04)' : 'var(--bg2)',
      border: '1px solid ' + (isReply ? 'rgba(140,180,235,0.08)' : 'var(--border)'),
      marginLeft: isReply ? 36 : 0,
      marginBottom: 8
    }}>
      <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
        <div style={{
          width: 32, height: 32, borderRadius: '50%', flexShrink: 0,
          background: 'linear-gradient(135deg, #5ed7c6, #4a9eff)',
          color: '#0a1226', fontSize: 14, fontWeight: 800,
          display: 'flex', alignItems: 'center', justifyContent: 'center'
        }}>{initial}</div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 4 }}>
            <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{comment.author?.name || (comment.author?.email || '').split('@')[0]}</span>
            <span style={{ fontSize: 11, color: 'var(--text3)' }}>{_agoTime(comment.createdAt, isRu)}{comment.editedAt && (isRu ? ' · изменено' : ' · edited')}</span>
          </div>
          {editOpen ? (
            <_CommentForm initial={comment.text} onSubmit={saveEdit} onCancel={() => setEditOpen(false)} autoFocus />
          ) : (
            <div style={{ fontSize: 13.5, lineHeight: 1.55, color: 'var(--text)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{comment.text}</div>
          )}
          {!editOpen && (
            <div style={{ display: 'flex', gap: 4, alignItems: 'center', marginTop: 8, flexWrap: 'wrap' }}>
              {CMT_REACTIONS.map(emoji => {
                const arr = comment.reactions?.[emoji] || [];
                const my = user && arr.includes(user.email);
                if (arr.length === 0 && !my) {
                  // Pure picker — show grey
                  return (
                    <button key={emoji} onClick={() => toggleReaction(emoji)}
                      title={isRu ? 'Реакция' : 'React'}
                      style={{
                        padding: '3px 7px', borderRadius: 999, fontSize: 13,
                        background: 'transparent', border: '1px solid rgba(140,180,235,0.10)',
                        cursor: 'pointer', opacity: 0.5, transition: 'opacity 0.15s, background 0.15s'
                      }}
                      onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; e.currentTarget.style.background = 'rgba(140,180,235,0.08)'; }}
                      onMouseLeave={(e) => { e.currentTarget.style.opacity = 0.5; e.currentTarget.style.background = 'transparent'; }}>
                      {emoji}
                    </button>
                  );
                }
                // With count, highlighted if mine
                return (
                  <button key={emoji} onClick={() => toggleReaction(emoji)}
                    style={{
                      padding: '3px 9px', borderRadius: 999, fontSize: 12, fontWeight: 600,
                      background: my ? 'rgba(94,215,198,0.18)' : 'rgba(140,180,235,0.08)',
                      border: '1px solid ' + (my ? 'rgba(94,215,198,0.5)' : 'rgba(140,180,235,0.18)'),
                      color: 'var(--text2)', cursor: 'pointer',
                      display: 'inline-flex', alignItems: 'center', gap: 4
                    }}>
                    <span style={{ fontSize: 13 }}>{emoji}</span>
                    <span>{arr.length}</span>
                  </button>
                );
              })}
              {!isReply && (
                <button onClick={() => setReplyOpen(v => !v)}
                  style={{
                    padding: '3px 9px', fontSize: 12, fontWeight: 600,
                    color: 'var(--accent)', background: 'transparent', border: 'none',
                    cursor: 'pointer', marginLeft: 4
                  }}>
                  {isRu ? '↩ Ответить' : '↩ Reply'}
                </button>
              )}
              {isOwn && (
                <>
                  <button onClick={() => setEditOpen(true)}
                    style={{
                      padding: '3px 8px', fontSize: 11, color: 'var(--text3)',
                      background: 'transparent', border: 'none', cursor: 'pointer'
                    }}>{isRu ? 'Изменить' : 'Edit'}</button>
                  <button onClick={deleteComment}
                    style={{
                      padding: '3px 8px', fontSize: 11, color: 'var(--text3)',
                      background: 'transparent', border: 'none', cursor: 'pointer'
                    }}>{isRu ? 'Удалить' : 'Delete'}</button>
                </>
              )}
            </div>
          )}
          {replyOpen && (
            <div style={{ marginTop: 10 }}>
              <_CommentForm
                onSubmit={addReply}
                onCancel={() => setReplyOpen(false)}
                autoFocus
                placeholder={isRu ? `Ответить ${comment.author?.name || ''}…` : `Reply to ${comment.author?.name || ''}…`}
              />
            </div>
          )}
        </div>
      </div>
      {replies.length > 0 && (
        <div style={{ marginTop: 8 }}>
          {replies.map(r => (
            <_CommentItem key={r.id} comment={r} threadKey={threadKey} allList={allList} onChange={onChange} user={user} isReply />
          ))}
        </div>
      )}
    </div>
  );
}

function CommentsThread({ threadKey }) {
  const { user, openSignUp } = window.useAuth();
  const { lang } = window.useI18n();
  const isRu = lang === 'ru';
  const [list, setList] = useState(() => _readComments(threadKey));
  const [sortBy, setSortBy] = useState('newest'); // newest | oldest | top

  useEffect(() => {
    const refresh = (e) => {
      if (!e?.detail?.threadKey || e.detail.threadKey === threadKey) {
        setList(_readComments(threadKey));
      }
    };
    refresh();
    window.addEventListener('ds-comments-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-comments-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [threadKey]);

  const topLevel = list.filter(c => !c.parentId);
  const totalCount = list.length;

  const sorted = [...topLevel].sort((a, b) => {
    if (sortBy === 'oldest') return (a.createdAt || '').localeCompare(b.createdAt || '');
    if (sortBy === 'top') {
      const score = (c) => Object.values(c.reactions || {}).reduce((s, arr) => s + arr.length, 0);
      return score(b) - score(a);
    }
    return (b.createdAt || '').localeCompare(a.createdAt || '');
  });

  const addComment = (text) => {
    const next = [...list, {
      id: 'c' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
      parentId: null,
      author: { email: user.email, name: user.name || user.email.split('@')[0] },
      text, createdAt: new Date().toISOString(),
      reactions: {}
    }];
    _writeComments(threadKey, next);
    setList(next);
  };

  return (
    <div style={{ marginTop: 28, paddingTop: 24, borderTop: '1px solid var(--border)' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 10 }}>
        <h3 style={{ fontSize: 16, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 }}>
          <span>💬</span>
          <span>{isRu ? 'Комментарии' : 'Comments'}</span>
          <span style={{ fontSize: 12, color: 'var(--text3)', fontWeight: 600 }}>{totalCount}</span>
        </h3>
        {topLevel.length > 1 && (
          <div style={{ display: 'flex', gap: 4 }}>
            {[['newest', isRu ? 'Новые' : 'Newest'], ['top', isRu ? 'Топ' : 'Top'], ['oldest', isRu ? 'Старые' : 'Oldest']].map(([v, l]) => (
              <button key={v} onClick={() => setSortBy(v)}
                style={{
                  padding: '4px 10px', borderRadius: 999, fontSize: 11, fontWeight: 600,
                  background: sortBy === v ? 'rgba(94,215,198,0.16)' : 'transparent',
                  color: sortBy === v ? 'var(--accent)' : 'var(--text3)',
                  border: '1px solid ' + (sortBy === v ? 'rgba(94,215,198,0.5)' : 'rgba(140,180,235,0.10)'),
                  cursor: 'pointer'
                }}>{l}</button>
            ))}
          </div>
        )}
      </div>

      {user ? (
        <div style={{ marginBottom: 18 }}>
          <_CommentForm onSubmit={addComment} />
        </div>
      ) : (
        <div style={{
          padding: 14, marginBottom: 18,
          background: 'rgba(140,180,235,0.06)', border: '1px dashed rgba(140,180,235,0.20)',
          borderRadius: 10, textAlign: 'center', fontSize: 13, color: 'var(--text2)'
        }}>
          {isRu ? 'Войдите, чтобы оставить комментарий' : 'Sign in to comment'} ·{' '}
          <button onClick={openSignUp}
            style={{ color: 'var(--accent)', background: 'transparent', border: 'none', fontWeight: 700, cursor: 'pointer' }}>
            {isRu ? 'Войти' : 'Sign in'}
          </button>
        </div>
      )}

      {sorted.length === 0 ? (
        <div style={{ padding: '20px 10px', textAlign: 'center', fontSize: 13, color: 'var(--text3)' }}>
          {isRu ? 'Пока нет комментариев — будь первым' : 'No comments yet — be the first'}
        </div>
      ) : (
        <div>
          {sorted.map(c => (
            <_CommentItem key={c.id} comment={c} threadKey={threadKey} allList={list} onChange={setList} user={user} />
          ))}
        </div>
      )}
    </div>
  );
}

// Хелпер для счётчиков непрочитанных в AvatarMenu.
// Notifications больше не хранятся локально — счётчик берётся из window
// (NotificationBell сам обновляет __dsUnreadNotifs через event).
// Messages пока остаются на SAMPLE-mock (будут переделаны в Phase 3 далее).
window.__dsGetUnreadCounts = () => ({
  notifications: (typeof window.__dsUnreadNotifs === 'number') ? window.__dsUnreadNotifs : 0,
  messages: (typeof SAMPLE_MESSAGES !== 'undefined') ? SAMPLE_MESSAGES.filter(m => !m.read).length : 0
});

// Экспонируем formatRelativeTime для использования из index.html (NotificationsPage)
window.formatRelativeTime = formatRelativeTime;

// Export to window
Object.assign(window, {
  Icon, SocialLoginButtons, LibraryStatusButton, FavoriteDramaButton, FavoriteDramaMark, RecommendBlock, ReviewsSection,
  ShareMenu, AdSlot, NotificationBell, MessagesIcon, SettingsGear, MoodChips, DramaAIButtons, PremiumPage, RichText, ReleaseNotifyButton,
  LIBRARY_STATUSES, LIB_ICONS, RECOMMEND_REASONS, MOOD_OPTIONS,
  DSEditModeToggle, DSDramaEditor, DSActorEditor,
  UserListsBlock, LibraryBlock, FavoritesBlock, SubscriptionsBlock, SavedNewsBlock, SavedTopicsBlock, AddToListButton,
  CommentsThread,
});
