/* =============================================================================
   Inkwell · App  (React via CDN + Babel, no build step)
   ============================================================================= */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

const CATALOG = window.INKWELL_CATALOG;
const SHELVES = window.INKWELL_SHELVES;
const STORE_KEY = "inkwell_v1";
function loadStore() { try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch { return {}; } }
const saved = loadStore();
// re-add books the user wrote in a previous session so they appear and open
(saved.userBooks || []).forEach((b) => { if (b && b.id && !window.INKWELL_CATALOG.some((x) => x.id === b.id)) window.INKWELL_CATALOG.push(b); });
const BY_ID = Object.fromEntries(CATALOG.map((b) => [b.id, b]));
const { coverSVG, comicPage, svgToDataUri } = window.Inkwell;

const TRIAL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
const SAMPLE_PAGES = 3; // free comic pages
const isVisual = (b) => b.format === "comic" || b.format === "manga" || b.format === "anime";

// honest length estimate: the reader shows ~150 words per screen at the default text size
const WORDS_PER_PAGE = 150;
function bookPages(b) {
  try {
    const w = (b.chapters || []).reduce((a, c) => a + (c.paragraphs || []).reduce((s, p) => s + (p.trim() ? p.trim().split(/\s+/).length : 0), 0), 0);
    return Math.max(1, Math.round(w / WORDS_PER_PAGE));
  } catch (e) { return 0; }
}

/* ---- content rules for user-created books ---- */
const BANNED = ["fuck", "shit", "bitch", "bastard", "asshole", "dick", "pussy", "cunt", "slut", "whore", "porn", "nude", "naked", "rape", "nigger", "faggot", "sex"];
const VIOLENT = ["kill", "killed", "killing", "blood", "bloody", "gore", "murder", "stab", "stabbed", "gun", "shoot", "shot", "knife", "behead", "decapitate", "torture", "slaughter", "massacre", "corpse", "brutal"];
function moderate(title, text) {
  const t = (title + "\n" + text).toLowerCase();
  for (const w of BANNED) if (new RegExp("\\b" + w).test(t)) return { ok: false, reason: `Please keep it appropriate for all ages — the word “${w}” isn't allowed.` };
  let vc = 0; VIOLENT.forEach((w) => { const m = t.match(new RegExp("\\b" + w, "g")); if (m) vc += m.length; });
  if (vc >= 5) return { ok: false, reason: "This reads as too violent for Nova Books. Please tone the violence down and try again." };
  if (text.trim().length < 60) return { ok: false, reason: "Add a little more to your story first — at least a few sentences." };
  return { ok: true };
}

const AGES = [
  { id: "kids", label: "Kids" },
  { id: "middle", label: "Middle Grade" },
  { id: "teen", label: "Teen" },
  { id: "adult", label: "Adult" },
];
const FORMATS = [
  { id: "book", label: "📖 Books" },
  { id: "comic", label: "💥 Comics" },
  { id: "manga", label: "🌸 Manga" },
  { id: "anime", label: "✨ Anime" },
];

/* ----------------------------- tiny icons --------------------------------- */
const I = {
  search: <path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 4.99L20.49 19zm-6 0A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14"/>,
  moon: <path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.39 5.39 0 0 1-4.4 2.26 5.4 5.4 0 0 1-5.4-5.4c0-1.81.9-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1"/>,
  sun: <path d="M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10m0-5v3m0 14v3M2 12h3m14 0h3M4.2 4.2l2.1 2.1m11.4 11.4 2.1 2.1M19.8 4.2l-2.1 2.1M6.3 17.7l-2.1 2.1" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round"/>,
  lib: <path d="M4 6h4v14H4zm6 0h4v14h-4zm7.5.2 3.8 13.5-3.8 1-3.8-13.5z"/>,
  lock: <path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5m3 8H9V6a3 3 0 0 1 6 0z"/>,
  heart: <path d="M12 21s-7-4.35-9.5-8.5C.5 9 2 5.5 5.5 5.5c2 0 3.5 1.5 6.5 4 3-2.5 4.5-4 6.5-4C22 5.5 23.5 9 21.5 12.5 19 16.65 12 21 12 21"/>,
  heartO: <path d="M12 21s-7-4.35-9.5-8.5C.5 9 2 5.5 5.5 5.5c2 0 3.5 1.5 6.5 4 3-2.5 4.5-4 6.5-4C22 5.5 23.5 9 21.5 12.5 19 16.65 12 21 12 21" fill="none" stroke="currentColor" strokeWidth="1.8"/>,
  chevR: <path d="m8.59 16.59 1.41 1.41 6-6-6-6-1.41 1.41L13.17 12z"/>,
  chevL: <path d="M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>,
  back: <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"/>,
  list: <path d="M3 9h14V7H3zm0 4h14v-2H3zm0 4h14v-2H3zm16 0h2v-2h-2zm0-10v2h2V7zm0 6h2v-2h-2z"/>,
  cog: <path d="M19.4 13c.04-.33.06-.66.06-1s-.02-.67-.07-1l2.1-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.6-.22l-2.5 1a7.3 7.3 0 0 0-1.7-1l-.38-2.65A.5.5 0 0 0 14 1h-4a.5.5 0 0 0-.5.42L9.1 4.07a7.3 7.3 0 0 0-1.7 1l-2.5-1a.5.5 0 0 0-.6.22l-2 3.46a.5.5 0 0 0 .12.64L4.6 11c-.05.33-.07.66-.07 1s.02.67.07 1l-2.1 1.65a.5.5 0 0 0-.12.64l2 3.46c.14.24.42.32.6.22l2.5-1c.52.4 1.08.74 1.7 1l.38 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.38-2.65c.62-.26 1.18-.6 1.7-1l2.5 1c.18.1.46.02.6-.22l2-3.46a.5.5 0 0 0-.12-.64zM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7"/>,
  close: <path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>,
  spark: <path d="M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z"/>,
  listen: <path d="M12 3a8 8 0 0 0-8 8v6a2 2 0 0 0 2 2h2v-7H6v-1a6 6 0 0 1 12 0v1h-2v7h2a2 2 0 0 0 2-2v-6a8 8 0 0 0-8-8"/>,
  mark: <path d="M17 3H7a2 2 0 0 0-2 2v16l7-3 7 3V5a2 2 0 0 0-2-2"/>,
  markO: <path d="M17 3H7a2 2 0 0 0-2 2v16l7-3 7 3V5a2 2 0 0 0-2-2" fill="none" stroke="currentColor" strokeWidth="1.8"/>,
  trophy: <path d="M18 2H6v2H2v3a4 4 0 0 0 4 4 6 6 0 0 0 4 3.87V18H7v2h10v-2h-3v-3.13A6 6 0 0 0 18 11a4 4 0 0 0 4-4V4h-4zM4 7V6h2v3a2 2 0 0 1-2-2m16 0a2 2 0 0 1-2 2V6h2z"/>,
  play: <path d="M8 5v14l11-7z"/>,
  pause: <path d="M6 5h4v14H6zm8 0h4v14h-4z"/>,
  kids: <path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M8.5 9a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3m7 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3M8 14h8a4 4 0 0 1-8 0"/>,
};
const Svg = ({ d, size = 20, fill = "currentColor", vb = "0 0 24 24" }) =>
  <svg viewBox={vb} width={size} height={size} fill={fill}>{d}</svg>;

/* ----------------------------- helpers ------------------------------------ */
function fmtLeft(ms) {
  if (ms <= 0) return "expired";
  const d = Math.floor(ms / 86400000);
  const h = Math.floor((ms % 86400000) / 3600000);
  const m = Math.floor((ms % 3600000) / 60000);
  if (d > 0) return `${d}d ${h}h`;
  if (h > 0) return `${h}h ${m}m`;
  return `${m}m`;
}
const Stars = ({ r }) => {
  const full = Math.round(r);
  return <div className="stars">{"★★★★★".slice(0, full)}<span style={{ color: "#d7dbe6" }}>{"★★★★★".slice(full)}</span> <span style={{ color: "var(--muted)" }}>{r.toFixed(1)}</span></div>;
};
const Cover = ({ book }) => <img alt={book.title} src={svgToDataUri(coverSVG(book))} loading="lazy" />;

/* ----------------------------- rewards & recs ----------------------------- */
const BADGES = [
  { id: "first", icon: "📖", name: "First Page", desc: "Open your very first book", test: (s) => s.books.length >= 1 },
  { id: "worm", icon: "🐛", name: "Bookworm", desc: "Start 5 different titles", test: (s) => s.books.length >= 5 },
  { id: "shelf", icon: "🧺", name: "Collector", desc: "Save 5 books to your library", test: (s, lib) => lib.length >= 5 },
  { id: "formats", icon: "🎭", name: "Omnivore", desc: "Read 3 different formats", test: (s) => s.formats.length >= 3 },
  { id: "time30", icon: "⏱️", name: "Page-Turner", desc: "Read for 30 minutes", test: (s) => s.minutes >= 30 },
  { id: "streak3", icon: "🔥", name: "On a Roll", desc: "Reach a 3-day streak", test: (s) => s.streak >= 3 },
  { id: "night", icon: "🌙", name: "Night Owl", desc: "Read in dark mode", test: (s) => s.night },
];
const computeBadges = (stats, library) => BADGES.map((b) => ({ ...b, earned: !!b.test(stats, library || []) }));
const dayKey = (ms) => new Date(ms).toDateString();

function recommend(progress, library) {
  const liked = {};
  [...Object.keys(progress || {}), ...(library || [])].forEach((id) => {
    const b = BY_ID[id]; if (b) b.genres.forEach((g) => (liked[g] = (liked[g] || 0) + 1));
  });
  const noSignal = Object.keys(liked).length === 0;
  const scored = CATALOG.filter((b) => !(progress && progress[b.id]))
    .map((b) => ({ b, s: noSignal ? b.rating : b.genres.reduce((a, g) => a + (liked[g] || 0), 0) + b.rating / 10 }));
  scored.sort((a, b) => b.s - a.s);
  return scored.slice(0, 14).map((x) => x.b);
}

