const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ──────────────────────────────────────────────────────────────
//  TWEAKABLE DEFAULTS
// ──────────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "model": "llama3.1:8b",
  "totalPages": 800,
  "showFinishedSection": true,
  "campaignCopy": "Every $ funds a real bee sanctuary"
}/*EDITMODE-END*/;

// ──────────────────────────────────────────────────────────────
//  FAKE DATA (mirrors /api/live shape)
// ──────────────────────────────────────────────────────────────
const ACTIVE_LANES = [
  { slug: "amygdala-threat-detection-patterns", lane: "cortex",    elapsed_seconds: 48, model: "llama3.1:8b" },
  { slug: "ceo-tiebreak-protocol-v2",           lane: "committee", elapsed_seconds: 92, model: "llama3.1:8b" },
  { slug: "byo-llm-openrouter-adapter",         lane: "substrate", elapsed_seconds: 14, model: "llama3.1:8b" },
];
const EST_LANE_SECONDS = 240; // ~4 min per page on llama3.1:8b CPU — real time

const RECENT_PAGES = [
  { slug: "hippocampus-memory-formation", category: "cortex", generated_at: minsAgo(0.2),
    preview: "The hippocampus in Apiary handles new memory formation — what gets promoted from working memory to long-term .md substrate. Triage runs every reconsideration cycle: if a decision was novel, escalated, or reversed, the hippocampus tags it for retention." },
  { slug: "ceo-tiebreak-protocol-v2", category: "committee", generated_at: minsAgo(0.9),
    preview: "When a sector committee deadlocks (e.g. 2-2 in a 4-rep panel), the elected CEO breaks the tie. The CEO does NOT vote on first round — only on ties — so they remain a neutral arbiter. CEOs are elected by track record (decisions ratified ÷ decisions proposed)." },
  { slug: "byo-llm-adapter-anthropic", category: "substrate", generated_at: minsAgo(2.4) },
  { slug: "founder-reservations-matrix", category: "oversee", generated_at: minsAgo(4.1) },
  { slug: "reconsideration-quorum-rules", category: "reconsider", generated_at: minsAgo(7) },
  { slug: "thalamus-input-routing", category: "cortex", generated_at: minsAgo(11) },
  { slug: "constitution-amendment-flow", category: "committee", generated_at: minsAgo(14) },
  { slug: "basal-ganglia-fast-paths", category: "cortex", generated_at: minsAgo(18) },
  { slug: "situational-felt-sense-calibration", category: "situational", generated_at: minsAgo(22) },
  { slug: "cater-loop-folder-as-lobe", category: "cater", generated_at: minsAgo(27) },
];

function minsAgo(m) { return new Date(Date.now() - m * 60_000).toISOString(); }

// ──────────────────────────────────────────────────────────────
//  HELPERS (mirror LiveClient.tsx)
// ──────────────────────────────────────────────────────────────
function computeProgressPct(elapsed) {
  const pct = Math.min(99, Math.round((elapsed / EST_LANE_SECONDS) * 100));
  return Math.max(2, pct);
}
function formatElapsed(s) {
  if (s < 60) return `${Math.round(s)}s`;
  const m = Math.floor(s / 60), r = s % 60;
  return `${m}m ${r}s`;
}
function formatRelative(iso) {
  const t = new Date(iso).getTime();
  const diff = Date.now() - t;
  const sec = Math.round(diff / 1000);
  if (sec < 5) return "just now";
  if (sec < 60) return `${sec}s ago`;
  const min = Math.round(sec / 60);
  if (min < 60) return `${min}m ago`;
  const hr = Math.round(min / 60);
  if (hr < 24) return `${hr}h ago`;
  return `${Math.round(hr / 24)}d ago`;
}

// ──────────────────────────────────────────────────────────────
//  ICONS
// ──────────────────────────────────────────────────────────────
const BrandMark = ({ size = 26 }) => (
  <svg width={size} height={size * 1.07} viewBox="0 0 56 60" fill="none">
    <polygon points="28,2 54,16 54,44 28,58 2,44 2,16" fill="none" stroke="#f5b800" strokeWidth="2"/>
    <polygon points="28,12 44,21 44,39 28,48 12,39 12,21" fill="#f5b800" opacity="0.18"/>
    <text x="28" y="36" textAnchor="middle" fontSize="20" fontWeight="700" fill="#f5b800" fontFamily="ui-sans-serif">A</text>
  </svg>
);

// ──────────────────────────────────────────────────────────────
//  NAV BAR (mock of the site chrome)
// ──────────────────────────────────────────────────────────────
const NavBar = () => (
  <header className="navbar">
    <div className="navbar__inner">
      <a className="brand" href="/">
        <span className="brand__mark"><BrandMark/></span>
        <span>Apiary</span>
      </a>
      <nav className="nav-links">
        <a href="/">Home</a>
        <a href="/dashboard">Dashboard</a>
        <a className="on" href="#">Live</a>
        <a href="#">Hives</a>
      </nav>
      <a className="nav-cta" href="#donate">Support</a>
    </div>
  </header>
);

// ──────────────────────────────────────────────────────────────
//  TOP DONATE STRIP (kept exactly like LiveClient)
// ──────────────────────────────────────────────────────────────
const TopDonateStrip = ({ copy }) => (
  <section className="donate-strip">
    <div className="donate-strip__icon" aria-hidden>🍯</div>
    <div className="donate-strip__copy">
      <strong>Support the hive</strong>
      <span className="sep">—</span>
      <span>{copy}</span>
    </div>
    <a className="donate-strip__btn" href="#donate">Support →</a>
  </section>
);

