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

/* ========== Reduced-motion flag ========== */
const PREFERS_REDUCED =
  typeof window !== "undefined" && window.matchMedia
    ? window.matchMedia("(prefers-reduced-motion: reduce)").matches
    : false;

/* ========== Modal accessibility hook ==========
   Esc-to-close, focus trap, focus restore, body scroll-lock.
   `active`: boolean, `onClose`: () => void, `containerRef`: ref to the
   overlay element (must have tabIndex={-1}). */
function useModalA11y(active, onClose, containerRef) {
  // Keep the latest onClose without re-running the open/close effect — so an
  // App re-render while the overlay is open doesn't re-capture prevFocus/
  // prevOverflow (which would break focus-restore and churn the listener).
  const onCloseRef = useRef(onClose);
  useEffect(() => { onCloseRef.current = onClose; });

  useEffect(() => {
    if (!active) return;
    const prevFocus = document.activeElement;
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const container = containerRef.current;
    const SEL =
      'a[href],button:not([disabled]),textarea,input:not([disabled]),select,[tabindex]:not([tabindex="-1"])';
    const focusables = () =>
      container ? [...container.querySelectorAll(SEL)].filter(el => el.offsetParent !== null) : [];
    const initial = focusables();
    (initial[0] || container)?.focus();

    const onKey = (e) => {
      if (e.key === "Escape") { e.preventDefault(); onCloseRef.current(); return; }
      if (e.key === "Tab") {
        const list = focusables();
        if (!list.length) return;
        const first = list[0];
        const last = list[list.length - 1];
        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
      }
    };
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;
      if (prevFocus && typeof prevFocus.focus === "function") prevFocus.focus();
    };
    // containerRef is a stable ref object; effect runs only on active flips.
  }, [active, containerRef]);
}

/* ========== Reveal primitive ========== */
function Reveal({ children, delay = 0, className = "", style = {} }) {
  const ref = useRef(null);
  const [shown, setShown] = useState(true);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    // If already on-screen at mount, reveal after delay unconditionally.
    const r = el.getBoundingClientRect();
    const visible = r.top < window.innerHeight && r.bottom > 0;
    if (visible) {
      const t = setTimeout(() => setShown(true), delay);
      return () => clearTimeout(t);
    }
    // Otherwise use IO, with a scroll-fallback just in case.
    let disposed = false;
    const reveal = () => { if (!disposed) setTimeout(() => setShown(true), delay); };
    try {
      const obs = new IntersectionObserver((entries) => {
        entries.forEach(e => { if (e.isIntersecting) { reveal(); obs.disconnect(); } });
      }, { threshold: 0.1, rootMargin: "0px 0px -10% 0px" });
      obs.observe(el);
      return () => { disposed = true; obs.disconnect(); };
    } catch {
      reveal();
    }
  }, [delay]);
  return <div ref={ref} className={`reveal ${shown ? "shown" : ""} ${className}`} style={style}>{children}</div>;
}

/* ========== Da Vinci-style codex studies ==========
   Each ~600x780, drawn in ink/sanguine with hatching and geometric
   underdrawings. Scale to any container via 100% width. Meant to feel
   like a notebook page, not an icon. ============================= */

// Shared hatch generator — sets of short parallel strokes
const Hatch = ({ cx, cy, w, h, angle = -55, density = 18, stroke = "#8A6F2E", opacity = 0.55, pad = 0 }) => {
  const lines = [];
  const step = w / density;
  const len = Math.sqrt(w*w + h*h) * 0.85;
  const rad = angle * Math.PI / 180;
  const dx = Math.cos(rad), dy = Math.sin(rad);
  for (let i = -density; i < density*2; i++) {
    const t = i * step;
    const x1 = cx - w/2 + t;
    const y1 = cy - h/2;
    const x2 = x1 + dx * len;
    const y2 = y1 + dy * len;
    lines.push(<line key={i} x1={x1} y1={y1} x2={x2} y2={y2}
      stroke={stroke} strokeWidth="0.5" opacity={opacity} />);
  }
  return <g clipPath={`url(#clip-${cx}-${cy})`}>{lines}</g>;
};

