// views.jsx — Home, MoodView, WorkDetail, About

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

/* ─────────────────────── Image placeholder helper ─────────────────────── */
// We don't have real images yet — show tonally-graded swatches with a quiet
// stripe pattern and a monospace label, so the layout reads as photographic.
function PhotoSlot({ tone = "fog", ratio = "4/5", label, src, alt, className = "", style = {} }) {
  const grad = window.TONES[tone] || window.TONES.fog;
  const stripes = `repeating-linear-gradient(
    115deg,
    rgba(255,255,255,.04) 0px,
    rgba(255,255,255,.04) 1px,
    transparent 1px,
    transparent 14px
  )`;
  // If a real photo URL is provided, use that as the background and skip the
  // stripe pattern. Falls back to the tone gradient placeholder otherwise.
  const bg = src
    ? `center / cover no-repeat url("${src}"), ${grad}`
    : `${stripes}, ${grad}`;
  return (
    <div
      className={`img ${className}`}
      role={src ? "img" : undefined}
      aria-label={src ? (alt || label || "") : undefined}
      style={{
        background: bg,
        aspectRatio: ratio.replace("/", " / "),
        position: "relative",
        ...style
      }}>
      
      {label && !src &&
      <span
        style={{
          position: "absolute",
          bottom: 8,
          left: 10,
          fontFamily: "var(--mono)",
          fontSize: 9,
          letterSpacing: ".18em",
          color: "rgba(255,255,255,.7)",
          textShadow: "0 1px 2px rgba(0,0,0,.4)"
        }}>
        
          {label}
        </span>
      }
    </div>);

}

/* ─────────────────────── Cursor companion ─────────────────────── */
function CursorInk() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf,tx = -100,ty = -100,x = -100,y = -100;
    const move = (e) => {tx = e.clientX;ty = e.clientY;};
    const over = (e) => {
      const t = e.target;
      if (t.closest && t.closest("[data-hot]")) el.classList.add("expand");else
      el.classList.remove("expand");
    };
    const tick = () => {
      x += (tx - x) * 0.22;
      y += (ty - y) * 0.22;
      el.style.transform = `translate(${x}px, ${y}px) translate(-50%,-50%)`;
      raf = requestAnimationFrame(tick);
    };
    window.addEventListener("mousemove", move);
    window.addEventListener("mouseover", over);
    raf = requestAnimationFrame(tick);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mouseover", over);
    };
  }, []);
  return <span ref={ref} className="cursor-ink" aria-hidden="true" />;
}

/* ─────────────────────── Chrome ─────────────────────── */
function TopFrame({ route, onGo, lang, onToggleLang }) {
  const S = window.STRINGS;
  return (
    <div className="top-frame">
      <div className="brand" onClick={() => onGo({ name: "home" })} data-hot style={{ cursor: "pointer" }}>
        <span className="name">{S.brand_zh}</span>
        <span className="name-en">{S.brand_en} · {S.tagline_en}</span>
      </div>
      <div style={{ display: "flex", gap: 32, alignItems: "center" }}>
        <nav style={{ display: "flex", gap: 28 }}>
          <NavLink active={route.name === "home"} onClick={() => onGo({ name: "home" })}>
            <span className="zh">首页</span><small>Home</small>
          </NavLink>
          <NavLink active={route.name === "index"} onClick={() => onGo({ name: "index" })}>
            <span className="zh">索引</span><small>Index · 按年</small>
          </NavLink>
          <NavLink active={route.name === "about"} onClick={() => onGo({ name: "about" })}>
            <span className="zh">关于</span><small>About</small>
          </NavLink>
        </nav>
        <div className="lang-toggle" data-hot onClick={onToggleLang} style={{ cursor: "pointer" }}>
          <span style={{ opacity: lang === "zh" ? 1 : 0.4, color: lang === "zh" ? "var(--ink)" : undefined }}>中</span>
          <span className="dot" />
          <span style={{ opacity: lang === "en" ? 1 : 0.4, color: lang === "en" ? "var(--ink)" : undefined }}>EN</span>
        </div>
      </div>
    </div>);

}

function NavLink({ active, onClick, children }) {
  return (
    <button
      onClick={onClick}
      data-hot
      style={{
        appearance: "none",
        background: "transparent",
        border: 0,
        padding: 0,
        cursor: "pointer",
        display: "inline-flex",
        flexDirection: "column",
        alignItems: "flex-end",
        lineHeight: 1.1,
        color: active ? "var(--accent)" : "var(--ink)",
        fontFamily: "var(--serif-zh)",
        fontSize: 14,
        letterSpacing: ".15em",
        transition: "color .25s"
      }}>
      
      {React.Children.map(children, (c, i) =>
      i === 0 ?
      c :
      React.cloneElement(c, {
        style: {
          fontFamily: "var(--serif-en)",
          fontStyle: "italic",
          fontSize: 10.5,
          color: "var(--ink-mute)",
          marginTop: 2,
          letterSpacing: ".05em"
        }
      })
      )}
    </button>);

}

function PageCounter({ route, total }) {
  const S = window.STRINGS;
  let label = "HOME";
  if (route.name === "index") label = "INDEX · CHRONOLOGY";
  if (route.name === "mood") label = (window.MOODS.find((m) => m.id === route.id)?.en || "").toUpperCase();
  if (route.name === "work") label = (window.WORKS.find((w) => w.id === route.id)?.en || "").toUpperCase();
  if (route.name === "about") label = "ABOUT";
  return (
    <>
      <div className="year-mark">© JOLINE · S — 2026 · HANGZHOU / 北京</div>
      <div className="page-counter">{label}</div>
    </>);

}

