// ═════════════════════════════════════════════════════════════════════════════
// MindForge · Layer 4 · play/screens.jsx
// ─────────────────────────────────────────────────────────────────────────────
// Role     : Every play-side screen: HomeScreen, PlayScreen, the question
//            components (mc_fr_kr, mc_kr_fr, image, audio, typing,
//            find_photo, flashcard), RevealScreen, SummaryScreen,
//            GameOverScreen, plus per-question helpers (useMcChoice,
//            AttributeRenderer, CustomFieldDisplay, MediaDemonstration).
// Loaded   : #7
// Reads    : window.{ React, MF, OptionRow, CardArt, Waveform, XPFloat,
//            Sparkles, parseVideoEmbed, useMediaURL, formatSeconds,
//            answerMatches, attributeLabel, attributeIcon,
//            effectiveQuizSides, CUSTOM_FIELD_TYPE_ICONS, INTERVAL_BY_CODE,
//            RevealPdfRegion, useDeck }
// Exposes  : window.{ HomeScreen, PlayScreen, RevealScreen, SummaryScreen,
//            GameOverScreen, and each Q* component }
// See also : docs/ARCHITECTURE.md §"Play state machine"
// ═════════════════════════════════════════════════════════════════════════════
//
// MindForge — full screens (Home, Play, Reveal, Summary, GameOver)

// ─────────────── Shared question helpers ───────────────
// All the multiple-choice question types (FR→KR, KR→FR, image, audio, video,
// find-photo) share the exact same "tap an option, lock the board, reveal
// after a short beat" behaviour. useMcChoice centralises it so each question
// component only has to describe what its prompt and options look like.
//
//   const { picked, pick, stateFor } = useMcChoice(q, onAnswer);
//
//   picked      null until the user taps, then the chosen option index
//   pick(i)     locks the board and, after `delay` ms, calls
//               onAnswer(isCorrect, chosenOption)
//   stateFor(i) the OptionRow visual state: 'idle' before a pick, then
//               'correct' / 'wrong' / 'dim' to grade the board afterwards
const useMcChoice = (q, onAnswer, delay = 600) => {
  const [picked, setPicked] = useState(null);
  const pick = (i) => {
    if (picked != null) return;          // ignore taps once the board is locked
    setPicked(i);
    setTimeout(() => onAnswer(q.options[i].id === q.card.id, q.options[i]), delay);
  };
  const stateFor = (i) => {
    if (picked == null) return 'idle';
    if (q.options[i].id === q.card.id) return 'correct';  // always green the answer
    if (i === picked) return 'wrong';                     // red the wrong pick
    return 'dim';                                         // fade the rest
  };
  return { picked, pick, stateFor };
};

// The scrolling body every question renders inside. Vertical padding differs
// slightly per question (hence the parameter); everything else — iOS momentum
// scroll, contained overscroll, hidden scrollbar (.mf-scroll) — is constant.
const qScrollStyle = (padding = '8px 22px 24px') => ({
  flex: 1, display: 'flex', flexDirection: 'column',
  padding, minHeight: 0,
  overflowY: 'auto', WebkitOverflowScrolling: 'touch', overscrollBehavior: 'contain',
});

