// Home, Results, Review, Stats, Tests (categories), Settings screens.
// Globals: React, VOCAB, CATEGORY_META, FAMILY_META, FAMILY_ORDER, lastNDays, formatDuration, todayKey.

const { useState: sUseState, useMemo: sUseMemo, useEffect: sUseEffect } = React;

// Direction-A desktop layouts switch on at 761px (matches the side-nav breakpoint).
function useIsDesktop() {
  const [d, setD] = sUseState(() => typeof window !== "undefined" && window.matchMedia("(min-width: 761px)").matches);
  sUseEffect(() => {
    const mq = window.matchMedia("(min-width: 761px)");
    const on = () => setD(mq.matches);
    mq.addEventListener ? mq.addEventListener("change", on) : mq.addListener(on);
    return () => { mq.removeEventListener ? mq.removeEventListener("change", on) : mq.removeListener(on); };
  }, []);
  return d;
}

// Accuracy -> status dot level (green / yellow / red / none)
function accLevel(seen, acc) {
  if (!seen) return "none";
  if (acc >= 80) return "ok";
  if (acc >= 50) return "warn";
  return "no";
}

// =====================================================================
// TOP NAV — charcoal pill bar, shared across main screens
// =====================================================================
function TopNav({ active, go, currentUser, onSignOut }) {
  const tabs = [
    ["home",       "Home"],
    ["categories", "Tests"],
    ["live",       "Live", true],
    ["mistakes",   "Mistakes"],
    ["class",      "Class"],
    ["stats",      "Stats"],
    ["settings",   "Settings"],
  ];
  const isGuest = !currentUser || currentUser === "guest";
  return (
    <div className="topnav-wrap">
      <nav className="topnav" aria-label="Primary">
        <button className="topnav-brand" onClick={() => go("home")}>
          <span className="tn-brand-main">iri</span>
          <span className="tn-brand-accent">kos</span>
        </button>
        <div className="topnav-tabs">
          {tabs.map(([id, label, live]) => (
            <button
              key={id}
              className={`topnav-tab ${active === id ? "is-active" : ""}`}
              onClick={() => go(id)}
              aria-current={active === id ? "page" : undefined}
            >{live && <span className="tn-live-dot" aria-hidden="true" />}{label}</button>
          ))}
        </div>
        <button
          className={`topnav-avatar ${isGuest ? "" : "is-handle"}`}
          aria-label={isGuest ? "Sign in" : "Account"}
          title={isGuest ? "Guest — sign in to sync your stats" : `Signed in as @${String(currentUser).replace(/^@/, "")} — tap to sign out`}
          onClick={onSignOut}
        >
          {isGuest ? (
            <svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" strokeWidth="1.6">
              <circle cx="12" cy="8.5" r="3.6" />
              <path d="M5 19.5c1.2-3.4 4-5 7-5s5.8 1.6 7 5" strokeLinecap="round" />
            </svg>
          ) : (
            <span className="tn-handle">@{String(currentUser).replace(/^@/, "")}</span>
          )}
        </button>
      </nav>
    </div>
  );
}

// =====================================================================
// BOTTOM TAB BAR — mobile only (CSS hides it ≥ 760px and hides TopNav < 760px).
// Thumb-reachable; Home · Tests · Mistakes · Stats · Settings. (Class lives in
// Settings on mobile to keep the bar to five comfortable targets.)
// =====================================================================
const NAV_ICON = {
  home: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 10.5L12 3l9 7.5" /><path d="M5 9.5V20h14V9.5" /><path d="M9.5 20v-5h5v5" />
    </svg>
  ),
  tests: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <rect x="4" y="3" width="16" height="18" rx="2.5" /><path d="M8.5 8.5h7M8.5 12h7M8.5 15.5h4" />
    </svg>
  ),
  trainer: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="8.5" /><circle cx="12" cy="12" r="4" /><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none" />
    </svg>
  ),
  mistakes: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3l9 16H3z" /><path d="M12 9.5v4.5" /><circle cx="12" cy="16.6" r="0.4" fill="currentColor" stroke="none" />
    </svg>
  ),
  stats: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <path d="M4 20v-7M10 20V5M16 20v-9" /><path d="M3 20.5h18" />
    </svg>
  ),
  settings: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="3.2" />
      <path d="M12 2v2.5M12 19.5V22M4.2 4.2l1.8 1.8M18 18l1.8 1.8M2 12h2.5M19.5 12H22M4.2 19.8l1.8-1.8M18 6l1.8-1.8" />
    </svg>
  ),
  class: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <circle cx="8.5" cy="8" r="2.7" />
      <path d="M3.6 19c0-2.8 2.2-4.7 4.9-4.7s4.9 1.9 4.9 4.7" />
      <path d="M15.6 6.1a2.6 2.6 0 0 1 0 5.1" />
      <path d="M16.2 14.5c2.2.4 3.8 2.1 3.8 4.5" />
    </svg>
  ),
  live: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="2.2" fill="currentColor" stroke="none" />
      <path d="M7.5 7.5a6.4 6.4 0 0 0 0 9M16.5 7.5a6.4 6.4 0 0 1 0 9" />
      <path d="M4.7 4.7a10.3 10.3 0 0 0 0 14.6M19.3 4.7a10.3 10.3 0 0 1 0 14.6" opacity="0.5" />
    </svg>
  ),
  mytasks: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <rect x="5" y="4" width="14" height="17" rx="2.5" />
      <path d="M9 4.5V3.6a1.1 1.1 0 0 1 1.1-1.1h3.8A1.1 1.1 0 0 1 15 3.6v0.9" />
      <path d="M8.5 13l2.2 2.2L15.5 10" />
    </svg>
  ),
  students: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <circle cx="8" cy="8.5" r="2.4" /><circle cx="16" cy="8.5" r="2.4" />
      <path d="M3.4 18.4c0-2.4 2-4 4.6-4 1.3 0 2.5.4 3.3 1.1" />
      <path d="M12.7 15.5c.8-.7 2-1.1 3.3-1.1 2.6 0 4.6 1.6 4.6 4" />
    </svg>
  ),
  profile: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={p.sw} strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="8" r="3.4" />
      <path d="M5 20c0-3.3 3-5.5 7-5.5s7 2.2 7 5.5" />
    </svg>
  ),
};
function BottomNav({ active, go, isTeacher }) {
  // Teachers get their tools (My Tasks · Students) in the bar; Mistakes + Stats
  // move into Profile for them. Students keep the original set. "Settings" is
  // now "Profile" for everyone. Tests stays inside Profile on mobile; Live is
  // launched from Home.
  const tabs = isTeacher
    ? [
        ["home", "Home", NAV_ICON.home],
        ["class", "Class", NAV_ICON.class],
        ["mytasks", "My Tasks", NAV_ICON.mytasks],
        ["trainer", "Trainer", NAV_ICON.trainer],
        ["settings", "Profile", NAV_ICON.profile],
      ]
    : [
        ["home", "Home", NAV_ICON.home],
        ["class", "Class", NAV_ICON.class],
        ["trainer", "Trainer", NAV_ICON.trainer],
        ["stats", "Stats", NAV_ICON.stats],
        ["settings", "Profile", NAV_ICON.profile],
      ];
  return (
    <nav className="bottom-nav" aria-label="Primary">
      {tabs.map(([id, label, Ic]) => {
        const on = active === id;
        return (
          <button key={id} className={`bn-tab ${on ? "is-active" : ""}`} aria-current={on ? "page" : undefined}
            onClick={(e) => { if (!on && window.tapFeedback) window.tapFeedback("selection", e); go(id); }}>
            <Ic sw={on ? 2.1 : 1.8} />
            <span className="bn-label">{label}</span>
          </button>
        );
      })}
    </nav>
  );
}

