/* hero.jsx — landing page */

const { useState: useStateH, useEffect: useEffectH, useRef: useRefH, useMemo: useMemoH } = React;

function VectorField() {
  const canvasRef = useRefH(null);
  const mouseRef = useRefH({ x: 0.5, y: 0.5, active: false });
  const timeRef = useRefH(0);

  useEffectH(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    let raf;
    let w = 0, h = 0;

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const rect = canvas.getBoundingClientRect();
      w = rect.width; h = rect.height;
      canvas.width = w * dpr;
      canvas.height = h * dpr;
      ctx.scale(dpr, dpr);
    };
    resize();
    window.addEventListener("resize", resize);

    const onMove = (e) => {
      const rect = canvas.getBoundingClientRect();
      mouseRef.current.x = (e.clientX - rect.left) / rect.width;
      mouseRef.current.y = (e.clientY - rect.top) / rect.height;
      mouseRef.current.active = true;
    };
    const onLeave = () => { mouseRef.current.active = false; };
    canvas.addEventListener("mousemove", onMove);
    canvas.addEventListener("mouseleave", onLeave);
    window.addEventListener("mousemove", onMove);

    // store smoothed angles per cell
    const cols = 28, rows = 18;
    const angles = Array(cols * rows).fill(0);
    const targets = Array(cols * rows).fill(0);

    const tick = () => {
      timeRef.current += 0.008;
      ctx.clearRect(0, 0, w, h);

      const cellW = w / cols;
      const cellH = h / rows;
      const len = Math.min(cellW, cellH) * 0.42;
      const mx = mouseRef.current.x * w;
      const my = mouseRef.current.y * h;

      for (let i = 0; i < cols; i++) {
        for (let j = 0; j < rows; j++) {
          const cx = (i + 0.5) * cellW;
          const cy = (j + 0.5) * cellH;
          const dx = mx - cx;
          const dy = my - cy;
          const dist = Math.sqrt(dx * dx + dy * dy);
          const idx = j * cols + i;

          let target;
          if (mouseRef.current.active) {
            // domain wall: vectors closer to mouse rotate to point at it,
            // farther vectors precess slowly (Landau-Lifshitz feel)
            const baseAngle = Math.atan2(dy, dx);
            const swirl = Math.sin(dist / 80 + timeRef.current * 1.5) * 0.6;
            target = baseAngle + swirl;
          } else {
            // idle: gentle perlin-ish flow
            target = Math.sin(i * 0.2 + timeRef.current) * 0.6
                   + Math.cos(j * 0.18 - timeRef.current * 0.7) * 0.6
                   + timeRef.current * 0.05;
          }
          // smooth toward target with shortest-path
          let cur = angles[idx];
          let diff = target - cur;
          while (diff > Math.PI) diff -= 2 * Math.PI;
          while (diff < -Math.PI) diff += 2 * Math.PI;
          angles[idx] = cur + diff * 0.08;

          // distance-based emphasis
          const closeness = Math.max(0, 1 - dist / 220);
          const stroke = 1 + closeness * 1.2;
          const opacity = 0.35 + closeness * 0.65;

          ctx.save();
          ctx.translate(cx, cy);
          ctx.rotate(angles[idx]);
          ctx.strokeStyle = `rgba(10,10,10,${opacity})`;
          ctx.lineWidth = stroke;
          ctx.lineCap = "round";
          ctx.beginPath();
          ctx.moveTo(-len * 0.5, 0);
          ctx.lineTo(len * 0.5, 0);
          ctx.stroke();
          // arrowhead
          ctx.beginPath();
          ctx.moveTo(len * 0.5, 0);
          ctx.lineTo(len * 0.5 - 4, -2.5);
          ctx.moveTo(len * 0.5, 0);
          ctx.lineTo(len * 0.5 - 4, 2.5);
          ctx.stroke();
          ctx.restore();
        }
      }

      raf = requestAnimationFrame(tick);
    };
    tick();

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

  return <canvas ref={canvasRef} style={{ width: "100%", height: "100%", display: "block" }} />;
}