// Mirror-script Latin-flavored gibberish (reads like Leonardo's notebook margins)
const mirrorScript = (text, x, y, size = 9) => (
  <text x={x} y={y} fontFamily="Fraunces" fontStyle="italic" fontSize={size}
    fill="#6B5B3E" opacity="0.75" transform={`scale(-1,1) translate(${-x*2},0)`}>{text}</text>
);

// Cross-hatch shading ball (study sphere)
const StudySphere = ({ cx, cy, r, angleOff = 35 }) => {
  const lines = [];
  const count = 24;
  for (let i = 0; i < count; i++) {
    const t = i / count;
    const yo = -r + t * 2 * r;
    const w = Math.sqrt(r*r - yo*yo) * 2;
    const shade = Math.max(0.12, 0.75 - Math.abs(yo + r*0.3) / r * 0.7);
    lines.push(<line key={`a${i}`}
      x1={cx - w/2 + 1} y1={cy + yo}
      x2={cx + w/2 - 1} y2={cy + yo}
      stroke="#6B5B3E" strokeWidth="0.35" opacity={shade} />);
  }
  // cross-hatch diagonal
  for (let i = 0; i < count*1.3; i++) {
    const a = (angleOff * Math.PI) / 180;
    const off = (i - count*0.65) * (r * 0.12);
    const x1 = cx - r, y1 = cy + off * 1.2;
    const x2 = cx + r, y2 = cy + off * 1.2 + Math.sin(a) * r * 0.3;
    lines.push(<line key={`b${i}`} x1={x1} y1={y1} x2={x2} y2={y2}
      stroke="#6B5B3E" strokeWidth="0.3" opacity="0.35" />);
  }
  return <g clipPath={`url(#sphere-${cx}-${cy})`}>
    <defs>
      <clipPath id={`sphere-${cx}-${cy}`}>
        <circle cx={cx} cy={cy} r={r-0.5}/>
      </clipPath>
    </defs>
    {lines}
  </g>;
};

// ——————————————————————————————————————————————
// I. ENGINEER — a planetary gearing study
// Interlocking wheels, escapement, fly-wheel. Like Codex Madrid.
// ——————————————————————————————————————————————
const GlyphEngineer = () => (
  <img
    src="assets/davinci/vitruvian.png"
    className="codex-plate-img"
    style={{ mixBlendMode: 'screen', transform: 'scale(1.05)' }}
    alt=""
    draggable="false"
  />
);

const GlyphBuilder = () => (
  <img src="assets/davinci/flying-machine.png" className="codex-plate-img"
       style={{ mixBlendMode: 'screen', transform: 'scale(1)' }}
       alt="" draggable="false" />
);

const GlyphPioneer = () => (
  <img src="assets/davinci/self-portrait.png" className="codex-plate-img"
       style={{ mixBlendMode: 'screen', transform: 'scale(1.1)' }}
       alt="" draggable="false" />
);

const GlyphFighter = () => (
  <img src="assets/davinci/skull.png" className="codex-plate-img"
       style={{ mixBlendMode: 'screen', transform: 'scale(1)' }}
       alt="" draggable="false" />
);

const GlyphSkeptic = () => (
  <img src="assets/davinci/scapigliata.png" className="codex-plate-img"
       style={{ mixBlendMode: 'screen', transform: 'scale(1.08)' }}
       alt="" draggable="false" />
);

const GLYPHS = {
  engineer: GlyphEngineer,
  builder: GlyphBuilder,
  pioneer: GlyphPioneer,
  fighter: GlyphFighter,
  skeptic: GlyphSkeptic,
};

const ANCHORS = window.ANCHORS_DATA.map(a => ({ ...a, Glyph: GLYPHS[a.id] }));

