// Layout for the whole site. All *content* (text, images, menus, footer)
// lives in content.json and is edited through the CMS in cms/ — this file
// only decides how that content is laid out. Which page renders is decided
// by `body.dataset.page`.
const { useState, useEffect } = React;

// ── content ─────────────────────────────────────────────────────────────
// Filled from content.json before the first render (see the bottom of this
// file). Each top-level key of content.json maps to one of these.
let SITE, HEADER, HOME, PROJECTS, WORK, ABOUT, CONTACT, FOOTER, UI;
function applyContent(c) {
  ({ site: SITE, header: HEADER, home: HOME, projects: PROJECTS, work: WORK,
     about: ABOUT, contact: CONTACT, footer: FOOTER, ui: UI } = c);
}

// Fills {placeholders} in content strings, e.g. "{count} projects".
function fill(text, vars) {
  return String(text).replace(/\{(\w+)\}/g, (m, k) => (k in vars ? vars[k] : m));
}

function Pic({ src, alt }) {
  if (!src) return null;
  return (
    <img className="ph-img" src={src} alt={alt || ""}
      onError={(e) => { e.currentTarget.style.display = "none"; }} />
  );
}

// ── header ─────────────────────────────────────────────────────────────
// Client-side routing. The three .html files stay as real entry points (deep
// links + reloads work), but in-app navigation swaps the view via React state
// instead of reloading the document — so Babel never re-compiles mid-session
// and nothing freezes. App registers its navigate fn here on mount; goRoute()
// is the global entry point used by links living outside <App/>'s props.
const ROUTES = {
  home:  { url: "index.html" },
  work:  { url: "work.html" },
  about: { url: "about.html" },
};
const pageTitle = (page) => SITE.pageTitles[page] || SITE.pageTitles.home;
let __routeNavigate = null;
function goRoute(target) {
  if (__routeNavigate) { __routeNavigate(target); return; }
  // Fallback before App has mounted: hard navigation.
  const r = ROUTES[target];
  if (r) window.location.href = r.url;
}

// Legacy hard-navigate (kept as a reduced-motion / no-JS fallback).
function navigateWithFade(href) {
  window.location.href = href;
}

function HomeIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
      strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"
      style={{ display: "block" }}>
      <path d="M3 11.5 12 4l9 7.5" />
      <path d="M5.5 10v9.5h13V10" />
    </svg>
  );
}

function Header({ onNav, page }) {
  const [scrolled, setScrolled] = useState(false);
  const navRef = React.useRef(null);
  const [hovered, setHovered] = useState(null);
  const [pill, setPill] = useState({ left: 0, width: 0, opacity: 0 });

  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 8);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // which segment the pill rests on when not hovering
  const restKey = page === "work" ? "work" : page === "about" ? "about" : "home";
  const visualKey = hovered || restKey;

  useEffect(() => {
    const measure = () => {
      const nav = navRef.current;
      if (!nav) return;
      const el = nav.querySelector(`[data-key="${visualKey}"]`);
      if (!el) { setPill((p) => ({ ...p, opacity: 0 })); return; }
      setPill({ left: el.offsetLeft, width: el.offsetWidth, opacity: 1 });
    };
    const raf = requestAnimationFrame(measure);
    window.addEventListener("resize", measure);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(measure);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", measure); };
  }, [visualKey, page]);

  // Menu items come from content.header.nav; `icon: true` swaps the label
  // for the house icon (the label is kept as the accessible name).
  const items = HEADER.nav.map((n) => ({
    key: n.target, nav: n.target,
    label: n.icon ? <HomeIcon /> : n.label,
    aria: n.icon ? n.label : undefined,
  }));

  return (
    <header className={"site" + (scrolled ? " scrolled" : "")}>
      <div className="row">
        <a href="index.html" onClick={(e) => { e.preventDefault(); onNav("home"); }}
          style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ display: "inline-block", width: 10, height: 10, borderRadius: 999, background: "var(--accent)" }} />
          <span style={{ fontFamily: "Open Sans", fontWeight: 600, fontSize: 18, letterSpacing: "-0.01em" }}>
            {HEADER.brand}
          </span>
        </a>
        <nav className="navseg" ref={navRef} onMouseLeave={() => setHovered(null)}>
          <span className="navseg-pill" style={{
            transform: `translateX(${pill.left}px)`,
            width: pill.width,
            opacity: pill.opacity,
            background: hovered ? "var(--ink)" : "var(--accent)",
          }} />
          {items.map((it) => (
            <a
              key={it.key}
              data-key={it.key}
              href={it.key === "home" ? "index.html" : it.key === "contact" ? "index.html#contact" : `${it.key}.html`}
              aria-label={it.aria}
              className={visualKey === it.key ? "active" : ""}
              onMouseEnter={() => setHovered(it.key)}
              onClick={(e) => { e.preventDefault(); onNav(it.nav); }}
            >
              {it.label}
            </a>
          ))}
        </nav>
      </div>
    </header>
  );
}