function WordReveal({ text, delay = 0, className = "" }) {
  const [shown, setShown] = useStateH(false);
  useEffectH(() => {
    const t = setTimeout(() => setShown(true), delay);
    return () => clearTimeout(t);
  }, [delay]);
  const words = text.split(" ");
  return (
    <span className={className}>
      {words.map((w, i) => (
        <span key={i} className={`word-mask ${shown ? "in" : ""}`} style={{ marginRight: "0.28em" }}>
          <span style={{ transitionDelay: `${i * 60}ms` }}>{w}</span>
        </span>
      ))}
    </span>
  );
}

function Marquee({ items, speed = 60 }) {
  const dup = [...items, ...items, ...items];
  return (
    <div style={{ overflow: "hidden", borderTop: "1px solid var(--line)", borderBottom: "1px solid var(--line)", padding: "20px 0", position: "relative" }}>
      <div style={{
        display: "flex", gap: 64, whiteSpace: "nowrap",
        animation: `marquee ${items.length * speed / 8}s linear infinite`,
      }}>
        {dup.map((it, i) => (
          <span key={i} style={{ display: "inline-flex", alignItems: "center", gap: 16, fontFamily: "var(--font-mono)", fontSize: 13, letterSpacing: "0.06em", textTransform: "uppercase" }}>
            <span style={{ width: 6, height: 6, background: "var(--fg)", borderRadius: "50%" }} />
            {it}
          </span>
        ))}
      </div>
      <style>{`@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-33.333%); } }`}</style>
    </div>
  );
}

const MINDMAP_TWEAKS = {
  layerSpacing: 0.95,
  sceneScale: 225,
  focusShift: 220,
  gridSteps: 7,
  tilt: 30,
  bgColor: "transparent",
  paperFill: "rgba(0,0,0,0.04)",
  inkColor: "#0a0a0a",
  gridColor: "#0a0a0a",
};