function Scene({ a, side, onOpen, children }) {
  const ref = useRef(null);
  const [prog, setProg] = useState(0.5); // start mid so visible on mount; scroll corrects
  useEffect(() => {
    const el = ref.current; if (!el) return;
    let raf;
    const recompute = () => {
      raf = null;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight;
      const total = r.height + vh;
      const travelled = vh - r.top;
      const p = Math.max(0, Math.min(1, travelled / total));
      setProg(p);
    };
    const onScroll = () => { if (raf) return; raf = requestAnimationFrame(recompute); };
    recompute();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, []);

  // Reveal lines & plate based on prog
  const inView = prog > 0.1 && prog < 0.95;
  // Softer curves: plate fades in from 0.05, fully visible by 0.35
  const plateProg = PREFERS_REDUCED ? 1 : Math.max(0, Math.min(1, (prog - 0.05) / 0.3));
  const textProg = PREFERS_REDUCED ? 1 : Math.max(0, Math.min(1, (prog - 0.12) / 0.3));
  const Glyph = a.Glyph;

  return (
    <section ref={ref} className={`scene scene-${a.id} side-${side}`} data-in={inView}>
      <div className="scene-inner">
        <div className="scene-plate" style={{
          opacity: plateProg,
          transform: `translateY(${(1-plateProg)*28}px)`,
          '--plate-reveal': plateProg
        }}>
          <div className="plate-stage"><Glyph /></div>
          <div className="plate-caption">
            <span className="ci-num">{a.num}</span>
            <span className="ci-sep">·</span>
            <span className="ci-tag">{a.tag}</span>
          </div>
        </div>
        <div className="scene-copy" style={{
          opacity: textProg,
          transform: `translateY(${(1-textProg)*20}px)`
        }}>
          <div className="scene-eyebrow">
            <span className="rule"/>
            <span>{a.num} · {a.tag}</span>
            <span className="rule"/>
          </div>
          <h2 className="scene-title">{a.title}</h2>
          <p className="scene-lede">{a.lede}</p>
          {a.aside && (() => {
            const parts = a.aside.split(' — ');
            const quote = parts[0];
            const attr = parts.slice(1).join(' — ');
            return (
              <p className="scene-aside">
                {quote}
                {attr && <><br/><span className="aside-attr">— {attr}</span></>}
              </p>
            );
          })()}
          {a.facts && a.facts.length > 0 && (
            <dl className="scene-facts">
              {a.facts.map((f, i) => (
                <div key={i} className="sf">
                  <dt>{f.n}</dt>
                  <dd>{f.l}</dd>
                </div>
              ))}
            </dl>
          )}
          <button className="scene-open" onClick={() => onOpen(a.id)}>
            <span className="so-lbl">Read the long version</span>
            <span className="so-arrow">→</span>
          </button>
        </div>
      </div>
    </section>
  );
}

/* ========== Expanded panel ========== */
function ExpandedPanel({ anchor, onClose, onRequestGate, onRedeemGate, unlocked }) {
  const open = !!anchor;
  const showUnlocked = !!anchor && unlocked;
  const panelRef = useRef(null);
  useModalA11y(open, onClose, panelRef);
  return (
    <>
      <div className={`expanded-scrim ${open ? "open" : ""}`} onClick={onClose} />
      <aside className={`expanded-panel ${open ? "open" : ""}`} aria-hidden={open ? undefined : true}
             role="dialog" aria-modal="true"
             aria-label={anchor ? `${anchor.title} — detail` : "Expanded reading"} tabIndex={-1}
             ref={panelRef}>
        {anchor && (
          <>
            <button className="close" aria-label="Close" onClick={onClose}>CLOSE</button>
            <div className="tag-line">{anchor.num} · {anchor.tag}</div>
            <h3>{anchor.title}</h3>
            <p className="lede">{anchor.lede}</p>
            <div className="body">
              {anchor.body.map((p, i) => (
                <p key={i} dangerouslySetInnerHTML={{ __html: p }} />
              ))}
            </div>
            {anchor.log && (
              <div className="log">
                {anchor.log.map((l, i) => (
                  <div key={i}><span className="caret">&gt;</span>{l}</div>
                ))}
              </div>
            )}
            {anchor.aside && <div className="aside">{anchor.aside}</div>}

            {anchor.gate && !showUnlocked && (
              <div className="gate-card">
                <svg className="icon" viewBox="0 0 48 48" fill="none" stroke="#C9982C" strokeWidth="1">
                  <path d="M 14 22 L 14 14 Q 14 8, 24 8 Q 34 8, 34 14 L 34 22" />
                  <rect x="10" y="22" width="28" height="20" stroke="#E8C47C"/>
                  <circle cx="24" cy="32" r="2" fill="#E8C47C"/>
                </svg>
                <div>
                  <div className="gate-tag">BEHIND THE GATE · {anchor.gate.tag}</div>
                  <h5>{anchor.gate.title}</h5>
                  <p>{anchor.gate.lede}</p>
                  <div className="gate-actions">
                    <button className="request-btn" onClick={onRequestGate}>Request access</button>
                    <button className="linklike" onClick={onRedeemGate}>I have a token</button>
                  </div>
                </div>
              </div>
            )}

            {anchor.gate && showUnlocked && anchor.gateContent && (
              <div className="preview-unlock">
                <div className="unlock-bar">
                  <span className="stamp">UNLOCKED</span>
                  <span className="label">BEHIND THE GATE · {anchor.gate.tag}</span>
                  {unlocked && <span className="preview-tag you">YOUR TOKEN</span>}
                </div>
                <h4 className="unlock-title">{anchor.gate.title}</h4>
                {anchor.gateContent.prose && anchor.gateContent.prose.map((p, i) => (
                  <p key={i} className="unlock-p" dangerouslySetInnerHTML={{ __html: p }} />
                ))}

                {anchor.gateContent.projects && anchor.gateContent.projects.length > 0 && (
                  <div className="gate-projects">
                    {anchor.gateContent.projects.map((p, i) => (
                      <div key={i} className="gate-project">
                        <div className="gp-head">
                          <span className="gp-name">{p.name}</span>
                          {p.status && <span className={`gp-status ${p.status}`}>{p.status}</span>}
                        </div>
                        <p className="gp-blurb">{p.blurb}</p>
                        {p.tags && p.tags.length > 0 && (
                          <div className="gp-tags">
                            {p.tags.map((t, j) => <span key={j} className="gp-tag">{t}</span>)}
                          </div>
                        )}
                        {p.links && p.links.length > 0 && (
                          <div className="gp-links">
                            {p.links.map((l, j) => (
                              <a key={j} className="gp-link" href={l.url} target="_blank" rel="noopener noreferrer">{l.label} →</a>
                            ))}
                          </div>
                        )}
                      </div>
                    ))}
                  </div>
                )}

                {anchor.gateContent.projectsLab && anchor.gateContent.projectsLab.length > 0 && (
                  <div className="gate-lab">
                    <span className="gl-label">Also built</span>
                    <span className="gl-list">{anchor.gateContent.projectsLab.join("  ·  ")}</span>
                  </div>
                )}

                {anchor.gateContent.careerNote && (
                  <p className="gate-career">{anchor.gateContent.careerNote}</p>
                )}

                {anchor.gateContent.writing && anchor.gateContent.writing.length > 0 && (
                  <div className="gate-writing">
                    {anchor.gateContent.writing.map((w, i) => (
                      <div key={i} className="gw-piece">
                        <div className="gw-meta">
                          <span className="gw-title">{w.title}</span>
                          <span className="gw-kind">{w.kind}</span>
                        </div>
                        <p className="gw-body">{w.body}</p>
                      </div>
                    ))}
                  </div>
                )}

                {anchor.gateContent.book && (
                  <div className="gate-book">
                    <div className="gb-label">The book</div>
                    <h5 className="gb-title">{anchor.gateContent.book.title}</h5>
                    <p className="gb-blurb">{anchor.gateContent.book.blurb}</p>
                    <a className="gb-link" href={anchor.gateContent.book.url} target="_blank" rel="noopener noreferrer">Read the rough draft →</a>
                  </div>
                )}

                {/* Nested "ask for more" inside the unlocked gate */}
                <NestedAsk anchor={anchor} />
              </div>
            )}
          </>
        )}
      </aside>
    </>
  );
}

/* Nested follow-up ask — appears inside an unlocked gate. Each anchor can
   define a custom prompt; defaults to a generic one. */
function NestedAsk({ anchor }) {
  const [state, setState] = useState("idle"); // idle | open | submitting | sent
  const [msg, setMsg] = useState("");
  const [err, setErr] = useState("");
  const prompt = (anchor.gateContent && anchor.gateContent.followupPrompt) ||
    "Want to go one step further on this? Ask a specific question.";
  const placeholder = (anchor.gateContent && anchor.gateContent.followupPlaceholder) ||
    "e.g. 'Can I see the resume?' · 'Tell me more about the book.' · 'Which game?'";

  async function send(e) {
    e.preventDefault();
    setState("submitting"); setErr("");
    try {
      const r = await submitGateRequest({
        gateId: anchor.id + ":followup",
        gateTag: anchor.gate.tag + " · FOLLOW-UP",
        email: "(already on file)",
        why: msg
      });
      if (r.ok) setState("sent");
      else { setErr("Didn't go through."); setState("open"); }
    } catch { setErr("Try again in a moment."); setState("open"); }
  }

  if (state === "idle") {
    return (
      <div className="nested-ask">
        <div className="nested-ask-prompt">{prompt}</div>
        <button className="linklike" onClick={() => setState("open")}>Ask a specific question →</button>
      </div>
    );
  }
  if (state === "open" || state === "submitting") {
    return (
      <form className="nested-ask open" onSubmit={send}>
        <div className="nested-ask-prompt">{prompt}</div>
        <textarea
          required
          rows={3}
          value={msg}
          placeholder={placeholder}
          onChange={e => setMsg(e.target.value)}
        />
        {err && <div className="gated-err">{err}</div>}
        <div className="nested-ask-actions">
          <button className="linklike" type="button" onClick={() => setState("idle")}>Cancel</button>
          <button className="submit small" type="submit" disabled={state === "submitting"}>
            {state === "submitting" ? "Sending…" : "Send"} <span className="arrow"/>
          </button>
        </div>
      </form>
    );
  }
  return (
    <div className="nested-ask sent">
      <span className="check">✓</span> Question sent. You'll hear back.
    </div>
  );
}

/* ========== Backend ==============================================
   Real calls to Vercel serverless endpoints backed by @vercel/kv +
   Resend. Email templates live in /emails/.
   ================================================================ */
async function submitGateRequest({ gateId, gateTag, email, why }) {
  try {
    const r = await fetch("/api/request", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ gateId, gateTag, email, why })
    });
    return await r.json();
  } catch (e) {
    return { ok: false, error: "Network error. Try again in a moment." };
  }
}

