/* === PHYSL 210 — shared UI: quiz engine, flashcards, grids === */
const { useState: useS, useEffect: useE, useRef: useR } = React;

const LETTERS = ['A','B','C','D','E'];
const fmtTime = (s) => `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`;

/* ---- module emblem + grid ---- */
function ModuleEmblem({ m, size=46 }) {
  const c = modColor(m.hue);
  return <div className="mod-emblem" style={{ width:size, height:size, background:c.bg, color:c.ink }}>
    <ModuleGlyph m={m} size={Math.round(size*0.54)} color={c.ink} />
  </div>;
}

function ModuleGrid({ modules, onPick, meta, corner }) {
  return (
    <div className="mod-grid">
      {modules.map((m, idx) => {
        const c = modColor(m.hue);
        const cn = corner ? corner(m) : null;
        return (
          <div key={m.id} className="mod-card fade" style={{ animationDelay:`${idx*40}ms` }} onClick={() => onPick(m)}>
            {cn && <span className="mod-corner">{cn}</span>}
            {m.current && (corner
              ? <span className="tag" style={{ alignSelf:'flex-start', marginBottom:6, background:'var(--coral)', color:'#fff' }}>This week</span>
              : <span className="tag" style={{ position:'absolute', top:18, right:18, background:'var(--coral)', color:'#fff' }}>This week</span>)}
            <ModuleEmblem m={m} />
            <div className="mod-name">{m.name}</div>
            <div className="mod-meta">{meta ? meta(m) : m.blurb}</div>
            {typeof m.progress === 'number' && (
              <div className="mini-bar" style={{ background:c.soft }}>
                <i style={{ width:`${Math.round(m.progress*100)}%`, background:c.ink }}></i>
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

/* ---- score ring (SVG circle = allowed simple shape) ---- */
function ScoreRing({ pct, size=150, accent='var(--coral)', stroke=11, track='var(--line)' }) {
  const r = size/2 - stroke, C = 2*Math.PI*r;
  return (
    <svg width={size} height={size} style={{ transform:'rotate(-90deg)' }}>
      <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={track} strokeWidth={stroke} />
      <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={accent} strokeWidth={stroke} strokeLinecap="round"
        strokeDasharray={C} strokeDashoffset={C*(1-pct/100)} style={{ transition:'stroke-dashoffset 1s cubic-bezier(.3,.7,.3,1)' }} />
    </svg>
  );
}

/* ---- motion helpers (honor prefers-reduced-motion everywhere) ---- */
function prefersReducedMotion() {
  try { return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); }
  catch (e) { return false; }
}

/* count a number up to `target` over ~`duration`ms on mount; instant when reduced */
function useCountUp(target, { duration = 800, decimals = 0 } = {}) {
  const to = Number(target) || 0;
  const [val, setVal] = useS(() => prefersReducedMotion() ? to : 0);
  useE(() => {
    if (prefersReducedMotion()) { setVal(to); return; }
    let raf, start = null;
    const step = (ts) => {
      if (start == null) start = ts;
      const t = Math.min(1, (ts - start) / duration);
      const eased = 1 - Math.pow(1 - t, 3);          // easeOutCubic
      setVal(to * eased);
      if (t < 1) raf = requestAnimationFrame(step); else setVal(to);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [to, duration]);
  return decimals ? Number(val).toFixed(decimals) : Math.round(val);
}

/* a ScoreRing that fills from zero on mount (instant when reduced) */
function AnimatedRing({ pct, ...rest }) {
  const target = Math.max(0, Math.min(100, Number(pct) || 0));
  const [p, setP] = useS(() => prefersReducedMotion() ? target : 0);
  useE(() => {
    if (prefersReducedMotion()) { setP(target); return; }
    const id = requestAnimationFrame(() => setP(target));   // next frame → CSS transition plays
    return () => cancelAnimationFrame(id);
  }, [target]);
  return <ScoreRing pct={p} {...rest} />;
}

/* a token-styled bar whose fill grows from zero on mount */
function AnimatedBar({ pct, color, track = 'var(--line)', height = 8 }) {
  const target = Math.max(0, Math.min(100, Number(pct) || 0));
  const [w, setW] = useS(() => prefersReducedMotion() ? target : 0);
  useE(() => {
    if (prefersReducedMotion()) { setW(target); return; }
    const id = requestAnimationFrame(() => setW(target));
    return () => cancelAnimationFrame(id);
  }, [target]);
  return (
    <div className="mini-bar" style={{ background: track, height }}>
      <i style={{ width: `${w}%`, background: color, transition: 'width .9s cubic-bezier(.3,.7,.3,1)' }} />
    </div>
  );
}

/* small corner mastery badge for the practice / flashcards pickers */
function MiniRing({ pct, color, size = 34 }) {
  const p = Math.max(0, Math.min(100, Math.round(Number(pct) || 0)));
  const done = p >= 100;
  return (
    <div className="mini-ring" title={`${p}% mastered`} style={{ width: size, height: size }}>
      <AnimatedRing pct={p} size={size} stroke={3.5} accent={color} track="var(--line-soft)" />
      <span className="mini-ring-lbl" style={{ color }}>{done ? '✓' : p}</span>
    </div>
  );
}

/* ---- reusable confirm/dialog modal (backdrop fade + gentle scale-in) ---- */
function Modal({ open, onClose, labelledBy, describedBy, children, maxWidth = 440, className = '' }) {
  useE(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose && onClose(); } };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onClose]);
  if (!open) return null;
  const node = (
    <div className="modal-backdrop" onClick={() => onClose && onClose()}>
      <div className={'modal-card' + (className ? ' ' + className : '')} role="dialog" aria-modal="true" aria-labelledby={labelledBy} aria-describedby={describedBy}
        style={{ maxWidth }} onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  );
  // Portal to <body> so the fixed backdrop escapes any transformed ancestor (the .fade
  // wrappers keep an identity-matrix transform via animation-fill-mode:both, which would
  // otherwise become the containing block and mis-place the overlay).
  return (typeof ReactDOM !== 'undefined' && ReactDOM.createPortal && document.body)
    ? ReactDOM.createPortal(node, document.body)
    : node;
}

/* ============ "EXPLAIN LIKE I'M NEW" — deep first-principles explanation ============
   Looks up a gold, hand-holding explanation for a question (built by the
   explanation workflow into window.MORE_EXPL, keyed module → normalized stem) and
   shows it in a modal: titled build-up sections + cited sources (textbook page/
   section, course notes, high-quality web). Renders nothing if none exists yet. */
function moreNorm(s) { return (s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); }
function lookupMore(q, moduleId) {
  try {
    const mod = (q && q._mod) || moduleId;
    const tbl = (window.MORE_EXPL || {})[mod]; if (!tbl) return null;
    return tbl[moreNorm(q && q.q)] || null;
  } catch (e) { return null; }
}
function RichText({ text }) {
  const bold = (s) => String(s).split(/(\*\*[^*]+\*\*)/g).map((p, i) =>
    (p.startsWith('**') && p.endsWith('**')) ? <b key={i}>{p.slice(2, -2)}</b> : <React.Fragment key={i}>{p}</React.Fragment>);
  const blocks = String(text || '').split(/\n{2,}/).filter(b => b.trim());
  return blocks.map((blk, bi) => {
    const lines = blk.split('\n');
    if (lines.length && lines.every(l => /^\s*[-•]\s+/.test(l)))
      return <ul key={bi} className="more-ul">{lines.map((l, li) => <li key={li}>{bold(l.replace(/^\s*[-•]\s+/, ''))}</li>)}</ul>;
    return <p key={bi} className="more-p">{bold(blk)}</p>;
  });
}
function SourceLine({ s }) {
  if (!s) return null;
  if (s.kind === 'textbook') return <li className="more-src"><span className="more-src-ic">📖</span><span>Vander's Human Physiology — <b>Ch {s.chapter}{s.section ? `, §${s.section}` : ''}, p.&nbsp;{s.page}</b>{s.quote ? <span className="more-q"> “{s.quote}”</span> : ''}</span></li>;
  if (s.kind === 'notes') return <li className="more-src"><span className="more-src-ic">📝</span><span>{s.label || 'Course study notes'}</span></li>;
  if (s.kind === 'faq')   return <li className="more-src"><span className="more-src-ic">❓</span><span>{s.label || 'Course FAQ'}</span></li>;
  if (s.kind === 'web')   return <li className="more-src"><span className="more-src-ic">🔗</span><span>{s.url ? <a href={s.url} target="_blank" rel="noopener noreferrer">{s.title || s.url}</a> : (s.title || '')}{s.publisher ? ` — ${s.publisher}` : ''}</span></li>;
  return null;
}
function MoreExplain({ q, moduleId, accent = 'var(--coral)' }) {
  const [open, setOpen] = useS(false);
  const more = lookupMore(q, moduleId);
  if (!more || !Array.isArray(more.sections) || !more.sections.length) return null;
  return (
    <>
      {' '}<button className="more-link" style={{ color: accent }} onClick={() => setOpen(true)}>More</button>
      <Modal open={open} onClose={() => setOpen(false)} maxWidth={1040} className="more-card">
        <div className="more-sheet">
          <div className="more-bar">
            <span className="more-bar-label" style={{ color: accent }}>In&nbsp;depth · explained from scratch</span>
            <button className="more-x" onClick={() => setOpen(false)} aria-label="Close">✕</button>
          </div>
          <div className="more-scroll">
            <div className="more-inner">
              <div className="more-q-eyebrow">The question</div>
              <div className="more-q-stem">{q.q}</div>
              {more.sections.map((sec, i) => (
                <div key={i} className="more-section fade" style={{ animationDelay: `${Math.min(i, 8) * 45}ms` }}>
                  {sec.title && <h4 className="more-h">{sec.title}</h4>}
                  <div className="more-body"><RichText text={sec.body} /></div>
                </div>
              ))}
              {Array.isArray(more.sources) && more.sources.length > 0 && (
                <div className="more-sources">
                  <div className="more-sources-t">Sources</div>
                  <ul>{more.sources.map((s, i) => <SourceLine key={i} s={s} />)}</ul>
                </div>
              )}
            </div>
          </div>
        </div>
      </Modal>
    </>
  );
}

/* ============ QUIZ RUNNER (practice + exam) ============ */
function QuizRunner({ questions, mode='practice', title, accent='var(--coral)', onExit, timeLimit=0, moduleId=null }) {
  const [i, setI] = useS(0);
  const [answers, setAnswers] = useS(() => questions.map(() => null));
  const [revealed, setRevealed] = useS(() => questions.map(() => false));
  const [done, setDone] = useS(false);
  const [confirm, setConfirm] = useS(false);
  const [timeLeft, setTimeLeft] = useS(timeLimit);
  const topRef = useR(null);
  const examLogged = useR(false);

  // stats: record the whole exam at submit (every question, unanswered = wrong),
  // once per submission. resets if the student retries.
  useE(() => {
    if (mode !== 'exam') return;
    if (done && !examLogged.current) {
      examLogged.current = true;
      try {
        questions.forEach((qq, k) => {
          const mod = qq._mod || moduleId;
          recordAnswer(mod, statsKey(mod, qq.q), qq.src === 'textbook' ? 'textbook' : 'course', answers[k] === qq.a);
        });
      } catch (e) {}
    }
    if (!done) examLogged.current = false;
  }, [done]);

  useE(() => {
    if (mode !== 'exam' || done || !timeLimit) return;
    if (timeLeft <= 0) { setDone(true); return; }
    const t = setTimeout(() => setTimeLeft(s => s - 1), 1000);
    return () => clearTimeout(t);
  }, [timeLeft, mode, done, timeLimit]);

  useE(() => { if (topRef.current) topRef.current.focus({ preventScroll:true }); }, [i, done]);

  const q = questions[i];
  const sel = answers[i];
  const isRevealed = revealed[i];
  const score = answers.reduce((n, a, k) => n + (a === questions[k].a ? 1 : 0), 0);
  const answeredCount = answers.filter(a => a !== null).length;

  function choose(idx) {
    if (mode === 'practice') {
      if (isRevealed) return;
      setAnswers(a => a.map((v, k) => k === i ? idx : v));
      setRevealed(r => r.map((v, k) => k === i ? true : v));
      // stats: record the moment the answer is revealed in practice
      try {
        const mod = q._mod || moduleId;
        recordAnswer(mod, statsKey(mod, q.q), q.src === 'textbook' ? 'textbook' : 'course', idx === q.a);
      } catch (e) {}
    } else {
      setAnswers(a => a.map((v, k) => k === i ? idx : v));
    }
  }
  const go = (d) => setI(v => Math.min(questions.length - 1, Math.max(0, v + d)));

  /* ---------- results ---------- */
  if (done) {
    const pct = Math.round(score / questions.length * 100);
    const pass = pct >= 50;
    const msg = pct >= 85 ? 'Outstanding — you really know this.' : pct >= 70 ? 'Strong work. A little polish and you’re there.' : pct >= 50 ? 'A pass — review the misses and go again.' : 'Keep going — revisit the notes and retry.';
    return (
      <div className="quiz-wrap fade">
        <div className="panel" style={{ textAlign:'center', padding:'40px 38px 34px' }}>
          <div style={{ position:'relative', width:150, height:150, margin:'0 auto 6px' }}>
            <ScoreRing pct={pct} accent={accent} />
            <div style={{ position:'absolute', inset:0, display:'grid', placeItems:'center', flexDirection:'column' }}>
              <div className="stat-num" style={{ fontSize:42, color:accent }}>{pct}%</div>
            </div>
          </div>
          <div className="disp" style={{ fontSize:26, fontWeight:600, marginTop:8 }}>{score} / {questions.length} correct</div>
          <div style={{ color:'var(--muted)', fontSize:16, marginTop:6 }}>{msg}</div>
          <div style={{ display:'flex', gap:12, justifyContent:'center', marginTop:24 }}>
            <button className="btn btn-md btn-ghost" onClick={onExit}>Back</button>
            <button className="btn btn-md btn-primary" onClick={() => { setI(0); setAnswers(questions.map(()=>null)); setRevealed(questions.map(()=>false)); setTimeLeft(timeLimit); setDone(false); }}>Try again</button>
          </div>
        </div>

        <div style={{ marginTop:26 }}>
          <div className="page-eyebrow" style={{ marginBottom:14 }}>Review</div>
          {questions.map((qq, k) => {
            const your = answers[k];
            const right = your === qq.a;
            return (
              <div key={k} className="panel" style={{ marginBottom:14, padding:'20px 24px' }}>
                <div style={{ display:'flex', gap:12, alignItems:'flex-start' }}>
                  <div className="row-num" style={{ background: right ? 'oklch(0.95 0.04 165)' : 'oklch(0.95 0.04 28)', color: right ? 'var(--p-mint-i)' : 'oklch(0.55 0.16 25)' }}>{right ? '✓' : '✕'}</div>
                  <div style={{ flex:1 }}>
                    <div className="disp" style={{ fontSize:17, fontWeight:600, lineHeight:1.35 }}>{qq.q}</div>
                    {qq.cite && <div className="qcite" style={{ margin:'5px 0 0' }}>📖 {qq.cite}</div>}
                    <div style={{ fontSize:14.5, marginTop:8, color:'var(--ink-soft)' }}>
                      {your === null ? <span style={{ color:'var(--muted)' }}>Not answered. </span> : !right && <span>Your answer: <b style={{ color:'oklch(0.55 0.16 25)' }}>{LETTERS[your]}. {qq.options[your]}</b>. </span>}
                      Correct: <b style={{ color:'var(--p-mint-i)' }}>{LETTERS[qq.a]}. {qq.options[qq.a]}</b>
                    </div>
                    <div style={{ fontSize:14, marginTop:8, color:'var(--muted)', lineHeight:1.5 }}>{qq.e}</div>
                    <MoreExplain q={qq} moduleId={moduleId} accent={accent} />
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  /* ---------- running ---------- */
  const lowTime = mode === 'exam' && timeLeft <= 60;
  return (
    <div className="quiz-wrap">
      <div className="quiz-top">
        <button className="crumb" style={{ margin:0 }} onClick={onExit}>← Exit</button>
        <div className="quiz-progress"><i style={{ width:`${(i+1)/questions.length*100}%` }}></i></div>
        {mode === 'exam' && timeLimit
          ? <div className="chip" style={{ background: lowTime ? 'oklch(0.95 0.04 28)' : 'var(--surface)', borderColor: lowTime ? 'oklch(0.62 0.16 25)' : 'var(--line)', color: lowTime ? 'oklch(0.55 0.16 25)' : 'var(--ink-soft)', fontVariantNumeric:'tabular-nums' }}>⏱ {fmtTime(Math.max(0,timeLeft))}</div>
          : <div className="chip" style={{ fontVariantNumeric:'tabular-nums' }}>{i+1} / {questions.length}</div>}
      </div>

      <div className="qcard fade" key={i} tabIndex={-1} ref={topRef} style={{ outline:'none' }}>
        <div className="page-eyebrow" style={{ color:accent }}>{title} · Question {i+1}</div>
        <div className="qstem">{q.q}</div>
        {q.cite && <div className="qcite">📖 {q.cite}</div>}
        {q.options.map((opt, idx) => {
          let cls = 'opt';
          if (mode === 'practice' && isRevealed) {
            if (idx === q.a) cls += ' correct';
            else if (idx === sel) cls += ' wrong';
          } else if (sel === idx) cls += ' sel';
          return (
            <button key={idx} className={cls} disabled={mode==='practice' && isRevealed} onClick={() => choose(idx)}>
              <span className="opt-key">{LETTERS[idx]}</span>
              <span style={{ flex:1 }}>{opt}</span>
            </button>
          );
        })}

        {mode === 'practice' && isRevealed && (
          <div className="explain fade" style={{ marginTop:14 }}>
            <b>{sel === q.a ? 'Correct! ' : 'Not quite. '}</b>{q.e}
            <MoreExplain q={q} moduleId={moduleId} accent={accent} />
          </div>
        )}

        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginTop:24, gap:12 }}>
          {mode === 'exam'
            ? <button className="btn btn-md btn-ghost" onClick={() => go(-1)} disabled={i===0} style={{ opacity:i===0?0.4:1 }}>← Previous</button>
            : <span style={{ fontSize:13.5, color:'var(--muted)', fontWeight:600 }}>Score so far: {score}/{answeredCount||0}</span>}

          {i < questions.length - 1
            ? <button className="btn btn-md btn-primary" onClick={() => go(1)} disabled={mode==='practice' && !isRevealed}>Next →</button>
            : (mode === 'practice'
                ? <button className="btn btn-md btn-primary" onClick={() => setDone(true)} disabled={!isRevealed}>See results</button>
                : <button className="btn btn-md btn-primary" onClick={() => setConfirm(true)}>Submit exam</button>)}
        </div>
      </div>

      {/* exam question navigator */}
      {mode === 'exam' && (
        <div style={{ display:'flex', flexWrap:'wrap', gap:8, justifyContent:'center', marginTop:24 }}>
          {questions.map((_, k) => (
            <button key={k} onClick={() => setI(k)} title={`Question ${k+1}`}
              style={{ width:34, height:34, borderRadius:10, cursor:'pointer', fontFamily:'Quicksand', fontWeight:700, fontSize:13,
                border:'1.5px solid', borderColor: k===i ? accent : (answers[k]!==null ? 'transparent' : 'var(--line)'),
                background: answers[k]!==null ? accent : 'var(--surface)', color: answers[k]!==null ? '#fff' : 'var(--ink-soft)' }}>
              {k+1}
            </button>
          ))}
        </div>
      )}

      {confirm && (
        <div onClick={() => setConfirm(false)} style={{ position:'fixed', inset:0, background:'oklch(0.3 0.02 50 / .5)', backdropFilter:'blur(6px)', display:'grid', placeItems:'center', zIndex:60 }}>
          <div onClick={e => e.stopPropagation()} className="panel fade" style={{ maxWidth:400, textAlign:'center' }}>
            <div className="disp" style={{ fontSize:23, fontWeight:600 }}>Submit your exam?</div>
            <div style={{ color:'var(--muted)', marginTop:8, fontSize:15 }}>You’ve answered {answeredCount} of {questions.length} questions. Unanswered questions are marked wrong.</div>
            <div style={{ display:'flex', gap:12, justifyContent:'center', marginTop:22 }}>
              <button className="btn btn-md btn-ghost" onClick={() => setConfirm(false)}>Keep going</button>
              <button className="btn btn-md btn-primary" onClick={() => { setConfirm(false); setDone(true); }}>Submit</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ============ FLASHCARD DECK ============
   mode='learn' → each rating advances the card's stored mastery level (persisted
                  via recordCardRating, bumps the daily activity / streak).
   mode='cram'  → ratings drive an in-session tally only; nothing is written to
                  storage, the streak, the rings, or the activity counts. */
const FC_RATINGS = [
  { key:'again', label:'Again', cls:'again' },
  { key:'hard',  label:'Hard',  cls:'hard'  },
  { key:'good',  label:'Good',  cls:'good'  },
  { key:'easy',  label:'Easy',  cls:'easy'  },
];
function FlashDeck({ cards, accent='var(--coral)', onExit, title, moduleId=null, mode='learn' }) {
  const track = mode === 'learn';
  const [order, setOrder] = useS(() => cards.map((_, k) => k));
  const [pos, setPos] = useS(0);
  const [flipped, setFlipped] = useS(false);
  const [ratings, setRatings] = useS(() => ({}));   // cardIdx → last rating this session
  const [cramLvl, setCramLvl] = useS(() => ({}));    // cardIdx → ephemeral level (cram only)

  const cardIdx = order[pos];
  const card = cards[cardIdx];
  const mod = (card && card._mod) || moduleId;

  // level shown under the card: real stored mastery in learn, in-session preview in cram
  const shownLevel = track ? cardLevel(mod, card && card.t) : (cramLvl[cardIdx] || 0);

  const rate = (rating) => {
    if (track) {
      // stats: persist the rating → advance the card's mastery level
      try { recordCardRating(mod, statsKey(mod, card.t), rating); } catch (e) {}
    } else {
      // cram: in-session only — never touches storage / streak / rings
      setCramLvl(m => ({ ...m, [cardIdx]: nextLevel(cramLvl[cardIdx] || 0, rating) }));
    }
    setRatings(r => ({ ...r, [cardIdx]: rating }));
    setFlipped(false);
    setTimeout(() => setPos(p => Math.min(cards.length - 1, p + 1)), 140);
  };
  const prev = () => { setFlipped(false); setPos(p => Math.max(0, p - 1)); };
  const goNext = () => { setFlipped(false); setPos(p => Math.min(cards.length - 1, p + 1)); };
  const shuffle = () => { setOrder([...order].sort(() => Math.random() - 0.5)); setPos(0); setFlipped(false); };

  const atEnd = pos === cards.length - 1;
  const reviewed = Object.keys(ratings).length;
  const masteredNow = track
    ? cards.filter(c => cardLevel((c._mod || moduleId), c.t) === CARD_MAX_LEVEL).length
    : Object.keys(cramLvl).filter(k => cramLvl[k] === CARD_MAX_LEVEL).length;

  return (
    <div style={{ maxWidth:620, margin:'0 auto' }}>
      <div className="quiz-top">
        <button className="crumb" style={{ margin:0 }} onClick={onExit}>← Exit</button>
        <div className="quiz-progress"><i style={{ width:`${(pos+1)/cards.length*100}%`, background:accent }}></i></div>
        <div className="chip" style={{ fontVariantNumeric:'tabular-nums' }}>{pos+1} / {cards.length}</div>
      </div>

      <div className="fc-stage">
        <div className={`fc${flipped ? ' flipped' : ''}`} onClick={() => setFlipped(f => !f)}>
          <div className="fc-face fc-front">
            <div className="fc-hint">{title}</div>
            <div className="fc-term">{card.t}</div>
            <div className="fc-hint" style={{ color:accent }}>Tap to reveal</div>
          </div>
          <div className="fc-face fc-back">
            <div className="fc-hint" style={{ color:'var(--coral-deep)' }}>Definition</div>
            <div className="fc-def">{card.d}</div>
            {card.cite && <div className="fc-hint" style={{ marginTop:12, opacity:.75 }}>📖 {card.cite}</div>}
          </div>
        </div>
      </div>

      <div className="fc-status">
        {track
          ? <><span className="fc-status-lbl">Mastery</span><span className={`lvl-badge lvl-${shownLevel}`}>{CARD_LEVEL_LABELS[shownLevel]}</span></>
          : <span className="cram-chip">⚡ Cram · progress not saved</span>}
      </div>

      <div className="fc-rate" role="group" aria-label="Rate your recall">
        {FC_RATINGS.map(r => (
          <button key={r.key} className={`fc-rate-btn ${r.cls}`} onClick={() => rate(r.key)}>{r.label}</button>
        ))}
      </div>

      <div className="fc-foot">
        <button className="btn btn-md btn-ghost" onClick={prev} disabled={pos===0} style={{ opacity:pos===0?0.4:1 }}>←</button>
        <span className="fc-session">
          {track ? <>Mastered <b style={{ color:'var(--p-mint-i)' }}>{masteredNow}</b> / {cards.length}</>
                 : <>Reviewed <b style={{ color:'var(--ink)' }}>{reviewed}</b> / {cards.length}</>}
        </span>
        <span className="fc-shuffle" style={{ color:accent }} onClick={shuffle}>⟳ Shuffle</span>
        <button className="btn btn-md btn-ghost" onClick={goNext} disabled={atEnd} style={{ opacity:atEnd?0.4:1 }}>→</button>
      </div>
    </div>
  );
}

Object.assign(window, { QuizRunner, FlashDeck, ModuleGrid, ModuleEmblem, ScoreRing, fmtTime, LETTERS,
  Modal, AnimatedRing, AnimatedBar, MiniRing, useCountUp, prefersReducedMotion });