function Hero() {
  useLang(); // re-render on language toggle
  const SCROLL_MIN = -1, SCROLL_MAX = 2;
  const [mmFocus, setMmFocus] = useStateH({ scroll: -1, focus: 0, overviewBlend: 1 });
  const [mmScrollTarget, setMmScrollTarget] = useStateH(SCROLL_MIN);
  const targetRef = useRefH(SCROLL_MIN);
  const wheelAccumRef = useRefH(0);
  const wheelLockUntilRef = useRefH(0);
  const lastBlendRef = useRefH(1);

  // Throttle parent re-renders: only re-render when overview blend moves enough.
  const handleFocus = (f) => {
    if (Math.abs(f.overviewBlend - lastBlendRef.current) > 0.01 ||
        (f.overviewBlend === 0 && lastBlendRef.current !== 0) ||
        (f.overviewBlend === 1 && lastBlendRef.current !== 1)) {
      lastBlendRef.current = f.overviewBlend;
      setMmFocus(f);
    }
  };

  // Unified wheel: while at top of page, drive mindmap; once mindmap is at last
  // layer, let the wheel propagate so the page scrolls into the entries below.
  // Mobile (<900px) skips this entirely — touch scrolling is native, and the
  // hero falls back to a stacked layout where the mindmap is just a panel.
  useEffectH(() => {
    const onWheel = (e) => {
      if (window.innerWidth < 900) return;
      if (window.scrollY > 10) return;
      const now = performance.now();
      if (now < wheelLockUntilRef.current) { e.preventDefault(); return; }
      const target = targetRef.current;
      const goingDown = e.deltaY > 0;
      if (goingDown && target >= SCROLL_MAX) return;   // let page scroll
      if (!goingDown && target <= SCROLL_MIN) return;  // already in overview at top
      e.preventDefault();
      wheelAccumRef.current += e.deltaY;
      const THRESH = 90;
      if (wheelAccumRef.current > THRESH && target < SCROLL_MAX) {
        const next = target + 1;
        targetRef.current = next; setMmScrollTarget(next);
        wheelAccumRef.current = 0; wheelLockUntilRef.current = now + 650;
      } else if (wheelAccumRef.current < -THRESH && target > SCROLL_MIN) {
        const next = target - 1;
        targetRef.current = next; setMmScrollTarget(next);
        wheelAccumRef.current = 0; wheelLockUntilRef.current = now + 650;
      }
    };
    window.addEventListener('wheel', onWheel, { passive: false });
    return () => window.removeEventListener('wheel', onWheel);
  }, []);

  // Once the user wheel-engages the mindmap (out of overview), fade the left text.
  const leftFade = Math.max(0, Math.min(1, 1 - mmFocus.overviewBlend));
  const leftOpacity = 1 - leftFade;
  return (
    <div style={{ minHeight: "100vh", paddingTop: 90, position: "relative", overflow: "hidden" }}>
      <section className="hero-stage" style={{
        position: "relative",
        padding: "80px var(--side-pad) 60px",
        minHeight: "calc(100vh - 200px)",
      }}>
        {/* Left — slogan (absolutely positioned so the mindmap can claim its space) */}
        <div className="hero-left" style={{
          position: "absolute",
          top: 80, bottom: 60,
          left: "var(--side-pad)",
          width: "calc(50% - var(--side-pad) - 24px)",
          display: "flex", flexDirection: "column", justifyContent: "space-between",
          opacity: leftOpacity,
          transform: `translateX(${-leftFade * 32}px)`,
          pointerEvents: leftFade > 0.6 ? "none" : "auto",
          zIndex: 1,
        }}>
          <div>
            <div className="micro" style={{ marginBottom: 32, display: "flex", alignItems: "center", gap: 12 }}>
              <span style={{ width: 24, height: 1, background: "var(--fg)" }} />
              <span>{t("Index 00 — Manifest")}</span>
            </div>
            <h1 className="display-xxl hero-title" style={{ marginBottom: 56, fontSize: "clamp(34px, 5.2vw, 92px)", lineHeight: 1.02 }}>
              <WordReveal text={t("Aligning physics,")} delay={300} />
              <br/>
              <WordReveal text={t("code, and culture")} delay={500} />
              <br/>
              <span style={{ fontStyle: "italic", color: "var(--fg-muted)" }}>
                <WordReveal text={t("toward a civilization-")} delay={900} />
              </span>
              <br/>
              <span style={{ fontStyle: "italic", color: "var(--fg-muted)" }}>
                <WordReveal text={t("scale solution.")} delay={1100} />
              </span>
            </h1>
          </div>

          <div>
            <div className="hr" style={{ marginBottom: 24, background: "var(--line-soft)" }} />
            <div className="three-col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 32 }}>
              <div>
                <div className="micro" style={{ marginBottom: 8 }}>{t("01 · Researcher")}</div>
                <div className="body-sm">{t("KAIST Ultrafast Spin Dynamics Lab. Ferrimagnetic domain-wall dynamics; bridge to neuromorphic.")}</div>
              </div>
              <div>
                <div className="micro" style={{ marginBottom: 8 }}>{t("02 · Founder")}</div>
                <div className="body-sm">{t("Stealth startup, co-founded. Personal-optimization engine, B2B2C, study-environment first.")}</div>
              </div>
              <div>
                <div className="micro" style={{ marginBottom: 8 }}>{t("03 · Director")}</div>
                <div className="body-sm">{t("HUSTLY ARCHIV — long-text editorial in Korean hip-hop. 3K+ followers, 1M+ views.")}</div>
              </div>
            </div>
          </div>
        </div>

        {/* Mindmap — slides left and expands to fill the hero as the user wheel-traverses strata */}
        <div className="hero-mindmap" style={{
          position: "absolute",
          top: 80, bottom: 60,
          right: "var(--side-pad)",
          left: `calc(${(1 - leftFade) * 50}% + var(--side-pad) * ${leftFade})`,
        }}>
          <MindmapScene
            tweaks={MINDMAP_TWEAKS}
            stageId="hero-mindmap-stage"
            onFocusChange={handleFocus}
            scrollTarget={mmScrollTarget}
            disableWheel={true}
          />
        </div>
      </section>

      <Marquee items={[
        t("KAIST Ultrafast Spin Dynamics Lab"),
        t("Stealth startup · pre-launch"),
        t("HUSTLY ARCHIV · 3K+ · 1M+ views"),
        t("Spintronics → Neuromorphic"),
        t("GDG on Campus KAIST"),
        t("Physics ∩ Code ∩ Culture"),
        t("Mark Lombardi was right"),
      ]} />

      <div className="responsive-stack" style={{
        padding: "120px var(--side-pad)",
        display: "grid", gridTemplateColumns: "1fr 2fr",
        gap: 64,
      }}>
        <div>
          <div className="micro" style={{ marginBottom: 16 }}>{t("§ 0.1 — On unification")}</div>
          <div className="body-sm" style={{ color: "var(--fg-muted)" }}>{t("Read 4 min ↓")}</div>
        </div>
        <div>
          <p className="display-md" style={{ marginBottom: 32, fontStyle: "italic", color: "var(--fg-muted)" }}>
            {t("\"Three identities held in one body. Not a portfolio of careers, but a single thesis on how the world should be assembled.\"")}
          </p>
          <p className="body-lg" style={{ marginBottom: 24 }}>
            {t("A ferrimagnetic domain wall is two opposing magnetizations balanced exactly so the wall moves fastest. The same logic — opposing strengths, intentionally cancelled, producing forward motion — is the geometry I'm trying to build for myself.")}
          </p>
          <p className="body-lg" style={{ color: "var(--fg-muted)" }}>
            {t("Physics gives me the substrate. Code gives me the lever. Culture gives me the audience. Each on its own is incomplete; held together they become a project I'd describe — not lightly — as")} <em style={{ fontStyle: "italic", color: "var(--fg)" }}>{t("civilization-scale")}</em>.
          </p>
          <div style={{ marginTop: 48 }}>
            <a className="link-arrow" data-cursor="link" data-cursor-label={t("Read")} href="#about">{t("Continue · The Thesis →")}</a>
          </div>
        </div>
      </div>

      <IndexCards />

      {/* Scroll hint */}
      <div style={{ position: "absolute", left: "50%", bottom: 24, transform: "translateX(-50%)" }}>
        <div className="micro" style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
          <span>{t("Scroll")}</span>
          <span style={{ width: 1, height: 32, background: "var(--fg)", animation: "scrolltick 1.6s infinite" }} />
        </div>
        <style>{`
          @keyframes scrolltick { 0% { transform: scaleY(0); transform-origin: top; } 50% { transform: scaleY(1); transform-origin: top; } 50.01% { transform: scaleY(1); transform-origin: bottom; } 100% { transform: scaleY(0); transform-origin: bottom; } }
        `}</style>
      </div>
    </div>
  );
}

