/* ============================================================================
   Operation Outbreak — Wrapped · MOBILE (Story-style) + view router
   Reuses proven inner pieces (EpiCurve, RadialGauge, CharacterCard, CountUp,
   canvas renderers) inside a phone chassis with rAF-driven motion, a 6s
   auto-advance story engine, native Web-Share export, and a hidden toggle to
   the print one-pager (PrintReport).
   ============================================================================ */

/* ── rAF motion (CSS transitions/keyframes don't tick in this preview; rAF does) */
function useRafProgress(run, duration = 560, delay = 0) {
  const [p, setP] = useState(0);
  useEffect(() => {
    if (!run) { setP(0); return; }
    let raf, start, to;
    const ease = (t) => 1 - Math.pow(1 - t, 3);
    const step = (now) => {
      if (start == null) start = now;
      const t = Math.min(1, (now - start) / duration);
      setP(ease(t));
      if (t < 1) raf = requestAnimationFrame(step);
    };
    to = setTimeout(() => { raf = requestAnimationFrame(step); }, delay);
    return () => { clearTimeout(to); cancelAnimationFrame(raf); };
  }, [run, duration, delay]);
  return p;
}
function Reveal({ run, delay = 0, duration = 560, y = 20, children, style }) {
  const p = useRafProgress(run, duration, delay);
  return <div style={{ opacity: p, transform: `translate3d(0,${(1 - p) * y}px,0)`, ...style }}>{children}</div>;
}
function ClipReveal({ run, delay = 0, duration = 640, children, style, className }) {
  const p = useRafProgress(run, duration, delay);
  return (
    <div className={className} style={{ clipPath: `inset(${(1 - p) * 100}% 0 0 0)`, transform: `translate3d(0,${(1 - p) * 16}px,0)`, opacity: Math.min(1, p * 1.4), ...style }}>
      {children}
    </div>
  );
}

/* ── ambient floating shapes (imperative rAF — cheap, no re-render) ───────── */
function FloatingShapes({ items }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    const nodes = [...el.children];
    let raf, t0;
    const loop = (now) => {
      if (t0 == null) t0 = now;
      const t = (now - t0) / 1000;
      nodes.forEach((n, i) => {
        const s = items[i];
        const dx = Math.sin(t * s.sx + i * 1.7) * s.drift;
        const dy = Math.cos(t * s.sy + i * 0.9) * s.drift;
        const rot = (s.spin || 0) * t;
        n.style.transform = `translate3d(${dx.toFixed(1)}px,${dy.toFixed(1)}px,0) rotate(${rot.toFixed(1)}deg)`;
      });
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [items]);
  return (
    <div ref={ref} aria-hidden="true" style={{ position: "absolute", inset: 0, overflow: "visible", pointerEvents: "none", zIndex: -1 }}>
      {items.map((s, i) => (
        <div key={i} style={{ position: "absolute", left: s.left, top: s.top, width: s.size, height: s.size, willChange: "transform" }}>
          {s.type === "orb"
            ? <div style={{ width: "100%", height: "100%", borderRadius: 999, background: s.color, opacity: s.opacity, filter: `blur(${s.blur}px)` }} />
            : <Star size={s.size} color={s.color} style={{ opacity: s.opacity }} />}
        </div>
      ))}
    </div>
  );
}

const GROUP_SHAPES = [
  { type: "orb", left: "-64px", top: "5%", size: 210, color: "#866AFF", opacity: 0.11, blur: 72, sx: 0.18, sy: 0.12, drift: 14 },
  { type: "orb", left: "66%", top: "58%", size: 230, color: "#2AAD9A", opacity: 0.09, blur: 82, sx: 0.13, sy: 0.17, drift: 18 },
  { type: "star", left: "80%", top: "8%", size: 58, color: "rgba(237,80,80,0.12)", opacity: 1, sx: 0.22, sy: 0.15, drift: 11, spin: 7 },
  { type: "star", left: "4%", top: "70%", size: 40, color: "rgba(134,106,255,0.14)", opacity: 1, sx: 0.16, sy: 0.20, drift: 9, spin: -6 },
];
const INDIV_SHAPES = [
  { type: "orb", left: "-54px", top: "3%", size: 205, color: "#866AFF", opacity: 0.13, blur: 72, sx: 0.15, sy: 0.13, drift: 14 },
  { type: "orb", left: "64%", top: "64%", size: 210, color: "#58DBA6", opacity: 0.08, blur: 82, sx: 0.12, sy: 0.18, drift: 16 },
  { type: "star", left: "82%", top: "12%", size: 50, color: "rgba(88,219,166,0.13)", opacity: 1, sx: 0.20, sy: 0.16, drift: 9, spin: 6 },
];

/* ── kinetic primitives ──────────────────────────────────────────────────── */
function Pop({ run, delay = 0, duration = 620, from = 0.72, y = 14, children, style }) {
  const p = useRafProgress(run, duration, delay);
  const s = from + (1 - from) * p;
  return <div style={{ opacity: Math.min(1, p * 1.3), transform: `translate3d(0,${(1 - p) * y}px,0) scale(${s})`, transformOrigin: "left center", ...style }}>{children}</div>;
}
function BigStat({ to, play, delay = 0, suffix = "", decimals = 0, color = T.fg, size = "clamp(62px,18.5vw,90px)" }) {
  return (
    <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: size, lineHeight: 0.84, color, letterSpacing: "-0.04em" }}>
      <CountUp to={to} play={play} suffix={suffix} decimals={decimals} duration={1600} delay={delay} />
    </div>
  );
}
function RafBar({ pct, play, color, delay = 0 }) {
  const p = useRafProgress(play, 1400, delay);
  return <div style={{ height: "100%", width: `${pct * p}%`, background: color, borderRadius: 99, boxShadow: `0 0 10px ${accentAlpha(color, 0.6)}` }} />;
}
function MiniMetric({ label, to, value, suffix = "", accent, icon, foot, play, delay = 0 }) {
  return (
    <Reveal run={play} delay={delay} style={{ height: "100%" }}>
      <div className="m-card" style={{ height: "100%", padding: "12px 13px", display: "flex", flexDirection: "column", gap: 6 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
          <span style={{ fontFamily: T.mono, fontSize: 9, fontWeight: 700, letterSpacing: "0.1em", color: T.fg3 }}>{label}</span>
          <Icon name={icon} size={15} color={accent} stroke={1.85} />
        </div>
        <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: 29, lineHeight: 1, color: T.fg, letterSpacing: "-0.02em" }}>
          {value != null ? value : <CountUp to={to} play={play} suffix={suffix} duration={1300} delay={delay + 100} />}
        </div>
        {foot && <div style={{ fontFamily: T.mono, fontSize: 8.5, letterSpacing: "0.05em", color: accent }}>{foot}</div>}
      </div>
    </Reveal>
  );
}