/* ─────────────────────── Star field (canvas, full-bleed) ─────────────────────── */
// Two canvases: a static background star map + a foreground FX layer
// (cursor trail + shooting stars + click bursts). Both span the entire
// home <section> so stars and meteors can drift across both halves.
function StarField({ containerRef }) {
  const bgRef = useRef(null);
  const fxRef = useRef(null);
  const mouseRef = useRef({ x: -9999, y: -9999, lx: 0, ly: 0 });
  const sparklesRef = useRef([]);
  const burstsRef = useRef([]);
  const shootersRef = useRef([]);

  useEffect(() => {
    const bg = bgRef.current, fx = fxRef.current;
    const host = containerRef.current;
    if (!bg || !fx || !host) return;
    const bctx = bg.getContext("2d");
    const fctx = fx.getContext("2d");
    let raf;
    let W = 0, H = 0;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    let stars = [];
    const rebuild = () => {
      stars = [];
      const count = Math.round(W * H / 7000);
      for (let i = 0; i < count; i++) {
        stars.push({
          x: Math.random() * W,
          y: Math.random() * H,
          r: Math.random() * 1.4 + 0.3,
          bright: Math.random() < 0.07,
          phase: Math.random() * Math.PI * 2,
          speed: 0.6 + Math.random() * 1.4,
          drift: (Math.random() - 0.5) * 0.08,
        });
      }
    };

    const resize = () => {
      const r = host.getBoundingClientRect();
      W = r.width; H = r.height;
      [bg, fx].forEach((cv) => {
        cv.width = W * dpr; cv.height = H * dpr;
        cv.style.width = W + "px"; cv.style.height = H + "px";
        cv.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0);
      });
      rebuild();
    };
    resize();
    window.addEventListener("resize", resize);

    // Listen on the host so events bubbling from any child still count.
    const localXY = (e) => {
      const r = host.getBoundingClientRect();
      return { x: e.clientX - r.left, y: e.clientY - r.top };
    };
    const onMove = (e) => {
      const { x, y } = localXY(e);
      const m = mouseRef.current;
      const dx = x - m.lx, dy = y - m.ly;
      const dist = Math.hypot(dx, dy);
      if (m.lx !== 0 && m.ly !== 0 && dist > 0) {
        const n = Math.min(Math.ceil(dist / 6), 6);
        for (let i = 0; i < n; i++) {
          const f = (i + 1) / n;
          sparklesRef.current.push({
            x: m.lx + dx * f + (Math.random() - .5) * 4,
            y: m.ly + dy * f + (Math.random() - .5) * 4,
            r: 0.6 + Math.random() * 1.6,
            t: 0, life: 40 + Math.random() * 40,
            vx: (Math.random() - .5) * 0.3,
            vy: -0.2 - Math.random() * 0.3,
            kind: Math.random() < 0.18 ? "cross" : "dot",
          });
        }
      }
      m.x = x; m.y = y; m.lx = x; m.ly = y;
    };
    const onLeave = () => {
      mouseRef.current.x = -9999; mouseRef.current.y = -9999;
      mouseRef.current.lx = 0; mouseRef.current.ly = 0;
    };
    const onClick = (e) => {
      const { x, y } = localXY(e);
      burstsRef.current.push({ x, y, t: 0, life: 70, rays: 9 });
      shootersRef.current.push({
        x, y,
        vx: -2.6 - Math.random() * 1.5,
        vy: 1.4 + Math.random() * 1.0,
        t: 0, life: 120,
      });
    };
    host.addEventListener("mousemove", onMove);
    host.addEventListener("mouseleave", onLeave);
    host.addEventListener("click", onClick);

    const palette = () => {
      const cs = getComputedStyle(document.documentElement);
      return {
        ink:  cs.getPropertyValue("--ink").trim()      || "#3a261a",
        acc:  cs.getPropertyValue("--accent").trim()   || "#8a4a2a",
        mute: cs.getPropertyValue("--ink-mute").trim() || "#a3917b",
      };
    };
    let col = palette();
    let frame = 0;
    let lastShoot = 0;

    const drawStar = (ctx, x, y, r, alpha, color) => {
      ctx.fillStyle = hexToRgba(color, alpha);
      ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
    };
    const drawCross = (ctx, x, y, len, alpha, color) => {
      ctx.strokeStyle = hexToRgba(color, alpha);
      ctx.lineWidth = 0.6;
      ctx.beginPath();
      ctx.moveTo(x - len, y); ctx.lineTo(x + len, y);
      ctx.moveTo(x, y - len); ctx.lineTo(x, y + len);
      ctx.stroke();
    };

    const draw = () => {
      frame++;
      if (frame % 60 === 0) col = palette();

      // ── Background layer: static-ish star map ──
      bctx.clearRect(0, 0, W, H);
      const grad = bctx.createRadialGradient(W * 0.35, H * 0.4, 0, W * 0.5, H * 0.5, Math.max(W, H) * 0.7);
      grad.addColorStop(0, "rgba(0,0,0,0)");
      grad.addColorStop(1, hexToRgba(col.ink, 0.05));
      bctx.fillStyle = grad;
      bctx.fillRect(0, 0, W, H);
      for (const s of stars) {
        s.y += 0.04 * s.speed;
        s.x += s.drift * 0.2;
        if (s.y > H + 4) s.y = -4;
        if (s.x > W + 4) s.x = -4;
        if (s.x < -4)    s.x = W + 4;
        const tw = 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(frame * 0.02 * s.speed + s.phase));
        const alpha = (0.18 + s.r * 0.18) * tw;
        drawStar(bctx, s.x, s.y, s.r, alpha, s.bright ? col.ink : col.mute);
        if (s.bright && tw > 0.6) {
          drawCross(bctx, s.x, s.y, 3.5 + s.r * 1.5, (tw - 0.6) * 0.7, col.ink);
        }
      }

      // ── FX layer: cursor trail + bursts + shooting stars ──
      fctx.clearRect(0, 0, W, H);

      const sp = sparklesRef.current;
      for (let i = sp.length - 1; i >= 0; i--) {
        const p = sp[i];
        p.t += 1;
        p.x += p.vx; p.y += p.vy;
        p.vy += 0.005;
        const k = 1 - p.t / p.life;
        if (k <= 0) { sp.splice(i, 1); continue; }
        if (p.kind === "cross") {
          drawCross(fctx, p.x, p.y, p.r * 3 * k, k * 0.85, col.acc);
          drawStar(fctx, p.x, p.y, p.r * 0.7, k * 0.95, col.acc);
        } else {
          drawStar(fctx, p.x, p.y, p.r * k, k * 0.85, i % 5 === 0 ? col.acc : col.ink);
        }
      }
      if (sp.length > 250) sp.splice(0, sp.length - 250);

      const bs = burstsRef.current;
      for (let i = bs.length - 1; i >= 0; i--) {
        const b = bs[i];
        b.t += 1;
        const k = 1 - b.t / b.life;
        if (k <= 0) { bs.splice(i, 1); continue; }
        const radius = (1 - Math.pow(1 - b.t / b.life, 3)) * 60;
        drawStar(fctx, b.x, b.y, 2 + 4 * k, k * 0.9, col.acc);
        drawCross(fctx, b.x, b.y, 10 + 16 * (1 - k), k * 0.7, col.acc);
        for (let r = 0; r < b.rays; r++) {
          const a = (r / b.rays) * Math.PI * 2;
          const x = b.x + Math.cos(a) * radius;
          const y = b.y + Math.sin(a) * radius;
          drawStar(fctx, x, y, 1.6 * k + 0.5, k * 0.95, col.acc);
        }
      }

      if (frame - lastShoot > 480 && Math.random() < 0.006) {
        lastShoot = frame;
        shootersRef.current.push({
          x: W + 20,
          y: Math.random() * H * 0.5,
          vx: -3 - Math.random() * 2,
          vy: 1.4 + Math.random() * 1.2,
          t: 0, life: 160,
        });
      }
      const shts = shootersRef.current;
      for (let i = shts.length - 1; i >= 0; i--) {
        const s = shts[i];
        s.t += 1;
        s.x += s.vx; s.y += s.vy;
        const k = 1 - s.t / s.life;
        if (k <= 0 || s.x < -40 || s.y > H + 40) { shts.splice(i, 1); continue; }
        const len = 36;
        const gx = s.x + s.vx * len * 0.4;
        const gy = s.y + s.vy * len * 0.4;
        const tg = fctx.createLinearGradient(s.x, s.y, gx, gy);
        tg.addColorStop(0, hexToRgba(col.acc, k * 0.9));
        tg.addColorStop(1, hexToRgba(col.acc, 0));
        fctx.strokeStyle = tg; fctx.lineWidth = 1.4;
        fctx.beginPath();
        fctx.moveTo(s.x, s.y); fctx.lineTo(gx, gy);
        fctx.stroke();
        drawStar(fctx, s.x, s.y, 1.6, k, col.acc);
      }

      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("resize", resize);
      host.removeEventListener("mousemove", onMove);
      host.removeEventListener("mouseleave", onLeave);
      host.removeEventListener("click", onClick);
    };
  }, [containerRef]);

  return (
    <>
      <canvas ref={bgRef} className="sf-canvas sf-bg" aria-hidden="true" />
      <canvas ref={fxRef} className="sf-canvas sf-fx" aria-hidden="true" />
    </>
  );
}