// ──────────────────────────────────────────────────────────────
//  PAGE HEADER (mirrors LiveClient header exactly)
// ──────────────────────────────────────────────────────────────
const LiveHeader = ({ model, autoRefresh, onToggle, isReading, activeUsers, totalUsers }) => (
  <header>
    {/* Founder 2026-05-25: mobile users were getting stuck on /live with no
        way out. Always-visible back button + the title itself leads home.
        Inline-styled so the locked white /live design + CSS stay untouched. */}
    <a
      href="/"
      aria-label="Back to Apiary home"
      style={{
        display: "inline-flex", alignItems: "center", gap: "6px",
        marginBottom: "12px", fontSize: "14px", fontWeight: 600,
        color: "#a8750a", textDecoration: "none",
      }}
    >
      <span aria-hidden>←</span> Back to Apiary
    </a>
    <h1 className="live-h1">
      <a href="/" aria-label="Apiary home" style={{ color: "inherit", textDecoration: "none" }}>
        <span className="live-h1__bee" aria-hidden>🐝</span>The Apiary
      </a>
    </h1>
    <div className="live-sub">
      <span>Generated by local Llama on the founder's machine</span>
      <span className="live-sub__dim" aria-hidden>·</span>
      <span className="live-sub__honey">these files: free (Ollama, local)</span>
      <span className="live-sub__dim" aria-hidden>·</span>
      <span>BYO-brain — Claude/GPT use their own tokens, that's on you</span>
      <span className="live-sub__dim" aria-hidden>·</span>
      <span className="live-pill-live">
        <span className="dot-pulse"/>
        LIVE
      </span>
      {typeof activeUsers === "number" && (
        <>
          <span className="live-sub__dim" aria-hidden>·</span>
          <span className="live-pill-here" title="visitors active on apiarybee.com right now — you (founder) are excluded">
            <span aria-hidden>🐝</span>
            {activeUsers} here now
          </span>
        </>
      )}
      {typeof totalUsers === "number" && (
        <>
          <span className="live-sub__dim" aria-hidden>·</span>
          <span className="live-pill-views" title="all-time visitors since last cold start (ephemeral) — founder excluded">
            <span aria-hidden>👁</span>
            {totalUsers.toLocaleString()} views
          </span>
        </>
      )}
    </div>
    <button
      type="button"
      className={`refresh-toggle ${autoRefresh ? "refresh-toggle--on" : "refresh-toggle--off"}`}
      onClick={onToggle}
      aria-pressed={autoRefresh}
    >
      {autoRefresh ? (
        <>
          <span className="refresh-toggle__dot"/>
          {isReading ? "paused — reading · tap to stop" : "live · tap to stop"}
        </>
      ) : (
        <>
          <span aria-hidden>▶</span>
          start live updates
        </>
      )}
    </button>
  </header>
);

// ──────────────────────────────────────────────────────────────
//  STAT CARDS (4-up grid, exact mirror)
// ──────────────────────────────────────────────────────────────
const StatCard = ({ num, label, small, extra }) => (
  <div className="stat-card">
    <div className={`stat-card__num ${small ? "stat-card__num--small" : ""}`}>{num}</div>
    <div className="stat-card__label">{label}</div>
    {extra && <div className="stat-card__extra">{extra}</div>}
  </div>
);
// Human-readable storage formatter — bytes -> "12 KB" / "3.4 MB" / "1.2 GB".
// Founder uses this to plan capacity / upgrade decisions on the free tier.
function formatBytes(n) {
  if (typeof n !== "number" || !isFinite(n) || n <= 0) return "0 MB";
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
  const mb = n / (1024 * 1024);
  if (mb < 1024) return mb < 10 ? `${mb.toFixed(1)} MB` : `${Math.round(mb)} MB`;
  const gb = mb / 1024;
  return `${gb.toFixed(1)} GB`;
}

// Archive jar gauge — visual % full of a 100 MB "small bee jar" target.
// 100 MB chosen as a planning ceiling on free Vercel hosting (text content
// only — well below the 100 GB bandwidth limit but enough headroom that
// the founder can plan an upgrade decision at ~75-80%). At ~3 KB/page
// average, 100 MB ≈ ~33k pages of substrate.
const ARCHIVE_TARGET_MB = 100;

// Estimate days until the archive fills the 100 MB jar at the current
// rate. Pure projection from observed inputs: pages/hr × avg bytes/page.
// Returns null when we don't have enough data to be honest (founder rule:
// don't fake numbers, say "—" when you can't compute).
function estimateDaysToFill({ bytes, totalPages, pagesPerHr }) {
  if (!bytes || !totalPages || !pagesPerHr) return null;
  const avgBytesPerPage = bytes / totalPages;
  if (!isFinite(avgBytesPerPage) || avgBytesPerPage <= 0) return null;
  const remainingBytes = ARCHIVE_TARGET_MB * 1024 * 1024 - bytes;
  if (remainingBytes <= 0) return 0;
  const pagesNeeded = remainingBytes / avgBytesPerPage;
  const hoursNeeded = pagesNeeded / pagesPerHr;
  return hoursNeeded / 24;
}

const ArchiveGauge = ({ bytes, totalPages, pagesPerHr }) => {
  const currentMB = (bytes || 0) / (1024 * 1024);
  const pct = Math.min(100, Math.max(0, (currentMB / ARCHIVE_TARGET_MB) * 100));
  const display = pct < 1 ? pct.toFixed(2) : pct.toFixed(1);
  const days = estimateDaysToFill({ bytes, totalPages, pagesPerHr });
  let etaLabel = null;
  if (days !== null) {
    if (days < 1) etaLabel = "fills today";
    else if (days < 60) etaLabel = `~${Math.round(days)}d to full`;
    else if (days < 365) etaLabel = `~${Math.round(days / 7)}wk to full`;
    else etaLabel = `~${(days / 365).toFixed(1)}yr to full`;
  }
  return (
    <div className="gauge" title={`${currentMB.toFixed(2)} MB of ${ARCHIVE_TARGET_MB} MB target`}>
      <div className="gauge__bar">
        <div className="gauge__fill" style={{ width: `${Math.max(2, pct)}%` }}/>
      </div>
      <div className="gauge__caption">
        {display}% of {ARCHIVE_TARGET_MB} MB jar
        {etaLabel && <span className="gauge__eta"> · {etaLabel}</span>}
      </div>
    </div>
  );
};

// Pages-per-hour rate, computed from the timestamps of the most-recent
// additions. Uses N-1 intervals across (newest - oldest) span — robust
// against single-page bursts. Falls back to null if we don't have enough
// data yet (founder rule: real-time only, no mock fallback).
function computePagesPerHr(recent) {
  if (!Array.isArray(recent) || recent.length < 3) return null;
  const sorted = [...recent].sort((a, b) =>
    (a.generated_at < b.generated_at ? 1 : -1)
  );
  const newest = new Date(sorted[0].generated_at).getTime();
  const oldest = new Date(sorted[sorted.length - 1].generated_at).getTime();
  const spanMin = (newest - oldest) / 60000;
  if (!isFinite(spanMin) || spanMin <= 0) return null;
  const intervals = sorted.length - 1;
  const rate = (intervals * 60) / spanMin;
  if (!isFinite(rate) || rate <= 0) return null;
  return Math.round(rate);
}