// ─────────────── HOME ───────────────
const HomeScreen = ({ profile, deck, decks, onSelectDeck, onStart, onEdit }) => {
  const [mode, setMode] = useState('daily');
  const activeDeck = deck || DECK_TKD;
  const previewCard = activeDeck.cards[3] || activeDeck.cards[0] || DECK_TKD.cards[0];
  const deckList = decks || [];
  const canPickDeck = deckList.length > 0 && typeof onSelectDeck === 'function';
  // No cards in the active deck → swap the run picker for an empty-state that
  // routes the user straight to the editor (which hosts both Importer + Créer).
  const isEmpty = activeDeck.cards.length === 0;
  const deckLabel = (d) => d.builtin ? 'Test général' : d.name;
  const deckIcon  = (d) => d.builtin ? '📚' : d.type === 'manual' ? '📝' : '✨';
  const modes = [
    { id: 'quick',    label: 'Quick',    sub: '5 cards · MC',                 time: '~2 min', n: 5 },
    { id: 'daily',    label: 'Daily',    sub: '24 mixed · with rating',        time: '~10 min', n: 24 },
    { id: 'survival', label: 'Survival', sub: '3 lives · combo',              time: '∞',      n: '∞' },
  ];
  return (
    <div className="mf" style={{height:'100%', background: MF.bg, display:'flex', flexDirection:'column', overflowY:'auto', overflowX:'hidden'}}>
      {/* App header */}
      <div style={{ padding: '12px 22px 8px', display:'flex', justifyContent:'space-between', alignItems:'center' }}>
        <div style={{display:'flex',alignItems:'center',gap:10}}>
          <svg width="26" height="26" viewBox="0 0 24 24" fill="none">
            <path d="M4 8 L12 4 L20 8 L20 12 L12 16 L4 12 Z" stroke={MF.purple} strokeWidth="1.8" strokeLinejoin="round"/>
            <path d="M12 16 L12 20 M8 20 L16 20" stroke={MF.purple} strokeWidth="1.8" strokeLinecap="round"/>
          </svg>
          <span style={{fontSize:16,fontWeight:700,letterSpacing:'-.01em'}}>MindForge</span>
        </div>
        <div style={{display:'flex',gap:8,alignItems:'center'}}>
          {onEdit && (
            <button
              onClick={onEdit}
              className="mf-tap"
              title="Éditer le deck"
              style={{
                width:32, height:32, borderRadius:99,
                background: MF.card, border:`1px solid ${MF.line}`,
                color: MF.textDim, cursor:'pointer', padding: 0,
                display:'flex', alignItems:'center', justifyContent:'center',
              }}
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 20h9"/>
                <path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/>
              </svg>
            </button>
          )}
          <span className="mf-tap" style={{padding:'5px 11px',borderRadius:99,background:MF.card,border:`1px solid ${MF.line}`,fontSize:12,fontWeight:600,color:MF.amber,display:'inline-flex',alignItems:'center',gap:5}}>
            <span>🔥</span><span>{profile.streak} d</span>
          </span>
          <div style={{
            width:32,height:32,borderRadius:99,background:`linear-gradient(135deg,${MF.purple},${MF.blue})`,
            display:'flex',alignItems:'center',justifyContent:'center',fontSize:13,fontWeight:700,color:'#0e0d12',
          }}>L{profile.level}</div>
        </div>
      </div>

      {/* Hero greeting + XP bar */}
      <div style={{padding:'16px 22px 18px'}}>
        <div style={{fontSize:13,color:MF.textDim,fontWeight:500}}>Welcome back,</div>
        <div style={{fontSize:26,fontWeight:800,letterSpacing:'-.02em',marginTop:2}}>Ready for a run?</div>
        <div style={{marginTop:14, display:'flex', alignItems:'center', gap:10}}>
          <div style={{flex:1, height:6, borderRadius:99, background: MF.card, overflow:'hidden', border:`1px solid ${MF.line}`}}>
            <div style={{width: profile.levelPct+'%', height:'100%', background:`linear-gradient(90deg,${MF.purpleDim},${MF.purple})`, borderRadius: 99, transition:'width .6s'}}/>
          </div>
          <span style={{fontSize:11,color:MF.textDim,fontWeight:600,fontVariantNumeric:'tabular-nums'}}>
            {profile.xp.toLocaleString()} / {profile.nextLevelXp.toLocaleString()}
          </span>
        </div>
      </div>

      {/* Training picker — choose a deck (or the general "Test général") */}
      {canPickDeck && (
        <div style={{padding:'0 22px 14px'}}>
          <div style={{
            display:'flex', alignItems:'center', justifyContent:'space-between',
            marginBottom: 10,
          }}>
            <span style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>
              Entraînement
            </span>
            {onEdit && (
              <button onClick={onEdit} className="mf-tap" style={{
                background:'transparent', border:'none', padding: 0,
                color: MF.textDim, fontSize: 11, fontWeight: 600, cursor:'pointer',
                textDecoration: 'underline dotted', fontFamily: 'inherit',
              }}>Gérer les decks</button>
            )}
          </div>
          <div className="mf-scroll" style={{
            display:'flex', gap: 8, overflowX:'auto', overflowY:'hidden',
            marginLeft: -4, paddingLeft: 4, paddingBottom: 4, paddingRight: 4,
          }}>
            {deckList.map(d => {
              const on = d.id === activeDeck.id;
              return (
                <button
                  key={d.id}
                  onClick={() => onSelectDeck(d.id)}
                  className="mf-tap"
                  title={d.sub || deckLabel(d)}
                  style={{
                    flexShrink: 0,
                    padding:'8px 14px', borderRadius: 99,
                    background: on ? MF.card2 : MF.card,
                    border:`1.5px solid ${on ? MF.purple : MF.line}`,
                    boxShadow: on ? `0 0 0 1px ${MF.purple}` : 'none',
                    color: on ? MF.text : MF.textDim,
                    fontSize: 12.5, fontWeight: 600, cursor:'pointer',
                    display:'inline-flex', alignItems:'center', gap: 7,
                    whiteSpace:'nowrap',
                  }}
                >
                  <span style={{fontSize: 12, lineHeight: 1}}>{deckIcon(d)}</span>
                  <span>{deckLabel(d)}</span>
                </button>
              );
            })}
            {onEdit && (
              <button
                onClick={onEdit}
                className="mf-tap"
                title="Créer un nouveau deck d'entraînement"
                style={{
                  flexShrink: 0,
                  padding:'8px 14px', borderRadius: 99,
                  background: 'transparent',
                  border:`1.5px dashed ${MF.line2}`,
                  color: MF.textDim,
                  fontSize: 12.5, fontWeight: 600, cursor:'pointer',
                  display:'inline-flex', alignItems:'center', gap: 6,
                  whiteSpace:'nowrap',
                }}
              >
                <span style={{fontSize: 14, lineHeight: 1}}>＋</span>
                <span>Créer</span>
              </button>
            )}
          </div>
        </div>
      )}

      {/* Empty deck → import / create CTA (no run picker until cards exist) */}
      {isEmpty && <EmptyDeckCTA deckName={activeDeck.name} onEdit={onEdit}/>}

      {/* Deck card */}
      {!isEmpty && (
      <div style={{padding:'0 22px'}}>
        <div style={{
          borderRadius:18, background:`linear-gradient(150deg, ${MF.card} 0%, ${MF.card2} 100%)`,
          border:`1px solid ${MF.line}`, padding:14, position:'relative', overflow:'hidden'
        }}>
          <div style={{display:'flex', gap: 14, alignItems:'center'}}>
            <div style={{width:72, height:96, flexShrink:0}}>
              <CardArt card={previewCard} size="sm"/>
            </div>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:11,color:MF.purple,fontWeight:700,letterSpacing:'.14em',textTransform:'uppercase'}}>Active deck</div>
              <div style={{fontSize:17,fontWeight:700,marginTop:3}}>{activeDeck.name}</div>
              <div style={{fontSize:12,color:MF.textDim,marginTop:3}}>{activeDeck.cards.length} cards · 3 mastered</div>
              <div style={{marginTop:8, display:'flex', alignItems:'center', gap:6}}>
                <div style={{flex:1, height:4, borderRadius:99, background:MF.bg2, overflow:'hidden'}}>
                  <div style={{width:'24%', height:'100%', background:MF.blue, borderRadius:99}}/>
                </div>
                <span style={{fontSize:10,color:MF.textDim,fontWeight:600}}>24%</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      )}

      {/* Mode picker */}
      {!isEmpty && (
      <div style={{padding:'18px 22px 8px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase',marginBottom:10}}>Pick your run</div>
        <div style={{display:'flex',flexDirection:'column',gap:8}}>
          {modes.map(m => {
            const on = mode === m.id;
            return (
              <div key={m.id} onClick={() => setMode(m.id)} className="mf-tap" style={{
                padding:'14px 16px', borderRadius:14,
                background: on ? MF.card2 : MF.card,
                border:`1.5px solid ${on ? MF.purple : MF.line}`,
                boxShadow: on ? `0 0 0 1px ${MF.purple}, 0 8px 24px rgba(167,139,250,.12)` : 'none',
                display:'flex', alignItems:'center', gap:14,
              }}>
                <div style={{
                  width:24,height:24,borderRadius:99,
                  border:`1.5px solid ${on ? MF.purple : MF.line2}`,
                  background: on ? MF.purple : 'transparent',
                  display:'flex',alignItems:'center',justifyContent:'center',
                }}>
                  {on && <span style={{width:8,height:8,borderRadius:99,background:MF.bg}}/>}
                </div>
                <div style={{flex:1}}>
                  <div style={{fontSize:15,fontWeight:700,color:on?MF.text:MF.text, display:'inline-flex', alignItems:'center', gap:8}}>
                    {m.badge && <span style={{fontSize:14}}>{m.badge}</span>}
                    <span>{m.label}</span>
                  </div>
                  <div style={{fontSize:12,color:MF.textDim,marginTop:2}}>{m.sub}</div>
                </div>
                <div style={{textAlign:'right'}}>
                  <div style={{fontSize:12,color:MF.textDim,fontWeight:600}}>{m.time}</div>
                  {m.id==='survival' && <div style={{fontSize:10,color:MF.bad,marginTop:2,fontWeight:600}}>HIGH XP</div>}
                </div>
              </div>
            );
          })}
        </div>
      </div>
      )}

      {/* Play CTA */}
      {!isEmpty && (
      <div style={{padding:'14px 22px 26px', marginTop:'auto'}}>
        <button onClick={() => onStart(mode)} className="mf-tap mf-btn-primary mf-press" style={{
          width:'100%', padding:'18px', borderRadius:16, fontSize:16, fontWeight:700, cursor:'pointer',
          display:'flex',alignItems:'center',justifyContent:'center',gap:8,
        }}>
          <span style={{fontSize:14}}>▶</span>
          <span>Start {modes.find(m=>m.id===mode).label.toLowerCase()} run</span>
        </button>
        <div style={{textAlign:'center',marginTop:10,fontSize:11,color:MF.textMute}}>
          Earn up to <span style={{color:MF.purple,fontWeight:700}}>+{mode==='survival'?500:mode==='daily'?180:80} XP</span> · keep the streak alive
        </div>
      </div>
      )}
    </div>
  );
};

