/* ===========================================================================
   Reimagined portfolio — single-scroll narrative.
   Hero · Atlas Camera flagship · Selected Work · Project detail · About/Contact.
   Reuses window.PROJECTS + window.ATLAS from data.js.
   =========================================================================== */
const { useState, useEffect, useRef, useCallback } = React;

const PROJECTS = window.PROJECTS;
const ATLAS = window.ATLAS;
const MAIL = "mike@mikejamesvfx.com";
const PORTFOLIO = "https://mikejamesvfx.com";
const IMDB = "https://www.imdb.com/name/nm3389908/";

/* ---- scroll reveal ---- */
function useReveal() {
  useEffect(() => {
    const els = document.querySelectorAll(".reveal");
    const io = new IntersectionObserver(
      (entries) => entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }),
      { threshold: 0.14, rootMargin: "0px 0px -8% 0px" }
    );
    els.forEach((el) => io.observe(el));
    return () => io.disconnect();
  });
}

/* graceful media: hide element if it fails to load */
const hideOnError = (e) => { e.target.style.display = "none"; };

/* treat these as looping video tiles; everything else renders as an image */
const isVideo = (src) => /\.(mp4|webm|mov|m4v)$/i.test(src || "");

/* ===========================================================================
   NAV
   =========================================================================== */
function Nav({ onOpenAbout }) {
  const [scrolled, setScrolled] = useState(false);
  const [open, setOpen] = useState(false);
  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 40);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  const link = (href, label) => (
    <a href={href} onClick={() => setOpen(false)}>{label}</a>
  );
  return (
    <header className={`nav ${scrolled ? "scrolled" : ""}`}>
      <a className="nav-brand" href="#top">
        <span className="mj">MJ</span>
        <span>Mike James</span>
        <span className="sub">VFX — CREATIVE TECH</span>
      </a>
      <button className="nav-burger" aria-label="Menu" onClick={() => setOpen((v) => !v)}>
        <span></span><span></span><span></span>
      </button>
      <nav className={`nav-links ${open ? "open" : ""}`}>
        {link("#atlas", "Atlas Camera")}
        {link("#work", "Work")}
        <a href="#about" onClick={() => setOpen(false)}>About</a>
        <a className="nav-cta" href={`mailto:${MAIL}`} onClick={() => setOpen(false)}>Contact</a>
      </nav>
    </header>
  );
}

/* ===========================================================================
   HERO
   =========================================================================== */
function Hero() {
  const h = window.HERO || {};
  const heroVideo = h.video || PROJECTS[0].heroVideo;
  const heroPoster = h.poster || PROJECTS[0].heroImage;
  return (
    <section className="hero" id="top">
      <div className="hero-media">
        <video src={heroVideo} poster={heroPoster} autoPlay muted loop playsInline />
      </div>
      <div className="hero-hud" aria-hidden="true">
        <div className="grid"></div>
        <div className="tick tl">LAT —33.86 · LON 151.20<br/>FOV 42.0° · f/2.0</div>
        <div className="tick tr">REC ●<br/>2.39 : 1</div>
        <div className="cross"></div>
      </div>
      <div className="hero-inner wrap">
        <div className="hero-eyebrow reveal in">
          <span className="rule"></span>
          <span className="kicker"><span className="dot">●</span> Sydney · Remote · Available 2026</span>
        </div>
        <h1 className="reveal in">
          Mike James<br/>
          <span className="line2">Visual effects &amp;<br/>creative technology.</span>
        </h1>
        <p className="hero-sub reveal in d1">
          Twenty-five years making <b>visual effects</b> for film and advertising — environments,
          matte painting, compositing, and virtual production — and now building the <b>tools and AI
          pipelines</b> that carry a real camera through generative work and back onto the shot.
        </p>
        <div className="hero-roles reveal in d2">
          <span>Visual Effects Artist</span>
          <span>Environments · VP · Comp</span>
          <span className="hot">Creative Technologist</span>
        </div>
      </div>
      <a className="hero-scroll" href="#atlas" aria-label="Scroll">
        <span>SCROLL</span>
        <span className="bar"></span>
      </a>
    </section>
  );
}

/* ===========================================================================
   ATLAS CAMERA — animated hyperspace warp canvas
   A chrome capital "A" recedes into a vanishing point through magenta/cyan
   warp-speed star streaks. Retro Buck-Rogers travel aesthetic.
   =========================================================================== */