const StatGrid = ({ totalPages, model, archiveBytes, pagesPerHr }) => (
  <section className="stats" aria-label="Hive stats">
    <StatCard
      num={totalPages.toLocaleString()}
      label="pages generated"
      extra={pagesPerHr ? `~${pagesPerHr} pages/hr` : null}
    />
    <StatCard num="$0.00" label="cost so far"/>
    <StatCard num={`🦙 ${model}`} label="local · running" small/>
    <StatCard
      num={formatBytes(archiveBytes)}
      label="archive size"
      extra={<ArchiveGauge bytes={archiveBytes} totalPages={totalPages} pagesPerHr={pagesPerHr}/>}
    />
  </section>
);

// ──────────────────────────────────────────────────────────────
//  CURRENTLY GENERATING (multi-lane + per-bar bee on completion)
// ──────────────────────────────────────────────────────────────
const NEXT_SLUGS = {
  cortex:    ["thalamus-routing-fallbacks", "basal-ganglia-fast-paths", "cerebellum-prediction-window"],
  committee: ["reconsideration-quorum-rules", "super-committee-cross-sector", "ceo-challenge-protocol"],
  substrate: ["byo-llm-openrouter-adapter", "ollama-streaming-handler", "md-substrate-versioning"],
  oversee:   ["founder-reservations-matrix", "escalation-threshold-tuning"],
  reconsider:["motion-approval-unanimity", "reconsideration-cooldown"],
};

const LaneCard = ({ initial, laneIndex, onComplete }) => {
  const [slug, setSlug] = useState(initial.slug);
  const [elapsed, setElapsed] = useState(initial.elapsed_seconds);
  const [beeKey, setBeeKey] = useState(0);
  const [justDone, setJustDone] = useState(false);

  // Re-anchor local state when the API delivers updated lane data (every
  // 15s poll). Without this, the LaneCard would ignore real progress and
  // run a pure mock loop. When the slug CHANGES (previous GEN OK'd, new
  // one started), fire the bee — that's the real completion signal, not
  // the local 240s timer. Pairs the bee with the actual upload.
  useEffect(() => {
    if (slug && slug !== initial.slug) {
      setBeeKey(k => k + 1);
      setJustDone(true);
      onComplete && onComplete();
      setTimeout(() => setJustDone(false), 900);
    }
    setSlug(initial.slug);
    setElapsed(initial.elapsed_seconds);
  }, [initial.slug, initial.elapsed_seconds, onComplete, slug]);

  useEffect(() => {
    // True real-time: +1 elapsed second per real second. Re-anchored to
    // server's actual elapsed on each /api/live poll (15s). DO NOT fire
    // the bee here. Real GENs often run past EST_LANE_SECONDS (240s), and
    // firing on local timer caused a 1-sec-loop churn: local hits 240 →
    // bee fires + reset → next poll re-anchors back to ~241 → bee fires
    // again → coco-for-cocoa nuts. Bee fires ONLY on real slug change
    // (handled by the re-anchor effect above). computeProgressPct already
    // caps the bar at 99% — it pins there until real completion lands.
    const t = setInterval(() => {
      setElapsed(e => e + 1);
    }, 1000);
    return () => clearInterval(t);
  }, [initial.lane]);

  const pct = computeProgressPct(elapsed);
  const visualPct = justDone ? 100 : pct;

  return (
    <div className={`lane ${justDone ? "lane--done" : ""}`} style={{ "--lane-i": laneIndex }}>
      <div className="lane__row">
        <div className="lane__title">
          <span className="lane__name">{initial.lane}:</span>{" "}
          <span className="lane__slug">{slug}</span>{" "}
          <span className="dot-pulse" aria-hidden style={{ display: "inline-block", verticalAlign: "middle", marginLeft: 4 }}/>
        </div>
        <div className="lane__elapsed">{formatElapsed(Math.round(elapsed))} elapsed · ~est {EST_LANE_SECONDS}s</div>
      </div>
      <div className="lane__bar-wrap">
        <div className="lane__bar">
          <div className="lane__fill" style={{ width: `${visualPct}%` }}>
            {visualPct >= 8 ? `${visualPct}%` : ""}
          </div>
        </div>
        {beeKey > 0 && (
          <span className="lane__bee" key={beeKey} aria-hidden>
            <BeeGlyph/>
          </span>
        )}
      </div>
    </div>
  );
};

const HiveGlyph = () => (
  <svg width="24" height="26" viewBox="0 0 28 30" fill="none" aria-hidden>
    {/* tiered honeycomb hive (skep) */}
    <ellipse cx="14" cy="27" rx="11" ry="1.8" fill="#a87a0c" opacity="0.18"/>
    <path d="M5 26 H23 V23 Q23 22 22 22 H6 Q5 22 5 23 Z" fill="#d4a017"/>
    <path d="M6 22 H22 V19 Q22 18 21 18 H7 Q6 18 6 19 Z" fill="#f5b800"/>
    <path d="M7 18 H21 V15 Q21 14 20 14 H8 Q7 14 7 15 Z" fill="#ffc21f"/>
    <path d="M8 14 H20 V11 Q20 10 19 10 H9 Q8 10 8 11 Z" fill="#ffd24d"/>
    <path d="M9 10 Q9 6 14 6 Q19 6 19 10 Z" fill="#ffe082"/>
    {/* entrance */}
    <ellipse cx="14" cy="23" rx="2.2" ry="2" fill="#4a3505"/>
    {/* tier lines */}
    <path d="M6 22 H22 M7 18 H21 M8 14 H20" stroke="#a87a0c" strokeWidth="0.6" opacity="0.5"/>
  </svg>
);

