/* global React, ReactDOM */
const { useState, useEffect, useRef, useMemo } = React;

const COMICS = window.COMICS;
const HoopySprite = window.HoopySprite;
const HoopyThinking = window.HoopyThinking;
const HoopyWave = window.HoopyWave;
const HoopyNod = window.HoopyNod;
const HoopyThink = window.HoopyThink;
const HoopyJump = window.HoopyJump;
const ComicPoster = window.ComicPoster;

// ---------- HoopyPose — reusable transparent-pose mascot -----------------------
// One of the 13 named poses from the project pose-library. Renders the
// transparent PNG at a given size with a soft drop-shadow.
// Sizes: xs=48 sm=80 md=128 lg=200 xl=280
function HoopyPose({ name = "wave", size = "md", style = {}, alt = "" }) {
  const sizes = { xs: 48, sm: 80, md: 128, lg: 200, xl: 280 };
  const px = sizes[size] || sizes.md;
  return (
    <img
      src={`assets/poses/hoopy-pose-${name}-transparent.png`}
      alt={alt}
      aria-hidden={!alt}
      loading="lazy"
      style={{
        width: px,
        height: px,
        objectFit: "contain",
        filter: "drop-shadow(0 4px 12px rgba(0,0,0,0.18))",
        userSelect: "none",
        ...style,
      }}
    />
  );
}
window.HoopyPose = HoopyPose;

// ---------- RotatingHoopyPose — cycles through hero-friendly poses --------------
// Rotates every 3.5s with a soft cross-fade. Pauses if the user prefers
// reduced motion. Excludes "stumped" + "shrug" + "reading-book" + "carrying-clipboard"
// because they read as "low energy" and don't fit a homepage greeting.
const HERO_POSE_NAMES = [
  "wave",
  "presenting-3q",
  "high-five",
  "fist-bump",
  "eureka",
  "pointing-right",
  "coffee-mug",
];

function RotatingHoopyPose({ size = "lg", interval = 3500 }) {
  const [idx, setIdx] = useState(0);
  const [fade, setFade] = useState(true);
  useEffect(() => {
    const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduced) return;
    const tick = setInterval(() => {
      setFade(false);
      setTimeout(() => {
        setIdx((i) => (i + 1) % HERO_POSE_NAMES.length);
        setFade(true);
      }, 240); // matches the CSS transition below
    }, interval);
    return () => clearInterval(tick);
  }, [interval]);
  const name = HERO_POSE_NAMES[idx];
  return (
    <div style={{
      transition: "opacity 240ms ease-in-out",
      opacity: fade ? 1 : 0,
    }}>
      <HoopyPose name={name} size={size} alt={`Hoopy in ${name} pose`} />
    </div>
  );
}
window.RotatingHoopyPose = RotatingHoopyPose;

// ---------- Tiny hash-router ---------------------------------------------------
function useRoute() {
  const [hash, setHash] = useState(window.location.hash || "#/");
  useEffect(() => {
    const onChange = () => setHash(window.location.hash || "#/");
    window.addEventListener("hashchange", onChange);
    return () => window.removeEventListener("hashchange", onChange);
  }, []);
  if (hash.startsWith("#/c/")) {
    return { name: "comic", slug: hash.slice(4) };
  }
  if (hash.startsWith("#/about")) {
    return { name: "about" };
  }
  return { name: "home" };
}
function go(path) {
  window.location.hash = path;
  window.scrollTo({ top: 0, behavior: "instant" });
}

// ---------- Theme --------------------------------------------------------------
function useTheme() {
  const [theme, setTheme] = useState(() => localStorage.getItem("hoopy-theme") || "dark");
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("hoopy-theme", theme);
  }, [theme]);
  return [theme, setTheme];
}

// ---------- Tweaks defaults ----------------------------------------------------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accentHue": 32,
  "tilt": 1.6,
  "paperTexture": 1,
  "spriteSize": 7,
  "headerStyle": "marker",
  "emptyState": false
}/*EDITMODE-END*/;