/* status flag pill — masked / vaccinated / infected / survived (yes-no) ───── */
function FlagPill({ icon, label, on, goodWhenOn = true, play, delay = 0 }) {
  const good = on === goodWhenOn;
  const c = good ? T.mint : T.coral2;
  return (
    <Pop run={play} delay={delay} from={0.8} style={{ flex: 1 }}>
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 5, background: accentAlpha(c, 0.08), border: `1px solid ${accentAlpha(c, 0.45)}`, borderRadius: 12, padding: "10px 4px" }}>
        <Icon name={icon} size={16} color={c} stroke={1.9} />
        <div style={{ fontFamily: T.mono, fontSize: 8.5, fontWeight: 700, letterSpacing: "0.08em", color: T.fg2 }}>{label}</div>
        <div style={{ display: "flex", alignItems: "center", gap: 3, fontFamily: T.mono, fontSize: 10, fontWeight: 700, letterSpacing: "0.04em", color: c }}>
          <Icon name={on ? "check" : "x"} size={11} color={c} stroke={2.5} />{on ? "YES" : "NO"}
        </div>
      </div>
    </Pop>
  );
}

/* intervention usage chips (counts) ─────────────────────────────────────── */
function InterventionChips({ data, play, delay = 0 }) {
  const items = [
    { k: "mask", icon: "shield", label: "MASKS", c: T.mint },
    { k: "vaccine", icon: "syringe", label: "VAX", c: T.blue2 },
    { k: "test", icon: "scan-line", label: "TESTS", c: T.purple },
    { k: "rx", icon: "pill", label: "RX", c: T.teal },
  ];
  return (
    <div style={{ display: "flex", gap: 8 }}>
      {items.map((it, i) => (
        <Pop key={it.k} run={play} delay={delay + i * 80} from={0.8} style={{ flex: 1 }}>
          <div className="m-card" style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6, padding: "11px 4px" }}>
            <Icon name={it.icon} size={16} color={it.c} stroke={1.85} />
            <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: 19, color: T.fg, lineHeight: 1 }}>
              <CountUp to={data[it.k]} play={play} duration={1100} delay={delay + i * 80 + 100} />
            </div>
            <div style={{ fontFamily: T.mono, fontSize: 8, fontWeight: 700, letterSpacing: "0.1em", color: T.fg3 }}>{it.label}</div>
          </div>
        </Pop>
      ))}
    </div>
  );
}

/* ── slide theme + text-free transition effects (rotate per advance) ─────── */
function themeFor(i) {
  if (i === 2) {
    const p = window.OO_DATA.personalities[window.OO_DATA.currentBucket];
    return { color: p.mode === "gradient" ? T.coral2 : p.color };
  }
  return [{ color: T.mint }, { color: T.purple }][i];
}
const FX_VARIANTS = ["bars", "wipe"];
const FX_COLORS = ["#58DBA6", "#866AFF", "#ED5050", "#5465E5"];
const smooth = (t) => t * t * (3 - 2 * t);

function TransitionFX({ prog, color, variant }) {
  if (prog <= 0 || prog >= 1) return null;
  const grad = `linear-gradient(140deg, ${color}, color-mix(in srgb, ${color}, #08090A 40%))`;
  const wrap = { position: "absolute", inset: 0, zIndex: 60, pointerEvents: "none", overflow: "hidden" };

  if (variant === "bars") {
    const N = 5, stag = 0.09, denom = 1 - (N - 1) * stag;
    return (
      <div style={{ ...wrap, display: "flex" }}>
        {Array.from({ length: N }).map((_, i) => {
          let ty;
          if (prog < 0.5) { const ph = prog / 0.5; const l = Math.max(0, Math.min(1, (ph - i * stag) / denom)); ty = -110 * (1 - smooth(l)); }
          else { const ph = (prog - 0.5) / 0.5; const l = Math.max(0, Math.min(1, (ph - i * stag) / denom)); ty = 112 * smooth(l); }
          return <div key={i} style={{ flex: 1, height: "100%", background: grad, transform: `translateY(${ty}%)` }} />;
        })}
      </div>
    );
  }

  // star curtain — diagonal opaque panel with a big rotating star (no text)
  const tx = prog < 0.5 ? 118 * (1 - 2 * prog) : -134 * (2 * prog - 1);
  const rot = prog * 200;
  return (
    <div style={wrap}>
      <div style={{ position: "absolute", top: -40, bottom: -40, left: 0, width: "165%", transform: `translateX(${tx}%) skewX(-9deg)`, background: grad, boxShadow: `0 0 80px ${accentAlpha(color, 0.5)}` }}>
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", transform: "skewX(9deg)" }}>
          <Star size={540} color="rgba(8,9,10,0.16)" style={{ position: "absolute", transform: `rotate(${rot}deg)` }} />
        </div>
      </div>
    </div>
  );
}

/* ── Story progress bars (driven by the autoplay clock) ──────────────────── */
function ProgressBars({ active, count, prog }) {
  return (
    <div style={{ position: "absolute", top: "max(12px, env(safe-area-inset-top))", left: 14, right: 14, display: "flex", gap: 6, zIndex: 40 }}>
      {Array.from({ length: count }).map((_, i) => {
        const w = i < active ? 100 : i === active ? prog * 100 : 0;
        return (
          <div key={i} style={{ flex: 1, height: 3, borderRadius: 99, background: "rgba(255,255,255,0.18)", overflow: "hidden" }}>
            <div style={{ height: "100%", width: `${w}%`, background: T.mint, borderRadius: 99 }} />
          </div>
        );
      })}
    </div>
  );
}