const BeeGlyph = () => (
  <svg width="22" height="18" viewBox="0 0 24 20" fill="none">
    {/* wings */}
    <ellipse cx="7"  cy="6" rx="5" ry="3.4" fill="#fff8e1" stroke="#a87a0c" strokeWidth="0.8" opacity="0.85"/>
    <ellipse cx="15" cy="6" rx="5" ry="3.4" fill="#fff8e1" stroke="#a87a0c" strokeWidth="0.8" opacity="0.85"/>
    {/* body */}
    <ellipse cx="12" cy="12" rx="7" ry="5" fill="#f5b800"/>
    {/* stripes */}
    <path d="M8 9.5 Q12 11 16 9.5" stroke="#2a1f0e" strokeWidth="1.6" fill="none" strokeLinecap="round"/>
    <path d="M7 12 Q12 13.5 17 12"  stroke="#2a1f0e" strokeWidth="1.6" fill="none" strokeLinecap="round"/>
    <path d="M8.5 14.5 Q12 15.7 15.5 14.5" stroke="#2a1f0e" strokeWidth="1.4" fill="none" strokeLinecap="round"/>
    {/* eye */}
    <circle cx="17" cy="11" r="0.9" fill="#2a1f0e"/>
  </svg>
);

const CurrentlyGenerating = ({ lanes }) => {
  const [hiveKey, setHiveKey] = useState(0);
  const onComplete = useCallback(() => setHiveKey(k => k + 1), []);
  return (
    <section className="section section--has-hive">
      <div className="section__hd-row">
        <div className="section__eyebrow">
          🦙 Currently Generating <span className="meta">({lanes.length} {lanes.length === 1 ? "lane" : "lanes"} active)</span>
        </div>
        <span className="section-hive" aria-hidden>
          <HiveGlyph/>
          {hiveKey > 0 && <span className="section-hive__pulse" key={hiveKey}/>}
        </span>
      </div>
      {lanes.length > 0 ? (
        <div className="lanes">
          {lanes.map((l, i) => <LaneCard key={i} laneIndex={i} initial={l} onComplete={onComplete}/>)}
        </div>
      ) : (
        <div className="idle">idle — Ollama queue empty momentarily</div>
      )}
    </section>
  );
};

// ──────────────────────────────────────────────────────────────
//  INLINE SUGGEST FORM (under Currently Generating)
//  Mirrors /suggest page structure: source types, content,
//  optional name/email, captcha — collapsed by default.
// ──────────────────────────────────────────────────────────────
const SOURCE_OPTIONS = [
  { id: "link", emoji: "🔗", label: "Link", hint: "URL the hive should crawl + learn from" },
  { id: "pdf", emoji: "📄", label: "PDF", hint: "Paste the URL or a summary" },
  { id: "repo", emoji: "🐙", label: "Repo", hint: "GitHub/GitLab URL or README" },
  { id: "channel", emoji: "📡", label: "Channel", hint: "RSS, Substack, podcast, forum" },
  { id: "text-dump", emoji: "📝", label: "Text dump", hint: "Paste raw text directly" },
  { id: "other", emoji: "✨", label: "Other", hint: "Tell us what kind of thing it is" },
];

const genCaptcha = () => {
  const a = Math.floor(Math.random() * 9) + 1;
  const b = Math.floor(Math.random() * 9) + 1;
  return { q: `${a} + ${b}`, expected: a + b };
};

const SuggestCard = () => {
  const [open, setOpen] = useState(false);
  const [sourceType, setSourceType] = useState("link");
  const [content, setContent] = useState("");
  const [topicHint, setTopicHint] = useState("");
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [captcha, setCaptcha] = useState(genCaptcha);
  const [captchaAns, setCaptchaAns] = useState("");
  const [submitted, setSubmitted] = useState(false);
  const [refId, setRefId] = useState("");

  const contentChars = content.length;
  const contentOk = contentChars >= 50 && contentChars <= 5000;
  const emailOk = email.trim().length === 0 || /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.trim());
  const captchaOk = Number(captchaAns) === captcha.expected;
  const canSubmit = contentOk && emailOk && captchaOk;

  const submit = (e) => {
    e.preventDefault();
    if (!canSubmit) return;
    setRefId("APRY-" + Math.random().toString(36).slice(2, 8).toUpperCase());
    setSubmitted(true);
  };

  const reset = () => {
    setSubmitted(false); setContent(""); setTopicHint(""); setName(""); setEmail("");
    setCaptcha(genCaptcha()); setCaptchaAns(""); setSourceType("link");
  };

  if (!open) {
    return (
      <button className="suggest-card" onClick={() => setOpen(true)}>
        <div className="suggest-card__collapsed">
          <div className="suggest-card__collapsed-l">
            <div className="suggest-card__eyebrow">🐝 Contribute to the hive</div>
            <div className="suggest-card__title">Got a source the hive should learn from?</div>
            <div className="suggest-card__sub">
              Drop a link, repo, channel, or paste a chunk of text. We triage,
              delegates review, the good stuff makes it in — you'll get an email if it does.
            </div>
          </div>
          <span className="suggest-card__open-cta">
            Suggest a source
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M3 8 H13 M9 4 L13 8 L9 12"/></svg>
          </span>
        </div>
      </button>
    );
  }

  if (submitted) {
    return (
      <div className="suggest-card">
        <div className="suggest-success">
          <div className="suggest-success__emoji" aria-hidden>🐝</div>
          <h3>Thanks — added to the suggestion queue.</h3>
          <p>
            The next time the founder pulls suggestions, this enters the drip.
            From there it's agent peek → delegate review → founder's final call.
            If it makes it in, you'll see it surface as a confirmed batch — and
            we'll email you. Quality earns badges over time.
          </p>
          <div className="suggest-success__ref">
            <div className="suggest-success__ref-l">Your reference ID</div>
            <div className="suggest-success__ref-id">{refId}</div>
          </div>
          <div className="suggest-success__actions">
            <button className="suggest-success__btn" onClick={reset}>Suggest another →</button>
            <button className="suggest-success__btn" onClick={() => { reset(); setOpen(false); }}>← Back to live</button>
          </div>
        </div>
      </div>
    );
  }

  return (
    <form className="suggest-card" onSubmit={submit}>
      <div className="suggest-card__form">
        <div className="suggest-card__hd">
          <div>
            <div className="suggest-card__eyebrow">🐝 Contribute to the hive</div>
            <h3>Suggest a source for the hive</h3>
          </div>
          <button type="button" className="suggest-card__close" onClick={() => setOpen(false)} aria-label="Close">✕</button>
        </div>

        <div className="field">
          <div className="field__label">
            <span className="field__label-l">Source type <span className="req">*</span></span>
            <span className="field__help">What kind of thing are you sharing?</span>
          </div>
          <div className="source-types">
            {SOURCE_OPTIONS.map(o => (
              <button
                key={o.id}
                type="button"
                className={`source-type ${sourceType === o.id ? "source-type--on" : ""}`}
                onClick={() => setSourceType(o.id)}
              >
                <div className="source-type__row">
                  <span aria-hidden>{o.emoji}</span>
                  <span>{o.label}</span>
                </div>
                <div className="source-type__hint">{o.hint}</div>
              </button>
            ))}
          </div>
        </div>

        <div className="field">
          <div className="field__label">
            <span className="field__label-l">URL or content <span className="req">*</span></span>
            <span className="field__help">{contentChars}/5000 · 50 min</span>
          </div>
          <textarea
            rows={5}
            value={content}
            onChange={e => setContent(e.target.value)}
            maxLength={5000}
            placeholder={
              ["link","pdf","repo","channel"].includes(sourceType)
                ? "Paste the URL + a sentence or two of context — why is this worth the hive's time?"
                : "Drop the text you'd like the hive to learn from. A few paragraphs is better than one line."
            }
          />
        </div>

        <div className="field">
          <div className="field__label">
            <span className="field__label-l">What's it about?</span>
            <span className="field__help">Optional · helps triage route it</span>
          </div>
          <input
            type="text"
            value={topicHint}
            onChange={e => setTopicHint(e.target.value)}
            placeholder="e.g. bee conservation, agent governance, BYO-LLM"
          />
        </div>

        <div className="field" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
          <div>
            <div className="field__label">
              <span className="field__label-l">Your name</span>
            </div>
            <input
              type="text"
              value={name}
              onChange={e => setName(e.target.value)}
              placeholder="Anonymous works too"
            />
          </div>
          <div>
            <div className="field__label">
              <span className="field__label-l">Email</span>
            </div>
            <input
              type="email"
              value={email}
              onChange={e => setEmail(e.target.value)}
              placeholder="you@example.com (optional)"
            />
          </div>
        </div>

        <div className="field">
          <div className="field__label">
            <span className="field__label-l">Quick check: what's {captcha.q}? <span className="req">*</span></span>
            <span className="field__help">No third-party widget</span>
          </div>
          <input
            type="text"
            inputMode="numeric"
            value={captchaAns}
            onChange={e => setCaptchaAns(e.target.value)}
            maxLength={4}
            className="captcha"
            placeholder="?"
          />
        </div>

        <div className="suggest-card__actions">
          <button type="submit" className="suggest-submit" disabled={!canSubmit}>
            Suggest to the hive
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M3 8 H13 M9 4 L13 8 L9 12"/></svg>
          </button>
          <span className="suggest-card__note">Reviewed by a human · we'll email you back</span>
        </div>
      </div>
    </form>
  );
};