// ---------- App ----------------------------------------------------------------
// ---------- Per-route metadata ------------------------------------------------
function setMeta(name, value, attr = "name") {
  let el = document.head.querySelector(`meta[${attr}="${name}"]`);
  if (!el) {
    el = document.createElement("meta");
    el.setAttribute(attr, name);
    document.head.appendChild(el);
  }
  el.setAttribute("content", value);
}
function applyRouteMeta(route) {
  const SITE_NAME = "Hoopy's Comic Explainers";
  const SITE_DESC = "AI-generated single-page educational comics, one topic at a time.";
  const SITE_IMG = "comics/excellent-claude-code-skill/comic.png";
  if (route.name === "comic") {
    const c = COMICS.find((x) => x.slug === route.slug);
    if (c) {
      const title = `${c.title} — ${SITE_NAME}`;
      const desc = c.hook;
      const img = c.image || SITE_IMG;
      document.title = title;
      setMeta("description", desc);
      setMeta("og:title", title, "property");
      setMeta("og:description", desc, "property");
      setMeta("og:image", img, "property");
      setMeta("og:type", "article", "property");
      setMeta("twitter:title", title);
      setMeta("twitter:description", desc);
      setMeta("twitter:image", img);
      return;
    }
  }
  const title = route.name === "about" ? `About — ${SITE_NAME}` : SITE_NAME;
  document.title = title;
  setMeta("description", SITE_DESC);
  setMeta("og:title", title, "property");
  setMeta("og:description", SITE_DESC, "property");
  setMeta("og:image", SITE_IMG, "property");
  setMeta("og:type", "website", "property");
  setMeta("twitter:title", title);
  setMeta("twitter:description", SITE_DESC);
  setMeta("twitter:image", SITE_IMG);
}

function App() {
  const route = useRoute();
  const [theme, setTheme] = useTheme();
  const [lightbox, setLightbox] = useState(null);
  const [tValues, setTweak] = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}];
  const t = tValues;

  // Update document title + meta tags whenever route changes (helps social sharing
  // even though hash routing means crawlers see only the homepage; once migrated
  // to real routes this stays correct).
  useEffect(() => {
    applyRouteMeta(route);
  }, [route.name, route.slug]);

  // Apply tweak vars to :root
  useEffect(() => {
    const r = document.documentElement;
    const hoopy = `oklch(72% 0.18 ${t.accentHue})`;
    const hoopySoft = `oklch(88% 0.10 ${t.accentHue})`;
    r.style.setProperty("--hoopy", hoopy);
    r.style.setProperty("--hoopy-soft", hoopySoft);
    r.style.setProperty("--tilt", `${t.tilt}deg`);
    r.style.setProperty("--paper", String(t.paperTexture));
  }, [t.accentHue, t.tilt, t.paperTexture]);

  // ESC / ArrowLeft / ArrowRight on the lightbox
  useEffect(() => {
    if (!lightbox) return;
    const onKey = (e) => {
      if (e.key === "Escape") {
        setLightbox(null);
      } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
        e.preventDefault();
        const all = window.COMICS || [];
        const idx = all.findIndex((c) => c.slug === lightbox.slug);
        if (idx === -1) return;
        const delta = e.key === "ArrowRight" ? 1 : -1;
        const nextIdx = (idx + delta + all.length) % all.length;
        setLightbox(all[nextIdx]);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [lightbox]);

  return (
    <div className="app">
      <TopBar theme={theme} setTheme={setTheme} />

      <div key={route.name + (route.slug || "")} className="route">
        {route.name === "home" && <Home onOpenComic={(slug) => go(`#/c/${slug}`)} emptyState={t.emptyState} />}
        {route.name === "about" && <About />}
        {route.name === "comic" && (
          <ComicView
            slug={route.slug}
            onBack={() => go("#/")}
            onOpenComic={(slug) => go(`#/c/${slug}`)}
            onLightbox={(c) => setLightbox(c)}
          />
        )}
        {route.name !== "home" && route.name !== "about" && route.name !== "comic" && (
          <NotFound />
        )}
      </div>

      <Footer headerStyle={t.headerStyle} />

      {lightbox && (
        <Lightbox
          comic={lightbox}
          onClose={() => setLightbox(null)}
          onPrev={() => {
            const all = window.COMICS || [];
            const idx = all.findIndex((c) => c.slug === lightbox.slug);
            if (idx === -1) return;
            setLightbox(all[(idx - 1 + all.length) % all.length]);
          }}
          onNext={() => {
            const all = window.COMICS || [];
            const idx = all.findIndex((c) => c.slug === lightbox.slug);
            if (idx === -1) return;
            setLightbox(all[(idx + 1) % all.length]);
          }}
        />
      )}
    </div>
  );
}

function TopBar({ theme, setTheme }) {
  return (
    <header className="topbar">
      <div className="brand" onClick={() => go("#/")} role="link" tabIndex={0}>
        <HoopyWave size="xs" />
        <span>Hoopy's&nbsp;Comic Explainers</span>
      </div>
      <nav className="nav">
        <a
          href="#/about"
          onClick={(e) => { e.preventDefault(); go("#/about"); }}
          style={{ cursor: "pointer" }}
        >About</a>
        <span><span className="num">{COMICS.length}</span> entries</span>
        <button
          className="theme-toggle"
          aria-label="Toggle dark mode"
          title="Toggle dark mode"
          onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
        >
          {theme === "dark" ? <SunIcon /> : <MoonIcon />}
        </button>
      </nav>
    </header>
  );
}

function SunIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
      <circle cx="12" cy="12" r="4" />
      <path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
    </svg>
  );
}
function MoonIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z" />
    </svg>
  );
}