// helper: parse hex/named color to rgba string with alpha
function hexToRgba(c, a) {
  if (!c) return `rgba(60,40,30,${a})`;
  c = c.trim();
  if (c.startsWith("rgb")) {
    return c.replace(/rgba?\(([^)]+)\)/, (_, body) => {
      const parts = body.split(",").map((s) => s.trim());
      return `rgba(${parts[0]},${parts[1]},${parts[2]},${a})`;
    });
  }
  let h = c.replace("#", "");
  if (h.length === 3) h = h.split("").map((ch) => ch + ch).join("");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
}

/* ─────────────────────── Home left content (variants) ─────────────────────── */

function LeftVerse() {
  const verses = window.HOME_VERSES || [];
  const [idx, setIdx] = useState(0);
  useEffect(() => {
    if (!verses.length) return;
    const id = setInterval(() => setIdx((i) => (i + 1) % verses.length), 7000);
    return () => clearInterval(id);
  }, [verses.length]);
  if (!verses.length) return <div className="lp lp-verse" />;
  const v = verses[idx];
  return (
    <div className="lp lp-verse">
      <div className="lp-eyebrow">
        <span className="ln" />
        <span>VERSE · 诗</span>
      </div>
      <div className="lp-verse-stage" key={idx}>
        <div className="lp-verse-zh">{v.zh}</div>
        <div className="lp-verse-en">— {v.en}</div>
      </div>
      <div className="lp-verse-dots">
        {verses.map((_, i) => (
          <span key={i} className={i === idx ? "on" : ""} />
        ))}
      </div>
    </div>
  );
}