/* ── Near-miss gauge (teal→mint, ring = evasion %, center = near-misses) ──── */
function NearMissGauge({ nearMisses, evasion, play, size = 188, stroke = 15 }) {
  const r = (size - stroke) / 2, circ = 2 * Math.PI * r;
  return (
    <div style={{ position: "relative", width: size, height: size }}>
      <svg width={size} height={size} style={{ transform: "rotate(-90deg)", overflow: "visible" }}>
        <defs><linearGradient id="nmg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stopColor="#2A3D9A" /><stop offset="100%" stopColor="#58DBA6" /></linearGradient></defs>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth={stroke} />
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="url(#nmg)" strokeWidth={stroke} strokeLinecap="round"
          strokeDasharray={circ} strokeDashoffset={play ? circ * (1 - evasion / 100) : circ} style={{ filter: "drop-shadow(0 0 10px rgba(88,219,166,0.5))" }}>
          {play && <animate attributeName="stroke-dashoffset" from={circ} to={circ * (1 - evasion / 100)} dur="1.6s" begin="0.15s" fill="freeze" calcMode="spline" keyTimes="0;1" keySplines="0.2 0.8 0.2 1" />}
        </circle>
      </svg>
      <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
        <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: size * 0.30, lineHeight: 1, color: T.fg, letterSpacing: "-0.02em" }}>
          <CountUp to={nearMisses} play={play} duration={1400} delay={220} />
        </div>
        <div style={{ fontFamily: T.mono, fontSize: 10, fontWeight: 700, letterSpacing: "0.12em", color: T.mint, marginTop: 4 }}>CLOSE CALLS</div>
      </div>
    </div>
  );
}

/* ── Mobile share sheet (native Web Share, PNG fallback) ─────────────────── */
function MobileShareSheet({ share, onClose }) {
  const canvasRef = useRef(null);
  const [ready, setReady] = useState(false);
  const spec = share ? SHARE_SPECS[share.type] : null;
  const p = useRafProgress(!!share, 360, 0);

  useEffect(() => {
    if (!share) return;
    setReady(false);
    let cancelled = false;
    (async () => {
      await ensureFonts();                                  // fonts.ready gate (mobile Safari)
      await new Promise((r) => requestAnimationFrame(r));
      const c = canvasRef.current; if (!c || cancelled) return;
      c.width = spec.w; c.height = spec.h;
      const ctx = c.getContext("2d");
      ctx.textBaseline = "alphabetic";
      if (share.type === "group") await renderGroup(ctx, spec);
      else if (share.type === "individual") await renderIndividual(ctx, spec);
      else await renderPersonality(ctx, spec, share.bucket);
      if (!cancelled) setReady(true);
    })();
    return () => { cancelled = true; };
  }, [share]);

  const blobOf = () => new Promise((res) => canvasRef.current.toBlob(res, "image/png"));
  const download = async () => {
    const blob = await blobOf(); const url = URL.createObjectURL(blob);
    const a = document.createElement("a"); a.href = url; a.download = `${spec.file}.png`; a.click();
    setTimeout(() => URL.revokeObjectURL(url), 2000);
  };
  const shareNative = async () => {
    if (!ready) return;
    const blob = await blobOf();
    const file = new File([blob], `${spec.file}.png`, { type: "image/png" });
    try {
      if (navigator.canShare && navigator.canShare({ files: [file] })) {
        await navigator.share({ files: [file], title: "Operation Outbreak — Wrapped", text: "My Outbreak Wrapped" });
      } else { await download(); }
    } catch (e) { /* user dismissed share sheet — no-op */ }
  };

  if (!share) return null;
  const accent = share.type === "individual" ? T.purple : share.type === "personality" ? window.OO_DATA.personalities[share.bucket].color : T.mint;

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 200, background: `rgba(4,5,6,${0.72 * p})`, display: "flex", alignItems: "flex-end", justifyContent: "center" }}>
      <div onClick={(e) => e.stopPropagation()} data-no-nav style={{
        width: "min(100vw, 460px)", background: T.panel, borderTopLeftRadius: 26, borderTopRightRadius: 26,
        borderTop: `1px solid ${T.line}`, padding: "14px 18px 26px", maxHeight: "94vh", overflow: "auto",
        transform: `translateY(${(1 - p) * 100}%)`,
      }}>
        <div style={{ width: 40, height: 4, borderRadius: 99, background: "rgba(255,255,255,0.2)", margin: "0 auto 14px" }} />
        <div style={{ fontFamily: T.mono, fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", color: accent, textAlign: "center" }}>{spec.label}</div>
        <div style={{ display: "flex", justifyContent: "center", margin: "14px 0" }}>
          <div style={{ position: "relative" }}>
            <canvas ref={canvasRef} style={{ display: "block", maxHeight: "48vh", maxWidth: "100%", width: "auto", borderRadius: 16, border: `1px solid ${T.line}`, opacity: ready ? 1 : 0, boxShadow: "0 20px 60px -20px rgba(0,0,0,0.8)" }} />
            {!ready && <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: T.mono, fontSize: 12, letterSpacing: "0.12em", color: T.fg3 }}>RENDERING…</div>}
          </div>
        </div>
        <div style={{ display: "flex", gap: 10 }}>
          <button onClick={shareNative} disabled={!ready} style={{
            flex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 9,
            fontFamily: T.mono, fontSize: 13, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase",
            color: T.ink, background: ready ? accent : T.fg3, border: "none", padding: "15px 18px", borderRadius: 999, cursor: ready ? "pointer" : "default",
          }}>
            <Icon name="share-2" size={16} color={T.ink} stroke={2.25} /> Add to Story
          </button>
          <button onClick={download} disabled={!ready} title="Save PNG" style={{
            fontFamily: T.mono, fontSize: 13, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase",
            color: T.fg, background: "transparent", border: `1px solid ${T.line}`, padding: "15px 18px", borderRadius: 999, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 8,
          }}><Icon name="download" size={15} color={T.fg2} stroke={2} /></button>
          <button onClick={onClose} style={{
            fontFamily: T.mono, fontSize: 13, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase",
            color: T.fg2, background: "transparent", border: `1px solid ${T.line}`, padding: "15px 18px", borderRadius: 999, cursor: "pointer",
          }}>Close</button>
        </div>
        <div style={{ marginTop: 13, textAlign: "center", fontFamily: T.mono, fontSize: 10.5, letterSpacing: "0.06em", color: T.fg3 }}>Opens your phone's native share sheet · {spec.w}×{spec.h}</div>
      </div>
    </div>
  );
}