async function redeemToken(token) {
  try {
    const params = new URLSearchParams(window.location.search);
    const gate = params.get("gate") || "";
    const r = await fetch("/api/validate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token, gate })
    });
    return await r.json();
  } catch (e) {
    return { ok: false, error: "Network error." };
  }
}

/* ========== Gated flow ========== */
function GatedFlow({ open, anchor, mode, onClose, onUnlock }) {
  // mode: "request" or "redeem"
  const [step, setStep] = useState("form");
  const [err, setErr] = useState("");
  const [email, setEmail] = useState("");
  const [why, setWhy] = useState("");
  const [token, setToken] = useState("");
  const envelopeRef = useRef(null);
  useModalA11y(open, onClose, envelopeRef);

  useEffect(() => {
    if (open) {
      setStep(mode === "redeem" ? "redeem" : "form");
      setErr(""); setEmail(""); setWhy(""); setToken("");
    }
  }, [open, mode]);

  if (!anchor && mode !== "redeem") return null;

  async function handleSubmit(e) {
    e.preventDefault();
    setStep("submitting");
    setErr("");
    try {
      const r = await submitGateRequest({
        gateId: anchor.id, gateTag: anchor.gate.tag, email, why
      });
      if (r.ok) setStep("sealed");
      else { setErr(r.error || "Something went sideways."); setStep("form"); }
    } catch (x) {
      setErr("Couldn't send. Try again in a moment."); setStep("form");
    }
  }

  async function handleRedeem(e) {
    e.preventDefault();
    setStep("redeeming");
    setErr("");
    try {
      const r = await redeemToken(token.trim());
      if (r.ok) {
        onUnlock(r.gateId || anchor?.id, token.trim(), r.all);
        setStep("unlocked");
      } else {
        setErr(r.error || "Token not recognized.");
        setStep("redeem");
      }
    } catch (x) {
      setErr("Couldn't verify. Try again."); setStep("redeem");
    }
  }

  const isRedeem = mode === "redeem" || step === "redeem" || step === "redeeming" || step === "unlocked";

  return (
    <div className={`gated-scrim ${open ? "open" : ""}`} onClick={onClose}>
      <div className="gated-envelope" role="dialog" aria-modal="true" aria-label="Gate access" tabIndex={-1}
           ref={envelopeRef} onClick={e => e.stopPropagation()}>
        <div className="wax" aria-hidden="true">B</div>
        <button className="close" aria-label="Close" onClick={onClose}>×</button>

        {step === "form" && anchor && (
          <>
            <div className="tag">REQUEST ACCESS · {anchor.gate.tag}</div>
            <h4>{anchor.gate.title}</h4>
            <p className="lede">{anchor.gate.lede}</p>
            <form onSubmit={handleSubmit}>
              <label><span>Email</span>
                <input type="email" required placeholder="you@somewhere" aria-invalid={!!err}
                       value={email} onChange={e => setEmail(e.target.value)} />
              </label>
              <label><span>One line on why</span>
                <input required placeholder="Optional context"
                       value={why} onChange={e => setWhy(e.target.value)} />
              </label>
              {err && <div className="gated-err">{err}</div>}
              <button className="submit" type="submit">Seal &amp; send <span className="arrow"/></button>
            </form>
            <div className="gated-alt">
              Already have a token? <button className="linklike" onClick={() => setStep("redeem")}>Paste it here.</button>
            </div>
          </>
        )}

        {step === "submitting" && (
          <div className="sealed-msg">
            <div className="seal spinning">B</div>
            <h5>Sealing…</h5>
            <p>Putting it in the envelope.</p>
          </div>
        )}

        {step === "sealed" && (
          <div className="sealed-msg">
            <div className="seal">B</div>
            <h5>Sealed.</h5>
            <p>If it reads well, you'll hear back with a token.</p>
            <p className="small">Check the inbox you used. It comes as a styled email.</p>
          </div>
        )}

        {step === "redeem" && (
          <>
            <div className="tag">REDEEM TOKEN{anchor ? ` · ${anchor.gate.tag}` : ""}</div>
            <h4>Paste your token.</h4>
            <p className="lede">Single-use. Opens one gate. Arrived in the email Bret sent after approving your request.</p>
            <form onSubmit={handleRedeem}>
              <label><span>Token</span>
                <input required placeholder="paste token here" aria-invalid={!!err}
                       value={token} onChange={e => setToken(e.target.value)} />
              </label>
              {err && <div className="gated-err">{err}</div>}
              <button className="submit" type="submit">Open the gate <span className="arrow"/></button>
            </form>
            {anchor && (
              <div className="gated-alt">
                Need to request one instead? <button className="linklike" onClick={() => setStep("form")}>Back to the form.</button>
              </div>
            )}
          </>
        )}

        {step === "redeeming" && (
          <div className="sealed-msg">
            <div className="seal spinning">B</div>
            <h5>Unsealing…</h5>
            <p>Checking the token.</p>
          </div>
        )}

        {step === "unlocked" && (
          <div className="sealed-msg">
            <div className="seal open-seal">B</div>
            <h5>Opened.</h5>
            <p>The gate is unlocked on this device.</p>
            <button className="submit" onClick={onClose}>Read it <span className="arrow"/></button>
          </div>
        )}
      </div>
    </div>
  );
}