function IndexCards() {
  const { go } = useRouter();
  useLang();
  const [hovered, setHovered] = useStateH(null);
  const [mouse, setMouse] = useStateH({});

  const cards = [
    {
      id: "about", num: "01", label: t("About"),
      title: t("About"),
      desc: t("Why three identities held in one body. The geometry of the project."),
      meta: t("Long-form · 9 min"),
      visual: "quote",
    },
    {
      id: "research", num: "02", label: t("Research"),
      title: t("Research"),
      desc: t("Ferrimagnetic domain-wall motion at angular-momentum compensation."),
      meta: t("4 papers · 2 labs"),
      visual: "wall",
    },
    {
      id: "projects", num: "03", label: t("Projects"),
      title: t("Projects"),
      desc: t("A canvas for thinking with LLMs in graphs. Plus tooling, simulations."),
      meta: t("7 entries"),
      visual: "graph",
    },
    {
      id: "venture", num: "04", label: t("Venture"),
      title: t("Venture"),
      desc: t("Stealth startup — a personal-optimization engine. B2B2C. Co-founded. Pre-launch."),
      meta: t("Since Oct 2025 · pre-launch"),
      visual: "redact",
    },
    {
      id: "archive", num: "05", label: t("Archive"),
      title: t("Archive"),
      desc: t("Aesthetics, philosophy, cinema, architecture — curated weekly."),
      meta: t("@hustlyarchiv.kr · 3K+"),
      visual: "stripes",
    },
    {
      id: "writing", num: "06", label: t("Writing"),
      title: t("Writing"),
      desc: t("Drafts that survived a second reading. Physics ∩ AI ∩ culture."),
      meta: t("12 entries"),
      visual: "lines",
    },
    {
      id: "contact", num: "07", label: t("Contact"),
      title: t("Contact"),
      desc: t("Builders and thinkers across disciplines. 1–3 day reply."),
      meta: t("Daejeon · KST"),
      visual: "constellation",
    },
  ];

  return (
    <section style={{ padding: "60px var(--side-pad) 120px", position: "relative" }}>
      <div style={{
        display: "flex", justifyContent: "space-between", alignItems: "baseline",
        marginBottom: 32, paddingBottom: 16, borderBottom: "1px solid var(--fg)",
      }}>
        <div className="micro" style={{ display: "flex", alignItems: "center", gap: 12 }}>
          <span style={{ width: 6, height: 6, background: "var(--fg)", borderRadius: "50%" }} />
          {t("Index — Seven entries")}
        </div>
        <div className="micro" style={{ color: "var(--fg-muted)" }}>
          {t("↓ pick a thread")}
        </div>
      </div>

      <div className="index-cards-grid bento-grid" style={{
        display: "grid",
        gridTemplateColumns: "repeat(12, 1fr)",
        gridAutoRows: "minmax(220px, auto)",
        gap: 0,
        borderLeft: "1px solid var(--line)",
        borderTop: "1px solid var(--line)",
      }}>
        {cards.map((c, i) => {
          // span layout: 01 wide, 02 wide, 03 narrow, 04 narrow, 05 wide, 06 narrow, 07 wide
          const spans = [
            { col: 6, row: 1 },
            { col: 6, row: 1 },
            { col: 4, row: 1 },
            { col: 4, row: 1 },
            { col: 4, row: 1 },
            { col: 5, row: 1 },
            { col: 7, row: 1 },
          ];
          const sp = spans[i] || { col: 4, row: 1 };
          const isHover = hovered === c.id;
          return (
            <button
              key={c.id}
              onMouseEnter={() => { if (window.__hasHover) setHovered(c.id); }}
              onMouseLeave={() => { if (!window.__hasHover) return; setHovered(null); setMouse(m => ({ ...m, [c.id]: { x: 0.5, y: 0.5, active: false } })); }}
              onMouseMove={(e) => {
                if (!window.__hasHover) return;
                const r = e.currentTarget.getBoundingClientRect();
                setMouse(m => ({ ...m, [c.id]: { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height, active: true } }));
              }}
              onClick={() => go(c.id)}
              data-cursor="link" data-cursor-label={t("Open")}
              className="index-card"
              style={{
                gridColumn: `span ${sp.col}`, gridRow: `span ${sp.row}`,
                position: "relative",
                textAlign: "left",
                padding: "28px",
                borderRight: "1px solid var(--line)",
                borderBottom: "1px solid var(--line)",
                background: isHover ? "var(--fg)" : "var(--bg)",
                color: isHover ? "var(--bg)" : "var(--fg)",
                transition: "background 320ms var(--easing-default), color 320ms var(--easing-default)",
                display: "flex", flexDirection: "column", justifyContent: "space-between",
                minHeight: 240, overflow: "hidden",
              }}>
              <CardVisual variant={c.visual} hover={isHover} cardId={c.id} mouse={mouse[c.id] || { x: 0.5, y: 0.5, active: false }} />

              <div style={{ position: "relative", zIndex: 2 }}>
                <div className="display-md" style={{
                  fontSize: "clamp(22px, 2.4vw, 36px)",
                  fontStyle: isHover ? "italic" : "normal",
                  marginBottom: 8,
                  transition: "font-style 200ms",
                }}>
                  {c.title}
                </div>
                <div className="body-sm" style={{
                  color: isHover ? "rgba(255,255,255,0.7)" : "var(--fg-muted)",
                  maxWidth: 420, marginBottom: 16,
                }}>
                  {c.desc}
                </div>
                <div style={{
                  display: "flex", justifyContent: "space-between", alignItems: "center",
                  paddingTop: 12, borderTop: `1px solid ${isHover ? "rgba(255,255,255,0.2)" : "var(--line-soft)"}`,
                }}>
                  <span className="micro" style={{ color: isHover ? "var(--bg)" : "var(--fg)" }}>
                    {isHover ? t("Open ") : t("Read ")}{c.label}
                  </span>
                  <span style={{
                    fontFamily: "var(--font-mono)", fontSize: 16,
                    transform: isHover ? "translateX(8px)" : "translateX(0)",
                    transition: "transform 280ms var(--easing-default)",
                  }}>→</span>
                </div>
              </div>
            </button>
          );
        })}
      </div>
      <style>{`
        @media (max-width: 900px) {
          .index-cards-grid { grid-template-columns: 1fr !important; }
          .index-cards-grid > * { grid-column: span 1 !important; }
        }
      `}</style>
    </section>
  );
}