// =====================================================================
// SIDE NAV — desktop left rail (Direction A). CSS shows it > 760px and hides
// the bottom bar there; ≤ 760px the bottom bar takes over and this is hidden.
// Replaces the top pill nav on wide screens so the content can use the width.
// =====================================================================
function SideNav({ active, go, currentUser, onSignOut, mistakesCount = 0, isTeacher = false, taskCount = 0, studentCount = 0 }) {
  const isGuest = !currentUser || currentUser === "guest";
  const handle = String(currentUser || "guest").replace(/^@/, "");
  // Grouped rail: Practice (everyone) · Classroom (teacher tools) · Review · Profile.
  const groups = [
    { label: "Practice", items: [["home", "Home", NAV_ICON.home], ["trainer", "Trainer", NAV_ICON.trainer], ["live", "Live", NAV_ICON.live]] },
    { label: "Classroom", items: isTeacher
        ? [["class", "Class", NAV_ICON.class], ["mytasks", "My Tasks", NAV_ICON.mytasks]]
        : [["class", "Class", NAV_ICON.class]] },
    { label: "Review", items: [["mistakes", "Mistakes", NAV_ICON.mistakes], ["stats", "Stats", NAV_ICON.stats]] },
  ];
  const badgeFor = (id) => id === "mistakes" ? mistakesCount : id === "mytasks" ? taskCount : id === "students" ? studentCount : 0;
  const renderItem = ([id, label, Ic], extraStyle) => {
    const on = active === id;
    const badge = badgeFor(id);
    return (
      <button key={id} className={`sidenav-item ${on ? "is-active" : ""}`} aria-current={on ? "page" : undefined} style={extraStyle}
        onClick={(e) => { if (!on && window.tapFeedback) window.tapFeedback("selection", e); go(id); }}>
        <span className="sidenav-ic"><Ic sw={on ? 2.1 : 1.8} /></span>
        <span className="sidenav-item-label">{label}</span>
        {badge > 0 ? <span className="sidenav-badge">{badge}</span> : null}
      </button>
    );
  };
  return (
    <aside className="sidenav" aria-label="Primary">
      <button className="sidenav-brand" onClick={() => go("home")}>
        <span className="tn-brand-main">iri</span>
        <span className="tn-brand-accent">kos</span>
      </button>
      <nav className="sidenav-items" style={{ display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0 }}>
        {groups.map((g) => (
          <React.Fragment key={g.label}>
            <div className="sidenav-label">{g.label}</div>
            {g.items.map((it) => renderItem(it))}
          </React.Fragment>
        ))}
        {renderItem(["settings", "Profile", NAV_ICON.profile], { marginTop: "auto" })}
      </nav>
      <button className="sidenav-account" onClick={onSignOut} title={isGuest ? "Sign in to sync your stats" : `Signed in as @${handle} — tap to sign out`}>
        <span className="sidenav-avatar">{isGuest ? "·" : handle.charAt(0).toUpperCase()}</span>
        <span className="sidenav-account-text">
          <span className="sidenav-handle">{isGuest ? "Guest" : "@" + handle}</span>
          <span className="sidenav-role">{isGuest ? "Not signed in" : "Signed in"}</span>
        </span>
        <span className="sidenav-signout" aria-hidden="true">
          <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
            <path d="M15 4H6a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h9" /><path d="M11 12h9M17 8l4 4-4 4" />
          </svg>
        </span>
      </button>
    </aside>
  );
}