// ──────────────────────────────────────────────────────────────
//  JUST FINISHED (last 10 — accordion + reading mode)
// ──────────────────────────────────────────────────────────────
const FinishedItem = ({ page, open, onToggle }) => {
  const [fullContent, setFullContent] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!open || fullContent !== null || loading) return;
    setLoading(true);
    setError(null);
    fetch(`/api/live/page?slug=${encodeURIComponent(page.slug)}`, { cache: "force-cache" })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(d => { setFullContent(typeof d.content === "string" ? d.content : ""); })
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, [open, page.slug, fullContent, loading]);

  return (
    <details
      className="finished-item"
      open={open}
      onToggle={(e) => onToggle(page.slug, e.currentTarget.open)}
    >
      <summary>
        <span className="finished-item__icon" aria-hidden>📄</span>
        <span className="finished-item__slug">{page.slug}</span>
        <span className="finished-item__cat">· {page.category}</span>
        <span className="finished-item__when">{formatRelative(page.generated_at)}</span>
      </summary>
      <div className="finished-item__body">
        {loading && (
          <div style={{ fontSize: 12, color: "var(--hive-500)", fontStyle: "italic", padding: "4px 2px" }}>
            loading…
          </div>
        )}
        {!loading && fullContent !== null && fullContent.length > 0 && (
          <pre className="finished-item__preview">{fullContent}</pre>
        )}
        {!loading && fullContent !== null && fullContent.length === 0 && page.preview && (
          <pre className="finished-item__preview">{page.preview}</pre>
        )}
        {!loading && error && page.preview && (
          <pre className="finished-item__preview">{page.preview}</pre>
        )}
        {!loading && error && !page.preview && (
          <div style={{ fontSize: 12, color: "var(--hive-500)", fontStyle: "italic", padding: "4px 2px" }}>
            couldn't load full page · {error}
          </div>
        )}
      </div>
    </details>
  );
};

// ──────────────────────────────────────────────────────────────
//  CATEGORIES (collapsible, alphabetical, same shape as Just Finished)
// ──────────────────────────────────────────────────────────────
const CATEGORY_DEFS = [
  { id: "amygdala",       blurb: "Safety agent — threat detection + containment" },
  { id: "basal-ganglia",  blurb: "Procedural memory + learned fast paths" },
  { id: "cater",          blurb: "Symbiotic developer-agent loop" },
  { id: "cerebellum",     blurb: "Prediction + next-step inference" },
  { id: "committee",      blurb: "House-of-Reps governance per sector" },
  { id: "cortex",         blurb: "Brain-region modular architecture" },
  { id: "frontal-cortex", blurb: "Planning, decisions, working memory" },
  { id: "hippocampus",    blurb: "New memory formation — what gets saved" },
  { id: "oversee",        blurb: "Founder as constitutional overseer" },
  { id: "reconsider",     blurb: "Self-correction via revote" },
  { id: "situational",    blurb: "Felt-sense judgment over rule-following" },
  { id: "substrate",      blurb: "BYO-LLM adapters + .md persistence" },
  { id: "temporal-lobe",  blurb: "Long-term memory / .md persistence" },
  { id: "thalamus",       blurb: "Input router — dispatches to correct region" },
  { id: "visual-cortex",  blurb: "Pattern recognition / OCR" },
];

// Coverage grouping — turn the flat 100+ category list into biggies
// (≥3 pages, shown as top-level rows with their own honey-fill bar)
// and smallies (<3 pages, collapsed into a long-tail group expandable
// on demand). Founder's ask: "biggies maybe can become smallies",
// i.e. hierarchy is fine, keep it scrollable, don't dump 193 flat rows.
const BIGGIE_THRESHOLD = 3;