/* ── compact metric tile (grids) ─────────────────────────────────────────── */
function MStat({ label, to, accent, play, decimals = 0, suffix = "", sub, delay = 0, big = 32 }) {
  return (
    <Reveal run={play} delay={delay} style={{ height: "100%" }}>
      <div style={{ height: "100%", background: T.panel, border: `1px solid ${T.line}`, borderRadius: 14, padding: "12px 13px", display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
        <div style={{ fontFamily: T.mono, fontSize: 9, fontWeight: 700, letterSpacing: "0.1em", color: T.fg3 }}>{label}</div>
        <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: big, lineHeight: 1, color: T.fg, letterSpacing: "-0.02em", marginTop: 8 }}>
          <CountUp to={to} play={play} decimals={decimals} suffix={suffix} duration={1400} delay={delay + 120} />
        </div>
        {sub && <div style={{ marginTop: 6, fontFamily: T.mono, fontSize: 9, letterSpacing: "0.05em", color: accent }}>{sub}</div>}
      </div>
    </Reveal>
  );
}

/* ── full-width metric row (Slide 1 vertical stack) ──────────────────────── */
function MRow({ label, value, sub, accent, play, delay, count, decimals = 0, suffix = "" }) {
  return (
    <Reveal run={play} delay={delay}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", background: T.panel, border: `1px solid ${T.line}`, borderRadius: 14, padding: "11px 15px" }}>
        <div>
          <div style={{ fontFamily: T.mono, fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", color: T.fg3 }}>{label}</div>
          {sub && <div style={{ fontFamily: T.mono, fontSize: 9.5, letterSpacing: "0.05em", color: accent, marginTop: 5 }}>{sub}</div>}
        </div>
        <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: 27, lineHeight: 1, color: T.fg, letterSpacing: "-0.02em" }}>
          {count ? <CountUp to={value} play={play} decimals={decimals} suffix={suffix} duration={1400} delay={delay + 120} /> : value}
        </div>
      </div>
    </Reveal>
  );
}

/* ── SLIDE 1 · GROUP ─────────────────────────────────────────────────────── */
function MGroup({ active, onShare }) {
  const D = window.OO_DATA.group, M = window.OO_DATA.meta;
  const infRate = Math.round(D.totalInfections / D.participants * 100);
  return (
    <div className="m-slide m-slide-fill" style={{ background: "radial-gradient(95% 55% at 14% 6%, rgba(134,106,255,0.20), rgba(8,9,10,0) 60%), radial-gradient(95% 65% at 92% 100%, rgba(42,173,154,0.16), rgba(8,9,10,0) 55%), #08090A" }}>
      <FloatingShapes items={GROUP_SHAPES} />

      <div className="m-top">
        <ClipReveal run={active} delay={60}>
          <div className="m-eyebrow" style={{ color: T.mint }}>SIMULATION SUMMARY // {M.runId}</div>
        </ClipReveal>
        <ClipReveal run={active} delay={140}><h1 className="m-h1">Our outbreak timeline</h1></ClipReveal>
        <Reveal run={active} delay={250}><div className="m-meta">{M.cohort.toUpperCase()} · {M.duration.toUpperCase()} · {M.date}</div></Reveal>
      </div>

      {/* epidemiological curve — fills the space between title and bento */}
      <Reveal run={active} delay={330} style={{ flex: "1 1 auto", minHeight: 0, display: "flex" }}>
        <div className="m-card" style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", padding: "12px 12px 12px", background: "linear-gradient(180deg,#12151B,#0E1116)" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0 2px" }}>
            <span className="m-cap" style={{ color: T.fg2 }}>EPIDEMIOLOGICAL CURVE</span>
            <span style={{ display: "flex", gap: 12 }}>
              {[["INFECTED", "#FF7171"], ["RECOVERED", "#5465E5"]].map(([l, c]) => (
                <span key={l} style={{ display: "flex", alignItems: "center", gap: 5, fontFamily: T.mono, fontSize: 8, fontWeight: 700, letterSpacing: "0.08em", color: T.fg3 }}>
                  <span style={{ width: 8, height: 8, borderRadius: 999, background: c }} />{l}
                </span>
              ))}
            </span>
          </div>
          <div style={{ flex: 1, minHeight: 0, position: "relative", marginTop: 12 }}>
            <EpiCurve play={active} fill />
          </div>
        </div>
      </Reveal>

      {/* secondary 2×2 bento */}
      <div className="m-grid2">
        <MiniMetric label="INFECTED" to={infRate} suffix="%" accent={T.coral2} icon="biohazard" foot={`${fmt(D.totalInfections)} OF ${D.participants}`} play={active} delay={520} />
        <MiniMetric label="DECEASED" to={D.deceased} accent={T.coral} icon="heart-pulse" foot={`${Math.round(D.deceased / (D.totalInfections || D.participants) * 100)}% OF INFECTED`} play={active} delay={560} />
        <MiniMetric label="MASKED" to={D.ledger.maskedPct} suffix="%" accent={T.mint} icon="shield" foot={`${fmt(D.ledger.maskedCount)} PLAYERS`} play={active} delay={600} />
        <MiniMetric label="VACCINATED" to={D.ledger.vaccinatedPct} suffix="%" accent={T.blue2} icon="syringe" foot={`${D.vaccinated} PLAYERS`} play={active} delay={640} />
      </div>

    </div>
  );
}

/* ── SLIDE 2 · INDIVIDUAL — Close Calls ─────────────────────────────────── */
/* little circular widget (icon badge) */
function MiniBadge({ icon, color, size = 58 }) {
  return (
    <div style={{ width: size, height: size, borderRadius: 999, border: `2px solid ${accentAlpha(color, 0.5)}`, background: accentAlpha(color, 0.1), display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, boxShadow: `0 0 16px -4px ${accentAlpha(color, 0.6)}` }}>
      <Icon name={icon} size={size * 0.44} color={color} stroke={1.75} />
    </div>
  );
}

/* big rolling (count-up) number with label */
function RollNum({ label, to, accent, play, suffix = "", delay = 0 }) {
  return (
    <div style={{ flex: 1, textAlign: "center" }}>
      <div style={{ fontFamily: T.mono, fontSize: 9.5, fontWeight: 700, letterSpacing: "0.12em", color: T.fg3 }}>{label}</div>
      <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: "clamp(54px,17vw,76px)", lineHeight: 0.92, color: accent, letterSpacing: "-0.03em", marginTop: 6 }}>
        <CountUp to={to} play={play} suffix={suffix} duration={1500} delay={delay} />
      </div>
    </div>
  );
}