// A-glyph strokes (apex-legs + crossbar), centered at origin, half-extents.
const A_STROKES = [
  [0, -70, -58, 70],
  [0, -70, 58, 70],
  [-34.8, 14, 34.8, 14],
];

function WarpCanvas() {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    let raf, w, h, dpr, cx, cy, maxR;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    const resize = () => {
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      const r = canvas.getBoundingClientRect();
      w = r.width; h = r.height;
      canvas.width = w * dpr; canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      cx = w / 2; cy = h * 0.46; maxR = Math.hypot(w, h) * 0.62;
    };
    resize();
    window.addEventListener("resize", resize);

    // warp starfield
    const N = 150;
    const seed = (atCenter) => ({
      a: Math.random() * Math.PI * 2,
      r: atCenter ? Math.random() * 14 + 2 : Math.random() * maxR,
      pr: 0,
      sp: Math.random() * 0.9 + 0.35,
      hue: Math.random() < 0.5 ? "c" : "m",
    });
    const stars = Array.from({ length: N }, () => seed(false));

    // A echoes: phase 0 (front/big) -> 1 (far/tiny)
    const M = 6;
    const echoes = Array.from({ length: M }, (_, i) => i / M);

    const drawA = (scale, alpha, style, lw) => {
      if (alpha <= 0.01) return;
      ctx.save();
      ctx.translate(cx, cy);
      ctx.scale(scale, scale);
      ctx.globalAlpha = alpha;
      ctx.strokeStyle = style;
      ctx.lineWidth = lw / scale;
      ctx.lineCap = "round";
      ctx.lineJoin = "round";
      ctx.beginPath();
      for (const s of A_STROKES) { ctx.moveTo(s[0], s[1]); ctx.lineTo(s[2], s[3]); }
      ctx.stroke();
      ctx.restore();
    };

    const frontScale = () => (Math.min(w, h) / 140) * 0.92;

    const render = (advance) => {
      // motion-blur wash instead of clear -> streak trails
      ctx.globalCompositeOperation = "source-over";
      ctx.globalAlpha = 1;
      ctx.fillStyle = "rgba(8,10,18,0.34)";
      ctx.fillRect(0, 0, w, h);

      // streaks (additive glow)
      ctx.globalCompositeOperation = "lighter";
      for (const s of stars) {
        s.pr = s.r;
        if (advance) s.r += s.sp * (1 + s.r * 0.012);
        if (s.r > maxR) { Object.assign(s, seed(true)); }
        const near = s.r / maxR;
        const x1 = cx + Math.cos(s.a) * s.pr, y1 = cy + Math.sin(s.a) * s.pr;
        const x2 = cx + Math.cos(s.a) * s.r, y2 = cy + Math.sin(s.a) * s.r;
        ctx.strokeStyle = s.hue === "c"
          ? `rgba(88,182,230,${0.12 + near * 0.6})`
          : `rgba(255,95,180,${0.10 + near * 0.5})`;
        ctx.lineWidth = 0.6 + near * 1.9;
        ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
      }

      // A echoes receding into the vanishing point
      ctx.globalCompositeOperation = "lighter";
      const fs = frontScale();
      for (let i = 0; i < M; i++) {
        if (advance) echoes[i] = (echoes[i] + 0.0042) % 1;
        const p = echoes[i];
        const scale = fs * (Math.pow(1 - p, 1.7) * 0.95 + 0.03);
        const alpha = Math.min(1, (1 - p) * 1.5) * (0.22 + 0.78 * (1 - p));
        let style;
        if (p < 0.55) {
          // chrome: magenta -> white -> cyan, top to bottom of the glyph
          const g = ctx.createLinearGradient(cx, cy - 70 * scale, cx, cy + 70 * scale);
          g.addColorStop(0, "#ff6ec7");
          g.addColorStop(0.5, "#ffffff");
          g.addColorStop(1, "#58b6e6");
          style = g;
        } else {
          style = `rgba(88,182,230,${alpha})`;
        }
        drawA(scale, alpha, style, 5.2);
      }

      // vanishing-point core
      ctx.globalCompositeOperation = "source-over";
      ctx.globalAlpha = 1;
      const gg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 30);
      gg.addColorStop(0, "rgba(255,240,210,0.95)");
      gg.addColorStop(0.4, "rgba(224,169,75,0.35)");
      gg.addColorStop(1, "rgba(224,169,75,0)");
      ctx.fillStyle = gg;
      ctx.beginPath(); ctx.arc(cx, cy, 30, 0, Math.PI * 2); ctx.fill();
    };

    if (reduce) {
      // settle to a representative static frame, no loop
      ctx.fillStyle = "#080a12"; ctx.fillRect(0, 0, w, h);
      for (let k = 0; k < 40; k++) render(true);
      render(false);
    } else {
      const loop = () => { render(true); raf = requestAnimationFrame(loop); };
      ctx.fillStyle = "#080a12"; ctx.fillRect(0, 0, w, h);
      loop();
    }

    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
  }, []);
  return <canvas ref={ref} />;
}