// Best-effort blurbs for category families that appear in the data
// but aren't in CATEGORY_DEFS. Kept short — the count is the headline,
// the blurb just orients the eyeball.
const FAMILY_BLURBS = {
  wiki:        "wikipedia mirror — bees, plants, conservation",
  the:         "essays — 'the X' titled long-reads",
  bee:         "core bee biology — castes, life cycle",
  quantum:     "physics excursions — superposition + waves",
  pollinator:  "pollinator ecology + crop interactions",
  apiary:      "this project — meta, governance, build notes",
  hive:        "hive structure + colony dynamics",
  queen:       "queen biology — pheromones, supersedure",
  prompt:      "prompt engineering + LLM ergonomics",
  founder:     "founder process, decisions, reservations",
  waggle:      "waggle dance — bee communication",
  test:        "testing patterns + spec-as-tests",
  interface:   "API + UI interface design",
  naming:      "naming + clarity in code",
  why:         "why-this-exists explainers",
  llm:         "LLMs, models, BYO-brain adapters",
};

const Categories = ({ coverage = [], recent = [] }) => {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const [showSmallies, setShowSmallies] = useState(false);

  // Real coverage is the array from /api/live (live data). Fall back to
  // counts derived from the mock recent_additions when no live data yet.
  const families = useMemo(() => {
    if (coverage.length > 0) return coverage.slice();
    const m = {};
    recent.forEach(p => { m[p.category] = (m[p.category] || 0) + 1; });
    return Object.entries(m).map(([category, count]) => ({ category, count }));
  }, [coverage, recent]);

  // Static-blurb lookup (CATEGORY_DEFS) — pre-indexed for O(1) hits
  const blurbOf = useMemo(() => {
    const m = {};
    CATEGORY_DEFS.forEach(c => { m[c.id] = c.blurb; });
    Object.entries(FAMILY_BLURBS).forEach(([k, v]) => { if (!m[k]) m[k] = v; });
    return m;
  }, []);

  // Bucket into biggies vs smallies, sorted by count desc.
  const { biggies, smallies, totalCount, maxBiggie } = useMemo(() => {
    const sorted = families.slice().sort((a, b) => b.count - a.count);
    const big = sorted.filter(c => c.count >= BIGGIE_THRESHOLD);
    const small = sorted.filter(c => c.count < BIGGIE_THRESHOLD);
    const total = sorted.reduce((s, c) => s + c.count, 0);
    const max = big.length > 0 ? big[0].count : 1;
    return { biggies: big, smallies: small, totalCount: total, maxBiggie: max };
  }, [families]);

  // Sub-cluster hints: for each biggie family, scan recent_additions for
  // slugs starting with `<family>-` and surface the most common SECOND
  // hyphen-token as a smallie hint. e.g. "wiki-x-bumblebee-..." surfaces
  // "x" as the dominant sub-cluster. Cheap, client-only, honest.
  const subHintsFor = useMemo(() => {
    const cache = {};
    return (familyId) => {
      if (cache[familyId]) return cache[familyId];
      const subs = {};
      recent.forEach(p => {
        if (!p.slug || !p.slug.startsWith(familyId + "-")) return;
        const rest = p.slug.slice(familyId.length + 1);
        const second = rest.split("-")[0];
        if (second) subs[second] = (subs[second] || 0) + 1;
      });
      const top = Object.entries(subs).sort((a, b) => b[1] - a[1]).slice(0, 3);
      cache[familyId] = top;
      return top;
    };
  }, [recent]);

  const filteredBiggies = useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return biggies;
    return biggies.filter(c =>
      c.category.includes(q) || (blurbOf[c.category] || "").toLowerCase().includes(q)
    );
  }, [biggies, search, blurbOf]);

  const filteredSmallies = useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return smallies;
    return smallies.filter(c =>
      c.category.includes(q) || (blurbOf[c.category] || "").toLowerCase().includes(q)
    );
  }, [smallies, search, blurbOf]);

  if (!open) {
    return (
      <button className="section section--clickable" onClick={() => setOpen(true)} type="button">
        <div className="section__hd-row">
          <div className="section__eyebrow">
            📚 Browse by category{" "}
            <span className="meta">
              ({biggies.length} biggies · {smallies.length} smallies · {totalCount} pages)
            </span>
          </div>
          <span className="section__open-cta">
            Open
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M3 8 H13 M9 4 L13 8 L9 12"/></svg>
          </span>
        </div>
        <div className="section__collapsed-hint">
          Biggies first (≥{BIGGIE_THRESHOLD} pages, with a honey-fill bar showing share);
          long-tail smallies tucked into an expandable group.
        </div>
      </button>
    );
  }

  return (
    <section className="section">
      <div className="section__hd-row">
        <div className="section__eyebrow">
          📚 Categories{" "}
          <span className="meta">
            ({biggies.length} biggies · {smallies.length} smallies)
          </span>
        </div>
        <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
          <input
            className="inline-search"
            type="search"
            value={search}
            onChange={e => setSearch(e.target.value)}
            placeholder="🔍 filter categories…"
            aria-label="Filter categories"
            autoFocus
          />
          <button className="section__close-btn" onClick={() => setOpen(false)} aria-label="Close" type="button">✕</button>
        </div>
      </div>

      <div className="cat-list cat-list--grouped">
        {filteredBiggies.length === 0 && filteredSmallies.length === 0 ? (
          <div style={{ padding: "16px 8px", color: "var(--ink-500)", fontStyle: "italic", fontSize: 14 }}>
            no categories match “{search}”
          </div>
        ) : (
          <>
            {filteredBiggies.map(c => {
              const sharePct = Math.round((c.count / maxBiggie) * 100);
              const hints = subHintsFor(c.category);
              const blurb = blurbOf[c.category] || "—";
              return (
                <a key={c.category} className="biggie" href={`#${c.category}`}>
                  <div className="biggie__row">
                    <span className="biggie__name">{c.category}</span>
                    <span className="biggie__count">{c.count}</span>
                  </div>
                  <div className="biggie__blurb">{blurb}</div>
                  <div className="biggie__bar">
                    <div className="biggie__fill" style={{ width: `${Math.max(3, sharePct)}%` }}/>
                  </div>
                  {hints.length > 0 && (
                    <div className="biggie__hints">
                      <span className="biggie__hints-l">recent sub-clusters:</span>{" "}
                      {hints.map(([k, n], i) => (
                        <span key={k} className="biggie__hint">
                          {k} ({n}){i < hints.length - 1 ? "," : ""}
                        </span>
                      ))}
                    </div>
                  )}
                </a>
              );
            })}

            {filteredSmallies.length > 0 && (
              <div className="smallies-group">
                <button
                  type="button"
                  className="smallies-toggle"
                  onClick={() => setShowSmallies(s => !s)}
                  aria-expanded={showSmallies}
                >
                  <span className="smallies-toggle__chev" aria-hidden>{showSmallies ? "▾" : "▸"}</span>
                  <span className="smallies-toggle__l">
                    long-tail smallies <span className="meta">({filteredSmallies.length} categories · &lt;{BIGGIE_THRESHOLD} pages each)</span>
                  </span>
                </button>
                {showSmallies && (
                  <div className="smallies-list">
                    {filteredSmallies.map(c => (
                      <a key={c.category} className="smallie" href={`#${c.category}`}>
                        <span className="smallie__name">{c.category}</span>
                        <span className="smallie__count">{c.count}</span>
                      </a>
                    ))}
                  </div>
                )}
              </div>
            )}
          </>
        )}
      </div>
      <div className="cat-foot">
        {totalCount} pages across {biggies.length + smallies.length} families
      </div>
    </section>
  );
};