/* ── ported sonar (green close-calls / red succumbed) ────────────────────── */
function MSonar({ count, accent, sweepDur = 4.8, blipColor = "#FF7171", sourceIndex = -1 }) {
  const C = 200, RMAX = 190;
  const A = (a) => accentAlpha(accent, a);
  const onArc = (r, ang) => [C + Math.cos(ang) * r, C + Math.sin(ang) * r];
  const jA = [12, 137, 263, 41, 196, 312, 78, 229, 349, 168, 301, 95];
  const jR = [72, 152, 112, 140, 94, 170, 134, 106, 152, 122, 160, 128];
  const blips = Array.from({ length: count }).map((_, i) => {
    const deg = jA[i % jA.length], rad = deg * Math.PI / 180, rr = jR[i % jR.length];
    return { x: C + Math.cos(rad) * rr, y: C + Math.sin(rad) * rr };
  });
  const N = 22, span = 2.15;
  const trail = Array.from({ length: N }).map((_, k) => {
    const a0 = -span * k / N, a1 = -span * (k + 1) / N;
    const [x0, y0] = onArc(RMAX, a0), [x1, y1] = onArc(RMAX, a1);
    return { d: `M ${C} ${C} L ${x0} ${y0} A ${RMAX} ${RMAX} 0 0 0 ${x1} ${y1} Z`, op: Math.pow(1 - k / N, 1.7) * 0.34 };
  });
  return (
    <svg viewBox="0 0 400 400" width="100%" height="100%" style={{ display: "block", overflow: "visible" }}>
      {[52, 104, 156, RMAX].map((r, i) => (
        <circle key={r} cx={C} cy={C} r={r} fill="none" stroke={i === 3 ? A(0.16) : A(0.10)} strokeWidth="1.8" strokeDasharray={i === 3 ? "2 8" : "none"} />
      ))}
      {[0, 45, 90, 135, 180, 225, 270, 315].map((a) => {
        const rad = a * Math.PI / 180;
        return <line key={a} x1={C} y1={C} x2={C + Math.cos(rad) * RMAX} y2={C + Math.sin(rad) * RMAX} stroke="rgba(255,255,255,0.045)" strokeWidth="1.4" />;
      })}
      {blips.map((b, i) => {
        const src = i === sourceIndex;
        return (
          <g key={i}>
            {src && <circle cx={b.x} cy={b.y} r="18" fill="none" stroke="rgba(255,113,113,0.5)" strokeWidth="1.6">
              <animate attributeName="r" values="15;21;15" dur="2.2s" repeatCount="indefinite" />
              <animate attributeName="opacity" values="0.7;0.2;0.7" dur="2.2s" repeatCount="indefinite" />
            </circle>}
            <circle cx={b.x} cy={b.y} r={src ? 13 : 11} fill="none" stroke={src ? "rgba(255,113,113,0.85)" : "rgba(219,58,58,0.30)"} strokeWidth="1.6" />
            <circle cx={b.x} cy={b.y} r={src ? 6.5 : 4.5} fill={src ? "#FF7171" : blipColor} opacity={src ? 1 : 0.6} />
          </g>
        );
      })}
      <g>
        <animateTransform attributeName="transform" type="rotate" from={`0 ${C} ${C}`} to={`360 ${C} ${C}`} dur={`${sweepDur}s`} repeatCount="indefinite" />
        {trail.map((s, k) => <path key={k} d={s.d} fill={accent} opacity={s.op} />)}
        <line x1={C} y1={C} x2={C + RMAX} y2={C} stroke={accent} strokeWidth="3" opacity="0.8" style={{ filter: `drop-shadow(0 0 6px ${A(0.7)})` }} />
        <circle cx={C + RMAX} cy={C} r="5.5" fill={accent} style={{ filter: `drop-shadow(0 0 8px ${A(0.9)})` }} />
      </g>
    </svg>
  );
}

function MStatCell({ icon, label, value, accent }) {
  return (
    <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, padding: "13px 5px" }}>
      <Icon name={icon} size={16} color={accent} stroke={1.9} />
      <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
        <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: 19, lineHeight: 1, color: T.fg, letterSpacing: "-0.02em" }}>{value}</div>
        <div style={{ fontFamily: T.mono, fontSize: 7.5, fontWeight: 700, letterSpacing: "0.08em", color: accent, marginTop: 3 }}>{label}</div>
      </div>
    </div>
  );
}