/* Repo icon, with an understated warm projection mark as fallback */
function AtlasMark() {
  const [ok, setOk] = useState(true);
  if (ok && ATLAS.icon) {
    return <img className="atlas-iconimg" src={ATLAS.icon} alt="Atlas Camera" onError={() => setOk(false)} />;
  }
  return (
    <svg className="atlas-fallback" viewBox="0 0 200 200" fill="none" aria-label="Atlas Camera">
      <g stroke="#c4b29a" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" opacity="0.9">
        <path d="M100 44 L58 150" />
        <path d="M100 44 L142 150" />
        <path d="M74 118 L126 118" />
      </g>
      <g stroke="#8f8571" strokeWidth="1.4" opacity="0.5">
        <path d="M100 44 L44 156" /><path d="M100 44 L156 156" />
        <path d="M40 156 H160" />
      </g>
      <circle cx="100" cy="44" r="4.5" fill="#eaa03a" />
      <circle cx="100" cy="44" r="10" stroke="#eaa03a" strokeWidth="1.2" opacity="0.4" />
    </svg>
  );
}

function AtlasSection({ onProof }) {
  return (
    <section className="section atlas" id="atlas">
      <div className="wrap">
        <div className="atlas-hero">
          <div className="reveal">
            <span className="atlas-badge"><span className="pulse"></span>{ATLAS.status} · {ATLAS.kicker}</span>
            <h2 className="atlas-wordmark">
              {(() => { const [w1, ...r] = ATLAS.name.split(" "); return (
                <>{w1} <span className="wm-dim">{r.join(" ")}</span></>
              ); })()}
            </h2>
            <p className="atlas-tag">{ATLAS.tagline}</p>
            <p className="atlas-lede">{ATLAS.lede}</p>
            <div className="atlas-cta">
              {ATLAS.links.map((l, i) => (
                <a key={i} className={`btn ${i === 0 ? "btn-primary" : ""}`} href={l.href} target="_blank" rel="noreferrer">
                  {l.label}{l.note ? <span className="note">· {l.note}</span> : null}
                </a>
              ))}
            </div>
            {ATLAS.badges && (
              <div className="atlas-badges">
                {ATLAS.badges.map((b, i) => {
                  const inner = (
                    <>
                      <span className="badge-l">{b.label}</span>
                      <span className="badge-v">{b.value}</span>
                    </>
                  );
                  return b.href
                    ? <a key={i} className="badge" href={b.href} target="_blank" rel="noreferrer" style={{ "--bc": b.color }}>{inner}</a>
                    : <span key={i} className="badge" style={{ "--bc": b.color }}>{inner}</span>;
                })}
              </div>
            )}
          </div>
          <div className="atlas-visual reveal d1">
            <AtlasMark />
            <span className="vcorner">SOLVE · PROJECT · EXPORT</span>
            <span className="vlabel">github.com/mikejamesvfx/atlas-camera</span>
          </div>
        </div>

        {ATLAS.media && ATLAS.media.length > 0 && (
          <div className="atlas-media reveal">
            <Carousel items={ATLAS.media} />
          </div>
        )}

        <div className="atlas-features">
          {ATLAS.features.map((f, i) => (
            <div className={`feat reveal ${i % 3 === 1 ? "d1" : i % 3 === 2 ? "d2" : ""}`} key={f.n}>
              <span className="fn">{f.n}</span>
              <h3>{f.title}</h3>
              <p>{f.body}</p>
            </div>
          ))}
        </div>

        <div className="atlas-flow">
          {ATLAS.workflow.map((s, i) => (
            <div className={`flow reveal ${i ? "d" + i : ""}`} key={s.step}>
              {i < ATLAS.workflow.length - 1 ? <span className="arrow">→</span> : null}
              <span className="fs">{s.step}</span>
              <h4>{s.label}</h4>
              <p>{s.text}</p>
            </div>
          ))}
        </div>

        <div className="atlas-why">
          <div className="qmark reveal">
            {ATLAS.why.map((p, i) => <p key={i}>{p}</p>)}
          </div>
          <div className="atlas-proof reveal d1">
            <div className="kicker tech"><span className="dot">●</span> Proven in production</div>
            <p>{ATLAS.proof}</p>
            <div className="stack">
              {ATLAS.stack.map((s) => <span key={s}>{s}</span>)}
            </div>
            <a className="prooflink" onClick={() => onProof(ATLAS.proofLink.to)} style={{ cursor: "pointer" }}>
              {ATLAS.proofLink.label} →
            </a>
          </div>
        </div>
      </div>
    </section>
  );
}