/* ----------------------- sharing, dates & print --------------------------- */
const encodeShare = (o) => btoa(unescape(encodeURIComponent(JSON.stringify(o)))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const decodeShare = (c) => { try { let b = String(c).replace(/-/g, "+").replace(/_/g, "/"); while (b.length % 4) b += "="; return JSON.parse(decodeURIComponent(escape(atob(b)))); } catch { return null; } };
const isoDay = (d = new Date()) => d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");

// lay a WRITE-tab book out as a real picture book (print → save as PDF)
function printBook(book) {
  const esc = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
  const chapters = (book.chapters || []).map((ch) =>
    `<section class="chapter"><h2>${esc(ch.title)}</h2>${ch.paragraphs.map((p) => `<p>${esc(p)}</p>`).join("")}</section>`).join("");
  const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(book.title)}</title>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600&family=Literata:opsz,wght@7..72,400;7..72,500&display=swap" rel="stylesheet">
<style>
  @page { size: A5; margin: 16mm 14mm; }
  body { font-family: 'Literata', Georgia, serif; color: #20243a; margin: 0; }
  .cover { text-align: center; page-break-after: always; padding-top: 6mm; }
  .cover .art { width: 72%; max-width: 340px; border-radius: 14px; overflow: hidden; box-shadow: 0 6px 24px rgba(0,0,0,.18); margin: 0 auto; }
  .cover .art svg { width: 100%; height: auto; display: block; }
  .cover h1 { font-family: 'Fraunces', Georgia, serif; font-size: 26pt; margin: 12mm 0 3mm; }
  .cover .by { font-size: 13pt; color: #5a6072; }
  .chapter { page-break-before: always; }
  .chapter:first-of-type { page-break-before: auto; }
  h2 { font-family: 'Fraunces', Georgia, serif; font-size: 17pt; margin: 0 0 7mm; }
  p { font-size: 12.5pt; line-height: 1.85; margin: 0 0 1em; text-indent: 1.4em; }
  p:first-of-type { text-indent: 0; }
  .colophon { page-break-before: always; text-align: center; color: #8a90a4; font-size: 10pt; padding-top: 60mm; line-height: 1.8; }
</style></head><body>
  <div class="cover"><div class="art">${coverSVG(book)}</div><h1>${esc(book.title)}</h1><div class="by">written by ${esc(book.author)}</div></div>
  ${chapters}
  <div class="colophon">✦<br>“${esc(book.title)}”<br>written with Nova Books — read · write · print<br>by Logan Lee Sanjaya</div>
<script>window.onload = () => setTimeout(() => window.print(), 500);<\/script>
</body></html>`;
  const w = window.open("", "_blank");
  if (!w) return false;
  w.document.write(html);
  w.document.close();
  return true;
}

/* ============================== BOOK CARD ================================= */
function BookCard({ book, progress, locked, onOpen }) {
  return (
    <div className="book-card" onClick={() => onOpen(book)}>
      <div className="cover-wrap">
        <Cover book={book} />
        {isVisual(book) && <span className={"cover-badge " + book.format}>{book.format}</span>}
        {book.free && <span className="cover-badge free">FREE</span>}
        {locked && <span className="lock-chip"><Svg d={I.lock} size={14} /></span>}
        {progress > 0 && <div className="progress-bar"><i style={{ width: Math.min(100, progress) + "%" }} /></div>}
      </div>
      <div className="book-meta">
        <b>{book.title}</b>
        <span>{book.author}</span>
        <Stars r={book.rating} />
      </div>
    </div>
  );
}

function SeriesCard({ book, onOpen }) {
  return (
    <div className="series-card" onClick={() => onOpen(book)}>
      <img className="bgsvg" alt="" src={svgToDataUri(coverSVG(book))} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
      <span className="tag">{book.format === "book" ? book.genres[0] : book.format}</span>
      <div className="lbl"><span>{book.author}</span><b>{book.title}</b></div>
    </div>
  );
}

/* ============================== SHELF ===================================== */
function Shelf({ title, books, render, app }) {
  const ref = useRef(null);
  const scroll = (dir) => ref.current && ref.current.scrollBy({ left: dir * ref.current.clientWidth * 0.8, behavior: "smooth" });
  if (!books.length) return null;
  return (
    <section className="shelf">
      <div className="shelf-head"><h2>{title}</h2><span className="see" onClick={() => app.goExplore()}>See all</span></div>
      <button className="shelf-arrow left" onClick={() => scroll(-1)}><Svg d={I.chevL} size={24} /></button>
      <div className="shelf-scroll" ref={ref}>{books.map((b) => render(b))}</div>
      <button className="shelf-arrow" onClick={() => scroll(1)}><Svg d={I.chevR} size={24} /></button>
    </section>
  );
}

/* ============================== HOME ====================================== */
function Home({ app }) {
  const recs = app.recommend.filter(app.ageAllowed).slice(0, 14);
  const card = (b) => <BookCard key={b.id} book={b} progress={app.progressPct(b)} locked={app.isLocked(b)} onOpen={app.openDetail} />;
  return (
    <div className="page app-scroll">
      <div className={"hero" + (app.kidsMode ? " hero-kids" : "")}>
        {app.kidsMode && <div className="hero-kid-tag">👶 Kids Mode</div>}
        <h1>{app.kidsMode ? "Stories made just for you!" : "Read everything. Every age. Every story."}</h1>
        <p>{app.kidsMode ? "Thousands of stories, all picked for younger readers." : "Thousands of novels and stories — for little kids, big kids, and grown-ups."} {app.plan === "premium" ? "You're all set with Premium." : app.trialStart ? "Your free trial is running." : "Start your free 3-day trial — no charge today."}</p>
        {app.plan !== "premium" && (
          app.trialStart
            ? <button className="cta" onClick={app.openPaywall}>{app.trialActive ? "Go Premium" : "Subscribe to keep reading"}</button>
            : <button className="cta" onClick={app.startTrial}>Start 3-day free trial</button>
        )}
        {app.plan !== "premium" && app.trialStart && <small>{app.trialActive ? `${fmtLeft(app.msLeft)} left in your free trial` : "Your free trial has ended"}</small>}
      </div>

      {recs.length > 0 && <Shelf app={app} title={app.kidsMode ? "Picked for You" : "Recommended for You"} books={recs} render={card} />}

      {SHELVES.map((s) => (
        <Shelf key={s.id} app={app} title={s.title} books={s.ids.map((id) => BY_ID[id]).filter(Boolean).filter(app.ageAllowed)} render={card} />
      ))}
    </div>
  );
}

/* ============================== EXPLORE =================================== */
const PAGE_SIZE = 60;
function Explore({ app, query }) {
  const [age, setAge] = useState(null);
  const [fmt, setFmt] = useState(null);
  const [genre, setGenre] = useState(null);
  const [visible, setVisible] = useState(PAGE_SIZE);
  const list = useMemo(() => CATALOG.filter((b) => {
    if (!app.ageAllowed(b)) return false;
    if (age && b.ageGroup !== age) return false;
    if (fmt && b.format !== fmt) return false;
    if (genre && !b.genres.includes(genre)) return false;
    if (query) {
      const q = query.toLowerCase();
      if (!(b.title + " " + b.author + " " + b.genres.join(" ")).toLowerCase().includes(q)) return false;
    }
    return true;
  }), [age, fmt, genre, query, app.kidsMode]);
  useEffect(() => { setVisible(PAGE_SIZE); }, [age, fmt, genre, query, app.kidsMode]);

  const genres = useMemo(() => { const s = {}; CATALOG.forEach((b) => b.genres.forEach((g) => (s[g] = (s[g] || 0) + 1))); let list = Object.entries(s).sort((a, b) => b[1] - a[1]).map((x) => x[0]); const pin = ["Free Books"].filter((g) => s[g]); list = [...pin, ...list.filter((g) => pin.indexOf(g) < 0)]; return list.slice(0, 16); }, []);
  const shown = list.slice(0, visible);

  return (
    <div className="page app-scroll">
      <div className="shelf-head" style={{ marginBottom: 16 }}><h2>{query ? `Results for “${query}”` : "Explore the library"}</h2><span className="see" style={{ cursor: "default", color: "var(--muted)" }}>{list.length.toLocaleString()} titles</span></div>
      <div className="filters">
        <button className={"chip" + (!age ? " on" : "")} onClick={() => setAge(null)}>All ages</button>
        {AGES.map((a) => <button key={a.id} className={"chip" + (age === a.id ? " on" : "")} onClick={() => setAge(age === a.id ? null : a.id)}>{a.label}</button>)}
      </div>
      <div className="filters" style={{ marginTop: -14 }}>
        <button className={"chip" + (!genre ? " on" : "")} onClick={() => setGenre(null)}>All genres</button>
        {genres.map((g) => <button key={g} className={"chip" + (genre === g ? " on" : "")} onClick={() => setGenre(genre === g ? null : g)}>{g}</button>)}
      </div>
      {list.length ? (<>
        <div className="grid">{shown.map((b) => <BookCard key={b.id} book={b} progress={app.progressPct(b)} locked={app.isLocked(b)} onOpen={app.openDetail} />)}</div>
        {visible < list.length && <div style={{ textAlign: "center", marginTop: 30 }}><button className="btn btn-ghost" onClick={() => setVisible((v) => v + PAGE_SIZE * 2)}>Show more ({(list.length - visible).toLocaleString()} more)</button></div>}
      </>) : <div className="empty"><div className="big">🔍</div><p>No titles match that. Try another filter.</p></div>}
    </div>
  );
}

/* ============================== SETTINGS ================================= */
const Toggle = ({ on, onChange }) => (
  <button className={"switch" + (on ? " on" : "")} role="switch" aria-checked={on} onClick={onChange}><span className="knob" /></button>
);

/* one account across Nova Books + Studlark (Firebase auth via logan-id.js) */
function LoganCard({ app }) {
  const [mode, setMode] = useState("in");
  const [email, setEmail] = useState("");
  const [pass, setPass] = useState("");
  const [name, setName] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  if (!window.LoganID) return null;
  const acct = window.LoganID.account();
  const run = (p) => {
    setBusy(true); setErr("");
    p.then(() => app.flash("🪪 Signed in with your logan ID!"))
      .catch((e) => setErr(window.LoganID.friendlyError(e)))
      .finally(() => setBusy(false));
  };
  const streak = window.LoganID.sharedStreak();
  return (
    <section className="set-card">
      <h3>logan ID</h3>
      {acct ? (
        <>
          <div className="set-row">
            <div><b>{acct.name}</b><span>{acct.email || acct.provider} — one account for Nova Books <b>and</b> Studlark.</span></div>
            <button className="btn btn-ghost" onClick={() => window.LoganID.signOut().then(() => app.flash("Signed out"))}>Sign out</button>
          </div>
          <div className="set-row">
            <div><b>🔥 {Math.max(streak.count, app.stats.streak)}-day shared streak</b>
              <span>Reading here or studying in Studlark both keep it alive. Finish a chapter and any “read chapter N” homework in Studlark ticks itself off.</span></div>
          </div>
        </>
      ) : (
        <>
          <div className="set-row">
            <div><b>One account, both apps</b><span>Sign in once and your streak, reading progress and homework connect with Studlark — read a chapter here, watch it check off there.</span></div>
          </div>
          <div style={{ display: "grid", gap: 8, maxWidth: 400 }}>
            <button className="btn btn-primary" disabled={busy} onClick={() => run(window.LoganID.signInGoogle())}>Continue with Google</button>
            {mode === "up" && <input placeholder="Your name" value={name} onChange={(e) => setName(e.target.value)} style={{ padding: "10px 12px", borderRadius: 10, border: "1px solid var(--line)", background: "var(--card)", color: "var(--ink)" }} />}
            <input placeholder="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} style={{ padding: "10px 12px", borderRadius: 10, border: "1px solid var(--line)", background: "var(--card)", color: "var(--ink)" }} />
            <input placeholder="Password (6+ characters)" type="password" value={pass} onChange={(e) => setPass(e.target.value)} style={{ padding: "10px 12px", borderRadius: 10, border: "1px solid var(--line)", background: "var(--card)", color: "var(--ink)" }} />
            {err && <div style={{ color: "#d64545", fontSize: 13 }}>⚠️ {err}</div>}
            <button className="btn btn-ghost" disabled={busy} onClick={() => {
              if (!email || !pass) { setErr("Enter your email and password"); return; }
              run(mode === "in" ? window.LoganID.signIn(email, pass) : window.LoganID.signUp(email, pass, name));
            }}>{mode === "in" ? "Sign in with email" : "Create my logan ID"}</button>
            <a href="#" style={{ fontSize: 13 }} onClick={(e) => { e.preventDefault(); setMode(mode === "in" ? "up" : "in"); setErr(""); }}>
              {mode === "in" ? "New here? Create a logan ID" : "Already have one? Sign in"}</a>
          </div>
        </>
      )}
    </section>
  );
}

function Settings({ app }) {
  const reset = () => { if (window.confirm("Reset Nova Books? This clears your trial, library, bookmarks, streak and settings on this device.")) app.resetAll(); };
  const planLabel = app.plan === "premium" ? (app.subPlan === "Family" ? "Premium Family — up to 6 readers" : "Premium — all titles unlocked")
    : app.trialStart ? (app.trialActive ? `Free trial · ${fmtLeft(app.msLeft)} left` : "Free trial ended")
    : "No plan yet";
  return (
    <div className="page app-scroll settings-page">
      <h2 className="settings-title">Settings</h2>

      <LoganCard app={app} />

      <section className="set-card">
        <h3>Appearance</h3>
        <div className="set-row">
          <div><b>Dark mode</b><span>Switch the whole app between bright and dark.</span></div>
          <Toggle on={app.appTheme === "dark"} onChange={app.toggleTheme} />
        </div>
      </section>

      <section className="set-card">
        <h3>Kids Mode</h3>
        <div className="set-row">
          <div><b>{app.kidsMode ? "Kids Mode is on" : "Kids Mode is off"}</b>
            <span>When on, Nova Books shows only Kids and Middle-Grade titles and a simpler, friendlier look. You can turn it back on or off here any time.</span></div>
          <Toggle on={app.kidsMode} onChange={app.toggleKids} />
        </div>
      </section>

      <section className="set-card">
        <h3>Your plan</h3>
        <div className="set-row">
          <div><b>{planLabel}</b><span>Books, comics, manga and anime for every age.</span></div>
          <div className="set-actions">
            {app.plan === "premium"
              ? <button className="btn btn-ghost" onClick={app.cancelPremium}>Cancel Premium</button>
              : <>
                  {!app.trialStart && <button className="btn btn-primary" onClick={app.startTrial}>Start free trial</button>}
                  <button className="btn btn-primary" onClick={app.openPaywall}>Go Premium</button>
                </>}
          </div>
        </div>
        <div className="set-row">
          <div><b>Rewards & streak</b><span>See your reading streak, minutes read and badges.</span></div>
          <button className="btn btn-ghost" onClick={app.openRewards}><Svg d={I.trophy} size={16} /> Open rewards</button>
        </div>
      </section>

      <section className="set-card">
        <h3>Data</h3>
        <div className="set-row">
          <div><b>Reset everything</b><span>Clears your trial, saved books, collections, bookmarks and streak on this device.</span></div>
          <button className="btn btn-ghost danger" onClick={reset}>Reset</button>
        </div>
      </section>

      <p className="set-foot">Nova Books · by Logan Lee Sanjaya — a demo. Your data stays in this browser; no real payment is processed.</p>
    </div>
  );
}

/* ============================== WRITE A BOOK ============================= */
const COVER_SWATCHES = [
  { from: "#16a6e0", to: "#4ec3f0", accent: "#ffd166" },
  { from: "#ff5c8a", to: "#ffb84d", accent: "#ffffff" },
  { from: "#11998e", to: "#38ef7d", accent: "#ffd166" },
  { from: "#614385", to: "#516395", accent: "#f6c177" },
  { from: "#232526", to: "#414345", accent: "#d4a14e" },
  { from: "#ee9ca7", to: "#ffdde1", accent: "#c44569" },
];
function CreateBook({ app }) {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  const [age, setAge] = useState("kids");
  const [genre, setGenre] = useState("Adventure");
  const [cover, setCover] = useState(0);
  const [story, setStory] = useState("");
  const [err, setErr] = useState("");
  const mine = app.userBooks;

  const publish = () => {
    if (!title.trim()) { setErr("Give your book a title first."); return; }
    const res = moderate(title, story);
    if (!res.ok) { setErr(res.reason); return; }
    const paras = story.split(/\n\s*\n/).map((s) => s.trim().replace(/\s*\n\s*/g, " ")).filter(Boolean);
    const book = app.createBook({ title: title.trim(), author: author.trim() || "You", ageGroup: age, genre, cover: COVER_SWATCHES[cover], blurb: (paras[0] || title).slice(0, 150), chapters: [{ title: "Chapter One", paragraphs: paras }] });
    setErr(""); setTitle(""); setStory(""); setAuthor("");
    app.flash("📖 Published “" + book.title + "” to your library!");
    app.openReader(book);
  };

  const preview = { id: "preview", title: title || "Your Title Here", author: author || "You", format: "book", cover: COVER_SWATCHES[cover], genres: [genre] };

  return (
    <div className="page app-scroll create-page">
      <h2 className="settings-title">Write your own book ✍️</h2>
      <div className="rules-box">
        <b>📜 Nova Books community rules</b>
        <ul>
          <li>Keep it <strong>appropriate for all ages</strong> — no swearing, sexual or hateful content.</li>
          <li><strong>Don't be too violent</strong> — gentle adventure is great; gore is not.</li>
          <li>Be original, and be kind. We run a quick check before publishing.</li>
        </ul>
      </div>

      <div className="create-grid">
        <div className="create-form">
          <div className="field"><label>Book title</label><input value={title} placeholder="The Brave Little Cloud" onChange={(e) => setTitle(e.target.value)} /></div>
          <div className="field"><label>Author name</label><input value={author} placeholder="Your name" onChange={(e) => setAuthor(e.target.value)} /></div>
          <div className="row" style={{ display: "flex", gap: 12 }}>
            <div className="field"><label>Age group</label>
              <select value={age} onChange={(e) => setAge(e.target.value)}>{AGES.map((a) => <option key={a.id} value={a.id}>{a.label}</option>)}</select></div>
            <div className="field"><label>Genre</label>
              <input value={genre} placeholder="Adventure" onChange={(e) => setGenre(e.target.value)} /></div>
          </div>
          <div className="field"><label>Cover colour</label>
            <div className="swatch-row">{COVER_SWATCHES.map((c, i) => <button key={i} className={"cover-swatch" + (cover === i ? " on" : "")} style={{ background: `linear-gradient(135deg, ${c.from}, ${c.to})` }} onClick={() => setCover(i)} />)}</div>
          </div>
          <div className="field"><label>Your story <span style={{ fontWeight: 400, color: "var(--muted)" }}>— leave a blank line between paragraphs</span></label>
            <textarea value={story} rows={10} placeholder={"Once upon a time…\n\nWrite your story here. Press enter twice to start a new paragraph."} onChange={(e) => setStory(e.target.value)} /></div>
        </div>
        <div className="create-preview">
          <div className="cover-wrap" style={{ width: 180 }}><Cover book={preview} /></div>
          <span>Live cover preview</span>
        </div>
      </div>

      {err && <div className="create-err">⚠️ {err}</div>}
      <div style={{ display: "flex", gap: 12, marginTop: 8, alignItems: "center" }}>
        <button className="btn btn-primary" onClick={publish}>Publish to my library</button>
        <span style={{ color: "var(--muted)", fontSize: 13 }}>{story.trim().length} characters</span>
      </div>

      {mine.length > 0 && <div style={{ marginTop: 36 }}>
        <div className="shelf-head" style={{ marginBottom: 16 }}><h2>Your stories ({mine.length})</h2></div>
        <div className="grid">{mine.map((b) => (
          <div key={b.id} className="col-item">
            <BookCard book={b} progress={app.progressPct(b)} locked={false} onOpen={app.openDetail} />
            <button className="col-remove" title="Print as a real book (PDF)" style={{ right: 42 }} onClick={(e) => { e.stopPropagation(); if (!printBook(b)) app.flash("Allow pop-ups to print your book"); }}>🖨</button>
            <button className="col-remove" title="Delete story" onClick={(e) => { e.stopPropagation(); if (window.confirm("Delete “" + b.title + "”?")) app.deleteBook(b.id); }}><Svg d={I.close} size={14} /></button>
          </div>
        ))}</div>
      </div>}
    </div>
  );
}

/* ============================== COLLECTIONS =============================== */
function CollectionPicker({ book, app }) {
  const [name, setName] = useState("");
  const create = () => { if (!name.trim()) return; const id = app.createCollection(name.trim()); app.toggleInCollection(id, book.id); setName(""); };
  return (
    <div className="col-picker" onClick={(e) => e.stopPropagation()}>
      <h4>Add “{book.title}” to…</h4>
      <div className="col-picker-list">
        {app.collections.length === 0 && <p className="muted-note">No collections yet — make your first one below.</p>}
        {app.collections.map((c) => {
          const inIt = c.ids.includes(book.id);
          return <button key={c.id} className={"col-row" + (inIt ? " on" : "")} onClick={() => app.toggleInCollection(c.id, book.id)}>
            <span>{c.name}</span><span className="mark">{inIt ? "✓ Added" : "＋ Add"}</span>
          </button>;
        })}
      </div>
      <div className="col-new">
        <input value={name} placeholder="New collection name…" onChange={(e) => setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && create()} />
        <button className="btn btn-primary" onClick={create}>Create</button>
      </div>
    </div>
  );
}

function CollectionCard({ col, onOpen }) {
  const covers = col.ids.map((id) => BY_ID[id]).filter(Boolean).slice(0, 4);
  return (
    <div className="col-card" onClick={onOpen}>
      <div className="col-covers">
        {covers.length ? covers.map((b) => <img key={b.id} alt="" src={svgToDataUri(coverSVG(b))} />) : <div className="col-empty-thumb">📚</div>}
      </div>
      <div className="col-card-meta"><b>{col.name}</b><span>{col.ids.length} {col.ids.length === 1 ? "book" : "books"}</span></div>
    </div>
  );
}

function CollectionView({ col, app, onBack }) {
  const [name, setName] = useState(col.name);
  const books = col.ids.map((id) => BY_ID[id]).filter(Boolean);
  return (
    <div className="page app-scroll">
      <div className="col-head">
        <button className="btn btn-ghost" onClick={onBack}><Svg d={I.back} size={16} /> Library</button>
        <input className="col-title-input" value={name} onChange={(e) => setName(e.target.value)}
          onBlur={() => app.renameCollection(col.id, name)} onKeyDown={(e) => e.key === "Enter" && e.target.blur()} />
        <button className="btn btn-ghost" title="Copy a link any friend can import" onClick={() => app.shareCollection(col)}>🔗 Share</button>
        <button className="btn btn-ghost" onClick={() => { app.deleteCollection(col.id); onBack(); }}>Delete</button>
      </div>
      {books.length ? (
        <div className="grid">{books.map((b) => (
          <div key={b.id} className="col-item">
            <BookCard book={b} progress={app.progressPct(b)} locked={app.isLocked(b)} onOpen={app.openDetail} />
            <button className="col-remove" title="Remove from collection" onClick={(e) => { e.stopPropagation(); app.toggleInCollection(col.id, b.id); }}><Svg d={I.close} size={14} /></button>
          </div>
        ))}</div>
      ) : <div className="empty"><div className="big">📦</div><p>This collection is empty. Open any book and tap <strong>＋ Collection</strong> to add it here.</p></div>}
    </div>
  );
}

/* ============================== LIBRARY =================================== */
function Library({ app }) {
  const [openId, setOpenId] = useState(null);
  const open = app.collections.find((c) => c.id === openId);
  if (openId && open) return <CollectionView col={open} app={app} onBack={() => setOpenId(null)} />;

  const reading = CATALOG.filter((b) => app.progress[b.id]);
  const saved = app.library.map((id) => BY_ID[id]).filter(Boolean);
  const newCollection = () => { const n = window.prompt("Name your collection:", ""); if (n && n.trim()) app.createCollection(n.trim()); };
  const nothing = !reading.length && !saved.length && !app.collections.length;

  return (
    <div className="page app-scroll">
      <div className="shelf-head" style={{ marginBottom: 16 }}><h2>Your Collections</h2></div>
      <div className="col-grid">
        {app.collections.map((c) => <CollectionCard key={c.id} col={c} onOpen={() => setOpenId(c.id)} />)}
        <button className="col-card col-new-card" onClick={newCollection}><span className="plus">＋</span><span>New collection</span></button>
        <button className="col-card col-new-card" onClick={() => { const s = window.prompt("Paste a Nova Books share link or code:", ""); if (s) app.importShareCode(s); }}><span className="plus">⤓</span><span>Import a share code</span></button>
      </div>

      {reading.length > 0 && <Shelf app={app} title="Continue reading" books={reading}
        render={(b) => <BookCard key={b.id} book={b} progress={app.progressPct(b)} locked={app.isLocked(b)} onOpen={app.openDetail} />} />}

      {saved.length > 0 && <>
        <div className="shelf-head" style={{ marginTop: 10, marginBottom: 16 }}><h2>Saved books</h2></div>
        <div className="grid">{saved.map((b) => <BookCard key={b.id} book={b} progress={app.progressPct(b)} locked={app.isLocked(b)} onOpen={app.openDetail} />)}</div>
      </>}

      {nothing && <div className="empty" style={{ paddingTop: 30 }}><p>Make a collection above, tap the heart on any title to save it, or just start reading.</p>
        <button className="btn btn-primary" style={{ marginTop: 14 }} onClick={app.goHome}>Browse the store</button></div>}
    </div>
  );
}

/* ============================== DETAIL ==================================== */
function Detail({ book, app, onClose }) {
  const locked = app.isLocked(book);
  const inLib = app.library.includes(book.id);
  const prog = app.progress[book.id];
  const [picker, setPicker] = useState(false);
  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}><Svg d={I.close} size={18} /></button>
        <div className="detail">
          <div className="dcover"><Cover book={book} /></div>
          <div>
            <h2>{book.title}</h2>
            <div className="by">by {book.author} · {book.year}</div>
            <Stars r={book.rating} />
            <div className="pills" style={{ marginTop: 12 }}>
              <span className="age">{AGES.find((a) => a.id === book.ageGroup).label}</span>
              {book.genres.map((g) => <span key={g}>{g}</span>)}
              <span>{book.format}</span>
              {!isVisual(book) && book.chapters && <span>{book.chapters.length} chapters · ~{bookPages(book)} pages</span>}
            </div>
            <p className="blurb">{book.blurb}</p>
            <div className="actions">
              {!locked ? (
                <button className="btn btn-primary" onClick={() => app.openReader(book)}>{prog ? "Continue reading" : (isVisual(book) ? "Start reading" : "Read now")}</button>
              ) : app.trialStart ? (
                <button className="btn btn-lock" onClick={app.openPaywall}><Svg d={I.lock} size={16} /> Subscribe to read</button>
              ) : (
                <button className="btn btn-primary" onClick={app.startTrial}><Svg d={I.spark} size={16} /> Start 3-day free trial</button>
              )}
              <button className="btn btn-ghost" onClick={() => app.openReader(book, true)}>Read free sample</button>
              <button className="btn btn-ghost" onClick={() => app.toggleLibrary(book.id)} title="Save to library">
                <Svg d={inLib ? I.heart : I.heartO} size={18} />{inLib ? "Saved" : "Save"}
              </button>
              <button className="btn btn-ghost" onClick={() => setPicker((p) => !p)} title="Add to a collection">＋ Collection</button>
              {book.userMade
                ? <button className="btn btn-ghost" onClick={() => { if (!printBook(book)) app.flash("Allow pop-ups to print your book"); }} title="Lay it out as a real picture book — print or save as PDF">🖨 Print my book</button>
                : !isVisual(book) && <button className="btn btn-ghost" onClick={() => app.makeHomework(book)} title="Send a 'read this chapter' task to Studlark — finishing it here checks it off there">🎓 Make it homework</button>}
            </div>
            {picker && <CollectionPicker book={book} app={app} />}
            {locked && <div className="locknote"><Svg d={I.lock} size={14} /> {app.trialStart ? "Your free trial has ended — subscribe to unlock the full title. The free sample is always available." : "The full title unlocks during your free trial. Read the sample any time."}</div>}
          </div>
        </div>
      </div>
    </div>
  );
}

/* ============================== READER ==================================== */
function Reader({ book, app, sampleMode, initialChapter, onClose }) {
  const entitled = book.free || book.userMade || (app.entitled && !sampleMode);
  // record open + accumulate reading minutes for Rewards
  useEffect(() => {
    app.recordOpen(book);
    const t0 = Date.now();
    return () => { const m = Math.round((Date.now() - t0) / 60000); if (m > 0) app.addMinutes(m); };
  }, []);
  return isVisual(book)
    ? <ComicReader book={book} app={app} entitled={entitled} onClose={onClose} />
    : <TextReader book={book} app={app} entitled={entitled} initialChapter={initialChapter} onClose={onClose} />;
}

function TextReader({ book, app, entitled, initialChapter, onClose }) {
  const start = app.progress[book.id] || {};
  const [chap, setChap] = useState(
    initialChapter != null
      ? Math.max(0, Math.min(initialChapter, (entitled ? book.chapters.length : 1) - 1))
      : (start.chapter || 0)
  );
  const [toc, setToc] = useState(false);
  const [settings, setSettings] = useState(false);
  const [speaking, setSpeaking] = useState(false);
  const [activePara, setActivePara] = useState(-1);
  const [sleepMins, setSleepMins] = useState(0); // Read-to-Me sleep timer: 0 = off
  const scrollRef = useRef(null);
  const pendingScroll = useRef(null);
  const speakId = useRef(0);
  const sleepAt = useRef(null);
  const p = app.prefs;
  const maxChap = entitled ? book.chapters.length - 1 : 0;
  const hasTTS = typeof window !== "undefined" && "speechSynthesis" in window;

  const ch = book.chapters[chap];
  const gate = !entitled && chap === 0 && book.chapters.length > 1;
  const overall = ((chap + 0.0) / book.chapters.length) * 100;

  // restore scroll on chapter change (honours bookmark jumps via pendingScroll)
  useEffect(() => {
    const el = scrollRef.current; if (!el) return;
    const stored = (app.progress[book.id] && app.progress[book.id].chapter === chap) ? (app.progress[book.id].pct || 0) : 0;
    const pct = pendingScroll.current != null ? pendingScroll.current : stored;
    pendingScroll.current = null;
    requestAnimationFrame(() => { el.scrollTop = pct * (el.scrollHeight - el.clientHeight); });
  }, [chap]);

  // Read-to-Me (text-to-speech)
  function stopSpeak() { speakId.current++; if (hasTTS) window.speechSynthesis.cancel(); setSpeaking(false); setActivePara(-1); }
  function startSpeak(from) {
    if (!hasTTS) { app.flash("Audio narration isn't available in this browser"); return; }
    window.speechSynthesis.cancel();
    const mine = ++speakId.current;
    setSpeaking(true);
    let i = Math.max(0, from);
    const next = () => {
      if (speakId.current !== mine) return;
      if (sleepAt.current && Date.now() >= sleepAt.current) { sleepStop(i); return; } // sleep timer: stop at a paragraph boundary
      if (i >= ch.paragraphs.length) { reportDone(chap); setSpeaking(false); setActivePara(-1); return; } // listened to the whole chapter → counts as read
      setActivePara(i);
      const u = new SpeechSynthesisUtterance(ch.paragraphs[i]);
      u.rate = 0.97;
      u.onend = () => { if (speakId.current === mine) { i++; next(); } };
      u.onerror = () => { if (speakId.current === mine) { setSpeaking(false); setActivePara(-1); } };
      window.speechSynthesis.speak(u);
    };
    next();
  }
  useEffect(() => () => { if (hasTTS) window.speechSynthesis.cancel(); }, []);
  useEffect(() => { stopSpeak(); }, [chap]);

  // finishing a chapter (scroll to the end, tap "next", or listen all the way
  // through) reports to the logan ID — Studlark uses it to tick off homework
  const reportDone = (chIdx) => { if (window.LoganID) window.LoganID.reportChapterDone(book, chIdx + 1, Math.round(((chIdx + 1) / book.chapters.length) * 100)); };
  // short chapters fit the screen with no scrollbar, so the scroll signal can
  // never fire — count them as read after dwelling on the chapter a while
  useEffect(() => {
    const t = setTimeout(() => {
      const el = scrollRef.current;
      if (el && el.scrollHeight <= el.clientHeight + 4) reportDone(chap);
    }, 8000);
    return () => clearTimeout(t);
  }, [chap]);

  // sleep timer: cycle off → 10 → 20 → 30 minutes
  const cycleSleep = () => {
    const nxt = sleepMins === 0 ? 10 : sleepMins === 10 ? 20 : sleepMins === 20 ? 30 : 0;
    setSleepMins(nxt);
    sleepAt.current = nxt ? Date.now() + nxt * 60000 : null;
    app.flash(nxt ? `⏾ Read-to-Me will stop in ${nxt} minutes and save a bookmark` : "Sleep timer off");
  };
  const sleepStop = (paraIdx) => {
    const el = scrollRef.current;
    let pct = 0;
    if (el) {
      const paras = el.querySelectorAll(".text-col p");
      const node = paras[Math.min(paraIdx, paras.length - 1)];
      if (node && el.scrollHeight > el.clientHeight) {
        pct = Math.max(0, Math.min(1, (node.getBoundingClientRect().top - el.getBoundingClientRect().top + el.scrollTop) / (el.scrollHeight - el.clientHeight)));
      }
    }
    app.addBookmark(book.id, { chapter: chap, pct, label: "⏾ " + ch.title, overall: Math.round((chap + pct) / book.chapters.length * 100) });
    setSleepMins(0); sleepAt.current = null;
    stopSpeak();
    app.flash("⏾ Sleep timer — narration stopped, bookmark saved");
  };

  const onScroll = () => {
    const el = scrollRef.current; if (!el) return;
    const pct = el.scrollHeight > el.clientHeight ? el.scrollTop / (el.scrollHeight - el.clientHeight) : 0;
    const overallNow = (chap + pct) / book.chapters.length * 100;
    if (pct >= 0.98) reportDone(chap);
    app.saveProgress(book.id, { chapter: chap, pct, overall: overallNow });
  };

  const addMark = () => {
    const el = scrollRef.current;
    const pct = el && el.scrollHeight > el.clientHeight ? el.scrollTop / (el.scrollHeight - el.clientHeight) : 0;
    app.addBookmark(book.id, { chapter: chap, pct, label: ch.title, overall: Math.round((chap + pct) / book.chapters.length * 100) });
    app.flash("🔖 Bookmark added");
  };
  const jumpTo = (chapter, pct) => {
    pendingScroll.current = pct || 0;
    if (chapter === chap) { const el = scrollRef.current; if (el) el.scrollTop = (pct || 0) * (el.scrollHeight - el.clientHeight); pendingScroll.current = null; }
    else setChap(chapter);
    setToc(false);
  };

  return (
    <div className="reader" data-theme={p.readerTheme} style={{ "--r-font": p.font, "--r-size": p.fontSize + "px", "--r-lh": p.lineHeight }}>
      <div className="reader-top">
        <button className="rbtn" onClick={onClose}><Svg d={I.back} /></button>
        <button className="rbtn" onClick={() => setToc(!toc)}><Svg d={I.list} /></button>
        <div className="rtitle"><b>{book.title}</b><small>{ch.title}{!entitled && <span style={{ color: "var(--brand)", fontWeight: 700 }}> · SAMPLE</span>}</small></div>
        <button className={"rbtn" + (speaking ? " on" : "")} onClick={() => (speaking ? stopSpeak() : startSpeak(0))} title="Read to me"><Svg d={speaking ? I.pause : I.listen} /></button>
        <button className="rbtn" onClick={addMark} title="Bookmark this spot"><Svg d={I.markO} /></button>
        <button className="rbtn" onClick={() => setSettings(!settings)}><Svg d={I.cog} /></button>
      </div>

      <div className="reader-stage">
        {toc && <TocDrawer book={book} chap={chap} entitled={entitled} bookmarks={app.bookmarks[book.id] || []} onPick={(i) => { setChap(i); setToc(false); }} onJump={jumpTo} onRemove={(id) => app.removeBookmark(book.id, id)} onClose={() => setToc(false)} app={app} />}
        {settings && <ReaderSettings app={app} onClose={() => setSettings(false)} />}
        {speaking && <div className="tts-bar"><span className="tts-dot" /> Reading aloud…
          <button onClick={cycleSleep} title="Sleep timer — stop narration after a while and save a bookmark">⏾ {sleepMins ? `${Math.max(1, Math.ceil((sleepAt.current - Date.now()) / 60000))} min` : "Timer"}</button>
          <button onClick={stopSpeak}>Stop</button></div>}
        <div className="text-scroll" ref={scrollRef} onScroll={onScroll}>
          <div className="text-col">
            <h3>{ch.title}</h3>
            {ch.paragraphs.map((para, i) => <p key={i} className={i === activePara ? "speaking" : ""} onClick={() => speaking && startSpeak(i)}>{para}</p>)}
            {gate ? (
              <div className="sample-gate">
                <div className="kbig">🔓</div>
                <h3>You've reached the end of the free sample</h3>
                <p>{app.trialStart ? "Subscribe to read the rest of this title and everything else on Nova Books." : "Start your free 3-day trial to keep reading — no charge today."}</p>
                {app.trialStart
                  ? <button className="btn btn-primary" onClick={app.openPaywall}>Subscribe to continue</button>
                  : <button className="btn btn-primary" onClick={() => { app.startTrial(); }}>Start 3-day free trial</button>}
              </div>
            ) : (
              <div className="chap-end">
                {chap < maxChap
                  ? <><div>End of “{ch.title}”</div><button className="btn btn-primary" onClick={() => { reportDone(chap); scrollRef.current.scrollTop = 0; setChap(chap + 1); }}>Next chapter →</button></>
                  : <div>{entitled ? "✦ The end of this preview ✦" : ""}</div>}
              </div>
            )}
          </div>
        </div>
      </div>

      <div className="reader-bottom">
        <div className="progress-row">
          <button className="rbtn" disabled={chap === 0} style={{ opacity: chap === 0 ? .3 : 1 }} onClick={() => chap > 0 && setChap(chap - 1)}><Svg d={I.chevL} /></button>
          <span>{Math.round(overall)}%</span>
          <input type="range" min="0" max={book.chapters.length - 1} value={chap} onChange={(e) => { const v = +e.target.value; setChap(Math.min(v, maxChap)); }} />
          <span>{chap + 1}/{book.chapters.length}</span>
          <button className="rbtn" disabled={chap >= maxChap} style={{ opacity: chap >= maxChap ? .3 : 1 }} onClick={() => chap < maxChap && setChap(chap + 1)}><Svg d={I.chevR} /></button>
        </div>
      </div>
    </div>
  );
}

function TocDrawer({ book, chap, entitled, bookmarks, onPick, onJump, onRemove, onClose, app }) {
  const [tab, setTab] = useState("toc");
  return (
    <div className="toc-drawer">
      <div className="drawer-tabs">
        <button className={tab === "toc" ? "on" : ""} onClick={() => setTab("toc")}>Chapters</button>
        <button className={tab === "marks" ? "on" : ""} onClick={() => setTab("marks")}>Bookmarks{bookmarks.length ? ` (${bookmarks.length})` : ""}</button>
      </div>
      {tab === "toc" ? book.chapters.map((c, i) => {
        const locked = !entitled && i > 0;
        return (
          <button key={i} className={"toc-item" + (i === chap ? " on" : "")} onClick={() => locked ? app.openPaywall() : onPick(i)}>
            <span>{i + 1}. {c.title}</span>
            {locked ? <Svg d={I.lock} size={14} /> : <small>{Math.round((i / book.chapters.length) * 100)}%</small>}
          </button>
        );
      }) : (
        bookmarks.length ? bookmarks.map((m) => (
          <div key={m.id} className="toc-item mark-item">
            <button className="mark-go" onClick={() => onJump(m.chapter, m.pct)}>
              <Svg d={I.mark} size={14} /> <span>{m.label}</span><small>{m.overall}%</small>
            </button>
            <button className="mark-del" title="Remove" onClick={() => onRemove(m.id)}><Svg d={I.close} size={13} /></button>
          </div>
        )) : <p className="muted-note" style={{ padding: "16px 10px" }}>No bookmarks yet. Tap the 🔖 icon while reading to save your spot.</p>
      )}
    </div>
  );
}

function ReaderSettings({ app, onClose }) {
  const p = app.prefs;
  const set = (k, v) => app.setPrefs({ ...p, [k]: v });
  const fonts = [["Literata", "'Literata', Georgia, serif"], ["Lora", "'Lora', Georgia, serif"], ["Fraunces", "'Fraunces', Georgia, serif"], ["Inter", "'Inter', system-ui, sans-serif"]];
  return (
    <div className="rpop">
      <div className="grp">
        <h4>Reading theme</h4>
        <div className="theme-row">
          {["light", "sepia", "dark"].map((t) => <button key={t} className={"swatch " + t + (p.readerTheme === t ? " on" : "")} onClick={() => set("readerTheme", t)} />)}
        </div>
      </div>
      <div className="grp">
        <h4>Font</h4>
        <div className="seg">{fonts.map((f) => <button key={f[0]} className={p.font === f[1] ? "on" : ""} onClick={() => set("font", f[1])}>{f[0]}</button>)}</div>
      </div>
      <div className="grp">
        <h4>Text size · {p.fontSize}px</h4>
        <div className="stepper">
          <button onClick={() => set("fontSize", Math.max(14, p.fontSize - 1))}>A−</button>
          <input type="range" min="14" max="30" value={p.fontSize} onChange={(e) => set("fontSize", +e.target.value)} />
          <button onClick={() => set("fontSize", Math.min(30, p.fontSize + 1))}>A+</button>
        </div>
      </div>
      <div className="grp" style={{ marginBottom: 0 }}>
        <h4>Line spacing · {p.lineHeight}</h4>
        <input type="range" min="1.4" max="2.2" step="0.1" value={p.lineHeight} style={{ width: "100%" }} onChange={(e) => set("lineHeight", +e.target.value)} />
      </div>
    </div>
  );
}

function ComicReader({ book, app, entitled, onClose }) {
  const rtl = book.format === "manga";
  const start = app.progress[book.id] || {};
  const [page, setPage] = useState(Math.min(start.page || 0, book.pages - 1));
  const maxPage = entitled ? book.pages - 1 : SAMPLE_PAGES - 1;
  const atGate = !entitled && page >= maxPage && book.pages > SAMPLE_PAGES;

  const go = useCallback((delta) => {
    setPage((prev) => {
      const next = prev + delta;
      if (next < 0) return prev;
      if (next > maxPage) { if (book.pages > maxPage + 1) app.flash("End of free sample"); return prev; }
      app.saveProgress(book.id, { page: next, overall: (next + 1) / book.pages * 100 });
      return next;
    });
  }, [maxPage]);

  useEffect(() => {
    const h = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft") go(-1);
      if (e.key === "ArrowRight") go(1);
    };
    window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
  }, [go, rtl]);

  return (
    <div className="reader" data-theme="dark">
      <div className="reader-top">
        <button className="rbtn" onClick={onClose}><Svg d={I.back} /></button>
        <div className="rtitle"><b>{book.title}</b><small>{book.format.toUpperCase()} · page {page + 1} of {book.pages}{!entitled && <span style={{ color: "var(--brand)", fontWeight: 700 }}> · SAMPLE</span>}{rtl && " · ← RTL"}</small></div>
      </div>
      <div className="reader-stage">
        <div className="comic-stage">
          {atGate ? (
            <div className="sample-gate" style={{ background: "transparent", borderColor: "rgba(255,255,255,.3)", color: "#fff" }}>
              <div className="kbig">🔓</div>
              <h3 style={{ color: "#fff" }}>End of the free preview</h3>
              <p style={{ color: "#aab" }}>{app.trialStart ? "Subscribe to read all " + book.pages + " pages." : "Start your free 3-day trial to read the whole issue."}</p>
              {app.trialStart
                ? <button className="btn btn-primary" onClick={app.openPaywall}>Subscribe to continue</button>
                : <button className="btn btn-primary" onClick={app.startTrial}>Start 3-day free trial</button>}
            </div>
          ) : (
            <div className="comic-page" dangerouslySetInnerHTML={{ __html: comicPage(book, page) }} />
          )}
          <button className="comic-nav left" onClick={() => go(-1)} aria-label="Previous page"><Svg d={I.chevL} size={40} /></button>
          <button className="comic-nav right" onClick={() => go(1)} aria-label="Next page"><Svg d={I.chevR} size={40} /></button>
        </div>
      </div>
      <div className="reader-bottom">
        <div className="progress-row">
          <span>{page + 1}/{book.pages}</span>
          <input type="range" min="0" max={book.pages - 1} value={page} onChange={(e) => { const v = +e.target.value; if (v <= maxPage) { setPage(v); } else { app.flash("Subscribe to read further"); } }} />
          <span>{Math.round((page + 1) / book.pages * 100)}%</span>
        </div>
      </div>
    </div>
  );
}

/* ============================== PAYWALL =================================== */
const PLANS = [
  { id: "monthly", name: "Individual", price: "$7.99", per: "billed monthly · 1 reader" },
  { id: "annual", name: "Annual", price: "$4.17", per: "$49.99/yr · 1 reader", save: "Save 48%" },
  { id: "family", name: "Family", price: "$9.19", per: "up to 6 readers · only +15%", save: "Best for families" },
];
function Paywall({ app, onClose }) {
  const [plan, setPlan] = useState("annual");
  const [step, setStep] = useState("plans"); // plans | checkout | done
  const [form, setForm] = useState({ name: "", card: "", exp: "", cvc: "" });
  const valid = form.name && form.card.replace(/\s/g, "").length >= 12 && form.exp.length >= 4 && form.cvc.length >= 3;

  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 600 }}>
        <button className="modal-close" onClick={onClose}><Svg d={I.close} size={18} /></button>
        {step === "plans" && <div className="paywall">
          <div className="kbig">✨</div>
          <h2>Unlock all of Nova Books</h2>
          <p className="sub">Unlimited books, comics, manga and anime for every age. {app.trialActive ? `Your free trial has ${fmtLeft(app.msLeft)} left — keep it going for $0 today.` : "Cancel anytime."}</p>
          <div className="plans">
            {PLANS.map((pl) => (
              <div key={pl.id} className={"plan" + (plan === pl.id ? " on" : "")} onClick={() => setPlan(pl.id)}>
                {pl.save && <span className="save">{pl.save}</span>}
                <b>{pl.name}</b>
                <div className="price">{pl.price}<span style={{ fontSize: 14, fontWeight: 600 }}>/mo</span></div>
                <div className="per">{pl.per}</div>
              </div>
            ))}
          </div>
          <button className="btn btn-primary" style={{ width: "100%", justifyContent: "center" }} onClick={() => setStep("checkout")}>Continue</button>
          <p className="fineprint">👨‍👩‍👧‍👦 The <strong>Family</strong> plan covers up to 6 readers for just <strong>15% more</strong> than an individual plan. This is a demo — no real payment is processed.</p>
        </div>}

        {step === "checkout" && <div className="paywall" style={{ textAlign: "left" }}>
          <h2 style={{ textAlign: "center" }}>Payment details</h2>
          <p className="sub" style={{ textAlign: "center" }}>{PLANS.find((p) => p.id === plan).name} plan · {PLANS.find((p) => p.id === plan).price}/mo</p>
          <div className="checkout-form">
            <div className="field"><label>Name on card</label><input value={form.name} placeholder="Alex Reader" onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>
            <div className="field"><label>Card number</label><input value={form.card} placeholder="4242 4242 4242 4242" inputMode="numeric"
              onChange={(e) => setForm({ ...form, card: e.target.value.replace(/[^\d ]/g, "") })} /></div>
            <div className="row">
              <div className="field"><label>Expiry</label><input value={form.exp} placeholder="MM/YY" onChange={(e) => setForm({ ...form, exp: e.target.value })} /></div>
              <div className="field"><label>CVC</label><input value={form.cvc} placeholder="123" inputMode="numeric" onChange={(e) => setForm({ ...form, cvc: e.target.value.replace(/\D/g, "") })} /></div>
            </div>
            <button className="btn btn-primary" style={{ width: "100%", justifyContent: "center", opacity: valid ? 1 : .5, pointerEvents: valid ? "auto" : "none" }}
              onClick={() => { setStep("done"); }}>Start Premium</button>
            <p className="fineprint">🔒 Demo checkout — use any numbers. Nothing is charged or transmitted.</p>
          </div>
        </div>}

        {step === "done" && <div className="success">
          <div className="check">✓</div>
          <h2 style={{ fontFamily: "'Fraunces',serif" }}>{plan === "family" ? "Welcome to Family! 🎉" : "You're Premium! 🎉"}</h2>
          <p className="sub">{plan === "family" ? "All " + (CATALOG.length).toLocaleString() + " titles are unlocked for up to 6 readers. Happy reading!" : "Every book, comic, manga and anime is now unlocked. Happy reading."}</p>
          <button className="btn btn-primary" onClick={() => { app.subscribe(PLANS.find((p) => p.id === plan).name); onClose(); }}>Start reading</button>
        </div>}
      </div>
    </div>
  );
}

/* ============================== REWARDS =================================== */
function Rewards({ app, onClose }) {
  const s = app.stats;
  const badges = computeBadges(s, app.library);
  const earned = badges.filter((b) => b.earned).length;
  const stat = (n, label) => <div className="rw-stat"><b>{n}</b><span>{label}</span></div>;
  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 560 }}>
        <button className="modal-close" onClick={onClose}><Svg d={I.close} size={18} /></button>
        <div className="rewards">
          <div className="rw-flame">🔥</div>
          <h2>{s.streak > 0 ? `${s.streak}-day reading streak!` : "Start your reading streak"}</h2>
          <p className="sub">Read a little every day to keep your streak alive and earn badges.</p>
          <div className="rw-stats">
            {stat(s.minutes, s.minutes === 1 ? "minute read" : "minutes read")}
            {stat(s.books.length, "books opened")}
            {stat(`${earned}/${badges.length}`, "badges earned")}
          </div>
          {(() => {
            const m = isoDay().slice(0, 7);
            const mins = Object.entries(s.daily || {}).filter(([d]) => d.startsWith(m)).reduce((a, [, v]) => a + (v || 0), 0);
            const opened = Object.values(s.openLog || {}).filter((d) => String(d).startsWith(m)).length;
            const monthName = new Date().toLocaleDateString(undefined, { month: "long" });
            return (
              <div style={{ background: "var(--bg-soft)", borderRadius: 12, padding: "12px 16px", margin: "16px 0 4px", fontSize: 14, textAlign: "center" }}>
                <b>{monthName} reading report 👪</b>
                <div style={{ marginTop: 4 }}>📚 {opened} book{opened === 1 ? "" : "s"} started · ⏱ {Math.floor(mins / 60) > 0 ? Math.floor(mins / 60) + "h " : ""}{mins % 60}m read · 🔥 {s.streak}-day streak</div>
                <div style={{ color: "var(--muted)", fontSize: 12, marginTop: 4 }}>{window.LoganID && window.LoganID.account() ? "Synced with your logan ID — reading here keeps your Studlark streak alive too." : "A little card to show a parent — it fills up as you read this month."}</div>
              </div>
            );
          })()}
          <h4 className="rw-h">Your badges</h4>
          <div className="rw-badges">
            {badges.map((b) => (
              <div key={b.id} className={"rw-badge" + (b.earned ? " on" : "")} title={b.desc}>
                <div className="rw-icon">{b.earned ? b.icon : "🔒"}</div>
                <b>{b.name}</b><span>{b.desc}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

/* ============================== DEMO MENU ================================= */
function DemoMenu({ app, onClose }) {
  const item = (label, emoji, fn) => <button className="item" onClick={() => { fn(); onClose(); }}>{emoji} {label}</button>;
  return (
    <div className="demo-menu" onClick={(e) => e.stopPropagation()}>
      <div className="head">Trial status: {app.plan === "premium" ? "Premium ✓" : app.trialStart ? (app.trialActive ? "On trial (" + fmtLeft(app.msLeft) + ")" : "Trial expired") : "Not started"}</div>
      {!app.trialStart && app.plan !== "premium" && item("Start 3-day free trial", "✨", app.startTrial)}
      {app.trialStart && app.plan !== "premium" && item("Expire trial now (test paywall)", "⏩", app.expireTrial)}
      {app.plan !== "premium" && item("Open subscribe screen", "💳", app.openPaywall)}
      {app.plan === "premium" && item("Cancel Premium", "↩", app.cancelPremium)}
      <div className="sep" />
      {item("logan ID — account & sign in", "🪪", app.goSettings)}
      {item("Rewards & reading streak", "🏆", app.openRewards)}
      {item((app.kidsMode ? "Turn off" : "Turn on") + " Kids Mode", "👶", app.toggleKids)}
      {item("Toggle " + (app.appTheme === "dark" ? "bright" : "dark") + " mode", app.appTheme === "dark" ? "☀️" : "🌙", app.toggleTheme)}
      <div className="sep" />
      {item("Reset everything", "🗑", app.resetAll)}
    </div>
  );
}

/* ============================== APP ROOT ================================== */
function App() {
  const [appTheme, setAppTheme] = useState(saved.appTheme || "light");
  const [trialStart, setTrialStart] = useState(saved.trialStart || null);
  const [subscribed, setSubscribed] = useState(saved.subscribed || false);
  const [library, setLibrary] = useState(saved.library || []);
  const [progress, setProgress] = useState(saved.progress || {});
  const [prefs, setPrefsState] = useState(saved.prefs || { readerTheme: "sepia", font: "'Literata', Georgia, serif", fontSize: 19, lineHeight: 1.7 });
  const [collections, setCollections] = useState(saved.collections || []);
  const [bookmarks, setBookmarks] = useState(saved.bookmarks || {});
  const [stats, setStats] = useState(saved.stats || { minutes: 0, books: [], formats: [], lastDate: null, streak: 0, night: false });
  const [kidsMode, setKidsMode] = useState(saved.kidsMode || false);
  const [userBooks, setUserBooks] = useState(saved.userBooks || []);
  const [subPlan, setSubPlan] = useState(saved.subPlan || null);

  const [tab, setTab] = useState("home");
  const [query, setQuery] = useState("");
  const [detail, setDetail] = useState(null);
  const [reader, setReader] = useState(null); // {book, sample}
  const [paywall, setPaywall] = useState(false);
  const [rewards, setRewards] = useState(false);
  const [demo, setDemo] = useState(false);
  const [toast, setToast] = useState(null);
  const [now, setNow] = useState(Date.now());

  // ticking clock for countdown
  useEffect(() => { const t = setInterval(() => setNow(Date.now()), 30000); return () => clearInterval(t); }, []);
  // re-render when the logan ID signs in/out or shared data changes
  const [, setLoganTick] = useState(0);
  useEffect(() => { if (window.LoganID) return window.LoganID.onChange(() => setLoganTick((t) => t + 1)); }, []);
  // theme on <html>
  useEffect(() => { document.documentElement.dataset.theme = appTheme; }, [appTheme]);
  // persist
  useEffect(() => {
    localStorage.setItem(STORE_KEY, JSON.stringify({ appTheme, trialStart, subscribed, library, progress, prefs, collections, bookmarks, stats, kidsMode, userBooks, subPlan }));
  }, [appTheme, trialStart, subscribed, library, progress, prefs, collections, bookmarks, stats, kidsMode, userBooks, subPlan]);

  const msLeft = trialStart ? trialStart + TRIAL_MS - now : 0;
  const trialActive = !!trialStart && msLeft > 0;
  const plan = subscribed ? "premium" : "free";
  const entitled = subscribed || trialActive;

  const flash = (m) => { setToast(m); setTimeout(() => setToast((c) => (c === m ? null : c)), 2200); };

  const app = {
    appTheme, plan, trialStart, trialActive, msLeft, entitled, library, progress, prefs, collections, bookmarks, stats, kidsMode, userBooks,
    recommend: recommend(progress, library),
    isLocked: (b) => (b.userMade || b.free) ? false : !entitled, // your own stories + Free Books need no plan; other full titles need entitlement
    createBook: (d) => {
      const book = { id: "user-" + Date.now(), title: d.title, author: d.author, year: new Date().getFullYear(), ageGroup: d.ageGroup, format: "book", genres: [d.genre || "Original", "By You"], rating: 5.0, cover: d.cover, blurb: d.blurb, chapters: d.chapters, userMade: true };
      window.INKWELL_CATALOG.push(book); BY_ID[book.id] = book;
      setUserBooks((u) => [...u, book]);
      return book;
    },
    deleteBook: (id) => {
      const i = window.INKWELL_CATALOG.findIndex((b) => b.id === id); if (i >= 0) window.INKWELL_CATALOG.splice(i, 1);
      delete BY_ID[id];
      setUserBooks((u) => u.filter((b) => b.id !== id));
      setProgress((p) => { const n = { ...p }; delete n[id]; return n; });
      setLibrary((l) => l.filter((x) => x !== id));
      flash("Story deleted");
    },
    ageAllowed: (b) => !kidsMode || b.ageGroup === "kids" || b.ageGroup === "middle",
    progressPct: (b) => (progress[b.id] && progress[b.id].overall) || 0,
    addBookmark: (bookId, m) => setBookmarks((bm) => ({ ...bm, [bookId]: [...(bm[bookId] || []), { id: Date.now(), ...m }] })),
    removeBookmark: (bookId, id) => setBookmarks((bm) => ({ ...bm, [bookId]: (bm[bookId] || []).filter((x) => x.id !== id) })),
    recordOpen: (b) => {
      if (window.LoganID) window.LoganID.recordActivity("read"); // reading keeps the shared logan streak alive
      setStats((s) => {
        const books = s.books.includes(b.id) ? s.books : [...s.books, b.id];
        const formats = s.formats.includes(b.format) ? s.formats : [...s.formats, b.format];
        const today = dayKey(Date.now());
        let streak = s.streak, lastDate = s.lastDate;
        if (lastDate !== today) { streak = lastDate === dayKey(Date.now() - 86400000) ? streak + 1 : 1; lastDate = today; }
        const openLog = { ...(s.openLog || {}) };
        if (!openLog[b.id]) openLog[b.id] = isoDay(); // first-open date, for the monthly report
        return { ...s, books, formats, streak, lastDate, openLog, night: s.night || appTheme === "dark" };
      });
    },
    addMinutes: (n) => setStats((s) => {
      const d = isoDay();
      return { ...s, minutes: s.minutes + n, daily: { ...(s.daily || {}), [d]: ((s.daily || {})[d] || 0) + n } };
    }),
    toggleKids: () => { setKidsMode((k) => !k); flash(kidsMode ? "Kids Mode off" : "👶 Kids Mode on — content filtered for younger readers"); },
    openRewards: () => setRewards(true),
    goHome: () => { setTab("home"); setQuery(""); },
    goExplore: () => { setTab("explore"); },
    openDetail: (b) => setDetail(b),
    openReader: (b, sample = false) => { setDetail(null); setReader({ book: b, sample }); },
    openPaywall: () => { setDetail(null); setPaywall(true); },
    toggleLibrary: (id) => setLibrary((l) => l.includes(id) ? l.filter((x) => x !== id) : [...l, id]),
    createCollection: (name) => { const c = { id: "col-" + Date.now(), name: (name || "New Collection").trim(), ids: [] }; setCollections((cs) => [...cs, c]); flash("Created “" + c.name + "”"); return c.id; },
    toggleInCollection: (colId, bookId) => setCollections((cs) => cs.map((c) => c.id === colId ? { ...c, ids: c.ids.includes(bookId) ? c.ids.filter((x) => x !== bookId) : [...c.ids, bookId] } : c)),
    renameCollection: (colId, name) => setCollections((cs) => cs.map((c) => c.id === colId ? { ...c, name: name.trim() || c.name } : c)),
    deleteCollection: (colId) => setCollections((cs) => cs.filter((c) => c.id !== colId)),
    importCollection: (name, ids) => {
      const valid = (ids || []).filter((id) => BY_ID[id]);
      if (!valid.length) { flash("That share code has no books this library recognises"); return 0; }
      const c = { id: "col-" + Date.now(), name: (name || "Shared collection").trim(), ids: valid };
      setCollections((cs) => [...cs, c]);
      flash(`📚 Imported “${c.name}” — ${valid.length} book${valid.length === 1 ? "" : "s"}`);
      return valid.length;
    },
    shareCollection: (col) => {
      const url = location.origin + location.pathname + "?share=" + encodeShare({ n: col.name, b: col.ids });
      const done = () => flash("🔗 Share link copied — send it to a friend!");
      if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(url).then(done).catch(() => window.prompt("Copy this share link:", url));
      else window.prompt("Copy this share link:", url);
    },
    importShareCode: (s) => {
      const m = String(s).match(/share=([^&\s]+)/);
      const data = decodeShare(m ? decodeURIComponent(m[1]) : String(s).trim());
      if (!data || !Array.isArray(data.b)) { flash("That doesn't look like a Nova Books share code"); return; }
      app.importCollection(data.n, data.b);
    },
    makeHomework: (book) => {
      if (!window.LoganID) return;
      if (!window.LoganID.account()) { setDetail(null); setTab("settings"); flash("🪪 Sign in with your logan ID first — it takes a minute"); return; }
      const total = book.chapters.length;
      const next = Math.min(total, (window.LoganID.chapterDone(book.id) || 0) + 1);
      const raw = window.prompt(`Which chapter of “${book.title}” should be homework? (1–${total})`, String(next));
      if (raw == null) return;
      const ch = parseInt(raw, 10);
      if (isNaN(ch) || ch < 1 || ch > total) { flash(`Pick a chapter between 1 and ${total}`); return; }
      window.LoganID.makeHomework(book, ch).then((r) => flash(r.ok
        ? `🎓 Sent to Studlark — read chapter ${ch} and the homework ticks itself off!`
        : "Couldn't reach your logan ID — check your connection"));
    },
    goSettings: () => setTab("settings"),
    saveProgress: (id, data) => setProgress((p) => ({ ...p, [id]: { ...p[id], ...data } })),
    setPrefs: (p) => setPrefsState(p),
    startTrial: () => { setTrialStart(Date.now()); flash("🎉 Your 3-day free trial has started!"); },
    expireTrial: () => { setTrialStart(Date.now() - TRIAL_MS - 1000); flash("Trial expired (demo)"); },
    subPlan,
    subscribe: (planName) => { setSubscribed(true); setSubPlan(planName || "Premium"); flash("✨ Welcome to " + (planName === "Family" ? "Nova Books Family!" : "Premium!")); },
    cancelPremium: () => { setSubscribed(false); setSubPlan(null); flash("Premium cancelled (demo)"); },
    toggleTheme: () => setAppTheme((t) => (t === "dark" ? "light" : "dark")),
    resetAll: () => { localStorage.removeItem(STORE_KEY); for (let i = window.INKWELL_CATALOG.length - 1; i >= 0; i--) { if (window.INKWELL_CATALOG[i].userMade) { delete BY_ID[window.INKWELL_CATALOG[i].id]; window.INKWELL_CATALOG.splice(i, 1); } } setTrialStart(null); setSubscribed(false); setLibrary([]); setProgress({}); setCollections([]); setBookmarks({}); setStats({ minutes: 0, books: [], formats: [], lastDate: null, streak: 0, night: false }); setKidsMode(false); setUserBooks([]); setSubPlan(null); flash("Reset complete"); },
    flash,
  };

  // deep links: ?book=…&chapter=… (reading homework from Studlark) and ?share=… (collections)
  useEffect(() => {
    const q = new URLSearchParams(location.search);
    const bid = q.get("book"), share = q.get("share");
    if (bid && BY_ID[bid]) {
      const b = BY_ID[bid];
      const chQ = parseInt(q.get("chapter") || "", 10);
      const chapter = isNaN(chQ) ? null : Math.max(0, chQ - 1);
      if (app.isLocked(b)) setDetail(b); // locked → land on the detail page (sample/trial buttons)
      else setReader({ book: b, sample: false, chapter });
    }
    if (share) {
      const data = decodeShare(share);
      if (data && Array.isArray(data.b) && window.confirm(`Import the shared collection “${data.n || "Books"}” (${data.b.length} book${data.b.length === 1 ? "" : "s"})?`)) {
        app.importCollection(data.n, data.b);
        setTab("library");
      }
    }
    if (bid || share) history.replaceState(null, "", location.pathname); // clean the URL after handling
  }, []);

  const loganAcct = window.LoganID ? window.LoganID.account() : null;
  const topStreak = loganAcct ? Math.max(stats.streak, window.LoganID.sharedStreak().count) : stats.streak;

  // trial strip
  let strip = null;
  if (plan !== "premium") {
    if (!trialStart) strip = <div className="trial-strip"><span>👋 Welcome to Nova Books — start your 3-day free trial to read everything.</span><button onClick={app.startTrial}>Start free trial</button></div>;
    else if (trialActive) strip = <div className={"trial-strip" + (msLeft < 86400000 ? " warn" : "")}><span>⏳ {fmtLeft(msLeft)} left in your free trial.</span><button onClick={app.openPaywall}>Go Premium</button></div>;
    else strip = <div className="trial-strip expired"><span>🔒 Your free trial has ended. Subscribe to keep reading.</span><button onClick={app.openPaywall}>Subscribe</button></div>;
  }

  return (
    <>
      <header className="topbar">
        <div className="topbar-inner">
          <div className="brand" onClick={app.goHome}><b>Nova <span>Books</span></b><i>by Logan Lee Sanjaya</i></div>
          <nav className="nav-tabs">
            <button className={tab === "home" ? "on" : ""} onClick={() => { setTab("home"); setQuery(""); }}>HOME</button>
            <button className={tab === "explore" ? "on" : ""} onClick={() => setTab("explore")}>EXPLORE</button>
            <button className={tab === "write" ? "on" : ""} onClick={() => setTab("write")}>WRITE</button>
            <button className={tab === "settings" ? "on" : ""} onClick={() => setTab("settings")}>SETTINGS</button>
          </nav>
          <div className="search">
            <Svg d={I.search} size={16} />
            <input value={query} placeholder="Search by title, author or keyword…"
              onChange={(e) => { setQuery(e.target.value); if (e.target.value && tab !== "explore") setTab("explore"); }} />
          </div>
          <div className="topbar-right">
            {kidsMode && <span className="kids-pill" onClick={app.toggleKids} title="Disable Kids Mode"><Svg d={I.kids} size={15} /> Disable Kids Mode</span>}
            {plan === "premium"
              ? <span className="trial-pill premium"><span className="dot" /> {subPlan === "Family" ? "Family" : "Premium"}</span>
              : trialStart
                ? <span className={"trial-pill" + (trialActive ? "" : " expired")} onClick={app.openPaywall} style={{ cursor: "pointer" }}><span className="dot" />{trialActive ? fmtLeft(msLeft) + " left" : "Trial ended"}</span>
                : <span className="trial-pill" onClick={app.startTrial} style={{ cursor: "pointer" }}><span className="dot" /> Free trial</span>}
            <button className="icon-btn" onClick={app.openRewards} title={loganAcct ? "Rewards & shared logan streak (Nova Books + Studlark)" : "Rewards & streak"}>
              <Svg d={I.trophy} size={20} /><span>{topStreak > 0 ? `🔥 ${topStreak}` : "Rewards"}</span>
            </button>
            <button className="icon-btn" onClick={app.toggleTheme} title="Toggle dark / bright">
              <Svg d={appTheme === "dark" ? I.sun : I.moon} size={20} /><span>{appTheme === "dark" ? "Bright" : "Dark"}</span>
            </button>
            <button className="icon-btn" onClick={() => setTab("library")} title="My Library">
              <Svg d={I.lib} size={20} /><span>Library</span>
            </button>
            <div style={{ position: "relative" }}>
              <button className="avatar" onClick={() => setDemo((d) => !d)} title="Account & demo controls">A</button>
              {demo && <><div style={{ position: "fixed", inset: 0, zIndex: 49 }} onClick={() => setDemo(false)} /><DemoMenu app={app} onClose={() => setDemo(false)} /></>}
            </div>
          </div>
        </div>
        {strip}
      </header>

      {tab === "home" && <Home app={app} />}
      {tab === "explore" && <Explore app={app} query={query} />}
      {tab === "library" && <Library app={app} />}
      {tab === "write" && <CreateBook app={app} />}
      {tab === "settings" && <Settings app={app} />}

      {detail && <Detail book={detail} app={app} onClose={() => setDetail(null)} />}
      {reader && <Reader book={reader.book} app={app} sampleMode={reader.sample} initialChapter={reader.chapter} onClose={() => setReader(null)} />}
      {paywall && <Paywall app={app} onClose={() => setPaywall(false)} />}
      {rewards && <Rewards app={app} onClose={() => setRewards(false)} />}
      {toast && <div className="toast">{toast}</div>}
    </>
  );
}

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