const JustFinished = ({ recent }) => {
  const [search, setSearch] = useState("");
  const [openSlugs, setOpenSlugs] = useState(new Set());
  const onToggle = useCallback((slug, isOpen) => {
    setOpenSlugs(prev => {
      const next = new Set(prev);
      isOpen ? next.add(slug) : next.delete(slug);
      return next;
    });
  }, []);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return recent.slice(0, 10);
    return recent.filter(r => r.slug.toLowerCase().includes(q)).slice(0, 10);
  }, [recent, search]);

  return (
    <section className="section">
      <div className="section__hd-row">
        <div className="section__eyebrow">
          🍯 Just Finished <span className="meta">(last {Math.min(recent.length, 10)})</span>
        </div>
        {recent.length > 0 && (
          <input
            className="inline-search"
            type="search"
            value={search}
            onChange={e => setSearch(e.target.value)}
            placeholder="🔍 filter…"
            aria-label="Filter recent"
          />
        )}
      </div>
      <div className="finished-list">
        {filtered.length === 0 ? (
          <div style={{ padding: "16px 8px", color: "var(--hive-500)", fontStyle: "italic", fontSize: 14 }}>
            {search ? `no pages match "${search}"` : "waiting for first page…"}
          </div>
        ) : (
          filtered.map(p => (
            <FinishedItem
              key={p.slug + p.generated_at}
              page={p}
              open={openSlugs.has(p.slug)}
              onToggle={onToggle}
            />
          ))
        )}
      </div>
    </section>
  );
};

// ──────────────────────────────────────────────────────────────
//  HELPER BEES STRIP
// ──────────────────────────────────────────────────────────────
const HelperStrip = () => (
  <section className="helper-strip">
    <div className="helper-strip__icon" aria-hidden>🐝</div>
    <div className="helper-strip__copy">
      <strong>Helper bees</strong>
      <span className="sep">—</span>
      <span>real people show up when the AI's not enough.</span>
    </div>
    <button className="helper-strip__btn" disabled title="coming soon">🐝 buzz</button>
    <p className="helper-strip__note">we all are sheryl to someone.</p>
  </section>
);