// Tiny visual signatures per card — interactive on mouse-move within the card.
function CardVisual({ variant, hover, cardId, mouse }) {
  const stroke = hover ? "rgba(255,255,255,0.85)" : "rgba(10,10,10,0.85)";
  const muted  = hover ? "rgba(255,255,255,0.3)"  : "rgba(10,10,10,0.25)";
  const accent = hover ? "rgba(255,255,255,1)"    : "rgba(10,10,10,1)";
  const common = { flex: 1, margin: "20px 0", position: "relative", overflow: "hidden" };

  const mx = mouse?.x ?? 0.5, my = mouse?.y ?? 0.5, active = mouse?.active;

  // tiny clock for idle motion
  const [t, setT] = useStateH(0);
  useEffectH(() => {
    let raf, last = performance.now();
    const tick = (now) => { setT(now / 1000); raf = requestAnimationFrame(tick); };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  if (variant === "quote") {
    // Italic " breathes; on hover it tilts toward cursor
    const tilt = active ? (mx - 0.5) * 14 : Math.sin(t * 1.2) * 4;
    const scale = 1 + (active ? (1 - my) * 0.12 : Math.sin(t) * 0.04);
    return (
      <div style={common}>
        <div style={{
          fontFamily: "var(--font-display)", fontSize: 96, lineHeight: 0.7,
          fontStyle: "italic", color: stroke, opacity: 0.9,
          transform: `rotate(${tilt}deg) scale(${scale})`,
          transformOrigin: "20% 80%",
          transition: active ? "transform 60ms linear" : "transform 600ms var(--easing-default)",
        }}>"</div>
        <div className="micro" style={{ position: "absolute", right: 0, bottom: 0, color: muted }}>
          § 1 — § 5
        </div>
      </div>
    );
  }

  if (variant === "wall") {
    // Domain wall: position follows mouse X; arrows precess relative to it.
    const wallX = active ? mx * 200 : 100 + Math.sin(t * 0.6) * 40;
    return (
      <div style={common}>
        <svg viewBox="0 0 200 80" preserveAspectRatio="none" style={{ width: "100%", height: "100%" }}>
          {Array.from({ length: 24 }).map((_, i) => {
            const x = 8 + i * 8;
            const dist = (x - wallX) / 14;
            const angle = Math.atan(dist) * 1.4;
            const cy = 40;
            const len = 14;
            const dx = Math.sin(angle) * (len / 2);
            const dy = Math.cos(angle) * (len / 2);
            const close = 1 - Math.min(1, Math.abs(x - wallX) / 60);
            return <line key={i} x1={x - dx} y1={cy + dy} x2={x + dx} y2={cy - dy}
              stroke={close > 0.5 ? accent : stroke}
              strokeWidth={1 + close * 0.6}
              strokeLinecap="round" />;
          })}
          <line x1={wallX} y1="6" x2={wallX} y2="74" stroke={muted} strokeWidth="0.6" strokeDasharray="2 2" />
        </svg>
      </div>
    );
  }

  if (variant === "graph") {
    // Nodes spring toward home positions but get pulled by cursor.
    const home = [[20,20],[80,30],[140,15],[180,40],[120,60],[60,70],[30,55]];
    const edges = [[0,1],[1,2],[1,4],[2,3],[3,4],[4,5],[5,6],[6,0],[1,5],[2,4]];
    const cx = mx * 200, cy = my * 80;
    const pts = home.map(([x, y], i) => {
      const wob = Math.sin(t * 1.5 + i) * 1.5;
      let nx = x + wob, ny = y + Math.cos(t * 1.2 + i * 0.7) * 1.2;
      if (active) {
        const dx = cx - x, dy = cy - y;
        const d = Math.hypot(dx, dy) + 0.01;
        const pull = Math.min(18, 220 / (d + 6));
        nx += (dx / d) * pull;
        ny += (dy / d) * pull;
      }
      return [nx, ny];
    });
    return (
      <div style={common}>
        <svg viewBox="0 0 200 80" preserveAspectRatio="none" style={{ width: "100%", height: "100%" }}>
          {edges.map(([a,b], i) => (
            <line key={i} x1={pts[a][0]} y1={pts[a][1]} x2={pts[b][0]} y2={pts[b][1]} stroke={muted} strokeWidth="0.7" />
          ))}
          {active && pts.map(([x,y], i) => (
            <line key={`m${i}`} x1={x} y1={y} x2={cx} y2={cy} stroke={stroke} strokeWidth="0.4" opacity="0.4" />
          ))}
          {pts.map(([x,y], i) => (
            <circle key={i} cx={x} cy={y} r={i % 3 === 0 ? 3 : 2}
              fill={i % 2 ? accent : (hover ? "var(--fg)" : "#fff")} stroke={stroke} strokeWidth="1" />
          ))}
          {active && <circle cx={cx} cy={cy} r="3" fill="none" stroke={stroke} strokeWidth="0.8" />}
        </svg>
      </div>
    );
  }

  if (variant === "redact") {
    // Black redaction bars get eaten away by the cursor — like a flashlight reveals
    return (
      <div style={{ ...common, display: "flex", flexDirection: "column", gap: 6, justifyContent: "center" }}>
        {[60, 90, 70, 50, 80].map((w, i) => {
          const rowY = (i + 0.5) / 5;
          const dy = Math.abs(rowY - my);
          const reveal = active ? Math.max(0, 1 - dy * 4) : 0;
          const jitter = active ? Math.sin(t * 14 + i) * 1 : 0;
          return (
            <div key={i} style={{ display: "flex", gap: 6, transform: `translateX(${jitter}px)` }}>
              <span style={{ height: 10, width: `${w * 0.4}%`, background: muted, opacity: 1 - reveal * 0.7 }} />
              <span style={{ height: 10, width: `${w * 0.3}%`, background: stroke, opacity: 1 - reveal }} />
              <span style={{ height: 10, width: `${w * 0.2}%`, background: muted, opacity: 1 - reveal * 0.7 }} />
            </div>
          );
        })}
      </div>
    );
  }

  if (variant === "stripes") {
    // 3 tiles; the one under cursor gets emphasized
    const idx = active ? Math.min(2, Math.floor(mx * 3)) : -1;
    const angle = active ? mx * 360 : 30 + Math.sin(t * 0.5) * 30;
    return (
      <div style={{ ...common, display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 6 }}>
        {[0,1,2].map(i => {
          const sel = i === idx;
          const rot = active ? (sel ? (mx - 0.5) * 30 : 0) : 0;
          return (
            <div key={i} style={{
              border: `1px solid ${sel ? accent : stroke}`,
              background: i === 1 ? `repeating-linear-gradient(${angle}deg, ${stroke} 0 1px, transparent 1px 5px)` : "transparent",
              position: "relative",
              transform: `scale(${sel ? 1.06 : 1}) rotate(${rot * 0.3}deg)`,
              transition: "transform 220ms var(--easing-default), border-color 220ms",
            }}>
              {i === 0 && <div style={{
                position: "absolute",
                inset: `${30 - (sel ? my * 10 : 0)}%`,
                borderRadius: "50%", background: stroke,
                transition: "inset 200ms",
              }} />}
              {i === 2 && <svg viewBox="0 0 30 30" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
                <polygon points={`15,${5 + (sel ? (my - 0.5) * 6 : 0)} 25,25 5,25`} fill={sel ? stroke : "none"} stroke={stroke} strokeWidth="1" />
              </svg>}
            </div>
          );
        })}
      </div>
    );
  }

  if (variant === "lines") {
    // Lines bend toward cursor; nearest line lights up
    const widths = [100, 78, 92, 60, 84, 70];
    return (
      <div style={{ ...common, display: "flex", flexDirection: "column", gap: 6, justifyContent: "center" }}>
        {widths.map((w, i) => {
          const rowY = (i + 0.5) / widths.length;
          const dy = Math.abs(rowY - my);
          const close = active ? Math.max(0, 1 - dy * 5) : 0;
          const shift = active ? (mx - 0.5) * close * 30 : 0;
          const newW = active ? Math.min(100, w + close * 20) : w;
          return (
            <div key={i} style={{
              height: 1 + close * 1.5,
              width: `${newW}%`,
              background: close > 0.4 ? accent : (i === 1 ? stroke : muted),
              transform: `translateX(${shift}px)`,
              transition: active ? "none" : "all 400ms var(--easing-default)",
            }} />
          );
        })}
      </div>
    );
  }

  if (variant === "constellation") {
    // Stars connect to cursor; the cursor is itself a node
    const home = [[15,30],[40,50],[70,25],[110,55],[150,30],[180,60],[60,70],[130,15]];
    const cx = mx * 200, cy = my * 80;
    const pts = home.map(([x,y], i) => [
      x + Math.sin(t * 0.8 + i) * 1.5,
      y + Math.cos(t * 0.7 + i * 0.5) * 1.2,
    ]);
    return (
      <div style={common}>
        <svg viewBox="0 0 200 80" preserveAspectRatio="none" style={{ width: "100%", height: "100%" }}>
          {pts.map((p, i) => pts.slice(i+1).map((q, j) => {
            const d = Math.hypot(p[0]-q[0], p[1]-q[1]);
            if (d > 70) return null;
            return <line key={`${i}-${j}`} x1={p[0]} y1={p[1]} x2={q[0]} y2={q[1]}
              stroke={muted} strokeWidth="0.5" opacity={1 - d/70} />;
          }))}
          {active && pts.map(([x,y], i) => {
            const d = Math.hypot(x - cx, y - cy);
            if (d > 90) return null;
            return <line key={`c${i}`} x1={x} y1={y} x2={cx} y2={cy}
              stroke={stroke} strokeWidth="0.7" opacity={1 - d/90} />;
          })}
          {pts.map(([x,y], i) => <circle key={i} cx={x} cy={y} r="2" fill={stroke} />)}
          {active && (
            <>
              <circle cx={cx} cy={cy} r="3" fill={accent} />
              <circle cx={cx} cy={cy} r="8" fill="none" stroke={stroke} strokeWidth="0.6" opacity="0.5" />
            </>
          )}
        </svg>
      </div>
    );
  }

  return null;
}

Object.assign(window, { Hero, VectorField, Marquee, WordReveal, IndexCards });