// ---------- HOME ---------------------------------------------------------------
// Read filter state from #/?... query so links/back-button work.
function readFilterParams() {
  const hash = window.location.hash || "";
  const qIdx = hash.indexOf("?");
  if (qIdx === -1) return { topic: "", q: "", sort: "newest" };
  const params = new URLSearchParams(hash.slice(qIdx + 1));
  return {
    topic: params.get("topic") || "",
    q: params.get("q") || "",
    sort: params.get("sort") || "newest",
  };
}
function writeFilterParams({ topic, q, sort }) {
  const params = new URLSearchParams();
  if (topic) params.set("topic", topic);
  if (q) params.set("q", q);
  if (sort && sort !== "newest") params.set("sort", sort);
  const qs = params.toString();
  const base = "#/";
  const newHash = qs ? `${base}?${qs}` : base;
  if (window.location.hash !== newHash) {
    history.replaceState(null, "", newHash);
  }
}

function Home({ onOpenComic, emptyState }) {
  const baseList = emptyState ? COMICS.slice(0, 1) : COMICS;
  const totalPanels = baseList.reduce((a, c) => a + c.panel_count, 0);
  const latest = baseList[0];
  const topicsCount = new Set(baseList.map((c) => c.topic)).size;
  const onlyOne = baseList.length === 1;

  const initial = readFilterParams();
  const [topic, setTopic] = useState(initial.topic);
  const [q, setQ] = useState(initial.q);
  const [sort, setSort] = useState(initial.sort);

  // Persist filter state to URL whenever it changes
  useEffect(() => { writeFilterParams({ topic, q, sort }); }, [topic, q, sort]);

  // Re-sync from URL on hashchange (e.g. user navigates to a filter deeplink
  // while Home is already mounted). Only takes effect when the URL actually
  // disagrees with current state, so it doesn't fight the persistence effect.
  useEffect(() => {
    const onHash = () => {
      const next = readFilterParams();
      if (next.topic !== topic) setTopic(next.topic);
      if (next.q !== q) setQ(next.q);
      if (next.sort !== sort) setSort(next.sort);
    };
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, [topic, q, sort]);

  // Unique topic list for the chip row, sorted by frequency desc then alpha
  const allTopics = useMemo(() => {
    const counts = {};
    baseList.forEach((c) => { counts[c.topic] = (counts[c.topic] || 0) + 1; });
    return Object.entries(counts)
      .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
      .map(([t]) => t);
  }, [baseList]);

  const visible = useMemo(() => {
    let list = baseList;
    if (topic) list = list.filter((c) => c.topic === topic);
    if (q) {
      const needle = q.toLowerCase();
      list = list.filter((c) =>
        c.title.toLowerCase().includes(needle) ||
        c.topic.toLowerCase().includes(needle) ||
        (c.hook || "").toLowerCase().includes(needle) ||
        (c.description || "").toLowerCase().includes(needle)
      );
    }
    const sorted = [...list];
    if (sort === "oldest") sorted.sort((a, b) => a.date.localeCompare(b.date));
    else if (sort === "title") sorted.sort((a, b) => a.title.localeCompare(b.title));
    else if (sort === "topic") sorted.sort((a, b) => a.topic.localeCompare(b.topic) || b.date.localeCompare(a.date));
    else sorted.sort((a, b) => b.date.localeCompare(a.date)); // newest default
    return sorted;
  }, [baseList, topic, q, sort]);

  const filtered = topic || q;
  const reset = () => { setTopic(""); setQ(""); setSort("newest"); };

  return (
    <main>
      <section className="wrap hero">
        <div className="eyebrow">
          <span className="dot" />
          <span>A personal gallery · {COMICS.length} comics · updated {fmtDate(latest.date)}</span>
        </div>
        <div className="hero-row">
          <h1>
            Hoopy<br />
            Explains<span className="ellip">…</span>
            <span className="hand-tag">that's the whole pitch</span>
          </h1>
          <div className="sprite-corner">
            <span className="arrow-anno">
              that's Hoopy
              <ScribbleArrow />
            </span>
            <RotatingHoopyPose />
          </div>
        </div>
      </section>

      <section className="wrap">
        <div className="strip">
          <div className="cell">
            <span className="k">Total comics</span>
            <span className="v">{COMICS.length}</span>
          </div>
          <div className="cell">
            <span className="k">Panels drawn</span>
            <span className="v">{totalPanels}</span>
          </div>
          <div className="cell">
            <span className="k">Topics covered</span>
            <span className="v">{topicsCount}</span>
          </div>
          <div className="cell">
            <span className="k">Latest</span>
            <span className="v" style={{ fontSize: 22, lineHeight: 1.15 }}>
              {fmtDate(latest.date)}
            </span>
          </div>
        </div>
      </section>

      <section className="wrap">
        <div className="gallery-head">
          <h2>The gallery<span className="ellip">.</span></h2>
          <span className="meta">
            {visible.length} of {baseList.length} entr{baseList.length === 1 ? "y" : "ies"}
          </span>
        </div>

        {!onlyOne && allTopics.length > 1 && (
          <div className="filters">
            <div className="filter-row chips" role="group" aria-label="Filter by topic">
              <button
                className={`chip ${topic === "" ? "is-active" : ""}`}
                onClick={() => setTopic("")}
              >All <span className="chip-count">{baseList.length}</span></button>
              {allTopics.map((t) => (
                <button
                  key={t}
                  className={`chip ${topic === t ? "is-active" : ""}`}
                  onClick={() => setTopic(topic === t ? "" : t)}
                >{t} <span className="chip-count">{baseList.filter((c) => c.topic === t).length}</span></button>
              ))}
            </div>
            <div className="filter-row controls">
              <input
                type="search"
                className="filter-search"
                placeholder="Search title, topic, hook…"
                value={q}
                onChange={(e) => setQ(e.target.value)}
                aria-label="Search comics"
              />
              <select
                className="filter-sort"
                value={sort}
                onChange={(e) => setSort(e.target.value)}
                aria-label="Sort"
              >
                <option value="newest">Newest first</option>
                <option value="oldest">Oldest first</option>
                <option value="title">A–Z by title</option>
                <option value="topic">Group by topic</option>
              </select>
              {filtered && (
                <button className="filter-reset" onClick={reset}>
                  Reset
                </button>
              )}
            </div>
          </div>
        )}

        {onlyOne && (
          <div className="empty-card">
            <HoopyNod size="md" />
            <div>
              <div style={{
                fontFamily: '"Source Serif 4", Georgia, serif',
                fontSize: 22,
                fontWeight: 500,
                lineHeight: 1.2,
                color: "var(--fg)",
                letterSpacing: "-0.01em",
              }}>Hoopy's just getting started.</div>
              <div style={{ fontSize: 14, color: "var(--fg-soft)", marginTop: 4 }}>
                One comic so far — more on the way.
              </div>
            </div>
          </div>
        )}

        {!onlyOne && visible.length === 0 && (
          <div className="empty-card">
            <HoopyPose name="shrug" size="md" alt="Hoopy shrugging" />
            <div>
              <div style={{
                fontFamily: '"Source Serif 4", Georgia, serif',
                fontSize: 22,
                fontWeight: 500,
                lineHeight: 1.2,
                color: "var(--fg)",
              }}>No comics match your filters.</div>
              <div style={{ fontSize: 14, color: "var(--fg-soft)", marginTop: 4 }}>
                <button className="filter-reset inline" onClick={reset}>Reset filters</button> and try again.
              </div>
            </div>
          </div>
        )}

        <div className="gallery">
          {visible.map((c) => (
            <article
              key={c.slug}
              className="card"
              onClick={() => onOpenComic(c.slug)}
              onKeyDown={(e) => { if (e.key === "Enter") onOpenComic(c.slug); }}
              tabIndex={0}
              role="link"
              aria-label={`Open ${c.title}`}
            >
              <div className="thumb">
                <ComicPoster comic={c} dense={true} />
              </div>
              <div className="body">
                <div className="topic">
                  <span>{c.topic}</span>
                  <span className="pcount">{c.panel_count} panels</span>
                </div>
                <h3>{c.title}</h3>
                <p className="hook">{c.hook}</p>
                <span className="view">View <span className="arr">→</span></span>
              </div>
            </article>
          ))}
        </div>
      </section>
    </main>
  );
}

function ScribbleArrow() {
  return (
    <svg className="scribble" viewBox="0 0 100 40" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M2 20 Q 25 5, 50 22 T 88 24" />
      <path d="M82 18 L 90 25 L 80 30" />
    </svg>
  );
}

// ---------- 404 / unknown route -----------------------------------------------
function NotFound() {
  return (
    <main className="wrap" style={{ paddingTop: 64, paddingBottom: 96, textAlign: "center" }}>
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 16 }}>
        <HoopyPose name="stumped" size="lg" alt="Hoopy looking confused" />
        <h1 style={{
          fontFamily: '"Caveat", cursive',
          fontSize: 56,
          margin: 0,
          color: "var(--fg)",
          lineHeight: 1.05,
        }}>
          Hmm<span style={{ color: "var(--terracotta)" }}>…</span>
        </h1>
        <div style={{ fontSize: 18, color: "var(--fg-soft)", maxWidth: 480 }}>
          Hoopy can't find that page. It might have been a typo, or maybe it never existed.
        </div>
        <a
          href="#/"
          onClick={(e) => { e.preventDefault(); go("#/"); }}
          style={{
            marginTop: 12,
            padding: "10px 20px",
            background: "var(--card-bg)",
            color: "var(--fg)",
            border: "1px solid var(--rule-color)",
            borderRadius: 999,
            fontSize: 14,
            textDecoration: "none",
          }}
        >
          ← back to gallery
        </a>
      </div>
    </main>
  );
}