/* ========== Tweaks panel ========== */
function TweaksPanel({ active, values, onChange, onClose }) {
  const [edits, setEdits] = useState(() => values || {});
  useEffect(() => setEdits(values || {}), [values]);
  const set = (k, v) => {
    const next = { ...edits, [k]: v };
    setEdits(next);
    onChange(next);
    window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { [k]: v } }, "*");
  };
  return (
    <div className={`tweaks-panel ${active ? "active" : ""}`}>
      <h6>Tweaks <span className="x" onClick={onClose}>×</span></h6>
      <div className="tweak">
        <label>Gold warmth <span className="value">{edits.goldWarmth ?? 34}°</span></label>
        <input type="range" min="20" max="50" value={edits.goldWarmth ?? 34} onChange={e => set("goldWarmth", +e.target.value)} />
      </div>
      <div className="tweak">
        <label>Parchment tone <span className="value">{edits.parchmentTone ?? 60}</span></label>
        <input type="range" min="40" max="80" value={edits.parchmentTone ?? 60} onChange={e => set("parchmentTone", +e.target.value)} />
      </div>
      <div className="tweak">
        <label>Motion intensity <span className="value">{edits.motionIntensity ?? 200}%</span></label>
        <input type="range" min="0" max="400" step="50" value={edits.motionIntensity ?? 200} onChange={e => set("motionIntensity", +e.target.value)} />
      </div>
      <div className="tweak">
        <label>
          <input type="checkbox" checked={edits.showBretAsides ?? true} onChange={e => set("showBretAsides", e.target.checked)} /> Plainspoken asides
        </label>
      </div>
    </div>
  );
}