/* ===========================================================================
   SELECTED WORK
   =========================================================================== */
// Uniform grid: every frame is the same size (3-up on desktop).
function WorkCard({ p, i, onOpen }) {
  return (
    <div className={`card reveal ${i % 3 === 1 ? "d1" : i % 3 === 2 ? "d2" : ""}`} onClick={() => onOpen(p.id)}>
      <div className="card-media">
        {p.heroVideo
          ? <video src={p.heroVideo} poster={p.heroImage} autoPlay muted loop playsInline onError={hideOnError} />
          : <img src={p.heroImage} alt="" onError={hideOnError} />}
      </div>
      <div className="card-grad"></div>
      <div className="card-open">↗</div>
      <div className="card-body">
        <div className="card-meta">
          <span className="num">{String(p.id).padStart(2, "0")}</span>
          <span>{p.type}</span>
          <span className="yr">{p.year}</span>
        </div>
        <h3>{p.title[0]}{p.title[1] ? <span className="sub2"> · {p.title[1]}</span> : null}</h3>
      </div>
    </div>
  );
}

function WorkSection({ onOpen }) {
  return (
    <section className="section work" id="work">
      <div className="wrap">
        <div className="section-head reveal">
          <div>
            <span className="idx">02 — SELECTED WORK</span>
            <h2>The craft behind the tool</h2>
          </div>
          <p>Feature film, virtual production, and campaign work — the visual-effects craft Atlas Camera is built on.</p>
        </div>
        <div className="work-grid">
          {PROJECTS.map((p, i) => <WorkCard key={p.id} p={p} i={i} onOpen={onOpen} />)}
        </div>
      </div>
    </section>
  );
}

/* ===========================================================================
   PROJECT DETAIL OVERLAY
   =========================================================================== */
function ProcessStep({ s }) {
  const [failed, setFailed] = useState(false);
  const media = failed
    ? <div className="ph">{s.label}</div>
    : isVideo(s.image)
      ? <video src={s.image} autoPlay muted loop playsInline preload="metadata" onError={() => setFailed(true)} />
      : <img src={s.image} alt={s.label} onError={() => setFailed(true)} />;
  return (
    <div className="proc">
      <div className="pimg">{media}</div>
      <div className="pcap">
        <div className="pl">{s.label}</div>
        <div className="pt">{s.text}</div>
      </div>
    </div>
  );
}

/* full-width cross-fade carousel — auto-advances, pauses on hover, and drops
   any slide whose file fails to load so the count/dots stay honest */