function LeftMonogram() {
  return (
    <div className="lp lp-monogram">
      <div className="lp-eyebrow">
        <span className="ln" />
        <span>JOLINE · S — 2026</span>
      </div>
      <div className="mono-cn">
        <span>苏</span>
        <span>焦</span>
        <span>琳</span>
      </div>
      <div className="mono-en">Joline <span style={{ fontStyle: "normal", opacity: .6 }}>·</span> Su.</div>
      <div className="mono-stamp">
        <div className="stamp-circle"><span>JS<br/>·2026·</span></div>
        <div className="mono-line">
          <span className="zh">摄影 · 手工书 · 装置</span>
          <span className="en">Photography · Books · Installation</span>
        </div>
      </div>
    </div>
  );
}

function LeftFeatured() {
  const works = window.WORKS;
  const [idx, setIdx] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setIdx((i) => (i + 1) % works.length), 6000);
    return () => clearInterval(id);
  }, [works.length]);
  const w = works[idx];
  return (
    <div className="lp lp-featured">
      <div className="lp-eyebrow">
        <span className="ln" />
        <span>FEATURED · 精选</span>
      </div>
      <div className="lp-feat-frame" key={w.id}>
        <PhotoSlot tone={w.cover} src={w.coverSrc} ratio="4/5" />
      </div>
      <div className="lp-feat-meta">
        <div className="ft-num mono">{String(idx + 1).padStart(2, "0")} / {String(works.length).padStart(2, "0")}</div>
        <div className="ft-ttl">
          <span className="zh">《{w.zh}》</span>
          <span className="en">— {w.en}</span>
        </div>
        <div className="ft-yr mono">{w.year}</div>
      </div>
    </div>
  );
}

function LeftMap({ onGo }) {
  const moods = window.MOODS;
  const pts = [
    { x: 0.18, y: 0.22 },
    { x: 0.55, y: 0.16 },
    { x: 0.78, y: 0.34 },
    { x: 0.35, y: 0.48 },
    { x: 0.66, y: 0.62 },
    { x: 0.22, y: 0.76 },
  ];
  const lines = [[0,1],[1,2],[1,3],[3,4],[3,5],[4,5]];
  const [hover, setHover] = useState(null);
  return (
    <div className="lp lp-map">
      <div className="lp-eyebrow">
        <span className="ln" />
        <span>MAP · 情绪星座</span>
      </div>
      <svg className="map-svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
        {lines.map(([a, b], i) => (
          <line key={i}
            x1={pts[a].x * 100} y1={pts[a].y * 100}
            x2={pts[b].x * 100} y2={pts[b].y * 100}
            stroke="currentColor" strokeWidth=".2"
            opacity={hover != null && (hover === a || hover === b) ? .6 : .22}
          />
        ))}
        {moods.map((m, i) => (
          <g key={m.id}
             onMouseEnter={() => setHover(i)}
             onMouseLeave={() => setHover(null)}
             onClick={() => onGo({ name: "mood", id: m.id })}
             style={{ cursor: "pointer" }} data-hot>
            <circle cx={pts[i].x * 100} cy={pts[i].y * 100} r={hover === i ? 1.5 : 1} fill="currentColor" />
            <circle cx={pts[i].x * 100} cy={pts[i].y * 100} r="4" fill="transparent" />
            <text x={pts[i].x * 100} y={pts[i].y * 100 - 2.6}
                  fontSize="2.4" textAnchor="middle" fill="currentColor"
                  opacity={hover === i ? 1 : .7}
                  style={{ fontFamily: "var(--serif-zh)", letterSpacing: ".12em" }}>{m.zh}</text>
            <text x={pts[i].x * 100} y={pts[i].y * 100 + 4.8}
                  fontSize="1.6" textAnchor="middle" fill="currentColor"
                  opacity={hover === i ? 0.85 : .35}
                  style={{ fontFamily: "var(--serif-en)", fontStyle: "italic" }}>{m.en}</text>
          </g>
        ))}
      </svg>
      <div className="map-hint">
        <span className="zh">点击关键词 · 进入对应作品集</span>
        <span className="en">Tap a word — enter that mood</span>
      </div>
    </div>
  );
}

function LeftPanel({ mode, onGo }) {
  if (mode === "monogram") return <LeftMonogram />;
  if (mode === "featured") return <LeftFeatured />;
  if (mode === "map")      return <LeftMap onGo={onGo} />;
  return <LeftVerse />;
}

/* ─────────────────────── Home ─────────────────────── */
function Home({ onGo, homeLeft }) {
  const works = window.WORKS;
  const sectionRef = useRef(null);

  const mediumLabel = (m) => m === "book" ? "手工书" : m === "photo-book" ? "摄影 · 手工书" : "摄影";
  const mediumEN = (m) => m === "book" ? "Handmade book" : m === "photo-book" ? "Photographs · Handmade book" : "Photographs";

  return (
    <section className="home page-enter" data-screen-label="01 Home" ref={sectionRef}>
      {/* Full-bleed starfield: bg + fx layers spanning the whole home */}
      <StarField containerRef={sectionRef} />

      {/* LEFT: decorative panel (variants) */}
      <div className="home-left">
        <LeftPanel mode={homeLeft || "featured"} onGo={onGo} />
      </div>

      {/* RIGHT: bilingual works list */}
      <div className="home-right fade-in-stagger">
        <div className="hr-head" style={{ animationDelay: ".05s" }}>
          <div className="eyebrow">
            <span style={{ marginRight: 12 }}>SELECTED WORKS</span>
            <span style={{ opacity: .5 }}>· {works.length} 件 / pieces</span>
          </div>
          <h2 className="hr-title">作品<small>— Works</small></h2>
        </div>

        <ol className="works-list">
          {works.map((w, i) =>
          <li
            key={w.id}
            className="work-row"
            data-hot
            style={{ animationDelay: `${0.15 + i * 0.08}s` }}
            onClick={() => onGo({ name: "work", id: w.id })}>
            
              <span className="wr-num">{String(i + 1).padStart(2, "0")}</span>
              <span className="wr-title">
                <span className="zh">《{w.zh}》</span>
                <span className="en">— {w.en}</span>
              </span>
              <span className="wr-medium">
                <span className="zh">{mediumLabel(w.medium)}</span>
                <span className="en">{mediumEN(w.medium)}</span>
              </span>
              <span className="wr-meta">
                <span className="wr-year">{w.year}</span>
              </span>
            </li>
          )}
        </ol>

        <div className="hr-foot" style={{ animationDelay: `${0.15 + works.length * 0.08}s` }}>
          <button
            className="hr-cta"
            data-hot
            onClick={() => onGo({ name: "index" })}>
            
            <span>按年查看完整索引</span>
            <small>View the full index by year →</small>
          </button>
        </div>
      </div>
    </section>);

}