// ── hero ────────────────────────────────────────────────────────────────
function ArrowDown() {
  return (
    <svg width="42" height="42" viewBox="0 0 42 42" fill="none"
      style={{ animation: "bob 2.4s ease-in-out infinite" }}>
      <circle cx="21" cy="21" r="20" stroke="currentColor" strokeOpacity="0.25" />
      <path d="M21 12 L21 30 M14 23 L21 30 L28 23"
        stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function Hero() {
  const [i, setI] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setI((n) => (n + 1) % HOME.hero.names.length), 2200);
    return () => clearInterval(id);
  }, []);
  const cur = HOME.hero.names[i];

  // Splits a string into <span class="word-reveal"> per word with a staggered
  // animation-delay. Each word reveals in sequence on first paint.
  const renderWords = (text, startMs, stepMs = 150) => {
    const ws = text.split(/\s+/);
    return ws.map((w, idx) => (
      <React.Fragment key={idx}>
        <span className="word-reveal" style={{ animationDelay: `${startMs + idx * stepMs}ms` }}>
          {w}
        </span>
        {idx < ws.length - 1 ? " " : ""}
      </React.Fragment>
    ));
  };

  return (
    <section className="pad-y" id="top" style={{ paddingTop: 80, paddingBottom: 56 }}>
      <div className="hero-grid" style={{
        display: "grid", gridTemplateColumns: "2fr 1fr", gap: 48, alignItems: "start",
      }}>
        <h1 className="h1" style={{ margin: 0, maxWidth: "none" }}>
          {renderWords(HOME.hero.greeting, 0)}
          {" "}
          <span key={i} className="word-reveal" style={{
            color: cur.color,
            animationDelay: "200ms",
          }}>
            {cur.word}
          </span>
          <br />
          {renderWords(HOME.hero.tagline, 300)}
        </h1>
        <div aria-hidden="true" />
      </div>
    </section>
  );
}

function HeroSideNote() {
  const scrollToWork = () => {
    const isMobile = window.innerWidth <= 800;
    const target = document.querySelector(isMobile ? ".hh-label" : ".hh-pic");
    if (!target) return;
    const rect = target.getBoundingClientRect();
    const absTop = rect.top + window.scrollY;
    const top = isMobile
      ? absTop - 90 // clear the sticky header, land on the "how I work" text
      : absTop - Math.max(0, (window.innerHeight - rect.height) / 2); // center the picture frame
    window.scrollTo({ top, behavior: "smooth" });
  };
  return (
    <div style={{ paddingBottom: 16 }}>
      <p className="body" style={{ fontSize: 15, lineHeight: 1.55, color: "var(--ink-soft)", margin: 0, maxWidth: "44ch" }}>
        {HOME.intro.text}
      </p>
      <button
        onClick={scrollToWork}
        aria-label="scroll to how I work"
        className="scroll-cue"
        style={{
          marginTop: 24, display: "inline-flex", alignItems: "center", gap: 10,
          background: "none", border: 0, padding: 0, cursor: "pointer",
          color: "var(--ink-soft)",
        }}
      >
        <span className="mono">{HOME.intro.scrollLabel}</span>
        <ArrowDown />
      </button>
    </div>
  );
}

function ImageDuo() {
  return (
    <section style={{ padding: "0 5vw 56px" }}>
      <div className="duo" style={{
        display: "grid", gridTemplateColumns: "2fr 1fr",
        gap: 16, height: "min(86vh, 880px)", alignItems: "stretch",
      }}>
        <div className="ph">
          <Pic src={HOME.intro.image.src} alt={HOME.intro.image.alt} />
        </div>
        <div className="duo-right" style={{
          display: "flex", flexDirection: "column", gap: 16, minHeight: 0,
        }}>
          <div style={{
            flex: "0 0 30%", display: "flex", flexDirection: "column", justifyContent: "center",
          }}>
            <HeroSideNote />
          </div>
          <div className="ph" style={{ flex: "1 1 auto", minHeight: 0 }}>
            <Pic src={HOME.intro.sideImage.src} alt={HOME.intro.sideImage.alt} />
          </div>
        </div>
      </div>
    </section>
  );
}

function HalfHalf() {
  return (
    <section style={{ padding: "0 5vw 96px" }}>
      <div className="hh">
        <div className="ph hh-pic">
          <video
            className="ph-img"
            src={HOME.howIWork.video}
            autoPlay
            loop
            muted
            playsInline
            preload="auto"
            style={{ objectFit: "cover" }}
          />
        </div>
        <p className="mono hh-label">{HOME.howIWork.label}</p>
        <p className="body hh-p1">{HOME.howIWork.paragraph1}</p>
        <p className="body hh-p2">{HOME.howIWork.paragraph2}</p>
      </div>
    </section>
  );
}