// ──────────────────────────────────────────────────────────────
//  "THOSE NICE THINGS" MODAL — cost breakdown, BYO, mission
// ──────────────────────────────────────────────────────────────
const NiceThingsModal = ({ onClose }) => {
  useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener("keydown", onKey);
    };
  }, [onClose]);

  return (
    <div className="modal-scrim" onClick={onClose} role="dialog" aria-modal="true">
      <div className="modal" onClick={e => e.stopPropagation()}>
        <button className="modal__close" onClick={onClose} aria-label="Close">✕</button>
        <div className="modal__inner">
          <div>
            <div className="modal__eyebrow">🐝 Those nice things</div>
            <div className="modal__sub">the honest story behind the hive</div>
          </div>

          <div className="cost-block">
            <div className="cost-block__h">🪙 Real cost — honest accounting</div>
            <div className="cost-block__sub">free to you ≠ free to make</div>
            <ul className="cost-list">
              <li>
                <span className="l">API (Anthropic):</span>
                <span className="v ok">$0.00</span>
                <span className="note">[BYO-LLM · no platform cost]</span>
              </li>
              <li>
                <span className="l">Electricity:</span>
                <span className="v">$0.0492</span>
                <span className="note">[estimated · ~$0.0002 / page on a ~250W workstation]</span>
              </li>
              <li>
                <span className="l">Founder's hours:</span>
                <span className="v">12 hrs</span>
                <span className="note">[real human time spent building + tending]</span>
              </li>
              <li>
                <span className="l">Hardware wear:</span>
                <span className="v">~$0.08</span>
                <span className="note">[amortized · founder-set]</span>
              </li>
            </ul>
          </div>

          <div className="byo-block">
            <div className="cost-block__h">🍯 BYO-LLM — bring your own brain</div>
            <p>
              Works with <strong>any</strong> LLM — Claude, ChatGPT, OpenRouter, anything
              OpenAI-compatible. If you wire in Claude or GPT, you pay their tokens
              directly to them — normal, fair, the way it should be. We never proxy
              or skim those calls. Free forever on <strong>Ollama</strong> because Ollama
              runs on your own machine — what Apiary was built for.
            </p>
          </div>

          <div className="mission-block">
            <div className="cost-block__h">🌍 Mission</div>
            <p>
              Donations offset the founder's real-world cost <strong>and</strong> fund a real bee
              sanctuary as the hive grows. Two curves, one mission — <strong>AI up, bees up.</strong> 🐝
            </p>
            <div className="mission-block__actions">
              <a href="#donate" className="mission-block__btn mission-block__btn--primary">Support the hive →</a>
              <a href="#suggest" className="mission-block__btn mission-block__btn--ghost">Suggest a source →</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
//  BOTTOM DONATE BAR (NEW — what user asked to add)
// ──────────────────────────────────────────────────────────────
const BottomBar = () => {
  const amts = [3, 7, 15, 50];
  const [picked, setPicked] = useState(7);
  const [custom, setCustom] = useState("");
  const active = custom || picked;
  return (
    <div className="bottom-bar" id="donate">
      <div className="bottom-bar__inner">
        <div className="bottom-bar__left">
          <span className="bottom-bar__icon" aria-hidden>🍯</span>
          <div className="bottom-bar__copy">
            <strong>Fund a real bee sanctuary</strong>
            <span className="sub">offsets electricity · supports the keepers</span>
          </div>
        </div>
        <div className="amt-row">
          {amts.map(a => (
            <button
              key={a}
              className={`amt ${!custom && picked === a ? "amt--on" : ""}`}
              onClick={() => { setPicked(a); setCustom(""); }}
            >${a}</button>
          ))}
          <div className={`amt amt--custom ${custom ? "amt--on" : ""}`}>
            <span>$</span>
            <input
              type="text"
              placeholder="other"
              value={custom}
              onChange={e => setCustom(e.target.value.replace(/[^\d]/g, ""))}
            />
          </div>
        </div>
        <button className="bottom-bar__give">Give ${active}</button>
      </div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────
//  MAIN APP
// ──────────────────────────────────────────────────────────────
const App = () => {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [autoRefresh, setAutoRefresh] = useState(true);
  const [niceOpen, setNiceOpen] = useState(false);
  const [live, setLive] = useState(null);
  const [activeUsers, setActiveUsers] = useState(null);
  const [totalUsers, setTotalUsers] = useState(null);
  const isReading = false; // wired but unused in mock

  useEffect(() => {
    let cancelled = false;
    const pullLive = () => {
      fetch("/api/live", { cache: "no-store" })
        .then(r => r.ok ? r.json() : null)
        .then(d => { if (!cancelled && d) setLive(d); })
        .catch(() => {});
    };
    const pullPresence = () => {
      // POST so the visitor's own heartbeat registers — /live is a static
      // page outside the Next.js root layout, so the global PresenceBeacon
      // doesn't run here. Without POST the user wouldn't see themselves.
      fetch("/api/presence", { method: "POST", cache: "no-store" })
        .then(r => r.ok ? r.json() : null)
        .then(d => {
          if (cancelled || !d) return;
          if (typeof d.active === "number") setActiveUsers(d.active);
          if (typeof d.total === "number") setTotalUsers(d.total);
        })
        .catch(() => {});
    };
    pullLive(); pullPresence();
    if (!autoRefresh) return () => { cancelled = true; };
    const liveId = setInterval(pullLive, 15_000);
    const presenceId = setInterval(pullPresence, 15_000);
    return () => { cancelled = true; clearInterval(liveId); clearInterval(presenceId); };
  }, [autoRefresh]);

  const liveTotalPages = live?.total_pages ?? t.totalPages;
  const liveModel = live?.model ?? t.model;
  const liveArchiveBytes = typeof live?.archive_bytes === "number" ? live.archive_bytes : 0;
  const liveCoverage = Array.isArray(live?.coverage_by_category) ? live.coverage_by_category : [];
  // Real-time only — no mock fallback once live data has arrived. API
  // returns content_preview; the components want `preview`. Newest first.
  const liveRecent = live
    ? (live.recent_additions ?? [])
        .map(p => ({
          slug: p.slug,
          category: p.category,
          generated_at: p.generated_at,
          preview: p.content_preview || "",
        }))
        .sort((a, b) => (a.generated_at < b.generated_at ? 1 : -1))
    : null;
  // Multi-lane real-time: prefer the new currently_generating_lanes array
  // from /api/live (sourced from local drip logs via sync-snapshot.mjs).
  // Fall back to the legacy single currently_generating object. If live
  // loaded with zero lanes, render the idle state (NO mock fallback —
  // founder ask: "say none active ya no shows paused its real time").
  const liveLanes = live
    ? (Array.isArray(live.currently_generating_lanes) && live.currently_generating_lanes.length > 0
        ? live.currently_generating_lanes.map(l => ({
            slug: l.slug,
            lane: l.lane,
            elapsed_seconds: l.elapsed_seconds,
            model: l.model || liveModel,
          }))
        : live.currently_generating
          ? [{
              slug: live.currently_generating.slug,
              lane: live.currently_generating.lane,
              elapsed_seconds: live.currently_generating.elapsed_seconds,
              model: live.currently_generating.model || liveModel,
            }]
          : [])
    : null;

  return (
    <>
      <div className="hex-tile" aria-hidden/>
      <NavBar/>
      <main className="page">
        <LiveHeader
          model={liveModel}
          autoRefresh={autoRefresh}
          isReading={isReading}
          onToggle={() => setAutoRefresh(v => !v)}
          activeUsers={activeUsers}
          totalUsers={totalUsers}
        />

        <TopDonateStrip copy={t.campaignCopy}/>

        <StatGrid
          totalPages={liveTotalPages}
          model={liveModel}
          archiveBytes={liveArchiveBytes}
          pagesPerHr={computePagesPerHr(liveRecent)}
        />

        <CurrentlyGenerating lanes={liveLanes ?? ACTIVE_LANES}/>

        {/* 👇 the suggest form, right under Currently Generating, per founder */}
        <SuggestCard/>

        {t.showFinishedSection && <Categories coverage={liveCoverage} recent={liveRecent ?? RECENT_PAGES}/>}
        {t.showFinishedSection && <JustFinished recent={liveRecent ?? RECENT_PAGES}/>}

        <HelperStrip/>

        <div className="nice-row">
          <button className="nice-btn" onClick={() => setNiceOpen(true)}>
            <span aria-hidden>🐝</span>
            Those nice things
          </button>
        </div>
      </main>

      <BottomBar/>

      {niceOpen && <NiceThingsModal onClose={() => setNiceOpen(false)}/>}

      <TweaksPanel title="Tweaks">
        <TweakSection title="Live data (mock)">
          <TweakNumber label="Pages generated" value={t.totalPages} onChange={v => setTweak("totalPages", v)} min={0} max={9999} step={1}/>
          <TweakText label="Model" value={t.model} onChange={v => setTweak("model", v)}/>
          <TweakToggle label="Show Just Finished" value={t.showFinishedSection} onChange={v => setTweak("showFinishedSection", v)}/>
        </TweakSection>
        <TweakSection title="Donate strip copy">
          <TweakText label="Top strip" value={t.campaignCopy} onChange={v => setTweak("campaignCopy", v)}/>
        </TweakSection>
      </TweaksPanel>
    </>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