/* Animated wavy underline drawn with SVG path morphing. */
function RippleLine({ x, y, w }) {
  const [phase, setPhase] = useState(0);
  useEffect(() => {
    let raf;
    const start = performance.now();
    const tick = (t) => {
      setPhase((t - start) / 600);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);
  const points = 32;
  const W = w + 40;
  const amp = 3.5;
  let d = `M 0 12`;
  for (let i = 1; i <= points; i++) {
    const px = i / points * W;
    const py = 12 + Math.sin(i * 0.6 - phase * 2) * amp * Math.exp(-Math.abs(i - points / 2) / 12);
    d += ` L ${px.toFixed(2)} ${py.toFixed(2)}`;
  }
  return (
    <svg
      className="ripple-line"
      width={W}
      height={24}
      viewBox={`0 0 ${W} 24`}
      style={{
        position: "fixed",
        left: x - 20,
        top: y,
        pointerEvents: "none",
        zIndex: 50
      }}>
      
      <path d={d} fill="none" stroke="var(--accent)" strokeWidth="1" opacity="0.7" />
    </svg>);

}

/* ─────────────────────── Mood View — scattered cards ─────────────────────── */
function MoodView({ id, onGo, layout }) {
  const mood = window.MOODS.find((m) => m.id === id);
  const works = window.WORKS.filter((w) => w.mood === id);
  if (!mood) return null;
  const Layout = layout === "grid" ? MoodGrid : MoodScatter;

  return (
    <section className="mood-view page-enter" data-screen-label={`02 Mood / ${mood.en}`}>
      <div className="mood-head fade-in-stagger">
        <div style={{ animationDelay: ".05s" }}>
          <h1>{mood.zh}</h1>
          <div className="h-en">— {mood.en}</div>
        </div>
        <div className="blurb" style={{ animationDelay: ".15s" }}>
          {mood.blurb_zh}
          <span className="en">{mood.blurb_en}</span>
        </div>
      </div>
      <Layout works={works} onGo={onGo} />
      {works.length === 0 &&
      <div style={{ opacity: 0.5, fontFamily: "var(--serif-en)", fontStyle: "italic", padding: 20 }}>
          — quiet, still in the dark room —
        </div>
      }
    </section>);

}

function MoodScatter({ works, onGo }) {
  // Use fixed scatter coords per work so layout is stable.
  return (
    <div className="scatter">
      {works.map((w, i) =>
      <div
        key={w.id}
        className={`scard ${i % 3 === 1 ? "taped" : ""}`}
        data-hot
        style={{
          left: `${w.scatter.x}%`,
          top: `${w.scatter.y}%`,
          width: w.scatter.w,
          "--tx": "0px", "--ty": "0px",
          "--r": `${w.scatter.r}deg`,
          "--tape-r": `${-w.scatter.r * 1.5}deg`,
          transform: `rotate(${w.scatter.r}deg)`,
          animation: `card-drop .8s cubic-bezier(.2,.7,.2,1) both`,
          animationDelay: `${0.15 + i * 0.08}s`
        }}
        onClick={() => onGo({ name: "work", id: w.id })}>
        
          <span className="tag">{w.medium === "book" ? "BOOK" : w.medium === "photo-book" ? "PHOTO·BOOK" : "PHOTOGRAPH"}</span>
          <PhotoSlot tone={w.cover} src={w.coverSrc} ratio="4/5" />
          <div className="meta">
            <div className="ttl">
              {w.zh}
              <small>{w.en}</small>
            </div>
            <div className="yr">{w.year}</div>
          </div>
        </div>
      )}
      <style>{`
        @keyframes card-drop {
          from { opacity: 0; transform: translateY(-30px) rotate(0deg) scale(.96); }
          to   { opacity: 1; }
        }
      `}</style>
    </div>);

}

function MoodGrid({ works, onGo }) {
  return (
    <div
      className="fade-in-stagger"
      style={{
        display: "grid",
        gridTemplateColumns: "repeat(3, 1fr)",
        gap: 40
      }}>
      
      {works.map((w, i) =>
      <div
        key={w.id}
        className="scard"
        data-hot
        style={{
          position: "relative",
          width: "100%",
          animationDelay: `${0.1 + i * 0.08}s`
        }}
        onClick={() => onGo({ name: "work", id: w.id })}>
        
          <span className="tag">{w.medium === "book" ? "BOOK" : w.medium === "photo-book" ? "PHOTO·BOOK" : "PHOTOGRAPH"}</span>
          <PhotoSlot tone={w.cover} src={w.coverSrc} ratio="4/5" />
          <div className="meta">
            <div className="ttl">{w.zh}<small>{w.en}</small></div>
            <div className="yr">{w.year}</div>
          </div>
        </div>
      )}
    </div>);

}

/* ─────────────────────── Side index (alt nav) ─────────────────────── */
function SideIndex({ route, onGo }) {
  const moods = window.MOODS;
  return (
    <div className="side-index">
      {moods.map((m, i) =>
      <div
        key={m.id}
        className={`si-item ${route.name === "mood" && route.id === m.id ? "active" : ""}`}
        data-hot
        onClick={() => onGo({ name: "mood", id: m.id })}>
        
          <span className="num">{String(i + 1).padStart(2, "0")}</span>
          <span className="ln" />
          <span>{m.en}</span>
        </div>
      )}
    </div>);

}

/* ─────────────────────── Lightbox ─────────────────────── */
function Lightbox({ images, startIndex = 0, onClose }) {
  const [idx, setIdx] = useState(startIndex);
  const [mode, setMode] = useState("single"); // 'single' | 'flip' | 'follow'
  const [coord, setCoord] = useState({ x: 0, y: 0 });
  const safe = (n) => (n + images.length) % images.length;
  const next = () => setIdx((i) => safe(i + 1));
  const prev = () => setIdx((i) => safe(i - 1));

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowRight" || e.key === "ArrowDown") next();
      if (e.key === "ArrowLeft" || e.key === "ArrowUp") prev();
    };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [images.length]);

  const onMove = (e) => {
    if (mode !== "follow") return;
    const r = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - r.left) / r.width - 0.5;
    const y = (e.clientY - r.top) / r.height - 0.5;
    setCoord({ x, y });
  };

  const cur = images[safe(idx)];
  const rt = images[safe(idx + 1)];

  return ReactDOM.createPortal(
    <div className="lightbox" role="dialog" aria-modal="true" onMouseMove={onMove}>
      <div className="lb-bg" onClick={onClose} />

      <button className="lb-close" data-hot onClick={onClose} aria-label="Close">
        <span>关闭</span><small>CLOSE · ESC</small>
      </button>

      <div className="lb-counter mono">
        {String(safe(idx) + 1).padStart(2, "0")} <span style={{ opacity: .5 }}>/ {String(images.length).padStart(2, "0")}</span>
      </div>

      <div className={`lb-stage lb-mode-${mode}`}>
        {mode === "single" &&
        <div className="lb-frame lb-single" key={`s-${idx}`}>
            <PhotoSlot tone={cur.tone} src={cur.src} ratio={cur.ratio} alt={cur.cap} />
            <div className="lb-cap mono">{cur.cap}</div>
          </div>
        }
        {mode === "flip" &&
        <div className="lb-flip" key={`f-${idx}`}>
            <div className="lb-flip-spread">
              <div className="lb-flip-page">
                <PhotoSlot tone={cur.tone} src={cur.src} ratio={cur.ratio} style={{ height: "100%", aspectRatio: "auto" }} />
              </div>
              <div className="lb-flip-page">
                <PhotoSlot tone={rt.tone} src={rt.src} ratio={rt.ratio} style={{ height: "100%", aspectRatio: "auto" }} />
              </div>
            </div>
            <div className="lb-cap mono">
              {cur.cap}&nbsp;&nbsp;·&nbsp;&nbsp;{rt.cap}
            </div>
          </div>
        }
        {mode === "follow" &&
        <div className="lb-follow" key={`fl-${idx}`}>
            <div
            className="lb-follow-frame"
            style={{
              transform: `translate(${coord.x * -36}px, ${coord.y * -36}px) rotateY(${coord.x * 6}deg) rotateX(${coord.y * -6}deg)`
            }}>
            
              <PhotoSlot tone={cur.tone} src={cur.src} ratio={cur.ratio} />
            </div>
            <div className="lb-cap mono">{cur.cap}</div>
          </div>
        }
      </div>

      {/* Prev / next */}
      {mode !== "follow" &&
      <>
          <button className="lb-nav prev" data-hot onClick={prev} aria-label="Previous">
            <span className="arr">←</span>
            <small>PREV</small>
          </button>
          <button className="lb-nav next" data-hot onClick={next} aria-label="Next">
            <span className="arr">→</span>
            <small>NEXT</small>
          </button>
        </>
      }

      {/* Mode selector */}
      <div className="lb-modes" data-hot>
        {[
        { v: "single", zh: "单张", en: "Single" },
        { v: "flip", zh: "翻页", en: "Flip" },
        { v: "follow", zh: "跟随", en: "Follow" }].
        map((m) =>
        <button
          key={m.v}
          className={`lb-mode-btn ${mode === m.v ? "active" : ""}`}
          onClick={() => setMode(m.v)}>
          
            <span className="zh">{m.zh}</span>
            <small>{m.en}</small>
          </button>
        )}
      </div>
    </div>,
    document.body
  );

}
function WorkDetail({ id, onGo }) {
  const w = window.WORKS.find((x) => x.id === id);
  const mood = window.MOODS.find((m) => m.id === w?.mood);
  const [lbIndex, setLbIndex] = useState(null);
  if (!w) return null;

  return (
    <section className="detail page-enter" data-screen-label={`03 Work / ${w.en}`}>
      <div className="detail-inner">
        <div className="crumb" data-hot onClick={() => onGo({ name: "mood", id: w.mood })}>
          <span className="arr">←</span>
          <span>返回 / Back to · {mood?.zh} {mood?.en}</span>
        </div>

        <div className="detail-head">
          <div>
            <h1>{w.zh}</h1>
            <div className="h-en">— {w.en}</div>
          </div>
          <div className="detail-meta">
            <span>YEAR &nbsp;·&nbsp; <b>{w.year}</b></span>
            <span>MEDIUM &nbsp;·&nbsp; <b>{w.medium_en}</b></span>
            <span>MOOD &nbsp;·&nbsp; <b>{mood?.en}</b></span>
          </div>
        </div>

        <div className="detail-body">
          <div>
            <div className="eyebrow">STATEMENT · 创作自述</div>
            <p className="copy-zh" style={{ marginTop: 18 }}>{w.desc_zh}</p>
            <p className="copy-en">{w.desc_en}</p>
            <div className="lb-hint">
              <span className="mono">↗</span>
              <span className="zh">点击照片放大</span>
              <small className="en">Click any photo — Single · Flip · Follow</small>
            </div>
          </div>
          <Gallery images={w.images} onOpen={(i) => setLbIndex(i)} />
        </div>

        {w.book && <BookSection book={w.book} title={w.zh} title_en={w.en} />}
      </div>

      {lbIndex !== null &&
      <Lightbox
        images={w.images}
        startIndex={lbIndex}
        onClose={() => setLbIndex(null)} />

      }
    </section>);

}