// ---------- ABOUT --------------------------------------------------------------
function About() {
  return (
    <main className="wrap comic-page">
      <div className="crumbs">
        <a href="#/" onClick={(e) => { e.preventDefault(); go("#/"); }}>← gallery</a>
        <span className="sep">/</span>
        <span style={{ color: "var(--fg)" }}>about</span>
      </div>

      <header className="comic-meta" style={{ gridTemplateColumns: "auto 1fr", alignItems: "center", gap: 24 }}>
        <HoopyPose name="presenting-3q" size="md" alt="Hoopy presenting" />
        <div>
          <h1>About this thing.</h1>
          <div className="topicline">
            <span>Design Doc</span>
            <span>Read time ~2 min</span>
          </div>
        </div>
      </header>

      <article style={{ maxWidth: 720, fontSize: 16, lineHeight: 1.7, color: "var(--fg)" }}>
        <p style={{ marginTop: 0 }}>
          <span style={{
            fontFamily: "Caveat, cursive",
            fontSize: 28,
            float: "left",
            lineHeight: 0.9,
            marginRight: 8,
            marginTop: 4,
            color: "var(--hoopy)",
          }}>H</span>
          oopy's Comic Explainers is a personal gallery of <span className="marker-u">single-page educational comic posters</span>. Each entry is one composite image — a printed cheat-sheet, not a scrolling thread. The goal is to make a tricky idea legible at a glance.
        </p>
      </article>
    </main>
  );
}