// =====================================================================
// HOME
// =====================================================================
function LiveGlyph({ size = 9 }) {
  return <span className="vb-live-glyph"><Shape i={0} size={size} /><Shape i={1} size={size} /><Shape i={2} size={size} /><Shape i={3} size={size} /></span>;
}
// Big live countdown to the English exam — 10:00 Moscow (UTC+3).
// OGE: 6 Jun 2026 · EGE: 15 Jun 2026.
function EgeCountdown({ examMode }) {
  const oge = examMode === "oge";
  const [now, setNow] = React.useState(() => Date.now());
  React.useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
  const target = oge
    ? Date.UTC(2026, 5, 6, 7, 0, 0)    // OGE — 6 Jun, 10:00 MSK == 07:00 UTC
    : Date.UTC(2026, 5, 15, 7, 0, 0);  // EGE — 15 Jun, 10:00 MSK == 07:00 UTC
  const examName = oge ? "OGE" : "EGE";
  const dateLabel = oge ? "6 Jun, 10:00 MSK" : "15 Jun, 10:00 MSK";
  if (target <= now) {
    return (
      <div className="ege-countdown">
        <div className="ege-cd-label">Exam day</div>
        <div className="ege-cd-now">It's time. Good luck! 🍀</div>
      </div>
    );
  }
  let diff = target - now;
  const d = Math.floor(diff / 86400000); diff -= d * 86400000;
  const h = Math.floor(diff / 3600000); diff -= h * 3600000;
  const m = Math.floor(diff / 60000); diff -= m * 60000;
  const s = Math.floor(diff / 1000);
  const pad = (n) => String(n).padStart(2, "0");
  const units = [[d, "days"], [pad(h), "hrs"], [pad(m), "min"], [pad(s), "sec"]];
  return (
    <div className="ege-countdown">
      <div className="ege-cd-label">until the {examName} · {dateLabel}</div>
      <div className="ege-cd-units">
        {units.map(([n, u], i) => (
          <React.Fragment key={u}>
            {i > 0 && <span className="ege-cd-sep">:</span>}
            <div className="ege-cd-unit"><span className="n">{n}</span><span className="u">{u}</span></div>
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

// Small mobile-Home strip (replaces the big Mistakes block; Trainer owns that
// now). Alerting = soft-red, tappable -> Mistakes; zero = quiet, reassuring.
function HomeMistakeStrip({ count, onOpen }) {
  if (!count) {
    return (
      <div className="mistake-strip is-zero">
        <span className="ms-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="m8.3 12 2.6 2.6 4.8-5.2" /></svg></span>
        <span className="ms-text">All caught up — nothing to review</span>
      </div>
    );
  }
  return (
    <button className="mistake-strip is-alert" onClick={onOpen}>
      <span className="ms-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 7.5v5" /><circle cx="12" cy="16.3" r="0.5" fill="currentColor" stroke="none" /></svg></span>
      <span className="ms-text"><b>{count}</b> mistake{count === 1 ? "" : "s"} to review</span>
      <span className="ms-arrow" aria-hidden="true">→</span>
    </button>
  );
}

function HomeScreen({ stats, settings, onStart, onOpenStats, onOpenSettings, onOpenCategories, onOpenMistakes, onOpenLive, onOpenStreak }) {
  const todayK = todayKey();
  const today = stats.dailyHistory[todayK] || { answered: 0, correct: 0 };
  const todayAcc = today.answered ? Math.round((today.correct / today.answered) * 100) : 0;
  const allAcc = stats.totalAnswered ? Math.round((stats.totalCorrect / stats.totalAnswered) * 100) : 0;

  const examMode = settings.examMode === "oge" ? "oge" : "ege";
  // OGE passages are long, so offer smaller sets; EGE keeps the original sizes.
  const lengthOptions = examMode === "oge"
    ? [["1", "1"], ["3", "3"], ["5", "5"], ["10", "10"], ["endless", "Endless"]]
    : [["10", "10"], ["20", "20"], ["endless", "Endless"]];
  const validLengths = lengthOptions.map(o => o[0]);
  const [length, setLength] = sUseState(() => {
    const d = String(settings.defaultLength || "");
    return validLengths.includes(d) ? d : (examMode === "oge" ? "5" : "10");
  });
  // Re-clamp the selected length when the exam switches (OGE has no 20, etc.).
  sUseEffect(() => {
    if (!validLengths.includes(length)) setLength(examMode === "oge" ? "5" : "10");
  }, [examMode]);
  const lenValue = length === "endless" ? Infinity : Number(length) || 10;
  const begin = () => onStart({ length: lenValue, endless: lenValue === Infinity });

  // Desktop gets the Direction-A dashboard; phones keep the single-column flow.
  const [isDesktop, setIsDesktop] = sUseState(() => typeof window !== "undefined" && window.matchMedia("(min-width: 761px)").matches);
  sUseEffect(() => {
    const mq = window.matchMedia("(min-width: 761px)");
    const on = () => setIsDesktop(mq.matches);
    mq.addEventListener ? mq.addEventListener("change", on) : mq.addListener(on);
    return () => { mq.removeEventListener ? mq.removeEventListener("change", on) : mq.removeListener(on); };
  }, []);

  // delegate to the shared streak helper (10-words-a-day rule, today-pending)
  const dayStreak = sUseMemo(
    () => (typeof window !== "undefined" && window.skDayStreak) ? window.skDayStreak(stats) : 0,
    [stats.dailyHistory]
  );

  // longest day-streak ever (for the pet tile sub) — reuse the shared helper
  const bestDayStreak = sUseMemo(
    () => Math.max(dayStreak, (typeof window !== "undefined" && window.skBestStreak ? window.skBestStreak(stats) : 0)),
    [stats.dailyHistory, dayStreak]
  );

  // weakest task family — for the Continue-your-streak card
  const weakestFamily = sUseMemo(() => {
    const byFamily = {};
    for (const cat in CATEGORY_META) {
      const fam = CATEGORY_META[cat].family;
      const pc = stats.perCategory[cat] || { seen: 0, correct: 0 };
      if (!byFamily[fam]) byFamily[fam] = { seen: 0, correct: 0 };
      byFamily[fam].seen += pc.seen;
      byFamily[fam].correct += pc.correct;
    }
    let pick = null;
    for (const fam of FAMILY_ORDER) {
      const f = byFamily[fam];
      if (!f || f.seen < 3) continue;
      const acc = f.correct / f.seen;
      if (!pick || acc < pick.acc) pick = { fam, acc, seen: f.seen };
    }
    return pick;
  }, [stats.perCategory]);

  // Mastery-by-source — per-family accuracy for the dashboard card (this exam's
  // families only, so OGE correctly omits multiple-choice / irregulars).
  const masteryByFamily = sUseMemo(() => {
    const examCats = new Set(window.examCategories ? window.examCategories(examMode) : Object.keys(CATEGORY_META));
    const byFamily = {};
    for (const cat in CATEGORY_META) {
      if (!examCats.has(cat)) continue;
      const fam = CATEGORY_META[cat].family;
      const pc = stats.perCategory[cat] || { seen: 0, correct: 0 };
      if (!byFamily[fam]) byFamily[fam] = { seen: 0, correct: 0 };
      byFamily[fam].seen += pc.seen; byFamily[fam].correct += pc.correct;
    }
    return FAMILY_ORDER
      .filter(f => byFamily[f] && byFamily[f].seen > 0)
      .map(f => ({ fam: f, label: (FAMILY_META[f] || {}).label || f, seen: byFamily[f].seen, acc: byFamily[f].correct / byFamily[f].seen }));
  }, [stats.perCategory, examMode]);

  // mistakes = items whose most recent attempt was wrong (clears when you get it right)
  const mistakeCount = sUseMemo(() => {
    let n = 0;
    for (const id in stats.perItem) {
      if (isMistakeStat(stats.perItem[id])) n++;
    }
    return n;
  }, [stats.perItem]);

  // items mastered — same rule as the Stats screen: correct >= 3 with >= 80%
  // accuracy, out of this exam's bank. Replaces the streak stat (the pet shows that).
  const modeTotal = sUseMemo(() => {
    const cats = new Set(window.examCategories ? window.examCategories(examMode) : Object.keys(window.CATEGORY_META || {}));
    return VOCAB.filter(v => cats.has(v.category)).length;
  }, [examMode]);
  const itemsMastered = sUseMemo(() => {
    let n = 0;
    for (const id in stats.perItem) {
      const s = stats.perItem[id];
      if (s.correct >= 3 && (s.correct / Math.max(1, s.seen)) >= 0.8) n++;
    }
    return n;
  }, [stats.perItem]);

  const last = stats.sessions[stats.sessions.length - 1];

  const enabledTypes = FAMILY_ORDER.filter(fam =>
    Object.entries(CATEGORY_META).some(([id, m]) => m.family === fam && settings.categories.includes(id))
  ).length;

  const subline = examMode === "oge"
    ? "Grammar and word formation — paced for the OGE exam."
    : "Word formation, grammar, and multiple choice — paced for the EGE exam.";

  const launchControls = (
    <>
      <div className="vb-controls">
        <div className="segmented big" role="radiogroup" aria-label="Set length">
          {lengthOptions.map(([v,l]) => (
            <button
              key={v}
              className={`segmented-btn ${length === v ? "is-on" : ""}`}
              role="radio" aria-checked={length === v}
              onClick={() => setLength(v)}
            >{l}</button>
          ))}
        </div>
        {onOpenLive && (
          <button className="cta-secondary lg vb-live-btn" onClick={(e) => { if (window.tapFeedback) window.tapFeedback("light", e); onOpenLive(); }} aria-label="Join a live game">
            <LiveGlyph /><span>Live</span>
          </button>
        )}
        <button className="cta-primary lg" onClick={begin}>
          <span>{lenValue === Infinity ? "Begin endless" : `Begin ${lenValue}`}</span>
          <span className="cta-arrow">→</span>
        </button>
      </div>
      {last && (
        <button className="vb-resume" onClick={() => onStart({ length: 10 })}>
          Resume practice · last set {last.correct}/{last.total}
        </button>
      )}
    </>
  );

  // mobile stat grid (desktop uses the editorial layout). Mirrors the editorial
  // stat set: today / streak / lifetime / best. The pet rides in the Home chip above.
  const tilesRow = (
    <section className="hero-meta-row" aria-label="Lifetime stats">
      <MetaTile label="Today"    value={`${today.answered}`} sub={today.answered ? `${todayAcc}% accuracy` : "no answers yet"} />
      <MetaTile label="Mastered" value={itemsMastered} sub={`of ${modeTotal}`} />
      <MetaTile label="Lifetime" value={stats.totalAnswered} sub={stats.totalAnswered ? `${allAcc}% accuracy` : "—"} />
      <MetaTile label="Best"     value={stats.bestSessionScore || "—"} sub={stats.bestStreak ? `best streak ${stats.bestStreak}` : "—"} />
    </section>
  );

  const streakCardNode = weakestFamily && weakestFamily.acc < 0.85 ? (
    <StreakCard
      family={weakestFamily.fam}
      acc={weakestFamily.acc}
      dayStreak={dayStreak}
      onStart={() => onStart({ length: 10, familyFilter: weakestFamily.fam })}
    />
  ) : null;

  const footer = (
    <footer className="home-footer">
      <span>{enabledTypes} of {FAMILY_ORDER.length} task types on</span>
      <button className="text-link" onClick={onOpenCategories}>Change</button>
    </footer>
  );

  if (isDesktop) {
    const weak = weakestFamily && weakestFamily.acc < 0.85 ? weakestFamily : null;
    const weakLabel = weak ? (FAMILY_META[weak.fam]?.label || weak.fam).toLowerCase() : null;
    return (
      <div className="ed-home">
        {/* ROW 1 · countdown (type event) + start (earned container) */}
        <div className="ed-row-top">
          <div className="ed-countdown">
            <EgeCountdown examMode={examMode} />
            <p className="ed-count-sub">{subline}</p>
          </div>
          <section className="ed-start">
            <span className="ed-eyebrow">start a session</span>
            <h2 className="ed-start-title">A short set, whenever you can.</h2>
            <div className="ed-start-spacer" />
            <div className="ed-start-row">
              <div className="segmented ed-seg" role="radiogroup" aria-label="Set length">
                {lengthOptions.map(([v, l]) => (
                  <button key={v} className={`segmented-btn ${length === v ? "is-on" : ""}`} role="radio" aria-checked={length === v} onClick={() => setLength(v)}>{l}</button>
                ))}
              </div>
              <button className="cta-primary ed-cta" onClick={begin}>
                <span>{lenValue === Infinity ? "begin ∞" : `begin ${lenValue}`}</span><span className="cta-arrow">→</span>
              </button>
            </div>
            {last
              ? <button className="ed-resume" onClick={() => onStart({ length: 10 })}>resume practice · last set {last.correct}/{last.total}</button>
              : <span className="ed-resume ed-resume-empty">your last set will show up here</span>}
          </section>
        </div>

        {/* ROW 2 · stats (no containers) */}
        <div className="ed-stats">
          <EdStat num={today.answered} label="today" sub={today.answered ? `${todayAcc}% accuracy` : "no answers yet"} />
          <EdStat num={itemsMastered} label="mastered" sub={`of ${modeTotal}`} />
          <EdStat num={stats.totalAnswered} label="lifetime" sub={stats.totalAnswered ? `${allAcc}% accuracy` : "—"} />
          <EdStat num={stats.bestSessionScore || "—"} label="best" sub={stats.bestStreak ? `best streak ${stats.bestStreak}` : "—"} />
        </div>

        {/* ROW 3 · mastery + (weakest container / mistakes line) */}
        <div className="ed-row-mid">
          <section className="ed-mastery">
            <h3 className="ed-heading">Mastery</h3>
            <hr className="ed-head-rule" />
            {masteryByFamily.length ? (
              <div className="ed-mrows">
                {masteryByFamily.map(m => {
                  const pct = Math.round(m.acc * 100);
                  return (
                    <div className="ed-mrow" key={m.fam}>
                      <span className="ed-mrow-nm">{m.label}</span>
                      <span className="ed-mrow-bar"><span style={{ width: `${Math.max(3, pct)}%` }} /></span>
                      <span className="ed-mrow-pct num">{pct}%</span>
                    </div>
                  );
                })}
              </div>
            ) : (
              <p className="ed-mastery-empty">Answer a few questions and your accuracy by task type shows up here.</p>
            )}
          </section>
          <div className="ed-right">
            <section className="ed-weakest">
              <span className="ed-eyebrow ed-eyebrow-accent">recommended</span>
              {weak ? (
                <>
                  <h3 className="ed-wtitle">Your weakest area is <span className="ed-em">{weakLabel}</span></h3>
                  <p className="ed-wsub">{Math.round(weak.acc * 100)}% accuracy across that family</p>
                  <button className="ed-tlink" onClick={() => onStart({ length: 10, familyFilter: weak.fam })}><span>study {weakLabel}</span><span className="ed-arr">→</span></button>
                </>
              ) : (
                <>
                  <h3 className="ed-wtitle">Keep the streak alive</h3>
                  <p className="ed-wsub">A short set today keeps your momentum going</p>
                  <button className="ed-tlink" onClick={() => onStart({ length: 10 })}><span>start a set</span><span className="ed-arr">→</span></button>
                </>
              )}
            </section>
            <section className="ed-mistakes">
              <div className="ed-heading-row">
                <svg className="ed-warn-tri" viewBox="0 0 20 20" fill="none" aria-hidden="true">
                  <path d="M10 2.6 18.2 16.4 H1.8 Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
                  <line x1="10" y1="8" x2="10" y2="12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
                  <circle cx="10" cy="14.2" r="0.95" fill="currentColor" />
                </svg>
                <h3 className="ed-heading">Mistakes</h3>
              </div>
              <hr className="ed-head-rule" />
              <div className="ed-mline">
                <span className="ed-mtext">{mistakeCount > 0 ? <><b>{mistakeCount}</b> words and forms collected to review</> : "No mistakes collected yet"}</span>
                {mistakeCount > 0 && <button className="ed-tlink ed-tlink-no" onClick={onOpenMistakes}><span>review</span><span className="ed-arr">→</span></button>}
              </div>
            </section>
          </div>
        </div>

        {/* ROW 4 · streak strip (compact footnote) */}
        <StreakStrip days={dayStreak} stats={stats} onClick={onOpenStreak} />
      </div>
    );
  }

  return (
    <div className="home-shell vb-home">
      <section className="vb-launch">
        <EgeCountdown examMode={examMode} />
        <p className="hero-sub vb-sub">{subline}</p>
        {launchControls}
      </section>

      <StreakChip days={dayStreak} onClick={onOpenStreak} />

      <HomeMistakeStrip count={mistakeCount} onOpen={onOpenMistakes} />

      {tilesRow}

      {footer}
    </div>
  );
}

function MistakesCard({ count, onOpen }) {
  return (
    <div className="card mistakes-card">
      <div className="mistakes-card-body">
        <div className="card-eyebrow">Mistakes</div>
        <h3 className="mistakes-title">
          {count > 0 ? `${count} collected to review` : "No mistakes collected yet"}
        </h3>
        <div className="mistakes-sub">
          {count > 0
            ? "Words and forms you've missed at least once, gathered automatically."
            : "Anything you miss is gathered here so you can come back to it."}
        </div>
      </div>
      <button className="cta-primary mistakes-cta" onClick={onOpen} disabled={count === 0}>
        <span>Review mistakes</span><span className="cta-arrow">→</span>
      </button>
    </div>
  );
}

function MetaTile({ label, value, sub }) {
  return (
    <div className="meta-tile">
      <div className="meta-tile-label">{label}</div>
      <div className="meta-tile-value">{value}</div>
      <div className="meta-tile-sub">{sub}</div>
    </div>
  );
}

// Editorial home: a bare stat (no container) — big number + label + sub.
function EdStat({ num, label, sub }) {
  return (
    <div className="ed-stat">
      <span className="ed-stat-num num">{num}</span>
      <span className="ed-stat-label">{label}</span>
      <span className="ed-stat-sub">{sub}</span>
    </div>
  );
}

// Editorial home: the full-width streak footnote — pet + line + evolution hint +
// this-week dots. Opens the full streak screen on click.
function StreakStrip({ days, stats, onClick }) {
  const tiers = (typeof window !== "undefined" && window.STREAK_TIERS) || [];
  const i = (typeof window !== "undefined" && window.tierIndexFor) ? Math.max(0, window.tierIndexFor(days)) : 0;
  const next = tiers[i + 1];
  const toNext = next ? next.min - days : 0;
  const wk = (typeof window !== "undefined" && window.skWeek) ? window.skWeek(stats) : { done: [], todayIdx: -1 };
  return (
    <button className="ed-streak" type="button" onClick={onClick}>
      <span className="ed-streak-pet"><Flame days={days || 1} px={46} variant="hero" /></span>
      <span className="ed-streak-info">
        <span className="ed-streak-line"><b className="num">{days}</b>-day streak</span>
        <span className="ed-streak-meta">{days > 0 ? (next ? `${toNext} days to evolve into ${next.name}` : "final form reached — keep it eternal") : "start a streak today"}</span>
      </span>
      <span className="ed-streak-week">
        {[0, 1, 2, 3, 4, 5, 6].map(d => (
          <span key={d} className={`ed-wd ${wk.done[d] ? "on" : ""} ${d === wk.todayIdx ? "today" : ""}`} />
        ))}
      </span>
      <span className="ed-streak-go">›</span>
    </button>
  );
}

function StreakCard({ family, acc, dayStreak, onStart }) {
  const fam = FAMILY_META[family];
  return (
    <div className="card streak-card">
      <div className="streak-body">
        <div className="card-eyebrow">{dayStreak >= 2 ? `Continue your ${dayStreak}-day streak` : "Pick up momentum"}</div>
        <h3 className="streak-title">
          Your weakest area right now is <span className="serif-italic">{fam.label.toLowerCase()}</span>
        </h3>
        <div className="streak-sub">{Math.round(acc * 100)}% accuracy across that family</div>
      </div>
      <button className="cta-primary streak-cta" onClick={onStart}>
        <span>10 from {fam.label}</span>
        <span className="cta-arrow">→</span>
      </button>
    </div>
  );
}

function ResumeCard({ stats, onStart }) {
  const last = stats.sessions[stats.sessions.length - 1];
  return (
    <div className="card resume-card">
      <div className="card-eyebrow">Pick up where you left off</div>
      <div className="resume-body">
        {last ? (
          <>
            <div className="resume-line">
              Last session · <b>{last.correct}/{last.total}</b> correct · score <b>{last.score}</b>
            </div>
            <div className="resume-sub">{formatDuration(last.durationMs)} · {new Date(last.ts).toLocaleDateString()}</div>
          </>
        ) : (
          <>
            <div className="resume-line">No sessions yet. The first one calibrates your stats.</div>
            <div className="resume-sub">Try ten questions to start.</div>
          </>
        )}
      </div>
      <div className="resume-cta">
        <button className="ghost-btn" onClick={() => onStart({ length: 10 })}>10</button>
        <button className="ghost-btn" onClick={() => onStart({ length: 20 })}>20</button>
        <button className="ghost-btn" onClick={() => onStart({ length: 30 })}>30</button>
      </div>
    </div>
  );
}

function CategoryGlance({ stats, onOpen }) {
  // Group categories by family for the home glance.
  const grouped = FAMILY_ORDER.map(fam => ({
    fam,
    label: FAMILY_META[fam].label,
    cats: Object.entries(CATEGORY_META).filter(([_, m]) => m.family === fam).map(([id]) => id),
  })).filter(g => g.cats.length > 0);

  return (
    <div className="card category-glance">
      <div className="card-eyebrow">Mastery by source</div>
      <ul className="cat-list">
        {grouped.map(g => {
          // family-level rollup
          let seen = 0, correct = 0;
          for (const c of g.cats) {
            const pc = stats.perCategory[c] || { seen: 0, correct: 0 };
            seen += pc.seen; correct += pc.correct;
          }
          const acc = seen ? Math.round((correct / seen) * 100) : 0;
          return (
            <li key={g.fam} className="cat-row">
              <span className="status-dot" data-level={accLevel(seen, acc)} aria-hidden="true" />
              <span className="cat-name">{g.label}</span>
              <span className="cat-bar" aria-hidden="true">
                <span className="cat-bar-fill" style={{ width: `${seen ? acc : 0}%` }} />
              </span>
              <span className="cat-pct">{seen ? `${acc}%` : "—"}</span>
            </li>
          );
        })}
      </ul>
      <button className="text-link card-action" onClick={onOpen}>Manage tests →</button>
    </div>
  );
}

// =====================================================================
// RESULTS
// =====================================================================
function ResultsScreen({ summary, onAgain, onReview, onHome }) {
  const acc = summary.total ? Math.round((summary.correct / summary.total) * 100) : 0;
  const verdict =
    acc >= 90 ? "Sharp." :
    acc >= 70 ? "A solid set." :
    acc >= 50 ? "Worth another pass." :
                "These are the ones to revisit.";
  return (
    <div className="results-shell">
      <div className="results-card">
        <div className="results-eyebrow">Session complete</div>
        <h1 className="results-headline serif-italic">{verdict}</h1>
        <ResultDonut correct={summary.correct} total={summary.total} acc={acc} />
        <div className="result-legend">
          <span><i style={{ background: "var(--ok)" }} />Correct · {summary.correct}</span>
          <span><i style={{ background: "var(--no)" }} />Missed · {Math.max(0, summary.total - summary.correct)}</span>
        </div>
        <div className="results-grid">
          <ResultStat label="Score"    value={summary.score} />
          <ResultStat label="Correct"  value={`${summary.correct}/${summary.total}`} />
          <ResultStat label="Accuracy" value={`${acc}%`} />
          <ResultStat label="Best streak" value={summary.bestStreak || 0} />
          <ResultStat label="Time"     value={formatDuration(summary.durationMs)} mono />
          <ResultStat label="Per question" value={summary.total ? formatDuration(Math.round(summary.durationMs / summary.total)) : "—"} mono />
        </div>
        <div className="results-actions">
          {summary.wrong.length > 0 && (
            <button className="cta-secondary" onClick={onReview}>
              Review {summary.wrong.length} wrong
            </button>
          )}
          <button className="cta-secondary" onClick={onHome}>Home</button>
          <button className="cta-primary" onClick={onAgain}>
            <span>Another set</span><span className="cta-arrow">→</span>
          </button>
        </div>
      </div>
    </div>
  );
}
function ResultStat({ label, value, mono }) {
  return (
    <div className="result-stat">
      <div className="result-stat-label">{label}</div>
      <div className={`result-stat-value ${mono ? "is-mono" : ""}`}>{value}</div>
    </div>
  );
}

function ResultDonut({ correct, total, acc }) {
  const r = 78, c = 2 * Math.PI * r;
  const frac = total ? correct / total : 0;
  const dash = c * frac;
  return (
    <div className="result-donut">
      <svg viewBox="0 0 184 184" width="184" height="184">
        <circle cx="92" cy="92" r={r} fill="none" stroke="var(--no-soft)" strokeWidth="22" />
        <circle
          cx="92" cy="92" r={r} fill="none"
          stroke="var(--ok)" strokeWidth="22" strokeLinecap="round"
          strokeDasharray={`${dash} ${c - dash}`}
        />
      </svg>
      <div className="result-donut-center">
        <div>
          <div className="result-donut-pct">{acc}%</div>
          <div className="result-donut-lbl">{correct}/{total} correct</div>
        </div>
      </div>
    </div>
  );
}

// =====================================================================
// REVIEW (wrong answers)
// =====================================================================
function ReviewScreen({ wrong, onBack }) {
  return (
    <div className="review-shell">
      <header className="sub-header">
        <button className="ghost-btn" onClick={onBack}>
          <span className="ghost-arrow">←</span><span>Back to results</span>
        </button>
        <div className="sub-title">Review · {wrong.length} item{wrong.length === 1 ? "" : "s"}</div>
        <div />
      </header>
      <div className="review-list">
        {wrong.map(({ item, userAnswer }, i) => (
          <article className="review-card" key={item.id + i}>
            <div className="review-prompt">
              <div className="review-eyebrow">
                {FAMILY_META[item.type]?.label}
                {item.base && <span className="review-base">{item.base}</span>}
              </div>
              <h2>{(item.sentence || item.text || "").replace(/⟦\d+⟧/g, " ______ ").replace(/_{2,}/g, "_______")}</h2>
            </div>
            <div className="review-answers">
              <div className="review-line">
                <span className="review-tag is-no">You</span>
                <span className="review-text strike">{userAnswer}</span>
              </div>
              <div className="review-line">
                <span className="review-tag is-ok">Answer</span>
                <span className="review-text">{item.gaps ? item.gaps.map(g => g.answer).join(", ") : item.answer}</span>
              </div>
              {!item.gaps && item.alts && item.alts.length > 0 && (
                <div className="review-alts">also accepted: {item.alts.join(", ")}</div>
              )}
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

// =====================================================================
// MISTAKES (every item missed at least once, most-missed first)
// =====================================================================
function MistakesScreen({ stats, onStart, onStartDrills, onJoinLive, onBack, examMode }) {
  const [filter, setFilter] = sUseState("all");
  const [expanded, setExpanded] = sUseState(false);
  const mistakes = sUseMemo(() => {
    // Verb-drill items aren't in VOCAB — resolve them from the drill registry.
    const VB = (typeof window !== "undefined" && window.VERB_MISTAKE_ITEMS) || {};
    return Object.entries(stats.perItem)
      .map(([id, s]) => ({ id, ...s, misses: s.seen - s.correct, acc: s.seen ? s.correct / s.seen : 0 }))
      // "in the pool" = last attempt was wrong; getting it right removes it (isMistakeStat).
      .filter(e => isMistakeStat(e))
      .map(e => ({ ...e, item: VOCAB.find(v => v.id === e.id) || VB[e.id] }))
      .filter(e => e.item)
      .sort((a, b) => (b.misses - a.misses) || (a.acc - b.acc));
  }, [stats.perItem]);

  const isDrill = (m) => m.item.category && m.item.category.indexOf("irregular-verbs") === 0;
  const drillMistakes = mistakes.filter(isDrill);
  const taskMistakes = mistakes.filter(m => !isDrill(m));
  const tasksLabel = examMode === "oge" ? "OGE tasks" : "EGE tasks";
  const shown = filter === "drills" ? drillMistakes : filter === "tasks" ? taskMistakes : mistakes;

  // The Review CTA practices the *shown* set. Drills run as a focused TrainerRun;
  // tasks (word-formation / grammar / MC) run through normal practice. When the set
  // is mixed (the "All" filter), Review takes the task items — drills have their own
  // filter + button.
  const onlyDrills = shown.length > 0 && shown.every(isDrill);
  const reviewIds = onlyDrills ? shown.map(m => m.id) : shown.filter(m => !isDrill(m)).map(m => m.id);
  const reviewCount = Math.min(20, reviewIds.length);
  const review = () => {
    if (!reviewCount) return;
    if (onlyDrills) onStartDrills(reviewIds.slice(0, reviewCount));
    else onStart({ itemIds: reviewIds, length: reviewCount });
  };

  const PREVIEW = 6;
  const previewRows = expanded ? shown : shown.slice(0, PREVIEW);
  const moreCount = shown.length - previewRows.length;
  const answerOf = (it) => it.gaps ? it.gaps.map(g => g.answer).join(", ") : (it.answer || "");

  // reset preview + filter coherence when the active filter changes
  sUseEffect(() => { setExpanded(false); }, [filter]);

  return (
    <div className="mistakes-shell mk-screen">
      <div className="mk-eyebrow">Review</div>

      {drillMistakes.length > 0 && mistakes.length > 0 && (
        <div className="segmented mk-seg" role="radiogroup" aria-label="Filter mistakes">
          {[["all", "All"], ["tasks", tasksLabel], ["drills", "Drills"]].map(([v, l]) => (
            <button key={v} className={`segmented-btn ${filter === v ? "is-on" : ""}`} onClick={() => setFilter(v)}>{l}</button>
          ))}
        </div>
      )}

      {shown.length === 0 ? (
        <div className="mk-card mk-empty">
          <div className="mistakes-empty-mark">✓</div>
          <div className="mistakes-empty-title">Nothing to review</div>
          <p className="muted">Mistakes you make in practice land here so you can come back to them. You're all clear right now.</p>
        </div>
      ) : (
        <div className="mk-card">
          <div className="mk-head">
            <div className="mk-head-l">
              <span className="mk-dot" />
              <span className="mk-title">Mistakes</span>
              <span className="mk-count">{shown.length}</span>
            </div>
            {reviewCount > 0 && (
              <button className="mk-review" onClick={review}>
                <span>Review</span><span className="mk-review-arrow">→</span>
              </button>
            )}
          </div>
          <p className="mk-sub">Most-missed first — clears as you get them right</p>
          <ul className="mk-list">
            {previewRows.map(m => (
              <li className="mk-row" key={m.id}>
                <span className="mk-row-l">
                  {m.item.base ? <><span className="mk-base">{m.item.base}</span><span className="mk-arrow">→</span></> : null}
                  <span className="mk-ans">{answerOf(m.item)}</span>
                </span>
                <span className="mk-miss">{m.misses}× missed</span>
              </li>
            ))}
          </ul>
          {moreCount > 0
            ? <button className="mk-more" onClick={() => setExpanded(true)}>+ {moreCount} more</button>
            : (expanded && shown.length > PREVIEW ? <button className="mk-more" onClick={() => setExpanded(false)}>Show less</button> : null)}
        </div>
      )}

    </div>
  );
}

// =====================================================================
// STATS
// =====================================================================
function StatsScreen({ stats, onBack, onReset, examMode }) {
  // Items mastered counts against the *active exam's* bank only — per-mode stats
  // never touch the other exam's items, so "of <all VOCAB>" produced impossible
  // ratios (mastered > total). Scope the denominator to this exam.
  const modeTotal = sUseMemo(() => {
    const cats = new Set((window.examCategories ? window.examCategories(examMode) : Object.keys(window.CATEGORY_META || {})));
    return VOCAB.filter(v => cats.has(v.category)).length;
  }, [examMode]);
  // Responsive chart length: 7 days on phone, 14 days on desktop
  const [chartDays, setChartDays] = sUseState(() => window.innerWidth < 720 ? 7 : 14);
  const [reviewSession, setReviewSession] = sUseState(null);
  sUseEffect(() => {
    const onResize = () => setChartDays(window.innerWidth < 720 ? 7 : 14);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  const days = lastNDays(chartDays);
  const series = days.map(k => stats.dailyHistory[k] || { answered: 0, correct: 0 });
  const maxAnswered = Math.max(1, ...series.map(s => s.answered));
  const totalAcc = stats.totalAnswered ? Math.round((stats.totalCorrect / stats.totalAnswered) * 100) : 0;

  // Items mastered: perItem entries with correct >= 3 and accuracy >= 80%
  const itemsMastered = sUseMemo(() => {
    let n = 0;
    for (const id in stats.perItem) {
      const s = stats.perItem[id];
      if (s.correct >= 3 && (s.correct / Math.max(1, s.seen)) >= 0.8) n++;
    }
    return n;
  }, [stats.perItem]);

  const weak = sUseMemo(() => {
    const entries = Object.entries(stats.perItem)
      .map(([id, s]) => ({ id, ...s, acc: s.seen ? s.correct / s.seen : 1 }))
      .filter(e => e.seen >= 2)
      .sort((a, b) => a.acc - b.acc)
      .slice(0, 6);
    return entries.map(e => ({ ...e, item: VOCAB.find(v => v.id === e.id) })).filter(e => e.item);
  }, [stats.perItem]);

  return (
    <div className="stats-shell">
      <header className="sub-header">
        <div className="sub-title">Statistics</div>
      </header>

      <section className="stats-row">
        <BigStat label="All-time points"   value={(stats.totalScore || 0).toLocaleString()} info="Every point you've ever scored across all practice sessions, added up. This is what the class leaderboard ranks by." />
        <BigStat label="Total answered"    value={stats.totalAnswered} info="How many questions you've answered in total — right or wrong — since you started." />
        <BigStat label="Lifetime accuracy" value={`${totalAcc}%`} info="Your correct answers as a share of everything you've answered, all-time." />
        <BigStat label="Items mastered"    value={itemsMastered} sub={`of ${modeTotal}`} info="Questions you've answered correctly at least 3 times with 80%+ accuracy. Get them all to master the whole bank." />
        <BigStat label="Best session"      value={stats.bestSessionScore || 0} sub="score" info="Your highest score in a single practice session." />
        <BigStat label="Longest streak"    value={stats.bestStreak || 0} sub="in a row" info="The most questions you've answered correctly in a row without a mistake." />
      </section>

      <section className="stats-card">
        <div className="stats-card-head">
          <div>
            <div className="card-eyebrow">Last {chartDays} days</div>
            <h2 className="card-title">Daily volume</h2>
          </div>
          <div className="legend">
            <span className="legend-dot is-bar" /> answered
            <span className="legend-dot is-line" /> accuracy
          </div>
        </div>
        <div className="chart" style={{ gridTemplateColumns: `repeat(${chartDays}, 1fr)` }}>
          {series.map((s, i) => {
            const h = (s.answered / maxAnswered) * 100;
            const acc = s.answered ? (s.correct / s.answered) * 100 : null;
            return (
              <div className="chart-col" key={days[i]}>
                <div className="chart-stack">
                  {acc != null && (
                    <div className="chart-acc" style={{ bottom: `${acc}%` }} />
                  )}
                  <div className="chart-bar" style={{ height: `${h}%` }} title={`${days[i]} · ${s.answered} answered`} />
                </div>
                <div className="chart-label">{days[i].slice(5)}</div>
              </div>
            );
          })}
        </div>
      </section>

      <div className="stats-grid">
        <section className="stats-card">
          <div className="card-eyebrow">By task type</div>
          <h2 className="card-title">Mastery</h2>
          <ul className="cat-list large">
            {FAMILY_ORDER.map(fam => {
              const cats = Object.keys(CATEGORY_META).filter(c => CATEGORY_META[c].family === fam);
              if (cats.length === 0) return null;
              let seen = 0, correct = 0;
              for (const c of cats) {
                const pc = stats.perCategory[c] || { seen: 0, correct: 0 };
                seen += pc.seen; correct += pc.correct;
              }
              const acc = seen ? Math.round((correct / seen) * 100) : 0;
              return (
                <li key={fam} className="cat-row">
                  <span className="status-dot" data-level={accLevel(seen, acc)} aria-hidden="true" />
                  <span className="cat-name">{FAMILY_META[fam].label}</span>
                  <span className="cat-bar" aria-hidden="true">
                    <span className="cat-bar-fill" style={{ width: `${seen ? acc : 0}%` }} />
                  </span>
                  <span className="cat-pct">{seen ? `${acc}%` : "—"}</span>
                  <span className="cat-count">{seen}</span>
                </li>
              );
            })}
          </ul>
        </section>

        <section className="stats-card">
          <div className="card-eyebrow">Needs work</div>
          <h2 className="card-title">Trickiest items</h2>
          {weak.length === 0 ? (
            <p className="muted">Answer a few more questions to see your weak spots.</p>
          ) : (
            <ul className="weak-list">
              {weak.map(e => (
                <li key={e.id} className="weak-row">
                  <div className="weak-prompt">
                    <div className="weak-en">{(e.item.sentence || e.item.text || "").replace(/⟦\d+⟧/g, " ______ ").replace(/_{2,}/g, "_____")}</div>
                    <div className="weak-es">{e.item.gaps ? e.item.gaps.map(g => g.answer).join(", ") : e.item.answer}</div>
                  </div>
                  <div className="weak-meta">
                    <div className="weak-acc">{Math.round(e.acc * 100)}%</div>
                    <div className="weak-seen">{e.correct}/{e.seen}</div>
                  </div>
                </li>
              ))}
            </ul>
          )}
        </section>
      </div>

      <section className="stats-card">
        <div className="card-eyebrow">History</div>
        <h2 className="card-title">Recent sessions</h2>
        {stats.sessions.length === 0 ? (
          <p className="muted">No sessions yet.</p>
        ) : (
          <ul className="session-list">
            {stats.sessions.slice().reverse().slice(0, 10).map((s, i) => {
              const reviewable = !!(s.questions && s.questions.length);
              return (
                <li key={i} className="session-row is-tappable"
                  onClick={() => setReviewSession(s)}
                  role="button" tabIndex={0}
                  title={reviewable ? "Review every task in this set" : "Per-task detail isn't kept for this set"}
                  onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setReviewSession(s); } }}>
                  <span className="session-date">{new Date(s.ts).toLocaleString()}</span>
                  <span className="session-stat"><b>{s.correct}</b>/{s.total}</span>
                  <span className="session-stat">{Math.round((s.correct / Math.max(1,s.total)) * 100)}%</span>
                  <span className="session-stat">score <b>{s.score}</b></span>
                  <span className="session-stat mono">{formatDuration(s.durationMs)}</span>
                  <span className="session-review">Review →</span>
                </li>
              );
            })}
          </ul>
        )}
      </section>

      {reviewSession && <SessionReviewModal session={reviewSession} onClose={() => setReviewSession(null)} />}
    </div>
  );
}

// Review every question of a past session (the last 10 sessions keep this detail).
function SessionReviewModal({ session, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, []);
  const qs = (session.questions || []).map(q => ({ ...q, item: VOCAB.find(v => v.id === q.id) })).filter(q => q.item);
  const pct = Math.round((session.correct / Math.max(1, session.total)) * 100);
  return (
    <div className="rv-scrim" onClick={onClose}>
      <div className="rv-modal" onClick={(e) => e.stopPropagation()}>
        <div className="rv-head">
          <div>
            <div className="rv-title">{new Date(session.ts).toLocaleDateString()} · {session.correct}/{session.total} correct</div>
            <div className="rv-sub">score {session.score} · {pct}% · {qs.length} question{qs.length === 1 ? "" : "s"}</div>
          </div>
          <button className="rv-x" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="rv-body">
          {qs.length === 0 ? (
            <p className="muted" style={{ margin: 0, lineHeight: 1.5 }}>Per-task detail is kept only for your 10 most recent sets — this one is older, so its individual tasks aren't stored. Newer sets show every task here.</p>
          ) : qs.map((q, i) => {
            const ok = q.c === 1;
            return (
              <div key={i} className={`rv-q ${ok ? "is-ok" : "is-no"}`}>
                <div className="rv-q-top">
                  <span className={`rv-mk ${ok ? "ok" : "no"}`}>{ok ? "✓" : "✗"}</span>
                  {q.item.base ? <span className="rv-q-cue">{q.item.base}</span> : null}
                  <span className="rv-q-fam">{FAMILY_META[q.item.type]?.label}</span>
                </div>
                <div className="rv-q-sentence">{(q.item.sentence || q.item.text || "").replace(/⟦\d+⟧/g, " ____ ").replace("___", " _____ ")}</div>
                <div className="rv-q-answers">
                  {q.item.gaps
                    ? <span className="rv-q-correct">Passage · your result: <b>{q.ua || "—"}</b></span>
                    : <>
                        <span className="rv-q-correct">Answer: <b>{q.item.answer}</b></span>
                        {!ok && q.ua ? <span className="rv-q-your">You: {q.ua}</span> : null}
                      </>}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function BigStat({ label, value, sub, info, accent }) {
  const [open, setOpen] = sUseState(false);
  if (!info) {
    return (
      <div className={`big-stat${accent ? " is-accent" : ""}`}>
        <div className="big-stat-label">{label}</div>
        <div className="big-stat-value">{value}</div>
        {sub && <div className="big-stat-sub">{sub}</div>}
      </div>
    );
  }
  return (
    <button type="button" className={`big-stat is-tappable${open ? " is-open" : ""}`} onClick={() => setOpen(o => !o)} aria-expanded={open}>
      <div className="big-stat-label">{label}<span className="big-stat-i" aria-hidden="true">{open ? "×" : "i"}</span></div>
      {open ? (
        <div className="big-stat-info">{info}</div>
      ) : (
        <>
          <div className="big-stat-value">{value}</div>
          {sub && <div className="big-stat-sub">{sub}</div>}
        </>
      )}
    </button>
  );
}

// =====================================================================
// TESTS (categories, grouped by task family)
// =====================================================================
function CategoriesScreen({ settings, setSettings, stats, onBack }) {
  const examOf = (m) => (m.exam || "ege");
  const wantExam = settings.examMode === "oge" ? "oge" : "ege";
  const catsOf = (fam) => Object.entries(CATEGORY_META).filter(([_, m]) => m.family === fam && examOf(m) === wantExam).map(([id]) => id);
  const familyOn = (fam) => catsOf(fam).some(c => settings.categories.includes(c));
  // Toggle a whole task type on/off (we keep file-level categories under the hood,
  // but the only user-facing unit is the task type).
  const toggleFamily = (fam) => {
    setSettings(s => {
      const cats = catsOf(fam);
      const on = cats.some(c => s.categories.includes(c));
      const next = on
        ? s.categories.filter(c => !cats.includes(c))
        : [...new Set([...s.categories, ...cats])];
      return { ...s, categories: next.length ? next : s.categories };
    });
  };
  const oge = settings.examMode === "oge";
  const families = FAMILY_ORDER.filter(fam => catsOf(fam).length > 0);
  const enabledCount = families.filter(fam => familyOn(fam)).length;
  // Coverage: how many of THIS exam's questions you've attempted at least once.
  const modePool = VOCAB.filter(v => CATEGORY_META[v.category] && examOf(CATEGORY_META[v.category]) === wantExam);
  const attemptedCount = modePool.filter(v => ((stats.perItem[v.id] || {}).seen || 0) > 0).length;
  const coveragePct = modePool.length ? Math.round((attemptedCount / modePool.length) * 100) : 0;

  return (
    <div className="categories-shell cats-grouped">
      <header className="sub-header cats-header">
        <div className="sub-title">Tests{oge ? " · ОГЭ" : " · ЕГЭ"}</div>
        <span className="cats-status"><span className="cats-status-dot" aria-hidden="true" />{oge ? "ОГЭ" : "ЕГЭ"} · {enabledCount} of {families.length} types on</span>
      </header>
      <p className="sub-lead">Pick which task types to practise. Mixed sets draw from everything that's on — at least one must stay on.</p>

      <div className="coverage-bar">
        <div className="coverage-head">
          <span className="coverage-n"><b>{attemptedCount.toLocaleString()}</b> of {modePool.length.toLocaleString()} questions tried</span>
          <span className="coverage-pct">{coveragePct}%</span>
        </div>
        <div className="coverage-track"><div className="coverage-fill" style={{ width: `${coveragePct}%` }} /></div>
      </div>

      <div className="cat-card-grid">
        {families.map(fam => {
          const on = familyOn(fam);
          const famCats = new Set(catsOf(fam));
          const count = VOCAB.filter(v => famCats.has(v.category)).length;
          let seen = 0, correct = 0;
          for (const c of catsOf(fam)) {
            const pc = stats.perCategory[c] || { seen: 0, correct: 0 };
            seen += pc.seen; correct += pc.correct;
          }
          const acc = seen ? Math.round((correct / seen) * 100) : null;
          const lvl = accLevel(seen, acc || 0);
          return (
            <button
              key={fam}
              className={`cat-card ${on ? "is-on" : ""}`}
              onClick={() => toggleFamily(fam)}
              aria-pressed={on}
            >
              <div className="cat-card-head">
                <div className="cat-card-titlewrap">
                  <span className="cat-card-dot" data-level={lvl} aria-hidden="true" />
                  <div className="cat-card-title">{FAMILY_META[fam].label}</div>
                </div>
                <div className={`switch ${on ? "is-on" : ""}`}><span /></div>
              </div>
              <div className="cat-card-blurb">{FAMILY_META[fam].blurb}</div>
              <div className="cat-card-bar">
                <span className="cat-card-track"><span className="cat-card-fill" data-level={lvl} style={{ width: `${acc != null ? Math.max(4, acc) : 0}%` }} /></span>
              </div>
              <div className="cat-card-foot">
                <span>{count} items</span>
                {acc != null ? <span className="cat-card-mastery" data-level={lvl}>{acc}% mastery</span> : <span className="cat-card-mastery is-muted">not started</span>}
              </div>
            </button>
          );
        })}
      </div>
      <p className="cats-footnote">Mastery is your lifetime accuracy in each type. Toggling a type off just hides it from new sets — your progress is kept.</p>
    </div>
  );
}

// =====================================================================
// SETTINGS
// =====================================================================
// "Report an issue" — general feedback/bug report (not tied to a task). Posts
// to the same /api/report store as the per-question reporter. Lives in Profile.
function SettingsReportIssue() {
  const [open, setOpen] = sUseState(false);
  const [sent, setSent] = sUseState(false);
  const [reason, setReason] = sUseState("");
  const [note, setNote] = sUseState("");
  const [busy, setBusy] = sUseState(false);
  const send = async () => {
    if (busy || (!reason && !note.trim())) return;
    setBusy(true);
    let token = null; try { token = localStorage.getItem(window.TOKEN_KEY || "vp.token.v1"); } catch (e) {}
    try {
      await fetch("/api/report", {
        method: "POST",
        headers: { "content-type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
        body: JSON.stringify({ category: "general", reason: reason || "issue", note: note.trim() }),
      });
    } catch (e) { /* best-effort */ }
    setBusy(false); setSent(true);
  };
  return (
    <section className="card set-group">
      <div className="set-group-label">Help</div>
      {sent ? (
        <div className="setting-row"><div className="setting-text"><div className="setting-label">✓ Thanks — we'll look into it.</div><div className="setting-sub">Your report was sent.</div></div></div>
      ) : !open ? (
        <button className="setting-row setting-nav" onClick={() => setOpen(true)}>
          <div className="setting-text"><div className="setting-label">Report an issue</div><div className="setting-sub">Something broken or confusing? Tell us — bug, idea, anything.</div></div>
          <span className="setting-chevron" aria-hidden="true">→</span>
        </button>
      ) : (
        <div className="setting-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 10 }}>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            {[["bug", "Bug"], ["content", "Content error"], ["suggestion", "Suggestion"], ["other", "Other"]].map(([v, l]) => (
              <button type="button" key={v} className={`report-chip ${reason === v ? "is-on" : ""}`} onClick={() => setReason(v)}>{l}</button>
            ))}
          </div>
          <textarea className="field-textarea" value={note} onChange={e => setNote(e.target.value)} placeholder="Describe the issue…" maxLength={600} rows={3} style={{ width: "100%" }} />
          <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
            <button className="cta-secondary" onClick={() => { setOpen(false); setReason(""); setNote(""); }}>Cancel</button>
            <button className="cta-primary" onClick={send} disabled={busy || (!reason && !note.trim())}><span>{busy ? "Sending…" : "Send report"}</span><span className="cta-arrow">→</span></button>
          </div>
        </div>
      )}
    </section>
  );
}

function SettingsScreen({ settings, setSettings, onBack, onOpenTests, currentUser, onSignOut, isTeacher = false, onOpenMistakes, onOpenStats }) {
  const guest = !currentUser || currentUser === "guest";
  const handle = String(currentUser || "").replace(/^@/, "");
  const isDesktop = useIsDesktop();
  const examLabel = settings.examMode === "oge" ? "ОГЭ" : "ЕГЭ";

  const examRow = (
    <Row label="Exam" sub="ОГЭ practises grammar & word formation only · each exam keeps its own progress">
      <Segmented
        value={settings.examMode === "oge" ? "oge" : "ege"}
        options={[{ v: "ege", label: "ЕГЭ" }, { v: "oge", label: "ОГЭ" }]}
        onChange={v => setSettings(s => ({ ...s, examMode: v }))}
      />
    </Row>
  );
  const countRow = (
    <Row label="Default question count" sub="Used by the main Begin button">
      <Segmented
        value={String(settings.defaultLength)}
        options={settings.examMode === "oge"
          ? [{ v: "1", label: "1" }, { v: "3", label: "3" }, { v: "5", label: "5" }, { v: "10", label: "10" }, { v: "endless", label: "Endless" }]
          : [{ v: "10", label: "10" }, { v: "20", label: "20" }, { v: "endless", label: "Endless" }]}
        onChange={v => setSettings(s => ({ ...s, defaultLength: v }))}
      />
    </Row>
  );
  const autoAdvanceRow = (
    <Row label="Auto-advance" sub="Move to the next question automatically after a correct answer">
      <Toggle on={settings.autoAdvance} onChange={v => setSettings(s => ({ ...s, autoAdvance: v }))} />
    </Row>
  );
  const soundRow = (
    <Row label="Sound" sub="Tones for correct & incorrect">
      <Toggle on={settings.soundOn} onChange={v => setSettings(s => ({ ...s, soundOn: v }))} />
    </Row>
  );
  const hapticsRow = (
    <Row label="Haptics" sub="Subtle vibration on taps & answers (where supported)">
      <Toggle on={settings.haptics !== false} onChange={v => setSettings(s => ({ ...s, haptics: v }))} />
    </Row>
  );
  const motionRow = (
    <Row label="Reduce motion" sub="Disables the blank-fill transition and progress-bar easing">
      <Toggle on={settings.reduceMotion} onChange={v => setSettings(s => ({ ...s, reduceMotion: v }))} />
    </Row>
  );
  const darkRow = (
    <Row label="Dark mode" sub="Easier in low light">
      <Toggle on={settings.dark} onChange={v => setSettings(s => ({ ...s, dark: v }))} />
    </Row>
  );
  const testsNav = onOpenTests && (
    <button className="setting-row setting-nav" onClick={onOpenTests}>
      <div className="setting-text">
        <div className="setting-label">Tests</div>
        <div className="setting-sub">Choose which task types to practise</div>
      </div>
      <span className="setting-chevron" aria-hidden="true">→</span>
    </button>
  );
  // Mobile teachers reach Mistakes + Stats from here (they're out of the mobile
  // nav bar). On desktop they're already in the sidebar, so this is redundant.
  const practiceNav = isTeacher && !isDesktop && (onOpenMistakes || onOpenStats) && (
    <section className="card set-group">
      <div className="set-group-label">Your practice</div>
      {onOpenStats && (
        <button className="setting-row setting-nav" onClick={onOpenStats}>
          <div className="setting-text">
            <div className="setting-label">Stats</div>
            <div className="setting-sub">Your accuracy, streaks and daily activity</div>
          </div>
          <span className="setting-chevron" aria-hidden="true">→</span>
        </button>
      )}
    </section>
  );

  return (
      <div className="settings-shell settings-grouped">
        <header className="sub-header set-header">
          <div className="sub-title">Profile</div>
          <span className="set-autosave"><span className="set-autosave-dot" aria-hidden="true" />Changes save automatically</span>
        </header>

        <section className="card set-account">
          <span className="set-avatar">{guest ? "·" : handle.charAt(0).toUpperCase()}</span>
          <div className="set-account-text">
            <div className="set-account-name">{guest ? "Guest" : "@" + handle}</div>
            <div className="set-account-sub">{guest ? "Sign in to sync your stats across devices" : <>Signed in · {isTeacher ? "Teacher" : "Student"} · practising for <strong>{examLabel}</strong></>}</div>
          </div>
          {onSignOut && (
            <button className="cta-secondary set-signout" onClick={onSignOut}>
              <span aria-hidden="true">⎋</span> {guest ? "Sign in" : "Sign out"}
            </button>
          )}
        </section>

        {practiceNav}

        <div className="set-groups">
          <section className="card set-group">
            <div className="set-group-label">Practice</div>
            {testsNav}
            {examRow}
            {countRow}
          </section>
          <div className="set-groups-col">
            <section className="card set-group">
              <div className="set-group-label">Answering &amp; feedback</div>
              {autoAdvanceRow}
              {soundRow}
              {hapticsRow}
            </section>
            <section className="card set-group">
              <div className="set-group-label">Appearance</div>
              {motionRow}
              {darkRow}
            </section>
            <SettingsReportIssue />
          </div>
        </div>
      </div>
    );
}
function Row({ label, sub, children }) {
  return (
    <div className="setting-row">
      <div className="setting-text">
        <div className="setting-label">{label}</div>
        <div className="setting-sub">{sub}</div>
      </div>
      <div className="setting-control">{children}</div>
    </div>
  );
}
function Toggle({ on, onChange }) {
  return (
    <button className={`switch ${on ? "is-on" : ""}`} onClick={() => onChange(!on)} aria-pressed={on}>
      <span />
    </button>
  );
}
function Segmented({ value, options, onChange }) {
  return (
    <div className="segmented" role="radiogroup">
      {options.map(o => (
        <button
          key={o.v}
          className={`segmented-btn ${value === o.v ? "is-on" : ""}`}
          onClick={(e) => { if (value !== o.v && window.tapFeedback) window.tapFeedback("selection", e); onChange(o.v); }}
          role="radio"
          aria-checked={value === o.v}
        >{o.label}</button>
      ))}
    </div>
  );
}

// Add-to-home-screen nudge — browser only (never in the installed PWA), mobile,
// at most once every few days. Android uses the real install prompt; iOS shows
// the manual Share → Add to Home Screen hint.
function InstallPrompt() {
  const [show, setShow] = React.useState(false);
  const [evt, setEvt] = React.useState(null);
  const [ios, setIos] = React.useState(false);
  React.useEffect(() => {
    const standalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
    if (standalone) return;
    const ua = navigator.userAgent || "";
    const mobile = window.innerWidth < 760 || /iphone|ipad|ipod|android/i.test(ua);
    if (!mobile) return;
    let last = 0; try { last = Number(localStorage.getItem("vp.install.dismissed") || 0); } catch (e) {}
    if (Date.now() - last < 5 * 86400000) return; // don't nag more than once / 5 days
    const isIos = /iphone|ipad|ipod/i.test(ua);
    setIos(isIos);
    const onBip = (e) => { e.preventDefault(); setEvt(e); setShow(true); };
    window.addEventListener("beforeinstallprompt", onBip);
    let t = null;
    if (isIos) t = setTimeout(() => setShow(true), 2500); // iOS has no install event
    return () => { window.removeEventListener("beforeinstallprompt", onBip); if (t) clearTimeout(t); };
  }, []);
  const dismiss = () => { setShow(false); try { localStorage.setItem("vp.install.dismissed", String(Date.now())); } catch (e) {} };
  const install = async () => { if (evt) { evt.prompt(); try { await evt.userChoice; } catch (e) {} } dismiss(); };
  if (!show) return null;
  return (
    <div className="install-banner" role="dialog" aria-label="Add to home screen">
      <span className="install-glyph"><LiveGlyph size={7} /></span>
      <div className="install-text">
        <div className="install-title">Add irikos to your home screen</div>
        <div className="install-sub">{ios ? "Tap the Share icon, then “Add to Home Screen”." : "Install for full-screen, offline practice."}</div>
      </div>
      <div className="install-actions">
        {!ios && evt ? <button className="cta-primary sm" onClick={install}>Install</button> : null}
        <button className="install-x" onClick={dismiss} aria-label="Dismiss">×</button>
      </div>
    </div>
  );
}

Object.assign(window, {
  TopNav, SideNav, BottomNav, HomeScreen, ResultsScreen, ReviewScreen, MistakesScreen, StatsScreen, CategoriesScreen, SettingsScreen,
  InstallPrompt, BigStat, SessionReviewModal // used by the Classrooms screens + app shell
});