function ProjectBlock({ project, onOpen, index }) {
  return (
    <section className="proj" style={{ padding: "24px 5vw", borderTop: "1px solid var(--line)" }}>
      <div onClick={() => onOpen(project)} className="ph proj-canvas"
        role="button" tabIndex={0}
        onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpen(project); } }}
        style={{
          position: "relative", width: "100%", minHeight: "82vh",
          padding: 0, border: 0, textAlign: "left", cursor: "pointer", display: "block",
        }}>
        <Pic src={project.cover.src} alt={project.title} />
        <span className="lbl">{project.cover.label}</span>

        <span className="mono" style={{
          position: "absolute", top: 28, left: 28,
          color: "var(--bg)", mixBlendMode: "difference", opacity: 0.85,
        }}>
          {String(index + 1).padStart(2, "0")} / {project.year} · {project.role}
        </span>

        <div className="proj-overlay" style={{
          position: "absolute", left: "50%", bottom: "30%",
          transform: "translate(-50%, 50%)",
          width: "calc(100% - clamp(40px, 8%, 112px))",
          display: "flex", alignItems: "flex-end", justifyContent: "space-between",
          gap: 32, padding: 0,
          background: "transparent", border: 0,
          pointerEvents: "auto",
        }}>
          <div style={{ display: "flex", flexDirection: "column", gap: 18, maxWidth: 720 }}>
            {/* logo is rendered OUTSIDE the mix-blend layer so it stays in its real colors */}
            {project.logoImage ? (
              <img src={project.logoImage} alt={`${project.title} logo`} style={{
                height: 72, width: "auto", maxWidth: 320, display: "block",
                objectFit: "contain",
                paddingRight: project.logoPadRight || 0,
              }} />
            ) : (
              <div style={{
                display: "inline-flex", alignItems: "center", justifyContent: "center",
                width: 72, height: 72, borderRadius: 20,
                border: "1px solid var(--bg)",
                color: "var(--bg)", mixBlendMode: "difference",
                fontFamily: "Open Sans", fontWeight: 600, fontSize: 28, letterSpacing: "-0.02em",
              }}>
                {project.logoMark}
              </div>
            )}
            {/* text stack is blended so it stays readable on any cover.
                Title is hidden when a logo image stands in for it. */}
            <div style={{
              display: "flex", flexDirection: "column", gap: 18,
              color: "var(--bg)", mixBlendMode: "difference",
            }}>
              {!project.logoImage && (
                <h2 className="h1" style={{ fontSize: "clamp(36px, 4.4vw, 64px)", margin: 0, color: "inherit" }}>
                  {project.title}
                </h2>
              )}
              <p className="body" style={{ fontSize: 18, lineHeight: 1.4, maxWidth: 560, margin: 0, color: "inherit" }}>
                {project.outcome}
              </p>
            </div>
          </div>
          <button onClick={(e) => { e.stopPropagation(); onOpen(project); }}
            className="mono view-work" style={{
              display: "inline-flex", alignItems: "center", gap: 12,
              padding: "16px 26px",
              border: "1px solid var(--bg)", borderRadius: 999,
              transition: "background 200ms ease, color 200ms ease",
              flexShrink: 0, background: "transparent",
              color: "var(--bg)", mixBlendMode: "difference",
              whiteSpace: "nowrap",
            }}
            onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(128, 128, 128, 0.55)"; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}>
            {UI.viewWork} <span aria-hidden="true">→</span>
          </button>
        </div>
      </div>
    </section>
  );
}

function WorkPage({ onOpen }) {
  return (
    <section style={{ padding: "80px 5vw 96px" }}>
      <div style={{
        display: "flex", justifyContent: "space-between", alignItems: "baseline",
        marginBottom: 48, flexWrap: "wrap", gap: 16,
      }}>
        <h1 className="h1" style={{ fontSize: "clamp(40px, 6vw, 80px)" }}>{WORK.title}</h1>
        <span className="mono" style={{ color: "var(--ink-soft)" }}>
          {fill(WORK.countLabel, { count: PROJECTS.length, s: PROJECTS.length === 1 ? "" : "s" })}
        </span>
      </div>
      <div className="work-grid" style={{
        display: "grid",
        gridTemplateColumns: "repeat(auto-fill, minmax(360px, 1fr))",
        gap: "32px 24px",
      }}>
        {PROJECTS.map((p, i) => (
          <article key={p.id} className="work-card"
            role="button" tabIndex={0}
            onClick={() => onOpen(p)}
            onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpen(p); } }}
            style={{ cursor: "pointer", display: "block" }}>
            <div className="ph" style={{ aspectRatio: "4 / 3", marginBottom: 16 }}>
              <Pic src={p.cover.src} alt={p.title} />
              <span className="lbl">{p.cover.label}</span>
            </div>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 16 }}>
              <h2 className="h2" style={{ fontSize: 22, fontWeight: 500 }}>{p.title}</h2>
              <span className="mono" style={{ color: "var(--ink-soft)", fontSize: 12, whiteSpace: "nowrap" }}>
                {String(i + 1).padStart(2, "0")} / {p.year}
              </span>
            </div>
            <p className="mono" style={{ color: "var(--ink-soft)", fontSize: 12, marginTop: 6 }}>{p.tag}</p>
            <p className="body" style={{ fontSize: 15, lineHeight: 1.5, marginTop: 10, color: "var(--ink-soft)" }}>
              {p.outcome}
            </p>
          </article>
        ))}
      </div>
    </section>
  );
}

function BigButton({ label, sub, height = "16.6vh", onClick }) {
  return (
    <button className="bigbtn" onClick={onClick} style={{ minHeight: height }}>
      <span className="h1" style={{ fontSize: "clamp(36px, 5vw, 64px)", fontWeight: 600 }}>{label}</span>
      <span style={{ display: "flex", alignItems: "center", gap: 24 }}>
        {sub && <span className="mono" style={{ color: "currentColor", opacity: 0.6 }}>{sub}</span>}
        <svg className="arr" width="42" height="42" viewBox="0 0 42 42" fill="none">
          <circle cx="21" cy="21" r="20" stroke="currentColor" strokeOpacity="0.4" />
          <path d="M14 21 H30 M23 14 L30 21 L23 28"
            stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </span>
    </button>
  );
}