function Gallery({ images, onOpen }) {
  // arrange images into rhythmic rows based on count
  const groups = [];
  let i = 0;
  const indexMap = []; // map back to source index
  while (i < images.length) {
    const remain = images.length - i;
    if (remain >= 2 && images[i].ratio !== images[i + 1].ratio) {
      groups.push({ kind: images[i].ratio === "4/3" ? "r-21" : "r-12", imgs: [images[i], images[i + 1]], idxs: [i, i + 1] });
      i += 2;
    } else if (remain >= 2) {
      groups.push({ kind: "r-2", imgs: [images[i], images[i + 1]], idxs: [i, i + 1] });
      i += 2;
    } else {
      groups.push({ kind: "r-1", imgs: [images[i]], idxs: [i] });
      i += 1;
    }
  }
  return (
    <div className="gallery fade-in-stagger">
      {groups.map((g, gi) =>
      <div key={gi} className={`g-row ${g.kind}`} style={{ animationDelay: `${0.1 + gi * 0.1}s` }}>
          {g.imgs.map((im, ii) =>
        <div
          className="frame"
          key={ii}
          data-hot
          onClick={() => onOpen && onOpen(g.idxs[ii])}
          style={{ cursor: "zoom-in" }}>
          
              <PhotoSlot tone={im.tone} src={im.src} ratio={im.ratio} />
              <div className="cap">{im.cap}</div>
            </div>
        )}
        </div>
      )}
    </div>);

}