function Carousel({ items }) {
  const [failed, setFailed] = useState({});
  const [idx, setIdx] = useState(0);
  const [paused, setPaused] = useState(false);
  const live = items.map((src, i) => ({ src, i })).filter((o) => !failed[o.i]);
  const n = live.length;
  useEffect(() => { if (idx >= n && n > 0) setIdx(0); }, [n, idx]);
  useEffect(() => {
    if (paused || n <= 1) return undefined;
    const t = setInterval(() => setIdx((i) => (i + 1) % n), 1500);
    return () => clearInterval(t);
  }, [paused, n]);
  const go = (d) => setIdx((i) => (i + d + n) % n);
  const mark = (i) => setFailed((f) => ({ ...f, [i]: true }));
  if (n === 0) return null;
  return (
    <div className="carousel" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
      <div className="carousel-stage">
        {live.map((o, li) => (
          <div className={`cslide ${li === idx ? "on" : ""}`} key={o.i} aria-hidden={li !== idx}>
            {isVideo(o.src)
              ? <video src={o.src} autoPlay muted loop playsInline onError={() => mark(o.i)} />
              : <img src={o.src} alt="" loading={li === 0 ? "eager" : "lazy"} onError={() => mark(o.i)} />}
          </div>
        ))}
        {n > 1 && (
          <>
            <button className="carousel-nav prev" onClick={() => go(-1)} aria-label="Previous image">‹</button>
            <button className="carousel-nav next" onClick={() => go(1)} aria-label="Next image">›</button>
          </>
        )}
        <div className="carousel-count">{String(idx + 1).padStart(2, "0")} / {String(n).padStart(2, "0")}</div>
      </div>
      {n > 1 && (
        <div className="carousel-dots">
          {live.map((o, li) => (
            <button className={`dot ${li === idx ? "on" : ""}`} key={o.i} onClick={() => setIdx(li)} aria-label={`Image ${li + 1}`} />
          ))}
        </div>
      )}
    </div>
  );
}

/* portrait poster wall (film credits, key art) — tiles hide if the file is missing */
function PosterTile({ src }) {
  const [ok, setOk] = useState(true);
  if (!ok) return null;
  return (
    <div className="poster-tile">
      <img src={src} alt="" loading="lazy" onError={() => setOk(false)} />
    </div>
  );
}
function PosterWall({ items, className }) {
  return (
    <div className={`poster-wall ${className || ""}`}>
      {items.map((src, i) => <PosterTile key={i} src={src} />)}
    </div>
  );
}

/* gallery tile: looping video or still, hides itself if the file is missing */
function GalleryMedia({ src }) {
  const [ok, setOk] = useState(true);
  if (!ok) return null;
  if (isVideo(src)) {
    return <video src={src} autoPlay muted loop playsInline preload="metadata" onError={() => setOk(false)} />;
  }
  return <img src={src} alt="" loading="lazy" onError={() => setOk(false)} />;
}

function Detail({ project, onClose }) {
  useEffect(() => {
    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 = ""; };
  }, [onClose]);

  const p = project;
  return (
    <div className="detail" onClick={onClose}>
      <button className="detail-close" onClick={onClose} aria-label="Close">✕</button>
      <div className="detail-inner" onClick={(e) => e.stopPropagation()}>
        <div className="detail-hero">
          {p.heroVideo
            ? <video src={p.heroVideo} poster={p.heroImage} autoPlay muted loop playsInline onError={hideOnError} />
            : <img src={p.heroImage} alt="" onError={hideOnError} />}
          <div className="dg"></div>
          <div className="detail-titlewrap">
            <div className="kicker"><span className="dot">●</span> {p.type} · {p.year} · {p.role}</div>
            <h2>{p.title[0]}{p.title[1] ? <span className="s"> — {p.title[1]}</span> : null}</h2>
          </div>
        </div>

        <div className="detail-body">
          <p className="detail-tagline">{p.tagline}</p>

          <div className="detail-cols">
            <div className="detail-approach">
              {p.approach.map((para, i) => (
                <p key={i} dangerouslySetInnerHTML={{ __html: para }} />
              ))}
            </div>
            <dl className="detail-specs">
              {p.specs.map(([k, v], i) => (
                <div className="spec-row" key={i}>
                  <dt>{k}</dt>
                  <dd dangerouslySetInnerHTML={{ __html: v }} />
                </div>
              ))}
            </dl>
          </div>

          {(() => {
            const all = p.posterGrid ? p.galleryImages : [...(p.breakdowns || []), ...p.galleryImages];
            // don't repeat the hero image/video inside the gallery below it
            const items = all.filter((s) => s !== p.heroVideo && s !== p.heroImage);
            return p.posterGrid ? <PosterWall items={items} /> : <Carousel items={items} />;
          })()}
        </div>
      </div>
    </div>
  );
}

/* ===========================================================================
   ABOUT / CONTACT / FOOTER
   =========================================================================== */