/* ========== App ========== */
function App() {
  const [openId, setOpenId] = useState(null);
  const [gatedAnchor, setGatedAnchor] = useState(null);
  const [gateMode, setGateMode] = useState("request"); // "request" | "redeem"
  const [unlocked, setUnlocked] = useState(() => {
    try {
      const raw = localStorage.getItem("bg.unlocked");
      return new Set(raw ? JSON.parse(raw) : []);
    } catch { return new Set(); }
  });
  const [toast, setToast] = useState(null);
  const [tweaks, setTweaks] = useState(window.TWEAKS || {});
  const [tweaksOn, setTweaksOn] = useState(false);

  // Persist unlocked set
  useEffect(() => {
    try { localStorage.setItem("bg.unlocked", JSON.stringify([...unlocked])); } catch {}
  }, [unlocked]);

  // URL token auto-unlock: ?t=<token>&gate=<id>
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const t = params.get("t");
    const gate = params.get("gate");
    if (t) {
      (async () => {
        const r = await redeemToken(t);
        if (r.ok) {
          const id = r.gateId && ANCHORS.find(a => a.id === r.gateId) ? r.gateId : (gate || "engineer");
          setUnlocked(prev => {
            const n = new Set(prev);
            if (r.all) ANCHORS.forEach(a => n.add(a.id)); else n.add(id);
            return n;
          });
          setOpenId(id);
          setToast({ kind: "ok", msg: r.all ? "All gates opened. Saved on this device." : `Gate opened: ${id.toUpperCase()}. Saved on this device.` });
          // clean URL without reloading
          const url = new URL(window.location.href);
          url.searchParams.delete("t");
          url.searchParams.delete("gate");
          window.history.replaceState({}, "", url.toString());
        } else {
          setToast({ kind: "err", msg: "That token didn't open anything." });
        }
      })();
    }
  }, []);

  // Auto-dismiss toasts
  useEffect(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(null), 4000);
    return () => clearTimeout(t);
  }, [toast]);

  function handleUnlock(gateId, token, all) {
    setUnlocked(prev => {
      const n = new Set(prev);
      if (all) ANCHORS.forEach(a => n.add(a.id)); else n.add(gateId);
      return n;
    });
    if (gateId) setOpenId(gateId);
  }

  useEffect(() => {
    const onMsg = (e) => {
      const d = e.data || {};
      if (d.type === "__activate_edit_mode") setTweaksOn(true);
      if (d.type === "__deactivate_edit_mode") setTweaksOn(false);
    };
    window.addEventListener("message", onMsg);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);

  useEffect(() => {
    const root = document.documentElement;
    if (tweaks.goldWarmth != null) {
      const h = tweaks.goldWarmth;
      root.style.setProperty("--gold", `oklch(70% 0.12 ${h + 50})`);
      root.style.setProperty("--gold-hi", `oklch(82% 0.10 ${h + 55})`);
    }
    if (tweaks.motionIntensity != null) {
      root.style.setProperty("--motion-intensity", tweaks.motionIntensity / 100);
    }
    document.body.classList.toggle("hide-asides", tweaks.showBretAsides === false);
  }, [tweaks]);

  const openAnchor = useMemo(() => ANCHORS.find(a => a.id === openId) || null, [openId]);

  return (
    <>
      <header className="hud-top">
        <div className="brand"><span className="dot" /><b>BretGold.com</b></div>
        <div className="meta">
          <span>2018</span>
          <button className="sys-link-inline tokenbtn"
                  onClick={() => { setGatedAnchor(null); setGateMode("redeem"); setGatedAnchor({_tokenOnly:true, gate:{tag:"TOKEN", title:"", lede:""}}); }}>
            ⌨ TOKEN
          </button>
        </div>
      </header>

      <main className="stage">
        <section className="masthead">
          <div className="mh-seal">
            <div className="mh-seal-stack">
              <img src="assets/davinci/star-of-bethlehem.png" className="mh-star-bg" alt="" draggable="false" />
              <svg viewBox="0 0 120 120" className="mh-monogram-svg">
                <text x="60" y="72" textAnchor="middle" fontFamily="Fraunces" fontStyle="italic" fontSize="48" fill="#E8C47C" opacity="0.92">BG</text>
              </svg>
            </div>
          </div>
          <Reveal>
            <div className="mh-plate">
              <div className="mh-folio">fo. 01r · ingressus</div>
              <h1 className="name">Bret <span className="gold-letter">Gold</span>.</h1>
              <p className="mh-lede">Work, side projects, and other things. Ask if you want more.</p>
            </div>
          </Reveal>
          <div className="mh-descend">
            <span className="d-lbl">descend</span>
            <span className="d-line"/>
          </div>
        </section>

        {ANCHORS.map((a, i) => (
          <Scene key={a.id} a={a} side={i % 2 === 0 ? "left" : "right"} onOpen={setOpenId} />
        ))}

        <section className="foot-scene">
          <div className="foot-rule"/>
          <div className="foot-seal">
            <svg viewBox="0 0 80 80">
              <circle cx="40" cy="40" r="36" fill="none" stroke="#8A6F2E" strokeWidth="0.5" opacity="0.6"/>
              <circle cx="40" cy="40" r="28" fill="none" stroke="#C9982C" strokeWidth="0.7"/>
              <text x="40" y="46" textAnchor="middle" fontFamily="Fraunces" fontStyle="italic" fontSize="16" fill="#E8C47C">fin.</text>
            </svg>
          </div>
          <div className="foot-year">BretGold.com · 2018</div>
        </section>
      </main>

      <ExpandedPanel
        anchor={openAnchor}
        onClose={() => setOpenId(null)}
        onRequestGate={() => {
          if (openAnchor?.gate) { setGateMode("request"); setGatedAnchor(openAnchor); setOpenId(null); }
        }}
        onRedeemGate={() => {
          if (openAnchor?.gate) { setGateMode("redeem"); setGatedAnchor(openAnchor); setOpenId(null); }
        }}
        unlocked={openAnchor && unlocked.has(openAnchor.id)}
      />
      <GatedFlow
        open={!!gatedAnchor}
        anchor={gatedAnchor && !gatedAnchor._tokenOnly ? gatedAnchor : null}
        mode={gateMode}
        onClose={() => setGatedAnchor(null)}
        onUnlock={(id, tok) => handleUnlock(id, tok)}
      />
      <div className="bg-toast-region" aria-live="polite" aria-atomic="true">
        {toast && (
          <div className={`bg-toast ${toast.kind}`} role="status">
            <span className="bg-toast-mark" aria-hidden="true">{toast.kind === "err" ? "!" : "✓"}</span>
            <span className="bg-toast-msg">{toast.msg}</span>
            <button className="bg-toast-x" aria-label="Dismiss" onClick={() => setToast(null)}>×</button>
            <span className="bg-toast-bar" />
          </div>
        )}
      </div>

      <TweaksPanel
        active={tweaksOn}
        values={tweaks}
        onChange={setTweaks}
        onClose={() => setTweaksOn(false)}
      />
    </>
  );
}

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