function BookSection({ book, title, title_en }) {
  const [page, setPage] = useState(0);
  const spreads = book.spreads || [];
  const safePage = Math.max(0, Math.min(spreads.length - 1, page));

  return (
    <div className="book-section">
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between" }}>
        <h2>
          手工书 / 内页
          <small>Handmade book · Interior spreads</small>
        </h2>
        <div className="eyebrow">SCAN · 06 PAGES</div>
      </div>

      <div className="book-stage fade-in-stagger">
        <div className="book-object" style={{ animationDelay: ".1s" }}>
          <PhotoSlot tone={book.object.tone} src={book.object.src} ratio="1/1" />
          <div className="label">{book.object.label}</div>
        </div>

        <div className="book-flip" style={{ animationDelay: ".25s" }}>
          <BookSpread spread={spreads[safePage]} key={safePage} />
          <div className="book-controls">
            <button data-hot onClick={() => setPage((p) => Math.max(0, p - 1))}>← prev</button>
            <button data-hot onClick={() => setPage((p) => Math.min(spreads.length - 1, p + 1))}>next →</button>
            <span className="num">SPREAD {String(safePage + 1).padStart(2, "0")} / {String(spreads.length).padStart(2, "0")}</span>
          </div>
        </div>
      </div>
    </div>);

}

function BookSpread({ spread }) {
  if (!spread) return null;
  const renderSide = (side) => {
    if (side.text) {
      return (
        <div className="page">
          <div className="ptext" style={{ whiteSpace: "pre-wrap" }}>{side.text}</div>
        </div>);

    }
    return (
      <div className="page" style={{ padding: 0 }}>
        <PhotoSlot tone={side.tone} src={side.src} ratio="1/1" style={{ height: "100%" }} />
      </div>);

  };
  return (
    <div className="book-flip-inner" style={{ animation: "flip-in .7s cubic-bezier(.2,.7,.2,1) both" }}>
      {renderSide(spread.left)}
      {renderSide(spread.right)}
      <style>{`
        @keyframes flip-in {
          from { opacity: 0; transform: rotateY(-12deg) scale(.98); transform-origin: center left; }
          to   { opacity: 1; transform: rotateY(0) scale(1); }
        }
      `}</style>
    </div>);

}