// ─────────────── Empty-deck call to action ───────────────
// Shown on Home when the active deck has zero cards. Both buttons route to the
// editor (onEdit) — the editor toolbar hosts the real "Importer" file picker
// and "+ Ajouter" actions; there's nothing to play until cards exist.
const EmptyDeckCTA = ({ deckName, onEdit }) => {
  if (typeof onEdit !== 'function') return null;
  return (
    <div style={{padding:'4px 22px', flex:1, display:'flex', flexDirection:'column', justifyContent:'center'}}>
      <div style={{
        borderRadius:18, background:`linear-gradient(150deg, ${MF.card} 0%, ${MF.card2} 100%)`,
        border:`1px solid ${MF.line}`, padding:'28px 22px', textAlign:'center',
      }}>
        <div style={{
          width:56, height:56, borderRadius:99, margin:'0 auto 14px',
          background: MF.bg2, border:`1px solid ${MF.line}`,
          display:'flex', alignItems:'center', justifyContent:'center', fontSize:26,
        }}>🃏</div>
        <div style={{fontSize:17, fontWeight:700}}>Ce deck est vide</div>
        <div style={{fontSize:13, color:MF.textDim, marginTop:6, lineHeight:1.5}}>
          {deckName ? <><b style={{color:MF.text}}>{deckName}</b> n'a aucune carte. </> : null}
          Importe un fichier ou crée tes premières cartes pour lancer une partie.
        </div>
        <div style={{display:'flex', gap:10, marginTop:20}}>
          <button onClick={onEdit} className="mf-tap mf-btn-ghost" style={{
            flex:1, padding:'14px', borderRadius:14, fontSize:14, fontWeight:600, cursor:'pointer',
            display:'inline-flex', alignItems:'center', justifyContent:'center', gap:7,
          }}>
            <span style={{fontSize:14}}>↥</span>
            <span>Importer</span>
          </button>
          <button onClick={onEdit} className="mf-tap mf-btn-primary mf-press" style={{
            flex:1, padding:'14px', borderRadius:14, fontSize:14, fontWeight:700, cursor:'pointer',
            display:'inline-flex', alignItems:'center', justifyContent:'center', gap:7,
          }}>
            <span style={{fontSize:16}}>＋</span>
            <span>Créer des cartes</span>
          </button>
        </div>
      </div>
    </div>
  );
};

// ─────────────── PLAY (delegates to question renderer) ───────────────
const PlayScreen = ({ session, onAnswer, onClose, surviveTimer }) => {
  const q = session.queue[session.idx];
  if (!q) return null;
  const total = session.mode === 'survival' ? Math.max(session.idx + 1, 5) : session.queue.length;
  return (
    <div className="mf" style={{height:'100%', background: MF.bg, display:'flex', flexDirection:'column', overflow:'hidden'}}>
      <QHeader
        idx={session.idx + 1}
        total={total}
        combo={session.combo}
        lives={session.lives}
        onClose={onClose}
        mode={session.mode}
      />
      <div key={session.idx} className="mf-fade-up" style={{flex:1, display:'flex', flexDirection:'column', overflow:'hidden'}}>
        {q.type === 'mc_fr_kr'   && <QFrToKr    q={q} onAnswer={onAnswer}/>}
        {q.type === 'mc_kr_fr'   && <QKrToFr    q={q} onAnswer={onAnswer}/>}
        {q.type === 'image'      && <QImage     q={q} onAnswer={onAnswer}/>}
        {q.type === 'typing'     && <QTyping    q={q} onAnswer={onAnswer}/>}
        {q.type === 'audio'      && <QAudio     q={q} onAnswer={onAnswer}/>}
        {q.type === 'find_photo' && <QFindPhoto q={q} onAnswer={onAnswer}/>}
        {q.type === 'flashcard'  && <QFlashcard q={q} onAnswer={onAnswer}/>}
      </div>
      {session.mode === 'survival' && surviveTimer != null && (
        <SurvivalTimer seconds={surviveTimer}/>
      )}
    </div>
  );
};

// ─────────────── Question: French → Korean (MC) ───────────────
const QFrToKr = ({ q, onAnswer }) => {
  const { pick, stateFor } = useMcChoice(q, onAnswer);
  return (
    <div className="mf-scroll" style={qScrollStyle()}>
      <div style={{padding:'18px 0 6px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>French → Korean</div>
        <div style={{fontSize:24, fontWeight:800, lineHeight:1.2, marginTop:6, letterSpacing:'-.01em'}}>{q.card.fr}</div>
      </div>
      <div style={{display:'flex',flexDirection:'column',gap:10, marginTop:'auto'}}>
        {q.options.map((o, i) => (
          <OptionRow
            key={o.id}
            kbd={i+1}
            option={o.ro}
            sub={o.kr}
            state={stateFor(i)}
            onPick={() => pick(i)}
          />
        ))}
      </div>
    </div>
  );
};

// ─────────────── Question: Korean → French (MC) ───────────────
const QKrToFr = ({ q, onAnswer }) => {
  const { pick, stateFor } = useMcChoice(q, onAnswer);
  return (
    <div className="mf-scroll" style={qScrollStyle()}>
      <div style={{padding:'14px 0 18px', textAlign:'center'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>Korean → French</div>
        <div className="mf-kr" style={{fontSize:42, fontWeight:700, marginTop:14, letterSpacing:'-.01em'}}>{q.card.kr}</div>
        <div style={{fontSize:14, color:MF.textDim, marginTop:6, fontWeight:500, letterSpacing:'.02em'}}>{q.card.ro}</div>
      </div>
      <div style={{display:'flex',flexDirection:'column',gap:10, marginTop:'auto'}}>
        {q.options.map((o, i) => (
          <OptionRow key={o.id} kbd={i+1} option={o.fr} state={stateFor(i)} onPick={() => pick(i)}/>
        ))}
      </div>
    </div>
  );
};

// ─────────────── Question: Image identification ───────────────
const QImage = ({ q, onAnswer }) => {
  const { pick, stateFor } = useMcChoice(q, onAnswer);
  return (
    <div className="mf-scroll" style={qScrollStyle('6px 22px 24px')}>
      <div style={{padding:'10px 0 12px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>Identify the technique</div>
      </div>
      <div style={{width:'100%', aspectRatio:'4 / 3', flexShrink:0}}>
        <CardArt card={q.card} size="lg" blind/>
      </div>
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:8,marginTop:14}}>
        {q.options.map((o, i) => (
          <OptionRow key={o.id} option={o.ro} state={stateFor(i)} onPick={() => pick(i)}/>
        ))}
      </div>
    </div>
  );
};

// ─────────────── Question: Audio dictation ───────────────
const QAudio = ({ q, onAnswer }) => {
  const { pick, stateFor } = useMcChoice(q, onAnswer);
  const [playing, setPlaying] = useState(false);
  const [plays, setPlays] = useState(0);
  useEffect(() => {
    // auto-play once on mount
    setPlaying(true);
    setPlays(1);
    const t = setTimeout(() => setPlaying(false), 1400);
    return () => clearTimeout(t);
  }, []);
  const replay = () => {
    if (plays >= 3 || playing) return;
    setPlaying(true);
    setPlays(p => p + 1);
    setTimeout(() => setPlaying(false), 1400);
  };
  return (
    <div className="mf-scroll" style={qScrollStyle()}>
      <div style={{padding:'18px 0 14px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>Listen · what did you hear?</div>
      </div>
      <div style={{
        padding:'24px 18px', borderRadius:18, background: MF.card, border:`1px solid ${MF.line}`,
        display:'flex',flexDirection:'column',alignItems:'center',gap:18,
        boxShadow: playing ? `0 0 0 1px ${MF.purple}, 0 0 40px rgba(167,139,250,.15)` : 'none',
        transition:'box-shadow .3s',
      }}>
        <button onClick={replay} className={`mf-tap ${playing ? 'mf-glow' : ''}`} style={{
          width:64,height:64,borderRadius:99,
          background: playing ? MF.purple : MF.card2,
          border:`1.5px solid ${playing ? MF.purple : MF.line2}`,
          color: playing ? MF.bg : MF.text,
          fontSize:24, display:'flex',alignItems:'center',justifyContent:'center',
          cursor: plays>=3 ? 'default' : 'pointer',
          opacity: plays>=3 && !playing ? .4 : 1,
        }}>{playing ? '❚❚' : '▶'}</button>
        <Waveform playing={playing} big/>
        <div style={{fontSize:11, color: MF.textDim, fontWeight: 600}}>Replays {plays} / 3</div>
      </div>
      <div style={{display:'flex',flexDirection:'column',gap:8,marginTop:'auto'}}>
        {q.options.map((o, i) => (
          <OptionRow key={o.id} kbd={i+1} option={o.ro} sub={o.kr} state={stateFor(i)} onPick={() => pick(i)}/>
        ))}
      </div>
    </div>
  );
};

// ─────────────── Question: Typing ───────────────
const QTyping = ({ q, onAnswer }) => {
  const [value, setValue] = useState('');
  const [submitted, setSubmitted] = useState(null); // null | 'correct' | 'wrong'
  const inputRef = useRef();
  useEffect(() => {
    const t = setTimeout(() => inputRef.current?.focus(), 100);
    return () => clearTimeout(t);
  }, []);
  const submit = () => {
    if (submitted) return;
    const ok = answerMatches(value, q.card);
    setSubmitted(ok ? 'correct' : 'wrong');
    setTimeout(() => onAnswer(ok, { ro: value }), 700);
  };
  const giveUp = () => {
    if (submitted) return;
    setSubmitted('wrong');
    setTimeout(() => onAnswer(false, { ro: '' }), 600);
  };
  const borderColor = submitted === 'correct' ? MF.good : submitted === 'wrong' ? MF.bad : value ? MF.purple : MF.line;
  return (
    <div className="mf-scroll" style={qScrollStyle('8px 22px 18px')}>
      <div style={{padding:'18px 0 12px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>Type the Korean (romaja)</div>
        <div style={{fontSize:26, fontWeight:800, lineHeight:1.2, marginTop:8, letterSpacing:'-.01em'}}>{q.card.fr}</div>
      </div>
      <div style={{
        padding:'16px 18px', borderRadius:14,
        background: MF.card, border:`1.5px solid ${borderColor}`,
        boxShadow: submitted ? `0 0 0 1px ${borderColor}` : 'none',
        transition:'all .2s',
      }} className={submitted === 'wrong' ? 'mf-shake' : ''}>
        <div style={{fontSize:10,color:MF.textMute,letterSpacing:'.14em',textTransform:'uppercase',fontWeight:700,marginBottom:6}}>Your answer</div>
        <input
          ref={inputRef}
          className="mf-input"
          value={value}
          onChange={e => !submitted && setValue(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && submit()}
          placeholder="dollyo chagi…"
          autoCapitalize="none"
          autoCorrect="off"
          spellCheck={false}
        />
        {submitted === 'correct' && (
          <div className="mf-fade-up" style={{marginTop:8,fontSize:12,color:MF.good,fontWeight:600}}>✓ {q.card.ro} · {q.card.kr}</div>
        )}
        {submitted === 'wrong' && (
          <div className="mf-fade-up" style={{marginTop:8,fontSize:12,color:MF.bad,fontWeight:600}}>✕ correct was {q.card.ro} · {q.card.kr}</div>
        )}
      </div>
      <div style={{marginTop:14, display:'flex', gap:10, flexWrap:'wrap'}}>
        <span style={{padding:'5px 10px',background:MF.card,border:`1px solid ${MF.line}`,borderRadius:99,fontSize:11,color:MF.textDim,fontWeight:600}}>romaja accepted</span>
        <span style={{padding:'5px 10px',background:MF.card,border:`1px solid ${MF.line}`,borderRadius:99,fontSize:11,color:MF.textDim,fontWeight:600}}>spaces ignored</span>
      </div>
      <div style={{marginTop:'auto', display:'flex', gap: 10}}>
        <button onClick={giveUp} className="mf-tap mf-btn-ghost" style={{flex:1, padding:'14px', borderRadius:14, fontSize:14, fontWeight:600, cursor:'pointer'}}>I don't know</button>
        <button onClick={submit} className="mf-tap mf-btn-primary" style={{flex:2, padding:'14px', borderRadius:14, fontSize:14, fontWeight:700, cursor:'pointer', opacity: value.trim() ? 1 : .55}} disabled={!value.trim()}>Submit</button>
      </div>
    </div>
  );
};

// ─────────────── Survival countdown bar ───────────────
const SurvivalTimer = ({ seconds }) => {
  const max = 10;
  const pct = Math.max(0, Math.min(100, (seconds / max) * 100));
  const low = seconds <= 3;
  return (
    <div style={{position:'absolute', bottom:0, left:0, right:0, height:4, background: MF.bg2}}>
      <div style={{
        position:'absolute', left:0, top:0, bottom:0,
        width: pct + '%', transition: 'width 1s linear',
        background: low ? MF.bad : MF.amber,
        boxShadow: low ? `0 0 12px ${MF.bad}` : 'none',
      }}/>
    </div>
  );
};

// ─────────────── Question: Korean name → pick the right PHOTO (visual MC) ───────────────
const QFindPhoto = ({ q, onAnswer }) => {
  const { picked, pick, stateFor } = useMcChoice(q, onAnswer, 700);
  // Photo grid maps the shared MC state to its own border/ring colours.
  const colorFor = (state) => ({
    idle:    { border: MF.line,   ring: 'transparent' },
    correct: { border: MF.good,   ring: MF.good },
    wrong:   { border: MF.bad,    ring: MF.bad },
    dim:     { border: MF.line,   ring: 'transparent' },
  }[state]);
  return (
    <div className="mf-scroll" style={qScrollStyle()}>
      <div style={{padding:'14px 0 12px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>
          Quelle photo correspond ?
        </div>
        <div className="mf-kr" style={{
          fontSize: 28, fontWeight: 800, lineHeight: 1.2,
          marginTop: 6, letterSpacing: '-.01em',
        }}>{q.card.kr}</div>
        <div style={{fontSize: 13, color: MF.textDim, marginTop: 2}}>{q.card.fr}</div>
      </div>
      <div style={{
        display:'grid', gridTemplateColumns:'1fr 1fr', gap: 10,
        marginTop: 4,
      }}>
        {q.options.map((o, i) => {
          const state = stateFor(i);
          const col = colorFor(state);
          const isDim = state === 'dim';
          return (
            <button
              key={o.id}
              onClick={() => pick(i)}
              className={`mf-tap${state === 'wrong' ? ' mf-shake' : ''}${state === 'correct' ? ' mf-pop' : ''}`}
              disabled={picked != null}
              style={{
                aspectRatio:'1 / 1', borderRadius: 14, padding: 0,
                background: o.photo
                  ? `url(${o.photo}) center/cover`
                  : cardPlaceholderBg(o.tone),
                border:`2px solid ${col.border}`,
                boxShadow: col.ring !== 'transparent' ? `0 0 0 3px ${col.ring}55` : 'none',
                cursor: picked != null ? 'default' : 'pointer',
                opacity: isDim ? .35 : 1,
                position: 'relative', overflow: 'hidden',
                transition: 'opacity .2s, box-shadow .2s, border-color .2s',
              }}
            >
              {!o.photo && (
                <span className="mf-kr" style={{
                  position:'absolute', inset: 0, display:'flex',
                  alignItems:'center', justifyContent:'center',
                  color:'#fff', fontSize: 36, fontWeight: 800,
                  textShadow:'0 1px 6px rgba(0,0,0,.5)',
                }}>{o.kr?.slice(0,2) || '?'}</span>
              )}
              <span style={{
                position:'absolute', top: 8, left: 8,
                width: 24, height: 24, borderRadius: 99,
                background:'rgba(0,0,0,.55)', color:'#fff',
                fontSize: 12, fontWeight: 700,
                display:'flex', alignItems:'center', justifyContent:'center',
                border:'1px solid rgba(255,255,255,.18)',
              }}>{i + 1}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
};

// ─────────────── Question: Flashcard (per-card Q/A composition) ───────────────
//
// Generic flashcard: render every attribute in q.sides.question as the prompt,
// the user self-rates, then the reveal shows every attribute in
// q.sides.answer. Backed by the per-card Q/A picker in the editor.

// Renders a single card attribute by its key. `attrKey` is one of the core
// attributes ('photo' | 'fr' | 'kr' | 'ro' | 'cat' | 'tags') or a custom-field
// reference 'custom:<fieldId>'. Used both for the flashcard prompt and for the
// reveal answer side; `big` scales the typography up for the (larger) prompt.
// Returns null when the attribute is empty or its custom field no longer exists.
const AttributeRenderer = ({ attrKey, card, customFields, big }) => {
  if (attrKey === 'photo') {
    return (
      <div style={{width:'100%', aspectRatio:'4 / 3'}}>
        <CardArt card={card} size="lg" blind/>
      </div>
    );
  }
  if (attrKey === 'fr') return (
    <div style={{fontSize: big ? 26 : 18, fontWeight: 800, letterSpacing:'-.02em'}}>{card.fr}</div>
  );
  if (attrKey === 'kr') return (
    <div className="mf-kr" style={{fontSize: big ? 36 : 22, fontWeight: 700, letterSpacing:'-.01em'}}>{card.kr}</div>
  );
  if (attrKey === 'ro') return (
    <div style={{fontSize: big ? 18 : 14, color: MF.textDim, fontWeight: 600}}>{card.ro}</div>
  );
  if (attrKey === 'cat') return (
    <div style={{
      display:'inline-flex', padding:'5px 12px', borderRadius: 99,
      background: MF.card, border:`1px solid ${MF.line}`,
      fontSize: 12, fontWeight: 600, color: MF.textDim, alignSelf:'flex-start',
    }}>{card.cat}</div>
  );
  if (attrKey === 'tags') return (
    <div style={{display:'flex', flexWrap:'wrap', gap: 5}}>
      {(card.tags || []).map(t => (
        <span key={t} style={{
          padding:'3px 9px', borderRadius: 99,
          background:'rgba(167,139,250,.12)', border:`1px solid ${MF.purple}`,
          color: MF.purple, fontSize: 11, fontWeight: 600,
        }}>{t}</span>
      ))}
    </div>
  );
  if (attrKey.startsWith('custom:')) {
    const fieldId = attrKey.slice(7);
    const field = (customFields || []).find(f => f.id === fieldId);
    if (!field) return null;
    const value = card.custom?.[field.id];
    if (!value) return null;
    return <CustomFieldDisplay field={field} value={value}/>;
  }
  return null;
};

// Dispatches a built-in field to the right reveal widget for its type. Kept
// separate from AttributeRenderer so the type→widget routing lives in one place
// (it's reused by MediaDemonstration below).
const CustomFieldDisplay = ({ field, value }) => {
  if (field.type === 'video_url')  return <RevealVideoUrl value={value}/>;
  if (field.type === 'pdf_region') return <RevealPdfRegion value={value}/>;
  return null;
};

const QFlashcard = ({ q, onAnswer }) => {
  const [picked, setPicked] = useState(null);
  const sides = q.sides || { question: [], answer: [] };
  // Image-only prompt → single "Reveal" affordance (no self-rate before the
  // reveal). The user looks at the photo, taps reveal, then rates in the
  // Daily rating screen (or just continues in Quick/Demo/Survival).
  const photoOnly = sides.question.length === 1 && sides.question[0] === 'photo';

  const choose = (ok) => {
    if (picked != null) return;
    setPicked(ok);
    setTimeout(() => onAnswer(ok, q.card), 400);
  };

  if (photoOnly) {
    return (
      <PhotoOnlyFlashcard
        card={q.card}
        picked={picked}
        onReveal={() => choose(true)}
      />
    );
  }

  return (
    <div className="mf-scroll" style={qScrollStyle()}>
      <div style={{padding:'14px 0 12px'}}>
        <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase'}}>
          Identifie la technique
        </div>
        <div style={{fontSize:12, color:MF.textDim, marginTop:4}}>
          Rappelle-toi mentalement · la réponse s'affiche ensuite
        </div>
      </div>
      <div style={{display:'flex', flexDirection:'column', gap: 14, flexShrink: 0}}>
        {sides.question.map(key => (
          <AttributeRenderer
            key={key}
            attrKey={key}
            card={q.card}
            customFields={q.customFields}
            big
          />
        ))}
      </div>
      <div style={{marginTop:'auto', paddingTop: 18, display:'grid', gridTemplateColumns:'1fr 1fr', gap: 10}}>
        <button
          onClick={() => choose(false)}
          disabled={picked != null}
          className={`mf-tap${picked === false ? ' mf-shake' : ''}`}
          style={{
            padding:'18px 12px', borderRadius: 14,
            background: picked === false ? 'rgba(244,138,138,.18)' : MF.card,
            border:`1.5px solid ${picked === false ? MF.bad : MF.line}`,
            color: MF.bad, fontSize: 14, fontWeight: 700,
            cursor: picked != null ? 'default' : 'pointer',
            display:'flex', flexDirection:'column', alignItems:'center', gap: 4,
            boxShadow: picked === false ? `0 0 0 1px ${MF.bad}` : 'none',
            opacity: picked === true ? .5 : 1,
          }}
        >
          <span style={{fontSize: 18}}>✕</span>
          <span>Je ne savais pas</span>
        </button>
        <button
          onClick={() => choose(true)}
          disabled={picked != null}
          className={`mf-tap${picked === true ? ' mf-pop' : ''}`}
          style={{
            padding:'18px 12px', borderRadius: 14,
            background: picked === true ? 'rgba(116,216,161,.18)' : MF.card,
            border:`1.5px solid ${picked === true ? MF.good : MF.line}`,
            color: MF.good, fontSize: 14, fontWeight: 700,
            cursor: picked != null ? 'default' : 'pointer',
            display:'flex', flexDirection:'column', alignItems:'center', gap: 4,
            boxShadow: picked === true ? `0 0 0 1px ${MF.good}` : 'none',
            opacity: picked === false ? .5 : 1,
          }}
        >
          <span style={{fontSize: 18}}>✓</span>
          <span>Je le savais</span>
        </button>
      </div>
    </div>
  );
};

// ─────────────── Photo-only flashcard prompt ───────────────
//
// Special case of QFlashcard when the user put only "photo" in the Question
// column. The photo expands to fill every pixel between the in-screen header
// and the reveal button, and tapping it requests fullscreen so the user can
// study the technique up close before committing to a recall.

const PhotoOnlyFlashcard = ({ card, picked, onReveal }) => {
  const photoRef = React.useRef(null);
  const goFullscreen = () => {
    const el = photoRef.current;
    if (!el) return;
    const fn = el.requestFullscreen
            || el.webkitRequestFullscreen
            || el.msRequestFullscreen;
    if (!fn) return;
    try { fn.call(el)?.catch?.((err) => console.warn('Fullscreen rejected:', err)); }
    catch (err) { console.warn('Fullscreen failed:', err); }
  };

  return (
    <div style={{
      flex: 1, display: 'flex', flexDirection: 'column',
      padding: '6px 18px 18px', minHeight: 0,
    }}>
      <div style={{padding:'10px 0 8px', flexShrink: 0}}>
        <div style={{fontSize:10, color:MF.textMute, fontWeight:700, letterSpacing:'.16em', textTransform:'uppercase'}}>
          Identifie la technique
        </div>
        <div style={{fontSize:11, color:MF.textDim, marginTop:2}}>
          Tape l'image pour le plein écran · Révéler pour voir la réponse
        </div>
      </div>

      <div
        ref={photoRef}
        onClick={goFullscreen}
        className="mf-tap"
        title="Cliquer pour le plein écran"
        style={{
          flex: 1, minHeight: 0, width: '100%',
          background: '#000',
          borderRadius: 14, overflow: 'hidden',
          border: `1px solid ${MF.line}`,
          position: 'relative',
          cursor: card.photo ? 'zoom-in' : 'default',
        }}
      >
        {card.photo ? (
          <img
            src={card.photo}
            alt=""
            style={{
              position: 'absolute', inset: 0,
              width: '100%', height: '100%',
              objectFit: 'contain', display: 'block',
            }}
          />
        ) : (
          // Card lacks an actual photo — keep the colored placeholder so the
          // quiz never falls into a blank black rectangle.
          <div style={{position:'absolute', inset:0}}>
            <CardArt card={card} size="lg" blind/>
          </div>
        )}
        {card.photo && (
          <span style={{
            position:'absolute', top: 10, right: 10,
            display:'inline-flex', alignItems:'center', gap: 6,
            padding:'5px 10px', borderRadius: 99,
            background:'rgba(0,0,0,.55)', color:'#fff',
            border:'1px solid rgba(255,255,255,.18)',
            fontSize: 11, fontWeight: 600,
            pointerEvents: 'none',
          }}>
            <span style={{fontSize: 12, lineHeight: 1}}>⛶</span>
            <span>Plein écran</span>
          </span>
        )}
      </div>

      <div style={{paddingTop: 12, flexShrink: 0}}>
        <button
          onClick={onReveal}
          disabled={picked != null}
          className={`mf-tap mf-btn-primary mf-press${picked === true ? ' mf-pop' : ''}`}
          style={{
            width:'100%', padding:'16px', borderRadius: 14,
            fontSize: 15, fontWeight: 700,
            cursor: picked != null ? 'default' : 'pointer',
            display:'flex', alignItems:'center', justifyContent:'center', gap: 10,
            opacity: picked != null ? .8 : 1,
          }}
        >
          <span style={{fontSize: 16}}>👁</span>
          <span>Révéler la réponse</span>
        </button>
      </div>
    </div>
  );
};

// ─────────────── Reveal: media demonstration block ───────────────
// Surfaces the two built-in fields attached to the answer card so the reveal
// becomes a small study card: the demo video plays the technique and the PDF
// region (Carte) shows the cropped diagram. Renders nothing if the card has
// neither — preserves the original reveal layout.

// Reveal block listing the card's non-empty built-in fields, each under a small
// labelled header. Shown on the (non-flashcard) reveal.
const MediaDemonstration = ({ card, customFields }) => {
  const fields = customFields || [];
  // Walk the schema in declared order so the field order is respected.
  const items = [];
  for (const f of fields) {
    const v = card?.custom?.[f.id];
    if (!v) continue;
    items.push({ field: f, value: v });
  }
  if (items.length === 0) return null;

  return (
    <div className="mf-scroll" style={{
      flex: 1, minHeight: 0, overflow:'auto',
      padding:'14px 22px 4px', display:'flex', flexDirection:'column', gap: 14,
    }}>
      {items.map(({ field, value }) => (
        <div key={field.id}>
          <div style={{
            fontSize: 10, color: MF.textMute, fontWeight: 700,
            letterSpacing:'.16em', textTransform:'uppercase',
            marginBottom: 6,
            display:'inline-flex', alignItems:'center', gap: 6,
          }}>
            <span>{field.type === 'video_url' ? '📺' : '📄'}</span>
            <span>
              {field.type === 'video_url' ? 'Démonstration · ' : ''}
              {field.name}
            </span>
          </div>
          {field.type === 'video_url' && <RevealVideoUrl value={value}/>}
          {field.type === 'pdf_region' && <RevealPdfRegion value={value}/>}
        </div>
      ))}
    </div>
  );
};

// External-URL video: pasted into the card, auto-embeds at reveal time.
// Appends autoplay=1 to the embed src, preserving any existing query or hash
// fragment. Used after the user clicks Rejouer.
const _withAutoplay = (src) => {
  if (!src) return src;
  const hashIdx = src.indexOf('#');
  const head = hashIdx >= 0 ? src.slice(0, hashIdx) : src;
  const hash = hashIdx >= 0 ? src.slice(hashIdx) : '';
  const sep = head.includes('?') ? '&' : '?';
  return `${head}${sep}autoplay=1${hash}`;
};

const RevealVideoUrl = ({ value }) => {
  const embed = parseVideoEmbed(value);
  // Bumping replayKey remounts the iframe / video element, which restarts
  // playback from the configured startSec (the URL params already encode it).
  // > 0 also flips the iframe src to include autoplay=1 so the player starts
  // immediately on remount (YouTube / Vimeo need the URL flag — they don't
  // accept play() calls from outside their JS API).
  const [replayKey, setReplayKey] = React.useState(0);
  const videoRef = React.useRef(null);

  if (!embed) {
    // Unknown host — fall back to a tap chip so the user still has access.
    return (
      <a href={value} target="_blank" rel="noreferrer noopener" className="mf-tap" style={{
        display:'inline-flex', alignItems:'center', gap: 8,
        padding:'8px 14px', borderRadius: 99,
        background: MF.card, border:`1px solid ${MF.line}`,
        color: MF.purple, fontSize: 13, fontWeight: 600,
        textDecoration:'none', maxWidth:'100%',
        whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis',
      }}>
        <span>↗</span><span style={{overflow:'hidden', textOverflow:'ellipsis'}}>{value}</span>
      </a>
    );
  }

  const hasInterval = embed.startSec != null || embed.endSec != null;

  // For HTML5 <video>, manually stop at endSec — media-fragment #t=start,end
  // is not honored by every browser.
  const onTimeUpdate = (e) => {
    if (!embed.endSec) return;
    const v = e.currentTarget;
    if (v.currentTime >= embed.endSec) v.pause();
  };

  const replay = () => {
    if (embed.kind === 'video' && videoRef.current) {
      const v = videoRef.current;
      v.currentTime = embed.startSec || 0;
      v.play().catch(() => {});
    } else {
      // iframe — remount; the URL already has start/end baked in.
      setReplayKey(k => k + 1);
    }
  };

  return (
    <div style={{display:'flex', flexDirection:'column', gap: 8}}>
      <div style={{
        width:'100%', borderRadius: 12, overflow:'hidden',
        background:'#000', border:`1px solid ${MF.line}`,
        aspectRatio:'16 / 9',
      }}>
        {embed.kind === 'iframe' && (
          <iframe
            key={replayKey}
            src={replayKey > 0 ? _withAutoplay(embed.src) : embed.src}
            title="Démonstration vidéo"
            allow="autoplay; encrypted-media; picture-in-picture; fullscreen"
            allowFullScreen
            referrerPolicy="strict-origin-when-cross-origin"
            style={{width:'100%', height:'100%', border:0}}
          />
        )}
        {embed.kind === 'video' && (
          <video
            ref={videoRef}
            key={replayKey}
            src={embed.src}
            autoPlay playsInline controls
            loop={!hasInterval}
            onTimeUpdate={onTimeUpdate}
            style={{width:'100%', height:'100%', objectFit:'contain', background:'#000'}}
          />
        )}
      </div>
      {hasInterval && (
        <button
          onClick={replay}
          className="mf-tap"
          style={{
            alignSelf: 'flex-start',
            padding:'7px 12px', borderRadius: 99,
            background: MF.card, border:`1px solid ${MF.line}`,
            color: MF.textDim, fontSize: 12, fontWeight: 600,
            cursor:'pointer', fontFamily:'inherit',
            display:'inline-flex', alignItems:'center', gap: 6,
          }}
        >
          <span style={{fontSize: 13}}>↻</span>
          <span>Rejouer</span>
          <span style={{color: MF.textMute, fontWeight: 500}}>
            {formatSeconds(embed.startSec || 0)}
            {embed.endSec ? ` → ${formatSeconds(embed.endSec)}` : ''}
          </span>
        </button>
      )}
    </div>
  );
};

// ─────────────── REVEAL (post-answer with rating buttons) ───────────────
const RevealScreen = ({ result, onRate, onContinue, showRating }) => {
  const { card, ok, xp, combo, comboMult: mult, customFields, sides } = result;
  const isFlashcard = !!sides;
  // For flashcards, the reveal shows the user-defined answer side only — keeps
  // the visual split honest (we don't dump the prompt content into the reveal).
  const banner = isFlashcard ? (
    // For the flashcard banner, show the first text attribute in the answer
    // (typically "fr" or "kr"). Falls back to the card's fr name.
    (() => {
      for (const k of sides.answer) {
        if (k === 'fr' && card.fr) return card.fr;
        if (k === 'kr' && card.kr) return card.kr;
        if (k === 'ro' && card.ro) return card.ro;
      }
      return card.fr || card.kr || '';
    })()
  ) : card.fr;
  return (
    <div className="mf" style={{height:'100%', background: MF.bg, display:'flex', flexDirection:'column', overflow:'hidden', position:'relative'}}>
      {ok && <XPFloat amount={xp} show/>}
      {/* Big banner */}
      <div className="mf-fade-up" style={{padding:'26px 22px 10px', textAlign:'left'}}>
        <div style={{
          display:'inline-flex',alignItems:'center',gap:8,
          padding:'6px 12px',borderRadius:99,
          background: ok ? 'rgba(116,216,161,.12)' : 'rgba(244,138,138,.12)',
          border:`1px solid ${ok ? MF.good : MF.bad}`,
          color: ok ? MF.good : MF.bad,
          fontSize:13, fontWeight:700,
        }}>
          <span style={{fontSize:14}}>{ok ? '✓' : '✕'}</span>
          <span>{ok ? 'Correct' : 'Not quite'}</span>
          {ok && mult > 1 && <span style={{color:MF.purple,marginLeft:4}}>· combo ×{mult}</span>}
        </div>
        <div className={isFlashcard ? 'mf-kr' : undefined} style={{fontSize:30, fontWeight:800, marginTop:14, letterSpacing:'-.02em'}}>{banner}</div>
      </div>

      {isFlashcard ? (
        // Flashcard reveal — only the user-chosen answer attributes, rendered
        // one per row via the same AttributeRenderer as the prompt.
        <div className="mf-scroll" style={{
          flex: 1, minHeight: 0, overflow:'auto',
          padding:'10px 22px', display:'flex', flexDirection:'column', gap: 14,
        }}>
          {sides.answer.map(key => (
            <AttributeRenderer key={key} attrKey={key} card={card} customFields={customFields}/>
          ))}
        </div>
      ) : (
        <>
          {/* Default reveal — card detail strip + every attached media. */}
          <div style={{padding:'10px 22px'}}>
            <div style={{
              borderRadius:18, background: MF.card, border:`1px solid ${MF.line}`, overflow:'hidden',
              display:'flex', gap:14, alignItems:'stretch',
            }}>
              <div style={{width: 110, flexShrink:0}}>
                <CardArt card={card} size="sm"/>
              </div>
              <div style={{flex:1, padding:'12px 14px 12px 0', display:'flex', flexDirection:'column', justifyContent:'center', gap:6, minWidth:0}}>
                <div>
                  <div style={{fontSize:10,color:MF.textMute,letterSpacing:'.14em',textTransform:'uppercase',fontWeight:700}}>Korean</div>
                  <div className="mf-kr" style={{fontSize:22, fontWeight:700, marginTop:2}}>{card.kr}</div>
                </div>
                <div>
                  <div style={{fontSize:10,color:MF.textMute,letterSpacing:'.14em',textTransform:'uppercase',fontWeight:700}}>Romaja</div>
                  <div style={{fontSize:14, color:MF.textDim, fontWeight:600, marginTop:2}}>{card.ro}</div>
                </div>
                <span className="mf-tap" style={{
                  marginTop:4,
                  alignSelf:'flex-start', padding:'4px 10px', borderRadius:99,
                  background: MF.bg2, border:`1px solid ${MF.line}`, fontSize:11, color: MF.textDim, fontWeight:600,
                  display:'inline-flex',alignItems:'center',gap:5,
                }}>🔉 replay</span>
              </div>
            </div>
          </div>

          <MediaDemonstration card={card} customFields={customFields}/>
        </>
      )}

      {showRating ? (() => {
        // The delay under each button is computed from the card's CURRENT
        // interval, so the gaps widen as the card matures (see ratingIntervalCodes).
        const codes = ratingIntervalCodes(card.interval);
        const shortOf = (c) => (INTERVAL_BY_CODE[c] || INTERVAL_BY_CODE.none).short;
        const ratings = [
          { l:'Again', c:MF.bad,    v:1 },
          { l:'Hard',  c:MF.amber,  v:2 },
          { l:'Good',  c:MF.text,   v:3, primary:true },
          { l:'Easy',  c:MF.good,   v:4 },
          { l:'Super', c:MF.purple, v:5 },
        ];
        return (
        <div style={{padding:'18px 22px 24px', marginTop:'auto'}}>
          <div style={{fontSize:11,color:MF.textMute,fontWeight:700,letterSpacing:'.16em',textTransform:'uppercase',marginBottom:10,textAlign:'center'}}>
            How well did you remember?
          </div>
          <div style={{display:'grid',gridTemplateColumns:'repeat(5,1fr)',gap:6}}>
            {ratings.map(b => (
              <button key={b.l} onClick={() => onRate(b.v)} className="mf-tap mf-press" style={{
                padding:'12px 4px', borderRadius:12, cursor:'pointer',
                background: b.primary ? MF.card2 : MF.card,
                border:`1.5px solid ${b.primary ? MF.purple : MF.line}`,
                display:'flex',flexDirection:'column',alignItems:'center',gap:2,
              }}>
                <span style={{fontSize:13,fontWeight:700,color:b.c}}>{b.l}</span>
                <span style={{fontSize:10,color:MF.textMute,fontWeight:600}}>{shortOf(codes[b.v])}</span>
              </button>
            ))}
          </div>
          <div style={{marginTop:10, fontSize:10, color:MF.textMute, textAlign:'center'}}>
            Press <span style={{fontFamily:MF.mono, color: MF.textDim}}>1–5</span> · gaps widen each time you remember
          </div>
        </div>
        );
      })() : (
        <div style={{padding:'18px 22px 24px', marginTop:'auto'}}>
          <button onClick={onContinue} className="mf-tap mf-btn-primary mf-press" style={{
            width:'100%', padding:'16px', borderRadius:14, fontSize:15, fontWeight:700, cursor:'pointer',
          }}>Continue ›</button>
        </div>
      )}
    </div>
  );
};

// ─────────────── SUMMARY ───────────────
const SummaryScreen = ({ session, profile, onHome, onAgain }) => {
  const acc = session.correct + session.wrong > 0 ? Math.round((session.correct / (session.correct + session.wrong)) * 100) : 0;
  const wave = Math.floor(session.idx / 5) + 1;
  const survived = session.mode === 'survival';
  return (
    <div className="mf" style={{height:'100%', background: MF.bg, display:'flex', flexDirection:'column', overflow:'hidden', position:'relative'}}>
      <Sparkles n={22}/>
      <div style={{padding:'40px 22px 20px', textAlign:'center', position:'relative', zIndex:1}}>
        <div className="mf-pop" style={{
          width:88, height:88, borderRadius:99, margin:'0 auto',
          background:`linear-gradient(135deg, ${MF.purple}, ${MF.blue})`,
          display:'flex',alignItems:'center',justifyContent:'center',fontSize:38,
          boxShadow:'0 14px 40px rgba(167,139,250,.45)',
        }}>{survived ? '🏆' : '⚡'}</div>
        <div style={{fontSize:26, fontWeight:800, marginTop:18, letterSpacing:'-.02em'}}>
          {survived ? `Wave ${wave} cleared` : 'Run complete'}
        </div>
        <div style={{fontSize:13, color:MF.textDim, marginTop:6}}>
          {survived ? 'Survival session' : `${session.queue.length} cards · ${acc}% accuracy`}
        </div>
      </div>

      {/* XP summary card */}
      <div style={{padding:'8px 22px'}}>
        <div style={{
          borderRadius:18, padding:18, position:'relative', overflow:'hidden',
          background:`linear-gradient(135deg, ${MF.card} 0%, ${MF.card2} 100%)`,
          border:`1px solid ${MF.purple}`,
        }}>
          <div style={{position:'absolute', top:-30, right:-30, width:130, height:130, borderRadius:99, background:'radial-gradient(circle, rgba(167,139,250,.25) 0%, transparent 70%)'}}/>
          <div style={{position:'relative'}}>
            <div style={{fontSize:11,color:MF.purple,fontWeight:700,letterSpacing:'.18em',textTransform:'uppercase'}}>XP earned</div>
            <div style={{fontSize:46, fontWeight:800, marginTop:4, color:MF.purple, letterSpacing:'-.02em', lineHeight:1}}>+{session.xp}</div>
            <div style={{display:'flex',gap:14,marginTop:14}}>
              {[
                ['Correct', session.correct],
                ['Wrong', session.wrong],
                ['Best combo', session.bestCombo],
              ].map(([k,v]) => (
                <div key={k} style={{flex:1}}>
                  <div style={{fontSize:10,color:MF.textMute,letterSpacing:'.12em',textTransform:'uppercase',fontWeight:700}}>{k}</div>
                  <div style={{fontSize:20,fontWeight:800,marginTop:2}}>{v}</div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>

      {/* Streak update */}
      <div style={{padding:'14px 22px 0'}}>
        <div style={{
          padding:'12px 14px', borderRadius:14,
          background: MF.card, border:`1px solid ${MF.line}`,
          display:'flex',alignItems:'center',gap:12,
        }}>
          <span style={{fontSize:24}}>🔥</span>
          <div style={{flex:1}}>
            <div style={{fontSize:14,fontWeight:700}}>{profile.streak + (session.streakBumped ? 0 : 1)}-day streak</div>
            <div style={{fontSize:12,color:MF.textDim,marginTop:2}}>Come back tomorrow to keep it alive</div>
          </div>
          {!session.streakBumped && <span style={{color:MF.amber, fontWeight:800, fontSize:13}}>+1</span>}
        </div>
      </div>

      <div style={{padding:'14px 22px 22px', marginTop:'auto', display:'flex', gap:10}}>
        <button onClick={onHome} className="mf-tap mf-btn-ghost" style={{flex:1, padding:'14px', borderRadius:14, fontSize:14, fontWeight:600, cursor:'pointer'}}>Home</button>
        <button onClick={onAgain} className="mf-tap mf-btn-primary mf-press" style={{flex:2, padding:'14px', borderRadius:14, fontSize:14, fontWeight:700, cursor:'pointer'}}>Play again</button>
      </div>
    </div>
  );
};

// ─────────────── GAME OVER (survival fail) ───────────────
const GameOverScreen = ({ session, onHome, onAgain }) => {
  const wave = Math.floor(session.idx / 5) + 1;
  return (
    <div className="mf" style={{height:'100%', background: MF.bg, display:'flex', flexDirection:'column', overflow:'hidden'}}>
      <div style={{padding:'60px 22px 16px', textAlign:'center'}}>
        <div className="mf-pop" style={{
          width:88, height:88, borderRadius:99, margin:'0 auto',
          background:`linear-gradient(135deg, ${MF.badDim}, ${MF.bad})`,
          display:'flex',alignItems:'center',justifyContent:'center',fontSize:42,
          boxShadow:'0 14px 40px rgba(244,138,138,.35)',
        }}>💀</div>
        <div style={{fontSize:26, fontWeight:800, marginTop:18, letterSpacing:'-.02em'}}>Run ended</div>
        <div style={{fontSize:13, color:MF.textDim, marginTop:6}}>You made it to wave {wave}</div>
      </div>
      <div style={{padding:'10px 22px'}}>
        <div style={{borderRadius:18, padding:18, background: MF.card, border:`1px solid ${MF.line}`, display:'flex', gap:18}}>
          {[
            ['Score', session.idx],
            ['Best combo', session.bestCombo],
            ['XP', `+${session.xp}`],
          ].map(([k,v]) => (
            <div key={k} style={{flex:1}}>
              <div style={{fontSize:10,color:MF.textMute,letterSpacing:'.12em',textTransform:'uppercase',fontWeight:700}}>{k}</div>
              <div style={{fontSize:24,fontWeight:800,marginTop:4,color: k==='XP'?MF.purple:MF.text}}>{v}</div>
            </div>
          ))}
        </div>
      </div>
      <div style={{padding:'14px 22px 22px', marginTop:'auto', display:'flex', gap:10}}>
        <button onClick={onHome} className="mf-tap mf-btn-ghost" style={{flex:1, padding:'14px', borderRadius:14, fontSize:14, fontWeight:600, cursor:'pointer'}}>Home</button>
        <button onClick={onAgain} className="mf-tap mf-btn-primary mf-press" style={{flex:2, padding:'14px', borderRadius:14, fontSize:14, fontWeight:700, cursor:'pointer'}}>Try again</button>
      </div>
    </div>
  );
};

Object.assign(window, {
  HomeScreen, PlayScreen, QFrToKr, QKrToFr, QImage, QAudio, QTyping,
  QFindPhoto, QFlashcard,
  SurvivalTimer, RevealScreen, SummaryScreen, GameOverScreen,
});