// ---------- COMIC VIEW ---------------------------------------------------------
function ComicView({ slug, onBack, onOpenComic, onLightbox }) {
  const idx = COMICS.findIndex((c) => c.slug === slug);
  const comic = COMICS[idx];
  const prev = COMICS[(idx - 1 + COMICS.length) % COMICS.length];
  const next = COMICS[(idx + 1) % COMICS.length];

  // Keyboard nav
  useEffect(() => {
    const onKey = (e) => {
      if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return;
      if (e.key === "ArrowLeft") onOpenComic(prev.slug);
      if (e.key === "ArrowRight") onOpenComic(next.slug);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [prev.slug, next.slug, onOpenComic]);

  if (!comic) {
    return (
      <main className="wrap comic-page">
        <p>Not found. <a href="#/">← Back to gallery</a></p>
      </main>
    );
  }

  return (
    <main className="wrap comic-page">
      <div className="crumbs">
        <a href="#/" onClick={(e) => { e.preventDefault(); onBack(); }}>
          ← gallery
        </a>
        <span className="sep">/</span>
        <span>c</span>
        <span className="sep">/</span>
        <span style={{ color: "var(--fg)" }}>{comic.slug}</span>
      </div>

      <header className="comic-meta">
        <div>
          <h1>{comic.title}<span className="ellip">…</span></h1>
          <div className="topicline">
            <span>{comic.topic}</span>
            <span>{comic.panel_count} panels</span>
            <span>{fmtDate(comic.date)}</span>
          </div>
        </div>
        <div className="comic-mascot">
          <span className="comic-mascot-anno">
            <ScribbleArrow />
            Hoopy approves
          </span>
          <HoopyNod size="lg" />
        </div>
        <div className="actions">
          <button className="btn primary" onClick={() => downloadComic(comic)}>
            <DownloadIcon /> Download PNG
          </button>
        </div>
      </header>

      <figure
        className="poster-frame"
        onClick={() => onLightbox(comic)}
        role="button"
        aria-label={`Open ${comic.title} in lightbox`}
        tabIndex={0}
        onKeyDown={(e) => { if (e.key === "Enter") onLightbox(comic); }}
      >
        <div className="poster">
          <ComicPoster comic={comic} />
        </div>
        <figcaption style={{
          fontFamily: "IBM Plex Mono, monospace",
          fontSize: 10,
          letterSpacing: "0.14em",
          textTransform: "uppercase",
          color: "var(--fg-soft)",
          marginTop: 14,
          display: "flex",
          justifyContent: "space-between",
        }}>
          <span>fig. {String(idx + 1).padStart(2, "0")} — click to enlarge</span>
          <span>/c/{comic.slug}/comic.png</span>
        </figcaption>
      </figure>

      <div className="comic-controls">
        <a className="btn ghost" href="#/" onClick={(e) => { e.preventDefault(); onBack(); }}>
          ← Back to gallery
        </a>
        <span style={{
          fontFamily: "IBM Plex Mono, monospace",
          fontSize: 10,
          letterSpacing: "0.12em",
          textTransform: "uppercase",
          color: "var(--fg-soft)",
        }}>
          <span className="kbd">←</span> prev <span className="kbd">→</span> next <span className="kbd">esc</span> close
        </span>
        <div className="pn">
          <button className="btn" onClick={() => onOpenComic(prev.slug)} title={prev.title}>
            ← {trim(prev.title, 22)}
          </button>
          <button className="btn" onClick={() => onOpenComic(next.slug)} title={next.title}>
            {trim(next.title, 22)} →
          </button>
        </div>
      </div>

      <p style={{
        marginTop: 36,
        maxWidth: 720,
        color: "var(--fg-soft)",
        fontSize: 14,
        lineHeight: 1.6,
        borderLeft: "2px solid var(--hoopy)",
        paddingLeft: 14,
      }}>
        <span style={{
          fontFamily: "IBM Plex Mono, monospace",
          fontSize: 10,
          letterSpacing: "0.16em",
          textTransform: "uppercase",
          display: "block",
          marginBottom: 6,
          color: "var(--fg)",
        }}>Alt text / description</span>
        {comic.description}
      </p>
    </main>
  );
}

function DownloadIcon() {
  return (
    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
      <polyline points="7 10 12 15 17 10" />
      <line x1="12" y1="15" x2="12" y2="3" />
    </svg>
  );
}

// ---------- LIGHTBOX -----------------------------------------------------------
function Lightbox({ comic, onClose, onPrev, onNext }) {
  const navBtn = {
    background: "rgba(0,0,0,0.55)",
    color: "#FFF8E0",
    border: "1px solid rgba(255,255,255,0.2)",
    padding: "10px 16px",
    borderRadius: 8,
    fontSize: 14,
    cursor: "pointer",
    fontFamily: "inherit",
    letterSpacing: "0.04em",
  };
  return (
    <div className="lightbox" onClick={onClose}>
      <button className="close" onClick={onClose}>esc · close</button>
      <button
        onClick={(e) => { e.stopPropagation(); onPrev(); }}
        style={{ ...navBtn, position: "absolute", left: 24, top: "50%", transform: "translateY(-50%)" }}
        aria-label="Previous comic"
      >← prev</button>
      <button
        onClick={(e) => { e.stopPropagation(); onNext(); }}
        style={{ ...navBtn, position: "absolute", right: 24, top: "50%", transform: "translateY(-50%)" }}
        aria-label="Next comic"
      >next →</button>
      <div className="lb-inner" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 1280 }}>
        <div style={{
          background: "#FFFDF6",
          padding: 24,
          borderRadius: 6,
          border: "1.5px solid #2A2A2A",
        }}>
          <ComicPoster comic={comic} />
        </div>
      </div>
    </div>
  );
}

// ---------- FOOTER -------------------------------------------------------------
function Footer({ headerStyle }) {
  return (
    <footer className="wrap foot">
      <div className="made">
        <HoopyWave size="sm" />
        <span style={{ letterSpacing: "0.01em", fontSize: 13, maxWidth: 560, lineHeight: 1.55 }}>
          Hoopy explains tricky concepts in single-page comics. Built with Claude Code and Gemini Flash.
        </span>
      </div>
      <div className="credits">
        <span>© 2026 · </span>
        <a href="#/" onClick={(e) => { e.preventDefault(); window.location.hash = "#/"; }}>back to top</a>
      </div>
    </footer>
  );
}

// ---------- helpers ------------------------------------------------------------
function fmtDate(iso) {
  try {
    const d = new Date(iso);
    return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
  } catch { return iso; }
}
function trim(s, n) { return s.length > n ? s.slice(0, n - 1) + "…" : s; }
function downloadComic(comic) {
  if (comic.image) {
    const a = document.createElement("a");
    a.href = comic.image;
    a.download = `${comic.slug}.png`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    return;
  }
  alert(`In the real site this downloads /comics/${comic.slug}/comic.png`);
}

// ---------- mount --------------------------------------------------------------
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