/* ─────────────────────── About ─────────────────────── */
function About({ onGo }) {
  const A = window.STRINGS.about;
  return (
    <section className="about page-enter" data-screen-label="04 About">
      <h1 className="fade-in-stagger">
        <span style={{ animationDelay: ".05s", display: "inline-block" }}>关于</span>
        <small style={{ animationDelay: ".15s", display: "block" }}>About · {A.name_en}</small>
      </h1>

      <div className="about-grid">
        <div className="about-portrait fade-in-stagger" style={{ animationDelay: ".1s" }}>
          <PhotoSlot tone="paper" src={window.PORTRAIT_SRC} ratio="3/4" label="PORTRAIT · 2026" />
          <div className="stamp">JOLINE<br />· S ·<br />2026</div>
        </div>

        <div className="about-text fade-in-stagger">
          <div className="eyebrow" style={{ animationDelay: ".15s" }}>
            {A.role_zh} &nbsp;·&nbsp; <span style={{ fontStyle: "italic", textTransform: "none", letterSpacing: ".04em" }}>{A.role_en}</span>
          </div>
          {A.bio_zh.map((p, i) =>
          <p className="bio-zh" key={i} style={{ animationDelay: `${0.25 + i * 0.08}s` }}>{p}</p>
          )}
          {A.bio_en.map((p, i) =>
          <p className="bio-en" key={"e" + i} style={{ animationDelay: `${0.4 + i * 0.08}s` }}>{p}</p>
          )}

          <div className="about-meta">
            <div>
              <h3>CV · 展览简历</h3>
              <ul>
                {A.cv.map((c, i) =>
                <li key={i}>
                    <b style={{ fontFamily: "var(--mono)", fontSize: 11, marginRight: 10, color: "var(--ink-mute)" }}>
                      {c.year}
                    </b>
                    {c.zh}
                    <small>{c.en}</small>
                  </li>
                )}
              </ul>
            </div>
            <div className="about-contact">
              <h3>Contact · 联系</h3>
              <ul>
                {A.contact.map((c, i) =>
                <li key={i} style={{ marginBottom: 8 }}>
                    <a href={c.href} data-hot>{c.label} → {c.value}</a>
                  </li>
                )}
              </ul>
            </div>
          </div>
        </div>
      </div>
    </section>);

}

/* ─────────────────────── Index View (chronological) ─────────────────────── */
function IndexView({ onGo }) {
  const works = [...window.WORKS].sort((a, b) => Number(b.year) - Number(a.year));
  // group by year
  const byYear = {};
  works.forEach((w) => {(byYear[w.year] = byYear[w.year] || []).push(w);});
  const years = Object.keys(byYear).sort((a, b) => Number(b) - Number(a));

  const [hover, setHover] = useState(null);
  const hovered = hover && works.find((w) => w.id === hover);

  const mediumLabel = (m) => m === "book" ? "手工书" : m === "photo-book" ? "摄影 + 手工书" : "摄影";
  const mediumEN = (m) => m === "book" ? "Handmade book" : m === "photo-book" ? "Photo + book" : "Photographs";

  return (
    <section className="index-view page-enter" data-screen-label="05 Index">
      <div className="index-inner">
        <div className="index-head fade-in-stagger">
          <div>
            <div className="eyebrow" style={{ animationDelay: ".0s" }}>INDEX · 索引 / CHRONOLOGY</div>
            <h1 style={{ animationDelay: ".05s" }}>索 　 引<small>— By year</small></h1>
          </div>
          <p className="index-note" style={{ animationDelay: ".2s" }}>
            按创作年份排列。氛围是入口，时间是脱口。
            <span className="en">Arranged by year of making. Mood is the way in, time is the way out.</span>
          </p>
        </div>

        <div className="index-grid">
          <ol className="index-list">
            {years.map((y) =>
            <li className="index-year-group" key={y}>
                <div className="index-year">
                  <span className="yr-num">{y}</span>
                  <span className="yr-ln" />
                  <span className="yr-count">{String(byYear[y].length).padStart(2, "0")} 件 / WORKS</span>
                </div>
                <ul className="index-rows">
                  {byYear[y].map((w, i) => {
                  const mood = window.MOODS.find((m) => m.id === w.mood);
                  return (
                    <li
                      key={w.id}
                      className="index-row"
                      data-hot
                      onMouseEnter={() => setHover(w.id)}
                      onMouseLeave={() => setHover((h) => h === w.id ? null : h)}
                      onClick={() => onGo({ name: "work", id: w.id })}>
                      
                        <span className="idx-num">{String(i + 1).padStart(2, "0")}</span>
                        <span className="idx-title">
                          <span className="zh">{w.zh}</span>
                          <span className="en">{w.en}</span>
                        </span>
                        <span className="idx-mood">
                          <span className="zh">{mood?.zh}</span>
                          <span className="en">{mood?.en}</span>
                        </span>
                        <span className="idx-medium">
                          <span className="zh">{mediumLabel(w.medium)}</span>
                          <span className="en">{mediumEN(w.medium)}</span>
                        </span>
                        <span className="idx-arr">→</span>
                      </li>);

                })}
                </ul>
              </li>
            )}
          </ol>

          {/* Floating preview */}
          <aside className="index-preview" aria-hidden={!hovered}>
            {hovered ?
            <div className="ipv" key={hovered.id}>
                <PhotoSlot tone={hovered.cover} src={hovered.coverSrc} ratio="3/4" />
                <div className="ipv-meta">
                  <div className="ipv-ttl">{hovered.zh} <small>{hovered.en}</small></div>
                  <div className="ipv-yr mono">{hovered.year} · {(window.MOODS.find((m) => m.id === hovered.mood)?.en || "").toUpperCase()}</div>
                </div>
              </div> :

            <div className="ipv ipv-empty">
                <div className="ipv-empty-inner">
                  <div className="mono" style={{ opacity: .5, letterSpacing: ".25em", fontSize: 10 }}>HOVER ————</div>
                  <div style={{ marginTop: 14, fontFamily: "var(--serif-zh)", fontSize: 14, color: "var(--ink-soft)", lineHeight: 2 }}>
                    鼠标移到条目上<br />
                    <span style={{ fontFamily: "var(--serif-en)", fontStyle: "italic", fontSize: 12, color: "var(--ink-mute)" }}>
                      hover a row to preview
                    </span>
                  </div>
                </div>
              </div>
            }
          </aside>
        </div>
      </div>
    </section>);

}

Object.assign(window, {
  Home, MoodView, WorkDetail, About, IndexView, Lightbox, StarField,
  TopFrame, PageCounter, CursorInk, SideIndex
});