// ── about page (separate /about.html) ───────────────────────────────────
function AboutPage() {
  return (
    <>
      {/* hero: intro on the left, picture on the right */}
      <section className="pad-y" style={{ paddingTop: 80, paddingBottom: 56 }}>
        <div className="about-hero" style={{
          display: "grid",
          gridTemplateColumns: "1.3fr 1fr",
          gap: 48,
          alignItems: "center",
        }}>
          <div>
            <p className="mono" style={{ color: "var(--ink-soft)", margin: "0 0 24px" }}>{ABOUT.label}</p>
            <h1 className="h1" style={{ margin: 0 }}>
              {ABOUT.title}{" "}
              <span style={{ color: "#ff4a1c" }}>{ABOUT.titleAccent}</span>{ABOUT.titleEnd}
            </h1>
            <p className="body" style={{ fontSize: 22, lineHeight: 1.4, margin: "32px 0 16px", maxWidth: 640 }}>
              {ABOUT.intro}
            </p>
            <p className="body" style={{ fontSize: 16, lineHeight: 1.65, color: "var(--ink-soft)", margin: 0, maxWidth: 640 }}>
              {ABOUT.blurb}
            </p>
          </div>
          <div className="ph about-pic" style={{ aspectRatio: "4 / 5" }}>
            <Pic src={ABOUT.image.src} alt={ABOUT.image.alt} />
          </div>
        </div>
      </section>

      {/* skills */}
      <section style={{ padding: "64px 5vw", borderTop: "1px solid var(--line)" }}>
        <div className="about-row" style={{
          display: "grid", gridTemplateColumns: "1fr 2fr", gap: 80, alignItems: "start",
        }}>
          <div>
            <p className="mono" style={{ color: "var(--ink-soft)", margin: 0 }}>{ABOUT.skills.label}</p>
            <p className="body" style={{ fontSize: 14, lineHeight: 1.55, color: "var(--ink-soft)", marginTop: 12, maxWidth: 280 }}>
              {ABOUT.skills.description}
            </p>
          </div>
          <div style={{
            display: "grid",
            gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
            gap: 32,
          }}>
            {ABOUT.skills.groups.map((s) => (
              <div key={s.group}>
                <p className="mono" style={{ color: "var(--ink)", margin: "0 0 12px" }}>
                  {s.group}
                </p>
                <ul style={{
                  listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 6,
                }}>
                  {s.items.map((item) => (
                    <li key={item} className="body" style={{ fontSize: 15, color: "var(--ink-soft)" }}>
                      {item}
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </div>
      </section>

      {/* experience */}
      <section style={{ padding: "64px 5vw", borderTop: "1px solid var(--line)" }}>
        <div className="about-row" style={{
          display: "grid", gridTemplateColumns: "1fr 2fr", gap: 80, alignItems: "start",
        }}>
          <div>
            <p className="mono" style={{ color: "var(--ink-soft)", margin: 0 }}>{ABOUT.experience.label}</p>
            <p className="body" style={{ fontSize: 14, lineHeight: 1.55, color: "var(--ink-soft)", marginTop: 12, maxWidth: 280 }}>
              {ABOUT.experience.description}
            </p>
          </div>
          <div>
            {ABOUT.experience.items.map((e, i) => (
              <div key={i} className="exp-item" style={{
                padding: "24px 0",
                borderTop: i === 0 ? "none" : "1px solid var(--line)",
              }}>
                <h3 className="h2" style={{ fontSize: 22, fontWeight: 500, margin: 0 }}>
                  {e.role} · <span style={{ color: "var(--ink-soft)" }}>{e.org}</span>
                </h3>
                <p className="body" style={{ fontSize: 15, lineHeight: 1.55, color: "var(--ink-soft)", margin: "8px 0 0", maxWidth: 640 }}>
                  {e.note}
                </p>
              </div>
            ))}
          </div>
        </div>
      </section>

      {/* side hustles */}
      <section style={{ padding: "64px 5vw 96px", borderTop: "1px solid var(--line)" }}>
        <div className="about-row" style={{
          display: "grid", gridTemplateColumns: "1fr 2fr", gap: 80, alignItems: "start",
        }}>
          <div>
            <p className="mono" style={{ color: "var(--ink-soft)", margin: 0 }}>{ABOUT.sideHustles.label}</p>
            <p className="body" style={{ fontSize: 14, lineHeight: 1.55, color: "var(--ink-soft)", marginTop: 12, maxWidth: 280 }}>
              {ABOUT.sideHustles.description}
            </p>
          </div>
          <ul style={{
            listStyle: "none", padding: 0, margin: 0,
            display: "flex", flexWrap: "wrap", gap: 12,
          }}>
            {ABOUT.sideHustles.items.map((h, i) => (
              <li key={i} className="mono" style={{
                fontSize: 13,
                padding: "10px 16px",
                border: "1px solid var(--line)",
                borderRadius: 999,
                color: "var(--ink)",
              }}>
                {h}
              </li>
            ))}
          </ul>
        </div>
      </section>
    </>
  );
}

function About() {
  return (
    <section id="about" className="about" style={{
      padding: "120px 5vw", borderTop: "1px solid var(--line)",
      display: "grid", gridTemplateColumns: "1fr 1.5fr",
      gap: 80, alignItems: "start",
    }}>
      <p className="mono" style={{ color: "var(--ink-soft)", margin: 0 }}>{HOME.aboutTeaser.label}</p>
      <div style={{ maxWidth: 680 }}>
        <p className="body" style={{ fontSize: 26, lineHeight: 1.4, marginTop: 0 }}>
          {ABOUT.intro}
        </p>
        <p className="body" style={{ fontSize: 16, lineHeight: 1.65, color: "var(--ink-soft)" }}>
          {ABOUT.blurb}
        </p>
        <a href="about.html"
          onClick={(e) => { e.preventDefault(); goRoute("about"); }}
          className="mono learn-more"
          style={{
            display: "inline-flex", alignItems: "center", gap: 12,
            marginTop: 32, padding: "14px 22px",
            border: "1px solid var(--ink)", borderRadius: 999,
            color: "var(--ink)", background: "transparent",
            transition: "background 200ms ease, color 200ms ease",
          }}
          onMouseEnter={(e) => { e.currentTarget.style.background = "var(--ink)"; e.currentTarget.style.color = "var(--bg)"; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = "var(--ink)"; }}
        >
          {HOME.aboutTeaser.linkLabel} <span aria-hidden="true">→</span>
        </a>
      </div>
    </section>
  );
}

// Icons available to content.footer.elsewhereLinks[].icon. Any other value
// (or an empty one) renders the link without an icon.
const SOCIAL_ICONS = {
  linkedin: (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style={{ flexShrink: 0 }}>
      <path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.34V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14zM7.12 20.45H3.55V9h3.57v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.22.79 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z"/>
    </svg>
  ),
  instagram: (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true" style={{ flexShrink: 0 }}>
      <rect x="2" y="2" width="20" height="20" rx="5.5" />
      <circle cx="12" cy="12" r="4.2" />
      <circle cx="17.6" cy="6.4" r="1.2" fill="currentColor" stroke="none" />
    </svg>
  ),
};

function SiteFooter() {
  return (
    <footer className="site" id="contact">
      <div className="grid">
        <div>
          <div className="mark">{FOOTER.mark}</div>
          <p className="mono" style={{ marginTop: 24, color: "color-mix(in oklab, var(--bg) 70%, transparent)", maxWidth: 320 }}>
            {`${SITE.email}  ·  ${SITE.location}`}
          </p>
        </div>
        <div>
          <h4>{FOOTER.indexTitle}</h4>
          <ul>
            {FOOTER.indexLinks.map((l, i) => (
              <li key={i}><a href={(ROUTES[l.target] || ROUTES.home).url} onClick={(e) => { e.preventDefault(); goRoute(l.target); }}>{l.label}</a></li>
            ))}
          </ul>
        </div>
        <div>
          <h4>{FOOTER.elsewhereTitle}</h4>
          <ul>
            {FOOTER.elsewhereLinks.map((l, i) => (
              <li key={i}>
                <a href={l.url} target="_blank" rel="noopener noreferrer"
                  style={{ display: "inline-flex", alignItems: "center", gap: 10 }}>
                  {SOCIAL_ICONS[l.icon] || null}
                  {l.label}
                </a>
              </li>
            ))}
            {FOOTER.elsewhereNote && <li><em>{FOOTER.elsewhereNote}</em></li>}
          </ul>
        </div>
        <div>
          <h4>{FOOTER.buildingTitle}</h4>
          <ul>
            {FOOTER.buildingItems.map((item, i) => <li key={i}>{item}</li>)}
          </ul>
        </div>
      </div>
      <div className="meta">
        <span>{fill(FOOTER.copyright, { year: new Date().getFullYear() })}</span>
        <span>{FOOTER.lastUpdated}</span>
      </div>
    </footer>
  );
}

function Meta({ label, value }) {
  return (
    <div>
      <div className="mono" style={{ color: "var(--ink-soft)", marginBottom: 8 }}>{label}</div>
      <div className="body" style={{ fontSize: 14 }}>{value}</div>
    </div>
  );
}

function ModalBody({ project, onClose }) {
  return (
    <div style={{ padding: "0 clamp(24px, 5vw, 64px) 64px" }}>
      <div style={{
        display: "flex", justifyContent: "space-between", alignItems: "center",
        paddingTop: 32, paddingBottom: 24, borderBottom: "1px solid var(--line)",
        position: "sticky", top: 0, background: "var(--bg)", zIndex: 2, marginBottom: 32,
      }}>
        <span className="mono" style={{ color: "var(--ink-soft)" }}>{project.tag} · {project.year}</span>
        <button onClick={onClose} className="mono" style={{
          border: "1px solid var(--line)", padding: "8px 16px",
          borderRadius: 999, background: "var(--bg)",
        }}>{UI.close}</button>
      </div>

      <h2 className="h1" style={{ marginBottom: 12, fontSize: "clamp(40px, 6vw, 80px)" }}>{project.title}</h2>
      <p className="body" style={{ fontSize: 22, lineHeight: 1.35, maxWidth: 760, marginTop: 0, color: "var(--ink)" }}>
        {project.outcome}
      </p>
      {project.link && project.link.url && (
        <a href={project.link.url} target="_blank" rel="noopener noreferrer" className="mono" style={{
          display: "inline-flex", alignItems: "center", gap: 6, marginTop: 14,
          color: "var(--accent)", borderBottom: "1px solid color-mix(in oklab, var(--accent) 40%, transparent)",
          paddingBottom: 1,
        }}>
          {project.link.label} <span aria-hidden="true">↗</span>
        </a>
      )}

      <div style={{
        display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
        gap: 32, margin: "48px 0", paddingTop: 32, borderTop: "1px solid var(--line)",
      }}>
        <Meta label={UI.modalRole} value={project.role} />
        <Meta label={UI.modalYear} value={project.year} />
        <Meta label={UI.modalCollaborators} value={
          <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 4 }}>
            {project.collaborators.map((c) => (
              <li key={c.name} className="body" style={{ fontSize: 14 }}>
                {c.name} <span style={{ color: "var(--ink-soft)" }}>— {c.role}</span>
              </li>
            ))}
          </ul>
        } />
        <Meta label={UI.modalProcesses} value={
          <ul style={{ listStyle: "none", padding: 0, margin: 0, display: "grid", gap: 4 }}>
            {project.processes.map((d) => (
              <li key={d} className="body" style={{ fontSize: 14 }}>{d}</li>
            ))}
          </ul>
        } />
      </div>

      {!project.hideCoverInModal && (
        <div className="ph" style={{ aspectRatio: "16 / 9", marginBottom: 48 }}>
          <Pic src={project.cover.src} alt={project.title} />
          <span className="lbl">{project.cover.label}</span>
        </div>
      )}

      <div style={{ maxWidth: 720, margin: "0 0 48px" }}>
        <p className="mono" style={{ color: "var(--ink-soft)", marginBottom: 16 }}>{UI.modalSummary}</p>
        <p className="body" style={{ fontSize: 18, lineHeight: 1.6, marginBottom: 32 }}>{project.summary}</p>
        {project.body.map((para, i) => (
          <p key={i} className="body" style={{ fontSize: 16, lineHeight: 1.65 }}>{para}</p>
        ))}
      </div>

      {project.features && project.features.length > 0 && (
        <div style={{ marginBottom: 56 }}>
          <p className="mono" style={{ color: "var(--ink-soft)", marginBottom: 24 }}>{UI.modalInstallations}</p>
          <div style={{ display: "flex", flexDirection: "column", gap: 64 }}>
            {project.features.map((f, i) => (
              <div key={i} style={{ maxWidth: 860 }}>
                <h3 className="h2" style={{ fontSize: 26, fontWeight: 500, margin: "0 0 12px" }}>{f.title}</h3>
                <p className="body" style={{ fontSize: 16, lineHeight: 1.65, color: "var(--ink-soft)", margin: "0 0 24px", maxWidth: 680 }}>
                  {f.blurb}
                </p>
                <div className="feature-media">
                  {f.video && (
                    <video
                      className="feature-item"
                      src={f.video}
                      autoPlay loop muted playsInline preload="auto"
                      style={{ height: "auto", display: "block", borderRadius: "var(--radius)" }}
                    />
                  )}
                  {f.image && (
                    <div className="ph feature-item" style={{ aspectRatio: "4 / 3" }}>
                      <Pic src={f.image} alt={f.imageLabel || f.title} />
                      {f.imageLabel && <span className="lbl">{f.imageLabel}</span>}
                    </div>
                  )}
                  {f.drawing && (
                    <div className="feature-item" style={{
                      background: "#fff",
                      border: "1px solid var(--line)",
                      borderRadius: "var(--radius)",
                      overflow: "hidden",
                    }}>
                      <img src={f.drawing} alt={f.title} style={{ width: "100%", height: "auto", display: "block" }} />
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
          <style>{`
            .feature-media {
              display: flex;
              flex-wrap: wrap;
              gap: 16px;
              align-items: flex-start;
            }
            .feature-media .feature-item {
              flex: 1 1 260px;
              max-width: 420px;
              width: 100%;
            }
            @media (max-width: 800px) {
              .feature-media .feature-item {
                flex-basis: 100%;
                max-width: 72%;
                margin-left: auto;
                margin-right: auto;
              }
            }
          `}</style>
        </div>
      )}

      <p className="mono" style={{ color: "var(--ink-soft)", marginBottom: 16 }}>{project.processLabel || UI.modalProcess}</p>
      {(() => {
        // Two independent flex columns. Right column starts offset down so the
        // pair reads like staggered steps, not aligned rows. Caption lengths
        // create further natural drift.
        const offsets = [0, 28, 12, 40, 8, 24];
        const left  = project.process.filter((_, i) => i % 2 === 0);
        const right = project.process.filter((_, i) => i % 2 === 1);
        const renderItem = (p, globalIdx, colIdx) => (
          <figure key={globalIdx} style={{
            margin: 0,
            marginTop: colIdx === 0 ? 0 : offsets[colIdx % offsets.length],
          }}>
            <div className="ph" style={{ aspectRatio: "4 / 3" }}>
              <Pic src={p.src} alt={p.label} />
            </div>
            <figcaption className="mono" style={{
              color: "var(--ink-soft)", marginTop: 14, fontSize: 13, lineHeight: 1.55,
              maxWidth: "48ch",
            }}>
              <span style={{ color: "var(--ink)" }}>{project.processUseLabels ? p.label : String(globalIdx + 1).padStart(2, "0")}</span>
              {"  ·  "}
              {p.note && p.note !== "—" ? p.note : UI.descriptionToCome}
            </figcaption>
          </figure>
        );
        return (
          <div className="proj-process" style={{
            display: "grid", gridTemplateColumns: "1fr 1fr", gap: 32,
          }}>
            <div style={{ display: "flex", flexDirection: "column", gap: 48 }}>
              {left.map((p, ci) => renderItem(p, ci * 2, ci))}
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 48, marginTop: 80 }}>
              {right.map((p, ci) => renderItem(p, ci * 2 + 1, ci))}
            </div>
          </div>
        );
      })()}
      <style>{`
        @media (max-width: 800px) {
          .proj-process { grid-template-columns: 1fr !important; gap: 24px !important; }
          .proj-process > div { margin-top: 0 !important; gap: 24px !important; }
          .proj-process figure { margin-top: 0 !important; }
        }
      `}</style>
    </div>
  );
}

function ContactModal({ open, onClose }) {
  const [revealed, setRevealed] = useState(false);

  // lock body scroll + Escape closes
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [open, onClose]);

  // Reset email reveal whenever the modal closes.
  useEffect(() => { if (!open) setRevealed(false); }, [open]);

  return (
    <div className={"modal-back" + (open ? " open" : "")} onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}
        style={{ maxWidth: 720 }}>
        <div className="modal-scroll" style={{ padding: "0 clamp(24px, 5vw, 64px) 56px" }}>
          {/* top bar */}
          <div style={{
            display: "flex", justifyContent: "space-between", alignItems: "center",
            paddingTop: 32, paddingBottom: 24, borderBottom: "1px solid var(--line)",
            position: "sticky", top: 0, background: "var(--bg)", zIndex: 2, marginBottom: 32,
          }}>
            <span className="mono" style={{ color: "var(--ink-soft)" }}>{CONTACT.label}</span>
            <button onClick={onClose} className="mono" style={{
              border: "1px solid var(--line)", padding: "8px 16px",
              borderRadius: 999, background: "var(--bg)",
            }}>{UI.close}</button>
          </div>

          {/* title + intro */}
          <h2 className="h1" style={{ marginBottom: 16, fontSize: "clamp(36px, 5vw, 64px)" }}>
            {CONTACT.title}
          </h2>
          <p className="body" style={{
            fontSize: 18, lineHeight: 1.45, color: "var(--ink-soft)",
            margin: "0 0 40px", maxWidth: 540,
          }}>
            {CONTACT.intro}
          </p>

          {/* socials list */}
          <p className="mono" style={{ color: "var(--ink-soft)", marginBottom: 8 }}>{CONTACT.socialsLabel}</p>
          <ul style={{ listStyle: "none", padding: 0, margin: "0 0 40px" }}>
            {CONTACT.socials.map((s, i) => (
              <li key={s.label}>
                <a href={s.url} target="_blank" rel="noopener noreferrer"
                  className="contact-row"
                  style={{
                    display: "flex", justifyContent: "space-between", alignItems: "center",
                    padding: "16px 0",
                    borderTop: "1px solid var(--line)",
                    borderBottom: i === CONTACT.socials.length - 1 ? "1px solid var(--line)" : "none",
                    transition: "padding-left 200ms ease, color 200ms ease",
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.paddingLeft = "8px"; e.currentTarget.style.color = "var(--accent)"; }}
                  onMouseLeave={(e) => { e.currentTarget.style.paddingLeft = "0"; e.currentTarget.style.color = "inherit"; }}
                >
                  <span className="body" style={{ fontSize: 18 }}>{s.label}</span>
                  <span className="mono" style={{ fontSize: 13, color: "var(--ink-soft)" }}>
                    {s.handle} <span aria-hidden="true">→</span>
                  </span>
                </a>
              </li>
            ))}
          </ul>

          {/* email button + reveal */}
          <p className="mono" style={{ color: "var(--ink-soft)", marginBottom: 12 }}>{CONTACT.emailLabel}</p>
          <a
            href={`mailto:${SITE.email}`}
            onClick={() => setRevealed(true)}
            className="mono"
            style={{
              display: "inline-flex", alignItems: "center", gap: 14,
              padding: "16px 24px",
              border: "1px solid var(--ink)", borderRadius: 999,
              transition: "background 200ms ease, color 200ms ease",
            }}
            onMouseEnter={(e) => { e.currentTarget.style.background = "var(--ink)"; e.currentTarget.style.color = "var(--bg)"; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = "var(--ink)"; }}
          >
            {CONTACT.emailButton} <span aria-hidden="true">→</span>
          </a>
          <p className="mono" style={{
            marginTop: 16, color: "var(--ink-soft)",
            opacity: revealed ? 1 : 0,
            transform: revealed ? "translateY(0)" : "translateY(-4px)",
            transition: "opacity 240ms ease, transform 240ms ease",
            // keep the layout space even when hidden so the modal doesn't jump
            minHeight: "1.4em",
          }}>
            {revealed ? SITE.email : ""}
          </p>
        </div>
      </div>
    </div>
  );
}

function ProjectModal({ project, onClose }) {
  const open = Boolean(project);
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [open, onClose]);
  return (
    <div className={"modal-back" + (open ? " open" : "")} onClick={onClose}>
      <div className="modal-card" onClick={(e) => e.stopPropagation()}>
        <div className="modal-scroll">
          {project && <ModalBody project={project} onClose={onClose} />}
        </div>
      </div>
    </div>
  );
}

// ── app ─────────────────────────────────────────────────────────────────
function pageFromLocation() {
  const path = (window.location.pathname || "").toLowerCase();
  if (path.indexOf("work.html") !== -1) return "work";
  if (path.indexOf("about.html") !== -1) return "about";
  return "home";
}

function App() {
  const initial = document.body.dataset.page;
  const initialPage = initial === "work" || initial === "about" ? initial : "home";
  const [page, setPage] = useState(initialPage);
  const [openProject, setOpenProject] = useState(null);
  const [contactOpen, setContactOpen] = useState(false);

  // Hide the first-load overlay once React has mounted (covers the one-time
  // Babel compile). Client-side navigation never shows it again.
  useEffect(() => {
    if (window.PageLoader) window.PageLoader.hide();
    document.title = pageTitle(initialPage);
    // Seed history state so back/forward has something to read.
    try { window.history.replaceState({ page: initialPage }, "", ROUTES[initialPage].url); } catch (e) {}
  }, []);

  // Client-side navigate: swap the view, push history, no document reload.
  const navigateTo = React.useCallback((target) => {
    const r = ROUTES[target];
    if (!r) return;
    setOpenProject(null);
    setContactOpen(false);
    setPage((cur) => {
      if (cur === target) {
        window.scrollTo({ top: 0, behavior: "smooth" });
        return cur;
      }
      try { window.history.pushState({ page: target }, "", r.url); } catch (e) {}
      document.title = pageTitle(target);
      window.scrollTo({ top: 0 });
      return target;
    });
  }, []);

  // Register as the global route handler + wire back/forward.
  useEffect(() => {
    __routeNavigate = navigateTo;
    const onPop = () => {
      const p = (window.history.state && window.history.state.page) || pageFromLocation();
      setOpenProject(null);
      setContactOpen(false);
      setPage(p);
      document.title = pageTitle(p);
    };
    window.addEventListener("popstate", onPop);
    return () => {
      window.removeEventListener("popstate", onPop);
      if (__routeNavigate === navigateTo) __routeNavigate = null;
    };
  }, [navigateTo]);

  // Honour any #anchor in the URL when the home view is shown (e.g. arriving
  // from another page via an "about" / "contact" link).
  useEffect(() => {
    if (page !== "home") return;
    const id = window.location.hash.slice(1);
    if (!id) return;
    const el = document.getElementById(id);
    if (!el) return;
    setTimeout(() => {
      window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 40, behavior: "smooth" });
    }, 60);
  }, [page]);

  const onNav = (target) => {
    if (target === "contact") { setContactOpen(true); return; }
    navigateTo(target);
  };

  return (
    <>
      <Header onNav={onNav} page={page} />

      <div className="route-view" key={page}>
        {page === "work" ? (
          <WorkPage onOpen={setOpenProject} />
        ) : page === "about" ? (
          <AboutPage />
        ) : (
          <>
            <Hero />
            <ImageDuo />
            <HalfHalf />
            <div id="work">
              {PROJECTS.slice(0, HOME.featuredCount).map((p, i) => (
                <ProjectBlock key={p.id} project={p} index={i} onOpen={setOpenProject} />
              ))}
            </div>
            <BigButton label={HOME.viewAllButton.label}
              sub={fill(HOME.viewAllButton.sub, { count: PROJECTS.length, s: PROJECTS.length === 1 ? "" : "s" })}
              onClick={() => onNav("work")} />
          </>
        )}

        <BigButton label={UI.connectButton} sub={SITE.email} height="12vh"
          onClick={() => onNav("contact")} />

        {page === "home" && <About />}
        <SiteFooter />
      </div>

      <ProjectModal project={openProject} onClose={() => setOpenProject(null)} />
      <ContactModal open={contactOpen} onClose={() => setContactOpen(false)} />
    </>
  );
}

// Render once content.json has arrived. loader.js starts that request in
// <head>, so it downloads in parallel with Babel compiling this file.
const root = ReactDOM.createRoot(document.getElementById("root"));
(window.__contentPromise || fetch("content.json", { cache: "no-cache" }).then((r) => r.json()))
  .then((content) => {
    applyContent(content);
    root.render(<App />);
  })
  .catch((err) => {
    console.error("content.json failed to load", err);
    if (window.PageLoader) window.PageLoader.hide();
    document.getElementById("root").innerHTML =
      '<p class="mono" style="padding:80px 5vw">Content failed to load. Please refresh.</p>';
  });