/* ── SLIDE 2 · INDIVIDUAL — sonar; close-calls (never infected) or succumbed (infected) */
function MIndividual({ active, onShare }) {
  const D = window.OO_DATA.individual;
  const SIZE = 580;
  const infected = !!D.infected;
  const indexCase = !!D.indexCase;

  const theme = indexCase
    ? {
        accent: T.coral2, headerColor: T.coral2, headerIcon: "biohazard", header: "// INDEX CASE",
        bg: "radial-gradient(120% 55% at 50% 6%, rgba(255,113,113,0.18), rgba(8,9,10,0) 55%), radial-gradient(120% 60% at 50% 100%, rgba(219,58,58,0.13), rgba(8,9,10,0) 55%), #08090A",
        count: (D.contactsBeforeInfection != null ? D.contactsBeforeInfection : 0),
        label: "INFECTED CONTACTS", sourceIndex: -1, blipColor: "#DB3A3A",
        message: "Patient zero. You caught it from no one — the outbreak started with you, and spread from here.",
        stats: [
          { icon: "git-fork", label: "DIRECT INFECTIONS", value: fmt(D.directInfections || 0), accent: T.coral2 },
          { icon: "biohazard", label: "DOWNSTREAM INF.", value: fmt(D.downstreamInfections), accent: T.coral },
        ],
      }
    : infected
    ? {
        accent: T.coral, headerColor: T.coral2, headerIcon: "skull", header: "// CLOSE CALLS",
        bg: "radial-gradient(120% 55% at 50% 6%, rgba(237,80,80,0.18), rgba(8,9,10,0) 55%), radial-gradient(120% 60% at 50% 100%, rgba(219,58,58,0.13), rgba(8,9,10,0) 55%), #08090A",
        count: (D.contactsBeforeInfection != null ? D.contactsBeforeInfection : D.infectedEncounters),
        label: "CONTACTS BEFORE INFECTION", sourceIndex: 3, blipColor: "#DB3A3A",
        message: "You crossed paths with this many infected individuals before becoming a vector yourself.",
        stats: [
          { icon: "hourglass", label: "TIME HEALTHY", value: (D.timeSurvived || "—"), accent: T.coral2 },
          { icon: "clock", label: "TIME / CONTACT", value: D.avgEncounterDuration, accent: T.purple },
          { icon: "biohazard", label: "DOWNSTREAM INF.", value: fmt(D.downstreamInfections), accent: T.coral },
        ],
      }
    : D.infectedEncounters === 0
    ? {
        accent: T.mint, headerColor: T.mint, headerIcon: "shield-check", header: "// UNTOUCHED",
        bg: "radial-gradient(105% 70% at 6% 0%, rgba(88,219,166,0.14), rgba(8,9,10,0) 55%), radial-gradient(95% 60% at 100% 100%, rgba(42,173,154,0.10), rgba(8,9,10,0) 55%), #08090A",
        count: 0,
        label: "INFECTED CONTACTS", sourceIndex: -1, blipColor: T.mint,
        message: "You crossed paths with this many infected individuals, but evaded infection.",
        stats: [
          { icon: "heart-pulse", label: "TIME HEALTHY", value: D.timeHealthy || "—", accent: T.mint },
          { icon: "shield", label: "TIME MASKED", value: D.timeMasked || "—", accent: T.mint },
          { icon: "syringe", label: "TIME VACCINATED", value: D.timeVaxed || "—", accent: T.blue2 },
        ],
      }
    : {
        accent: T.mint, headerColor: T.purple, headerIcon: null, header: "// CLOSE CALLS",
        bg: "radial-gradient(105% 70% at 6% 0%, rgba(134,106,255,0.20), rgba(8,9,10,0) 55%), radial-gradient(95% 60% at 100% 100%, rgba(88,219,166,0.12), rgba(8,9,10,0) 55%), #08090A",
        count: D.infectedEncounters,
        label: "INFECTED CONTACTS", sourceIndex: -1, blipColor: "#FF7171",
        message: "You crossed paths with this many infected individuals, but evaded infection.",
        stats: [
          { icon: "clock", label: "TIME / CONTACT", value: D.avgEncounterDuration, accent: T.purple },
          { icon: "shield", label: "WHILE MASKED", value: fmt(D.contactsMasked), accent: T.mint },
          { icon: "syringe", label: "WHILE VAXED", value: fmt(D.contactsVaccinated), accent: T.blue2 },
        ],
      };

  return (
    <div className="m-slide" style={{ justifyContent: "space-between", background: theme.bg }}>
      {/* status header */}
      <div style={{ position: "relative", zIndex: 2, display: "flex", alignItems: "center", justifyContent: "space-between", opacity: active ? 1 : 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 7, fontFamily: T.mono, fontSize: 10.5, fontWeight: 700, letterSpacing: "0.18em", color: theme.headerColor }}>
          {theme.headerIcon && <Icon name={theme.headerIcon} size={13} color={theme.headerColor} stroke={2} />}{theme.header}
        </div>
        <span style={{ width: 6, height: 6, borderRadius: 999, background: theme.accent, boxShadow: `0 0 8px ${theme.accent}` }} />
      </div>

      {/* hero — bleeding sonar with count at core */}
      <div style={{ position: "relative", flex: "1 1 auto", minHeight: 0, display: "flex", alignItems: "center", justifyContent: "center" }}>
        <div style={{ position: "absolute", left: "50%", top: "50%", transform: "translate(-50%,-50%)", width: SIZE, height: SIZE, zIndex: 0, pointerEvents: "none" }}>
          <MSonar count={theme.count} accent={theme.accent} blipColor={theme.blipColor} sourceIndex={theme.sourceIndex} />
        </div>
        <div style={{ position: "relative", zIndex: 1, textAlign: "center", opacity: active ? 1 : 0 }}>
          <div style={{ fontFamily: T.mono, fontWeight: 700, fontSize: "clamp(96px,30vw,150px)", lineHeight: 0.78, color: T.fg, letterSpacing: "-0.05em", textShadow: "0 0 26px rgba(8,9,10,0.95), 0 0 54px rgba(8,9,10,0.85)" }}>
            <CountUp to={theme.count} play={active} duration={1700} delay={300} />
          </div>
          <div style={{ fontFamily: T.mono, fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", color: theme.accent, marginTop: 8, textShadow: "0 0 16px rgba(8,9,10,0.95)" }}>{theme.label}</div>
        </div>
      </div>

      {/* message */}
      <Reveal run={active} delay={300} style={{ flex: "0 0 auto" }}>
        <p style={{ fontFamily: T.sans, fontSize: 14, lineHeight: 1.5, color: T.fg2, textAlign: "center", margin: 0, textWrap: "pretty" }}>{theme.message}</p>
      </Reveal>

      {/* stat cells */}
      <Reveal run={active} delay={380} style={{ flex: "0 0 auto" }}>
        <div style={{ display: "flex", alignItems: "stretch", background: T.panel, border: `1px solid ${T.line}`, borderRadius: 16 }}>
          {theme.stats.map((s, i) => (
            <React.Fragment key={s.label}>
              {i > 0 && <div style={{ width: 1, background: T.line, margin: "10px 0" }} />}
              <MStatCell icon={s.icon} label={s.label} value={s.value} accent={s.accent} />
            </React.Fragment>
          ))}
        </div>
      </Reveal>

      {/* footer */}
      <Reveal run={active} delay={460} style={{ flex: "0 0 auto" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
          <Logo kind="globe" height={22} />
          <div style={{ fontFamily: T.mono, fontSize: 8.5, fontWeight: 700, letterSpacing: "0.12em", color: T.fg3, lineHeight: 1.4 }}>OPERATION<br />OUTBREAK</div>
        </div>
      </Reveal>
    </div>
  );
}

/* ── SLIDE 3 · PERSONALITY (reveal-only, flippable) ──────────────────────── */
function FlipBack({ visible }) {
  return (
    <div style={{ position: "absolute", inset: 0, transform: "rotateY(180deg)", backfaceVisibility: "hidden", WebkitBackfaceVisibility: "hidden", opacity: visible ? 1 : 0, borderRadius: 26, border: "1.5px solid rgba(255,255,255,0.12)", background: "linear-gradient(165deg,#14131f 0%,#0c0f12 100%)", overflow: "hidden", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", textAlign: "center", padding: "40px 28px" }}>
      <Star size={230} color="rgba(134,106,255,0.10)" style={{ position: "absolute", top: "46%", left: "50%", transform: "translate(-50%,-50%)" }} />
      <div style={{ position: "relative", display: "flex", flexDirection: "column", alignItems: "center" }}>
        <Logo kind="globe" height={42} />
        <div style={{ fontFamily: T.mono, fontSize: 11, fontWeight: 700, letterSpacing: "0.18em", color: T.fg2, marginTop: 18 }}>OPERATION OUTBREAK</div>
        <h2 style={{ fontFamily: T.sans, fontWeight: 300, fontSize: 34, lineHeight: 1.12, letterSpacing: "-0.02em", color: T.fg, margin: "14px 0 0", maxWidth: 280 }}>Your OO personality is…</h2>
        <div style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: T.mono, fontSize: 11, fontWeight: 700, letterSpacing: "0.14em", color: T.mint, marginTop: 30 }}>
          <Icon name="rotate-cw" size={14} color={T.mint} stroke={2} /> TAP TO REVEAL
        </div>
      </div>
    </div>
  );
}

function FlipCard({ active }) {
  const bucket = window.OO_DATA.currentBucket;
  const p = window.OO_DATA.personalities[bucket];
  const [flipped, setFlipped] = useState(false);
  useEffect(() => { if (!active) setFlipped(false); }, [active]);
  const [angle, setAngle] = useState(180);
  const ref = useRef(180);
  useEffect(() => {
    const target = flipped ? 0 : 180, from = ref.current, start = performance.now(), dur = 660;
    const ease = (t) => 1 - Math.pow(1 - t, 3);
    let raf;
    const step = (now) => { const t = Math.min(1, (now - start) / dur); const v = from + (target - from) * ease(t); ref.current = v; setAngle(v); if (t < 1) raf = requestAnimationFrame(step); };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [flipped]);
  return (
    <div data-no-nav onClick={() => setFlipped((f) => !f)} style={{ width: "100%", maxWidth: 420, perspective: 1400, cursor: "pointer" }}>
      <div style={{ position: "relative", transformStyle: "preserve-3d", transform: `rotateY(${angle}deg)` }}>
        <div style={{ backfaceVisibility: "hidden", WebkitBackfaceVisibility: "hidden", opacity: angle < 90 ? 1 : 0 }}>
          <CharacterCard p={p} animKey={bucket} descOverride={p.altDesc && window.OO_DATA.individual.downstreamInfections === 0 ? p.altDesc : undefined} />
        </div>
        <FlipBack visible={angle >= 90} />
      </div>
    </div>
  );
}

function MPersonality({ active, onShare }) {
  const bucket = window.OO_DATA.currentBucket;
  const p = window.OO_DATA.personalities[bucket];
  const accent = p.mode === "gradient" ? T.coral2 : p.color;
  return (
    <div className="m-slide" style={{ justifyContent: "space-between", background: `radial-gradient(110% 60% at 50% 0%, ${accentAlpha(accent, 0.18)}, rgba(8,9,10,0) 55%), radial-gradient(90% 55% at 50% 100%, rgba(134,106,255,0.12), rgba(8,9,10,0) 55%), #08090A` }}>
      <Reveal run={active} delay={120} duration={620} y={28} style={{ flex: "1 1 auto", display: "flex", alignItems: "center", justifyContent: "center", minHeight: 0, padding: "0 20px" }}>
        <div style={{ transform: "scale(0.78)", transformOrigin: "center center", width: "100%" }}>
          <FlipCard active={active} />
        </div>
      </Reveal>
      <div style={{ flex: "0 0 auto", display: "flex", flexDirection: "column", gap: 8 }}>
        <Reveal run={active} delay={520}>
          <button onClick={() => window.open('./org_report.html', '_blank')} style={{
            width: "100%", display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
            fontFamily: T.mono, fontSize: 12, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase",
            color: T.ink, background: T.mint, border: "none", padding: "13px 20px", borderRadius: 999, cursor: "pointer",
          }}>
            <Icon name="file-text" size={14} color={T.ink} stroke={2.25} /> View Group Report
          </button>
        </Reveal>
      </div>
    </div>
  );
}

/* ── Mobile chassis + story autoplay + curtain-wipe transitions ──────────── */
const M_SLIDES = [MGroup, MIndividual, MPersonality];
const STORY_MS = 7000;
const CURTAIN_MS = 3200;

function MobileApp({ onBack }) {
  const initial = 0;
  const [shown, setShown] = useState(initial);   // currently rendered slide
  const [active, setActive] = useState(initial);  // navigation target
  const [share, setShare] = useState(null);
  const [prog, setProg] = useState(0);             // story-clock fill (0..1)
  const [curtain, setCurtain] = useState({ p: 0, color: T.mint, variant: "wipe" });
  const fxRef = useRef(0);
  const runningRef = useRef(false);
  const chassis = useRef(null);
  const count = M_SLIDES.length;

  // press-and-hold to pause (story style)
  const [paused, setPaused] = useState(false);
  const pausedRef = useRef(false);
  useEffect(() => { pausedRef.current = paused; }, [paused]);
  const holdTimer = useRef(null);
  const heldRef = useRef(false);
  const activeRef = useRef(false);
  const downXRef = useRef(0);

  useEffect(() => { localStorage.setItem("oo_wrapped_m", String(shown)); }, [shown]);

  const go = (target) => {
    target = Math.max(0, Math.min(count - 1, target));
    if (runningRef.current || target === active) return;
    setActive(target);
  };
  const next = () => go(active + 1);
  const prev = () => go(active - 1);

  // transition whenever the nav target diverges from what's shown — rotates FX
  useEffect(() => {
    if (active === shown) return;
    runningRef.current = true;
    const variant = FX_VARIANTS[Math.floor(Math.random() * FX_VARIANTS.length)];
    const I = window.OO_DATA.individual || {};
    const danger = !!(I.infected || I.indexCase);
    // transition color carries meaning: →individual green (safe) / red (infected/index), →personality purple
    const color = active === 2 ? T.purple : active === 1 ? (danger ? T.coral : T.mint) : T.mint;
    fxRef.current += 1;
    let raf, start, swapped = false;
    const step = (now) => {
      if (start == null) start = now;
      const p = Math.min(1, (now - start) / CURTAIN_MS);
      setCurtain({ p, color, variant });
      if (p >= 0.5 && !swapped) { swapped = true; setShown(active); }   // swap mid-transition
      if (p < 1) raf = requestAnimationFrame(step);
      else { runningRef.current = false; setCurtain({ p: 0, color, variant }); }
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [active]);

  // keyboard nav
  useEffect(() => {
    const onKey = (e) => { if (share) return; if (e.key === "ArrowRight") next(); if (e.key === "ArrowLeft") prev(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [share, active]);

  // story autoplay clock — keyed on the shown slide; pauses while sharing or held
  useEffect(() => {
    setProg(0);
    if (share) return;
    let raf, last = null, elapsed = 0;
    const step = (now) => {
      if (last == null) last = now;
      const dt = now - last; last = now;
      if (!pausedRef.current) elapsed += dt;            // freeze progress while held
      const t = Math.min(1, elapsed / STORY_MS);
      setProg(t);
      if (t < 1) raf = requestAnimationFrame(step);
      else if (shown < count - 1) go(shown + 1);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [shown, share]);

  // press-and-hold = pause; quick tap on an edge = navigate
  const onPointerDown = (e) => {
    if (share || runningRef.current) { activeRef.current = false; return; }
    if (e.target.closest("button, a, [data-no-nav]")) { activeRef.current = false; return; }
    activeRef.current = true;
    heldRef.current = false;
    downXRef.current = e.clientX;
    holdTimer.current = setTimeout(() => { heldRef.current = true; setPaused(true); }, 200);
  };
  const endPress = (e, isCancel) => {
    if (!activeRef.current) return;
    activeRef.current = false;
    if (holdTimer.current) { clearTimeout(holdTimer.current); holdTimer.current = null; }
    if (heldRef.current) { heldRef.current = false; setPaused(false); return; }  // release a hold → resume
    if (isCancel) return;
    const r = chassis.current.getBoundingClientRect();
    const x = (e.clientX || downXRef.current) - r.left;
    if (x < r.width * 0.30) prev();
    else if (x > r.width * 0.70) next();
    else { setPaused(p => { pausedRef.current = !p; return !p; }); }
  };

  const Active = M_SLIDES[shown];
  return (
    <div className="m-stage">
      <div className="m-phone" ref={chassis} onPointerDown={onPointerDown} onPointerUp={(e) => endPress(e, false)} onPointerLeave={(e) => endPress(e, true)} onPointerCancel={(e) => endPress(e, true)}>
        <ProgressBars active={shown} count={count} prog={prog} />
        {onBack && (
          <button data-no-nav onClick={onBack} style={{
            position: "absolute", top: 20, left: 14, zIndex: 50,
            background: "rgba(0,0,0,0.45)", border: "none", borderRadius: 999,
            padding: "7px 9px", cursor: "pointer", display: "flex", alignItems: "center",
          }}>
            <Icon name="arrow-left" size={15} color="rgba(255,255,255,0.6)" stroke={2} />
          </button>
        )}
        <div key={shown} className="m-pane" style={{ width: "100%" }} data-screen-label={String(shown + 1).padStart(2, "0")}>
          <Active active={true} onShare={(t, b) => setShare({ type: t, bucket: b })} />
        </div>
        <TransitionFX prog={curtain.p} color={curtain.color} variant={curtain.variant} />
        <div className="m-pausepill" style={{ opacity: paused ? 1 : 0 }}><Icon name="pause" size={12} color={T.fg} stroke={2.5} /> PAUSED</div>
        <div className="m-hint left" style={{ opacity: !paused && shown > 0 ? 0.7 : 0 }}><Icon name="chevron-left" size={20} color="rgba(255,255,255,0.9)" style={{ transform: "scaleY(1.45)" }} /></div>
        <div className="m-hint right" style={{ opacity: !paused && shown < count - 1 ? 0.7 : 0 }}><Icon name="chevron-right" size={20} color="rgba(255,255,255,0.9)" style={{ transform: "scaleY(1.45)" }} /></div>
      </div>
      <MobileShareSheet share={share} onClose={() => setShare(null)} />
    </div>
  );
}

/* ── Print preview (scaled to fit screen; real size on print) ────────────── */
function PrintPreview() {
  const [s, setS] = useState(1);
  useEffect(() => {
    const calc = () => setS(Math.min((window.innerWidth - 48) / 816, (window.innerHeight - 48) / 1056, 1));
    calc(); window.addEventListener("resize", calc);
    return () => window.removeEventListener("resize", calc);
  }, []);
  return (
    <div className="oo-print-stage">
      <div className="oo-print-scale" style={{ "--ps": s }}><PrintReport /></div>
    </div>
  );
}

/* ── Hidden staging toggle (mobile ↔ report) ─────────────────────────────── */
function ViewToggle({ view, setView }) {
  return (
    <div className="oo-toggle" data-no-nav>
      <button onClick={() => setView("mobile")} className={view === "mobile" ? "on" : ""}>Mobile</button>
      <button onClick={() => setView("print")} className={view === "print" ? "on" : ""}>Report</button>
      {view === "print" && <button onClick={() => window.print()} className="go">Print / PDF</button>}
    </div>
  );
}

function Root() {
  const [view, setView] = useState(() => {
    const q = new URLSearchParams(location.search).get("view");
    if (q === "print" || q === "mobile") return q;
    return localStorage.getItem("oo_view") || "mobile";
  });
  useEffect(() => { localStorage.setItem("oo_view", view); }, [view]);
  useEffect(() => {
    const onKey = (e) => {
      if (e.target.closest && e.target.closest("input,textarea")) return;
      if (e.key === "p" || e.key === "P") setView("print");
      if (e.key === "m" || e.key === "M") setView("mobile");
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  return (
    <React.Fragment>
      {view === "mobile" ? <MobileApp /> : <PrintPreview />}
      <ViewToggle view={view} setView={setView} />
    </React.Fragment>
  );
}

window.MobileApp = MobileApp;
Object.assign(window, { MGroup, MIndividual, MPersonality, FlipCard });

if (!window.__OO_PDF_GENERATOR && !window.OO_NO_AUTOMOUNT) {
  ReactDOM.createRoot(document.getElementById("root")).render(<Root />);
  if (window.lucide) { try { window.lucide.createIcons(); } catch (e) {} }
}
