/* Portfolio — HOME and WORKS screens.
   Home  : full-bleed hero (opening animation lives here) + draggable "selected works" rail + statement band.
   Works : the whole archive as a masonry wall, with the three filters (series / medium / year).
   Reads: window.HU_DATA (works + series), window.HU_RATIOS (pixel ratios), window.HU_STATEMENTS.
   Opens the lightbox by calling the openWork(id, idList) prop handed down from app.jsx. */
const { HUButton, HUIconButton, HUTag, HUEyebrow, HUSiteHeader, HUArtworkCard,
  HUMuseumLabel, HUSectionHeading, HUInput, HUSelect } = window;

const { HU_ASSET: ASSET, HU_smBreaks: smBreaks, HU_workById: workById } = window;

/* ---------------- Home ---------------- */
function HomeScreen({ t, lang, go, openWork, lastWorkId }) {
  const D = window.HU_DATA;
  const R = window.HU_RATIOS || {};
  const isLandscape = (w) => { const r = (R[w.img] || "").split("/"); return r.length === 2 && (+r[0]) >= (+r[1]); };
  // a different series each visit: never the one shown last time
  const series = React.useMemo(() => {
    const list = window.HU_SERIES || [];
    if (!list.length) return null;
    let last = null;
    try { last = localStorage.getItem("hu:lastSeries"); } catch (e) {}
    const pool = list.length > 1 ? list.filter((s) => s.id !== last) : list;
    const pick = pool[Math.floor(Math.random() * pool.length)];
    try { localStorage.setItem("hu:lastSeries", pick.id); } catch (e) {}
    return pick;
  }, []);
  // candidate works for this series: its own curated list, topped up with same-year works if sparse
  const candidates = React.useMemo(() => {
    if (!series) return D.works;
    const picked = (series.workIds || []).map(workById).filter(Boolean);
    if (picked.length >= 4) return picked;
    const sameYear = D.works.filter((w) => w.year === series.year && !picked.some((p) => p.id === w.id));
    const filled = picked.concat(sameYear);
    return filled.length ? filled : D.works;
  }, [series]);
  const hero = React.useMemo(() => {
    const land = candidates.filter(isLandscape);
    const pool = land.length ? land : candidates;
    return pool[Math.floor(Math.random() * pool.length)];
  }, [candidates]);
  const heroAR = ((window.HU_RATIOS && window.HU_RATIOS[hero.img]) || "").replace("/", " / ");
  // never upscale past native pixels (avoids blur): cap the mat to the image's real width
  const capHero = (e) => { const nat = e.target.naturalWidth; const plate = e.target.closest(".hu-hero__plate"); if (plate && nat) plate.style.maxWidth = (nat + 40) + "px"; };
  const gridRef = React.useRef(null);
  const [cols, setCols] = React.useState(4);
  // portrait phone: the selected-works row becomes a hand-swipeable, slowly drifting strip
  const [marquee, setMarquee] = React.useState(false);
  React.useLayoutEffect(() => {
    const mq = window.matchMedia("(max-width: 680px) and (orientation: portrait)");
    const sync = () => setMarquee(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);
  React.useLayoutEffect(() => {
    const measure = () => {
      const el = gridRef.current;
      if (!el || marquee || !classicHero) return;
      const n = getComputedStyle(el).gridTemplateColumns.split(" ").filter(Boolean).length;
      if (n && n !== cols) setCols(n);
    };
    measure();
    window.addEventListener("resize", measure);
    return () => window.removeEventListener("resize", measure);
  });
  // the four plates: the work you were last looking at stays on show, as in the works grid
  const featured = React.useMemo(() => {
    const n = classicHero ? cols : Math.min(marquee ? 10 : 12, candidates.length);
    const base = candidates.slice(0, n);
    const seen = lastWorkId && candidates.find((w) => w.id === lastWorkId);
    if (!seen || base.some((w) => w.id === lastWorkId)) return base;
    return [seen].concat(base.slice(0, n - 1));
  }, [candidates, lastWorkId, cols, marquee]);
  // idle drift right-to-left; a finger on the strip stops it, and it eases back a beat later
  const railSet = React.useMemo(() => featured.map((w) => w.id), [featured]);
  const stripRef = React.useRef(null);
  const trackRef = React.useRef(null);
  React.useEffect(() => {
    const el = stripRef.current, tr = trackRef.current;
    if (classicHero || !el || !tr) return;
    // portrait: an endless slow drift you can swipe. desktop: drag with the mouse, clamped at the ends
    const loop = true, auto = marquee;
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let raf = 0, last = 0, holdUntil = 0, x = 0, vel = 0, stopped = false, dragging = false, px0 = 0, t0 = 0, moved = 0;
    let snapTo = null, snapFrom = 0, snapT0 = 0, snapDur = 420;
    const gapPx = () => {
      const cell = tr.firstElementChild;
      const m = cell ? parseFloat(getComputedStyle(cell).marginRight) : 0;
      return m || parseFloat(getComputedStyle(tr).columnGap || getComputedStyle(tr).gap) || 0;
    };
    // fit a whole number of works across the rail, so nothing is ever cut in half
    const fit = () => {
      if (marquee) return 0;
      const cs = getComputedStyle(el);
      const w = el.clientWidth - parseFloat(cs.paddingLeft || 0) - parseFloat(cs.paddingRight || 0), g = gapPx();
      if (!w) return 0;
      // same rhythm as the grid this replaced: as many 230px-plus columns as fit
      const n = Math.max(1, Math.min(6, Math.floor((w + g) / (230 + g))));
      const cw = (w - g * (n - 1)) / n;
      tr.style.setProperty("--cellw", cw + "px");
      return cw + g;
    };
    let pitch = fit();
    const half = () => tr.scrollWidth / 2 || 1;
    const put = () => { tr.style.transform = "translate3d(" + -Math.round(x) + "px,0,0)"; };
    const wrap = () => {
      const h = half();
      // keep any running snap aligned with the wrap, or the tween drags it off the pitch
      while (x >= h) { x -= h; if (snapTo !== null) { snapTo -= h; snapFrom -= h; } }
      while (x < 0) { x += h; if (snapTo !== null) { snapTo += h; snapFrom += h; } }
    };
    const step = (ts) => {
      raf = requestAnimationFrame(step);
      const dt = last ? Math.min(64, ts - last) : 16;
      last = ts;
      if (dragging) return;
      if (snapTo !== null) {
        if (!snapT0) snapT0 = ts;
        const p = Math.min(1, (ts - snapT0) / snapDur);
        x = snapFrom + (snapTo - snapFrom) * (1 - Math.pow(1 - p, 3));
        if (p >= 1) { snapTo = null; snapT0 = 0; }
        wrap(); put(); return;
      }
      // ease the speed toward its resting value (idle drift, or a standstill) instead of cutting it off
      const rest = (auto && !reduced && ts >= holdUntil) ? 14 : 0;
      if (Math.abs(vel - rest) > 0.5) vel += (rest - vel) * (1 - Math.pow(0.94, dt / 16.67));
      else vel = rest;
      if (vel) x += (vel * dt) / 1000;
      wrap(); put();
    };
    const down = (e) => {
      if (e.button != null && e.button !== 0) return;
      dragging = true; vel = 0; moved = 0; snapTo = null; snapT0 = 0; px0 = e.clientX; t0 = performance.now();
      // no pointer capture: capturing on the rail swallows the click that opens a work
      window.addEventListener("pointermove", move);
      window.addEventListener("pointerup", up);
      window.addEventListener("pointercancel", up);
    };
    const move = (e) => {
      if (!dragging) return;
      const dx = e.clientX - px0, dt = Math.max(4, performance.now() - t0);
      px0 = e.clientX; t0 = performance.now();
      moved += Math.abs(dx);
      // smooth the reading: one jittery frame should not decide the throw
      vel = vel * 0.72 + ((-dx / dt) * 1000) * 0.28;
      x -= dx; wrap(); put();
    };
    const up = () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      window.removeEventListener("pointercancel", up);
      if (!dragging) return;
      dragging = false; holdUntil = performance.now() + 2600;
      // settle on a whole work rather than stopping mid-picture
      if (!marquee && pitch) {
        snapFrom = x; snapT0 = 0;
        const lim = pitch * 2;
        const proj = x + Math.max(-lim, Math.min(lim, vel * 0.26));
        snapTo = Math.round(proj / pitch) * pitch;
        snapDur = Math.max(320, Math.min(760, 280 + Math.abs(snapTo - x) * 0.55));
        vel = 0;
      }
      // a drag must not open the work it finished on
      if (moved > 8) { el.dataset.dragged = "1"; setTimeout(() => { delete el.dataset.dragged; }, 80); }
    };
    const onResize = () => { pitch = fit(); if (!marquee && pitch) { x = Math.round(x / pitch) * pitch; wrap(); put(); } };
    window.addEventListener("resize", onResize);
    const swallow = (e) => { if (el.dataset.dragged) { e.stopPropagation(); e.preventDefault(); } };
    el.addEventListener("pointerdown", down);
    el.addEventListener("click", swallow, true);
    // hold the strip still until the opening has finished, so it doesn't drift behind the curtain
    const start = () => { if (stopped) return; x = 0; last = 0; pitch = fit(); put(); raf = requestAnimationFrame(step); };
    if (window.__huIntroDone) start();
    else window.addEventListener("hu:intro-done", start, { once: true });
    return () => {
      stopped = true;
      cancelAnimationFrame(raf);
      window.removeEventListener("hu:intro-done", start);
      window.removeEventListener("resize", onResize);
      el.removeEventListener("pointerdown", down);
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      window.removeEventListener("pointercancel", up);
      el.removeEventListener("click", swallow, true);
      tr.style.transform = "";
    };
  }, [marquee, featured.length]);
  // keep the stacked hero's picture box on whole pixels: at fractional browser zoom a
  // fractional row height shows up as a hairline seam when a neighbour repaints
  React.useEffect(() => {
    const fix = () => {
      const p = document.querySelector(".hu-hero-full__plate");
      if (!p) return;
      p.style.height = "";
      if (!window.matchMedia("(max-width: 1080px)").matches) return;
      const h = p.getBoundingClientRect().height;
      if (h) p.style.height = Math.round(h) + "px";
    };
    fix();
    const t = setTimeout(fix, 200);
    window.addEventListener("resize", fix);
    return () => { clearTimeout(t); window.removeEventListener("resize", fix); };
  }, [hero]);
  // the whole series is the browsing set in the lightbox, hero first
  const lbSet = React.useMemo(() => {
    const ids = candidates.map((w) => w.id);
    return hero ? [hero.id].concat(ids.filter((id) => id !== hero.id)) : ids;
  }, [hero, candidates]);
  const cjk = lang !== "en";
  const seriesTitle = series ? (lang === "en" ? series.en : lang === "jp" ? series.jp : series.cjk) : t.selTitle;
  const philosophy = series && window.HU_WRITINGS && window.HU_WRITINGS.philosophy.find((p) => p.id === series.id);
  const seriesSub = philosophy && philosophy.quote ? philosophy.quote[lang] : t.selSub;
  const bandRef = React.useRef(null);
  React.useLayoutEffect(() => {
    const fit = () => {
      const el = bandRef.current && bandRef.current.querySelector(".hu-sh__sub");
      if (!el) return;
      el.style.fontSize = "";
      el.style.whiteSpace = "nowrap";
      let size = parseFloat(getComputedStyle(el).fontSize);
      const min = 13;
      while (size > min && el.scrollWidth > el.clientWidth) { size -= 0.5; el.style.fontSize = size + "px"; }
      // still too long even at the floor — let it wrap instead of swallowing the sentence
      if (el.scrollWidth > el.clientWidth) { el.style.whiteSpace = "normal"; el.style.fontSize = "15px"; }
    };
    fit();
    window.addEventListener("resize", fit);
    return () => window.removeEventListener("resize", fit);
  }, [lang, seriesTitle, seriesSub, cjk]);
  // the statement band follows the series on show: its own pull-quote from the writings
  const stQuote = React.useMemo(() => {
    const map = window.HU_STATEMENTS || {};
    const s = series && map[series.id];
    if (s) return { tw: s.tw, en: s.en };
    return { tw: t.stBig, en: t.stSmall };
  }, [series, t]);
  // the band's measure is fixed, the type scales to it: every series' quote fills the same
  // column width whatever its length, so no version looks oversized next to another
  const stRef = React.useRef(null);
  React.useLayoutEffect(() => {
    const fit = () => {
      const box = stRef.current; if (!box) return;
      const cn = box.querySelector(".hu-statement__cjk"); if (!cn) return;
      const en = box.querySelector(".hu-statement__en");
      const target = Math.min(760, box.clientWidth);
      cn.style.whiteSpace = "nowrap"; cn.style.maxWidth = "none"; cn.style.fontSize = "100px";
      const w = cn.scrollWidth || 1;
      const min = target < 560 ? 15 : 22;
      const size = Math.max(min, Math.min(38, 100 * target / w));
      cn.style.fontSize = size + "px";
      const lineW = Math.min(target, w * size / 100);
      if (size <= min) { cn.style.whiteSpace = "normal"; cn.style.maxWidth = target + "px"; }
      if (en) { en.style.fontSize = Math.max(14, Math.min(22, size * .58)) + "px"; en.style.maxWidth = Math.max(260, lineW * .82) + "px"; }
    };
    fit();
    window.addEventListener("resize", fit);
    return () => window.removeEventListener("resize", fit);
  }, [stQuote, lang]);
  const classicHero = typeof location !== "undefined" && /hero=classic/.test(location.search);
  // ---- first-load intro: the real lockup starts centre-stage, its parts appear in turn, then it
  // travels to its seat in the layout while the picture is unveiled left to right behind it
  const heroRef = React.useRef(null);
  const lockRef = React.useRef(null);
  const [intro, setIntro] = React.useState(() => !window.__huIntroDone);
  const [settled, setSettled] = React.useState(false);
  const unveilRef = React.useRef(false);
  const runRef = React.useRef(false);
  React.useLayoutEffect(() => {
    if (!intro) return;
    const hero = heroRef.current, lock = lockRef.current;
    const finish = () => { document.documentElement.classList.remove("hu-intro-lock"); window.__huIntroDone = true; setIntro(false); window.dispatchEvent(new Event("hu:intro-done")); };
    if (!hero || !lock || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { finish(); return; }
    const done = () => { unlock(); finish(); };
    let moved = false;
    // no scrollbar over an empty page while the opening plays
    document.documentElement.classList.add("hu-intro-lock");
    const hold = (e) => { if (document.documentElement.classList.contains("hu-intro-lock")) e.preventDefault(); };
    const holdKeys = (e) => {
      if (!document.documentElement.classList.contains("hu-intro-lock")) return;
      if (["ArrowDown", "ArrowUp", "PageDown", "PageUp", "Home", "End", " "].includes(e.key)) e.preventDefault();
    };
    window.addEventListener("wheel", hold, { passive: false });
    window.addEventListener("touchmove", hold, { passive: false });
    window.addEventListener("keydown", holdKeys);
    const unlock = () => {
      document.documentElement.classList.remove("hu-intro-lock");
      window.removeEventListener("wheel", hold);
      window.removeEventListener("touchmove", hold);
      window.removeEventListener("keydown", holdKeys);
    };
    const place = () => {
      lock.style.transform = "none";
      const lr = lock.getBoundingClientRect();
      const ebEl = lock.querySelector(".hu-eyebrow"), nsEl = lock.querySelector(".hu-namestack");
      if (!ebEl || !nsEl) return;
      const eb = ebEl.getBoundingClientRect(), ns = nsEl.getBoundingClientRect();
      const gw = Math.max(eb.width, ns.width), gh = ns.bottom - eb.top;
      if (!gw || !gh) return;
      // no scaling: the lockup is shown at its final size from the first frame, only moved
      const tx = (window.innerWidth - gw) / 2 - eb.left;
      const ty = window.innerHeight * 0.44 - gh / 2 - eb.top;
      lock.style.transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
    };
    place();
    // the header publishes its height one effect later, so take the final reading next frame
    const r1 = requestAnimationFrame(() => place());
    const t0 = setTimeout(place, 140);
    const timers = [];
    const run = () => {
      if (hero.classList.contains("is-run")) return;
      clearTimeout(cap);
      place();
      runRef.current = true;
      hero.classList.add("is-run");
      timers.push(setTimeout(() => {
        moved = true;
        // paint the lockup once onto its own layer for the trip, then hand it back
        lock.style.willChange = "transform";
        lock.style.transition = "transform 1.15s cubic-bezier(.62,0,.12,1)";
        lock.style.transform = "none";
        // class flipped by hand: a React re-render in the middle of the move can cost frames
        unveilRef.current = true;
        hero.classList.add("is-unveil");
        window.dispatchEvent(new Event("hu:intro-unveil"));
        const clear = (e) => { if (e.target !== lock) return; lock.style.willChange = ""; lock.removeEventListener("transitionend", clear); };
        lock.addEventListener("transitionend", clear);
      }, 1900));
      timers.push(setTimeout(() => setSettled(true), 3050));
      timers.push(setTimeout(done, 3700));
    };
    // the picture is fully read and decoded before a single letter shows, so the
    // opening runs to the same beat every time (with a hard cap so it can never hang)
    const heroImg = hero.querySelector(".hu-hero-full__img");
    const cap = setTimeout(run, 2500);
    const ready = heroImg
      ? (heroImg.complete && heroImg.naturalWidth
          ? (heroImg.decode ? heroImg.decode().catch(() => {}) : Promise.resolve())
          : new Promise((res) => { heroImg.addEventListener("load", res, { once: true }); heroImg.addEventListener("error", res, { once: true }); })
            .then(() => (heroImg.decode ? heroImg.decode().catch(() => {}) : null)))
      : Promise.resolve();
    ready.then(() => { requestAnimationFrame(run); setTimeout(run, 60); });
    // a background tab never paints a frame — pick the opening up when it comes forward
    const onVis = () => { if (!document.hidden) run(); };
    document.addEventListener("visibilitychange", onVis);
    const onResize = () => { if (!moved) place(); };
    window.addEventListener("resize", onResize);
    return () => {
      timers.forEach(clearTimeout); clearTimeout(cap); clearTimeout(t0);
      document.removeEventListener("visibilitychange", onVis);
      unlock();
      cancelAnimationFrame(r1); window.removeEventListener("resize", onResize);
      lock.style.transform = ""; lock.style.transition = ""; lock.style.willChange = "";
    };
  }, [intro]);
  // the series line keeps CJK + English on one row: shrink the type until it fits the column
  const serRef = React.useRef(null);
  React.useLayoutEffect(() => {
    const fit = () => {
      const el = serRef.current;
      if (!el) return;
      el.style.fontSize = "";
      el.style.whiteSpace = "nowrap";
      el.style.overflow = "hidden";
      let size = parseFloat(getComputedStyle(el).fontSize);
      const min = 13;
      while (size > min && el.scrollWidth > el.clientWidth) { size -= 0.5; el.style.fontSize = size + "px"; }
      // too long even at the floor (narrow phones) — wrap rather than cut the title off
      if (el.scrollWidth > el.clientWidth) { el.style.whiteSpace = "normal"; el.style.overflow = "visible"; }
    };
    fit();
    window.addEventListener("resize", fit);
    return () => window.removeEventListener("resize", fit);
  }, [lang, series, seriesTitle]);
  return (
    <div className={"hu-home" + (intro ? " is-intro" : "") + (settled ? " is-settled" : "")}>
      {classicHero ? (
      <section className="hu-hero hu-wrap">
        <div className="hu-hero__lockup">
          <HUEyebrow rule>{t.heroEyebrow}</HUEyebrow>
          <div className="hu-namestack">
            <h1 className="hu-hero__name">胡朝景</h1>
            <p className="hu-hero__rom"><span className="hu-rom-a">Chau<span className="hu-rom-sep">-</span>Jin</span><span className="hu-rom-b">Hu</span></p>
          </div>
        </div>
        <figure className="hu-hero__plate" onClick={() => openWork(hero.id, lbSet)}
          onPointerEnter={() => window.HU_PREFETCH && window.HU_PREFETCH(hero.img)}>
          <div className="hu-hero__mat">
            <div className="hu-hero__frame" style={heroAR ? { aspectRatio: heroAR } : undefined}>
              <img src={`${ASSET}/works/${hero.img}`} alt={hero.en} onLoad={capHero} />
            </div>
          </div>
          <figcaption><HUMuseumLabel work={hero} lang={lang} /></figcaption>
        </figure>
      </section>
      ) : (
      <section className={"hu-hero-full" + (intro ? " is-intro" : "") + (runRef.current ? " is-run" : "") + (unveilRef.current ? " is-unveil" : "") + (settled ? " is-settled" : "")} ref={heroRef}>
        <div className="hu-hero-full__lockup" ref={lockRef}>
          <HUEyebrow rule><span className="lbl">{t.heroEyebrow}</span></HUEyebrow>
          <div className="hu-namestack">
            <h1 className="hu-hero__name">胡朝景</h1>
            <p className="hu-hero__rom"><span className="hu-rom-a">Chau<span className="hu-rom-sep">-</span>Jin</span><span className="hu-rom-b">Hu</span></p>
          </div>
          <div className="hu-hf-series">
            <span className="hu-hf-series__title" ref={serRef}>
              <span className="hu-hf-series__cjk">{series ? series.cjk : seriesTitle}</span>
              {series && series.en ? <span className="hu-hf-series__en">{series.en}</span> : null}
            </span>
            <span className="hu-hf-series__sub">{seriesSub}</span>
          </div>
        </div>
        <div className="hu-hero-full__plate" onClick={() => openWork(hero.id, lbSet)}
          onPointerEnter={() => window.HU_PREFETCH && window.HU_PREFETCH(hero.img)}>
          <img className="hu-hero-full__img" src={`${ASSET}/works/${hero.img}`} alt={hero.en} />
          <div className="hu-hero-full__scrim"></div>
          <div className="hu-hero-full__veil"></div>
          <div className="hu-hero-full__label">
            <span className="hu-hf-cap"><span className="cjk">{hero.cjk}</span><span className="sep"> · </span><span className="en">{hero.en}</span><span className="sep"> — </span><span className="met">{window.HU_DATA.medium[hero.tag][lang]}, {hero.yearLabel || hero.year}</span></span>
          </div>
          <div className="hu-hero-full__curtain"></div>
        </div>
      </section>
      )}

      <section className={"hu-band" + (classicHero ? " hu-wrap" : " hu-band--flush")}>
        {classicHero ? (
        <div className="hu-band__head" ref={bandRef}>
          <HUSectionHeading eyebrow={t.selEyebrow} title={seriesTitle} cjk={cjk} sub={seriesSub} />
          <HUButton variant="ghost" onClick={() => go("works")}>{t.allWorks}</HUButton>
        </div>
        ) : (
        <div className="hu-band__eyebrow"><HUEyebrow rule>{t.selEyebrow}</HUEyebrow></div>
        )}
        {classicHero ? (
        <div className="hu-grid-4" ref={gridRef}>
          {featured.map((w) => <HUArtworkCard key={w.id} work={w} lang={lang} onClick={() => openWork(w.id, lbSet)} />)}
        </div>
        ) : marquee ? (
        <div className="hu-strip" ref={stripRef}>
          <div className="hu-strip__track" ref={(n) => { trackRef.current = n; gridRef.current = n; }}>
            {featured.concat(featured).map((w, i) => (
              <div className="hu-strip__cell" key={w.id + "-" + i} aria-hidden={i >= featured.length ? "true" : undefined}>
                <HUArtworkCard work={w} lang={lang} onClick={() => openWork(w.id, railSet)} />
              </div>
            ))}
          </div>
        </div>
        ) : (
        <div className="hu-strip hu-strip--wide" ref={stripRef}>
          <div className="hu-strip__track" ref={(n) => { trackRef.current = n; gridRef.current = n; }}>
            {featured.concat(featured).map((w, i) => (
              <div className="hu-strip__cell" key={w.id + "-" + i} aria-hidden={i >= featured.length ? "true" : undefined}>
                <HUArtworkCard work={w} lang={lang} onClick={() => openWork(w.id, railSet)} />
              </div>
            ))}
          </div>
        </div>
        )}
      </section>

      <section className="hu-statement">
        <div className="hu-wrap hu-statement__inner" ref={stRef}>
          <span className="hu-statement__eyebrow">Artist Statement</span>
          <p className="hu-statement__cjk">{smBreaks(stQuote.tw)}</p>
          <p className="hu-statement__en">{stQuote.en}</p>
          <span className="hu-statement__rule"></span>
          <p className="hu-statement__by">{series ? `${series.cjk} · ${series.en}` : "胡朝景 · Chau-Jin Hu"}</p>
        </div>
      </section>
    </div>
  );
}

/* ---------------- Works ---------------- */
function WorksScreen({ t, lang, openWork, registerYearSetter }) {
  const D = window.HU_DATA;
  const [medium, setMedium] = React.useState("all");
  const [year, setYear] = React.useState("all");
  const [ser, setSer] = React.useState("all");
  React.useEffect(() => { if (registerYearSetter) registerYearSetter((y) => { setMedium("all"); setSer("all"); setYear(y); }); }, []);
  const [floating, setFloating] = React.useState(false);
  const filtersRef = React.useRef(null);
  const gridRef = React.useRef(null);
  const pendingScroll = React.useRef(false);
  React.useEffect(() => {
    if (!pendingScroll.current) return;
    pendingScroll.current = false;
    const grid = gridRef.current;
    if (!grid) return;
    // absolute document offset, so a scroll still in flight can't skew the maths
    const absTop = (el) => { let y = 0; for (let n = el; n; n = n.offsetParent) y += n.offsetTop; return y; };
    const gap = () => {
      const h = document.querySelector(".hu-hdr");
      return (h ? h.getBoundingClientRect().height : 88) + 20;
    };
    const target = () => Math.max(0, absTop(grid) - gap());
    // drive the glide ourselves: the browser's own smooth scroll gets dropped while the grid
    // is still re-laying out, which is why the jump only worked sometimes
    let stop = false, raf = 0, t0 = 0, from = window.scrollY, to = target(), dur = 620;
    const se = document.scrollingElement || document.documentElement;
    const giveWay = () => { stop = true; cancelAnimationFrame(raf); };
    window.addEventListener("wheel", giveWay, { passive: true, once: true });
    window.addEventListener("touchstart", giveWay, { passive: true, once: true });
    const ease = (p) => 1 - Math.pow(1 - p, 3);
    const frame = (ts) => {
      if (stop) return;
      if (!t0) t0 = ts;
      const p = Math.min(1, (ts - t0) / dur);
      to = target(); // the grid settles as rows lay out — keep aiming at the live position
      se.scrollTop = from + (to - from) * ease(p);
      if (p < 1) raf = requestAnimationFrame(frame);
    };
    const start = () => {
      window.dispatchEvent(new Event("hu:reveal-bar"));
      from = window.scrollY; to = target(); t0 = 0;
      raf = requestAnimationFrame(frame);
    };
    const r = requestAnimationFrame(() => requestAnimationFrame(start));
    return () => {
      cancelAnimationFrame(r); cancelAnimationFrame(raf); stop = true;
      window.removeEventListener("wheel", giveWay); window.removeEventListener("touchstart", giveWay);
    };
  }, [medium, year, ser]);
  React.useEffect(() => {
    const onScroll = () => {
      const el = filtersRef.current;
      if (!el) return;
      const hdr = document.querySelector(".hu-hdr");
      const headerBottom = hdr ? hdr.getBoundingClientRect().bottom : 0;
      setFloating(el.getBoundingClientRect().bottom < headerBottom);
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); };
  }, []);
  const years = Array.from(new Set(D.works.map((w) => String(w.year)))).sort((a, b) => b - a);
  const mediumOpts = [{ value: "all", label: t.fAll }, { value: "Watercolor", label: t.fWatercolor }, { value: "Oil", label: t.fOil }];
  const serList = (window.HU_SERIES || []).filter((s) => (s.workIds || []).some((id) => D.works.some((w) => w.id === id)));
  const serOpts = [{ value: "all", label: t.fAll }, ...serList.map((s) => ({ value: s.id, label: lang === "en" ? s.en : lang === "jp" ? s.jp : s.cjk }))];
  const yearOpts = [{ value: "all", label: t.fAll }, ...years.map((y) => ({ value: y, label: y }))];
  const list = D.works
    .filter((w) => (medium === "all" || w.tag === medium) && (year === "all" || String(w.year) === year) && (ser === "all" || w.series === ser))
    .sort((a, b) => b.year - a.year);
  return (
    <div className="hu-page hu-wrap hu-wrap--wide">
      <div className="hu-page__head">
        <HUSectionHeading eyebrow={t.worksEyebrow(D.works.length, (() => { const ys = D.works.map((w) => w.year).filter(Boolean); const a = Math.min(...ys), b = Math.max(...ys); return a === b ? `${b}` : `${a}–${b}`; })())} title={t.worksTitle} cjk={lang !== "en"} />
        <div className="hu-filters" ref={filtersRef}>
          <HUSelect label={t.fSeries} value={ser} onChange={setSer} options={serOpts} align={lang === "en" ? "wide" : "exact"} />
          <HUSelect label={t.fMedium} value={medium} onChange={setMedium} options={mediumOpts} align="center" />
          <HUSelect label={t.fYear} value={year} onChange={setYear} options={yearOpts} align="exact" />
        </div>
      </div>
      <div className="hu-masonry" ref={gridRef}>
        {list.map((w) => <HUArtworkCard key={w.id} work={w} lang={lang} onClick={() => openWork(w.id)} />)}
      </div>
      <p className="hu-copyright">{t.copyright}</p>
      <div className={"hu-filters--float" + (floating ? " is-shown" : "")}>
        <div className="hu-filters">
        <HUSelect label={t.fSeries} value={ser} onChange={(v) => { pendingScroll.current = true; setSer(v); }} options={serOpts} align={lang === "en" ? "wide" : "exact"} />
        <HUSelect label={t.fMedium} value={medium} onChange={setMedium} options={mediumOpts} align="center" />
        <HUSelect label={t.fYear} value={year} onChange={(v) => { pendingScroll.current = true; setYear(v); }} options={yearOpts} align="exact" />
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { HomeScreen, WorksScreen });