function About() {
  return (
    <section className="section about" id="about">
      <div className="wrap">
        <div className="section-head reveal">
          <div>
            <span className="idx">03 — ABOUT</span>
            <h2>Artist &amp; technologist</h2>
          </div>
          <p>Available 2026 · remote or studio · features, commercials, virtual production, and AI pipeline work.</p>
        </div>

        <div className="about-lead">
          <div className="big reveal">
            A <b>visual effects artist and creative technologist</b> with two decades across feature
            film, commercials, and virtual production — building the <span className="tech">tools and
            AI pipelines</span> behind the work as much as the shots themselves.
          </div>
          <div className="about-body reveal d1">
            <p>
              My work spans environments, matte painting, compositing, and virtual production —
              wherever composition, lens behaviour, texture, and pipeline discipline all have to
              agree. Over the last few years I've gone deep on GenAI as a production tool: ComfyUI
              systems, LoRA training, ControlNet conditioning, projection workflows, and Python
              automation that moves from concept to final shot.
            </p>
            <p>
              Atlas Camera is where the two halves meet — decades of on-set camera and projection
              knowledge, encoded into tooling other artists can pick up.
            </p>
          </div>
        </div>

        <div className="about-cols">
          <div className="about-col reveal">
            <h4>Selected Credits</h4>
            <span className="li">Happy Gilmore 2 · Luma Pictures · 2025</span>
            <span className="li">Cheetos · ByAutonomy · 2026</span>
            <span className="li">Mercy Road / RUR · Heretic · 2024</span>
            <span className="li">Argo · The Wolverine · Infini</span>
            <span className="li">The Railway Man · Method Studios</span>
          </div>
          <div className="about-col reveal d1">
            <h4>Stack</h4>
            <span className="li">Maya · Nuke · Photoshop · Mari</span>
            <span className="li">ComfyUI · AI Toolkit · LoRA training</span>
            <span className="li">Unreal Engine · Meshroom · Trellis</span>
            <span className="li">Python · Kornia · PyTorch · Three.js</span>
          </div>
          <div className="about-col reveal d2">
            <h4>Contact</h4>
            <a href={`mailto:${MAIL}`}>{MAIL}</a>
            <a href={PORTFOLIO} target="_blank" rel="noreferrer">mikejamesvfx.com</a>
            <a href={IMDB} target="_blank" rel="noreferrer">IMDb — filmography</a>
            <span className="li dim" style={{ marginTop: 8 }}>Showreel &amp; breakdowns on request.</span>
          </div>
        </div>

        <div className="about-credits reveal">
          <h4>Selected Film Credits</h4>
          <PosterWall items={window.CREDITS} className="credits-wall" />
          <a className="credits-imdb" href={IMDB} target="_blank" rel="noreferrer">Full filmography on IMDb →</a>
        </div>
      </div>
    </section>
  );
}

function Contact() {
  return (
    <section className="contact" id="contact">
      <div className="wrap">
        <div className="kicker reveal"><span className="dot">●</span> Let's build something</div>
        <h2 className="reveal d1">Have a shot,<br/>a show, or a pipeline?</h2>
        <a className="mail reveal d2" href={`mailto:${MAIL}`}>{MAIL}</a>
        <div className="subline reveal d2">VISUAL EFFECTS · CREATIVE TECHNOLOGY · AI PIPELINES</div>
      </div>
    </section>
  );
}

function Footer() {
  return (
    <footer className="footer">
      <span>MIKE JAMES — VFX ARTIST / CREATIVE TECHNOLOGIST</span>
      <span>© {new Date().getFullYear()} · BUILT WITH ATLAS CAMERA IN MIND</span>
    </footer>
  );
}

/* ===========================================================================
   APP
   =========================================================================== */
function App() {
  const [openId, setOpenId] = useState(null);
  useReveal();

  const open = useCallback((id) => {
    setOpenId(id);
    window.history.pushState({ p: id }, "", `#p-${id}`);
  }, []);
  const close = useCallback(() => {
    setOpenId(null);
    window.history.pushState({}, "", "#work");
  }, []);

  useEffect(() => {
    const onPop = () => {
      const m = window.location.hash.match(/^#p-(\d+)$/);
      setOpenId(m ? parseInt(m[1], 10) : null);
    };
    onPop();
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  const project = openId ? PROJECTS.find((p) => p.id === openId) : null;

  return (
    <>
      <Nav />
      <Hero />
      <AtlasSection onProof={open} />
      <WorkSection onOpen={open} />
      <About />
      <Contact />
      <Footer />
      {project && <Detail project={project} onClose={close} />}
    </>
  );
}

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