// Merilum main app
// ----------------

const { useEffect: useEffect_, useRef: useRef_, useState: useState_ } = React;

/* ---------- Header ---------- */

const MERILUM_LOGO = "assets/merilum-logo.svg";

// Inline M-glyph — renders with currentColor so it inherits from the logo
// squircle's color. `size` is any CSS length (e.g. "30px" or "calc(...)").
const MerilumGlyph = ({ size = '24px' }) => (
  <svg width={size} height={size} viewBox="0 0 369.375 235.656" preserveAspectRatio="xMidYMid meet"
       fill="currentColor" aria-hidden="true" style={{ display: 'block', width: size, height: 'auto' }}>
    <path d="M88.451,186.736l67,0.8a9.569,9.569,0,0,1,9.513,10.114L154.487,412.527A10.457,10.457,0,0,1,144,422.4l-67-.8a9.569,9.569,0,0,1-9.513-10.114L77.963,196.61A10.457,10.457,0,0,1,88.451,186.736Z" transform="translate(-67.5 -186.75)"/>
    <path d="M217.545,186.736l-66.765.8c-5.5.066-8.436,4.594-6.551,10.114l73.4,214.875c1.885,5.519,7.875,9.94,13.379,9.874l66.765-.8c5.5-.066,8.436-4.594,6.551-10.114l-73.4-214.875C229.039,191.091,223.049,186.67,217.545,186.736Z" transform="translate(-67.5 -186.75)"/>
    <path d="M349.545,186.736l-66.765.8c-5.5.066-8.436,4.594-6.551,10.114l73.4,214.875c1.885,5.519,7.875,9.94,13.379,9.874l66.765-.8c5.5-.066,8.436-4.594,6.551-10.114l-73.4-214.875C361.039,191.091,355.049,186.67,349.545,186.736Z" transform="translate(-67.5 -186.75)"/>
  </svg>
);

const Header = ({ pastWave, pillRevealed }) => {
  const [menuOpen, setMenuOpen] = useState_(false);
  const [scrolled, setScrolled] = useState_(false);
  const [isMobile, setIsMobile] = useState_(
    typeof window !== 'undefined' ? window.innerWidth <= 640 : false
  );

  useEffect_(() => {
    const onResize = () => setIsMobile(window.innerWidth <= 640);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  useEffect_(() => {
    if (!pillRevealed) setMenuOpen(false);
  }, [pillRevealed]);

  useEffect_(() => {
    const onScroll = () => {
      setScrolled(window.scrollY > 12);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);

  // Header is now ABOVE all content (zIndex 100). It's split visually into
  // two squircle-shaped pills:
  //   1. Logo squircle on the left (always solid ink, glyph reversed white).
  //   2. Nav pill on the right containing Press / Projects / Research +
  //      hamburger button. Reads as one coherent grouped object.
  //
  // Nav pill reveal model:
  //   - Above the wave: pill is HIDDEN (opacity 0, non-interactive). This
  //     gives a quiet hero with just the logo as chrome — same visual effect
  //     as the previous mix-blend-mode experiment, but WITHOUT the per-frame
  //     composite cost of `mix-blend-mode`.
  //   - Past the wave: pill reveals via opacity + color transition. Ink
  //     text on OPAQUE PAPER fill with ink border. Opaque fill is critical
  //     so canvas circles (behind) can't be seen through the pill — even
  //     though z-index puts the pill above, a transparent fill would let
  //     circles bleed visually through the empty interior.
  const pillVisible  = pillRevealed;
  const pillBg       = 'var(--paper)';
  const pillInk      = 'var(--ink)';
  const pillBorder   = 'var(--ink)';

  return (
    <div
      className="site-header"
      style={{
        position: 'fixed',
        top: scrolled ? 14 : 22,
        left: 0, right: 0,
        zIndex: 100,
        pointerEvents: 'none',
        transition: 'top 0.5s cubic-bezier(.2,.8,.2,1)',
        isolation: 'isolate',
      }}
    >
      <div style={{
        width: '100%',
        padding: '0 clamp(16px, 4vw, 40px)',
        display: 'flex',
        alignItems: 'center',
        gap: 12,
        pointerEvents: 'auto',
      }}>
        {/* Logo squircle — flips colors with pastWave. Squircle bg is the
            INVERSE of the glyph color so the logo always reads:
              Above wave: white glyph on ink squircle (sits in the dark zone).
              Past wave:  ink glyph on paper squircle (sits in the light zone).
            Both flip in 0.4s lockstep with the rest of the page. */}
        <a
          href="#"
          onClick={(e) => e.preventDefault()}
          aria-label="Merilum"
          className="logo-squircle"
          style={{
            width: 'var(--header-h)',
            height: 'var(--header-h)',
            borderRadius: 'calc(var(--header-h) * 0.28)',
            // Past wave: bg matches the canvas wave's actual rendered color.
            // The wave is drawn via `difference` blend: #ffffff XOR #0a0a0a (ink)
            // = #f5f5f5. Using --paper (#fafaf7) here would NOT match because
            // --paper has a slight cream tint. #f5f5f5 is exact.
            background: pastWave ? '#f5f5f5' : 'var(--ink)',
            color: pastWave ? 'var(--ink)' : 'var(--paper)',
            display: 'grid',
            placeItems: 'center',
            flexShrink: 0,
            transition: 'background 0.4s ease, color 0.4s ease',
            textDecoration: 'none',
          }}
        >
          <MerilumGlyph size="calc(var(--header-h) * 0.58)" />
        </a>

        <div style={{ flex: 1 }} />

        {/* Nav pill — same height as logo squircle. Hidden above the wave,
            revealed past it. On narrow screens the NavLinks hide via .nav-link
            CSS, leaving only the hamburger inside the pill. */}
        <nav
          className="site-nav-pill"
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 0,
            height: 'var(--header-h)',
            background: pillBg,
            color: pillInk,
            border: `1.5px solid ${pillBorder}`,
            borderRadius: 'calc(var(--header-h) * 0.28)',
            padding: '0 4px 0 4px',
            opacity: pillVisible ? 1 : 0,
            pointerEvents: pillVisible ? 'auto' : 'none',
            transform: pillVisible ? 'translateY(0)' : 'translateY(-4px)',
            transition: 'opacity 0.5s ease, transform 0.5s cubic-bezier(.2,.8,.2,1), background 0.4s ease, color 0.4s ease, border-color 0.4s ease',
            flexShrink: 0,
          }}
        >
          <NavLink>Press</NavLink>
          <NavLink>Projects</NavLink>
          <NavLink>Research</NavLink>

          <button
            type="button"
            onClick={() => setMenuOpen((v) => !v)}
            aria-label="Menu"
            aria-expanded={menuOpen}
            style={{
              width: 'calc(var(--header-h) - 12px)',
              height: 'calc(var(--header-h) - 12px)',
              display: 'grid', placeItems: 'center',
              background: 'transparent',
              color: 'inherit',
              border: 'none',
              borderRadius: 'calc(var(--header-h) * 0.22)',
              cursor: 'pointer',
              fontFamily: 'inherit',
              flexShrink: 0,
              transition: 'color 0.4s ease',
            }}
          >
            <Hamburger open={menuOpen} />
          </button>
        </nav>
      </div>

      {/* Dropdown — flips color state with scroll, like the pill above.
          On mobile we prepend Press/Projects/Research (since the pill hides
          those labels), and on desktop we omit Research (avoids duplicate). */}
      <div
        style={{
          position: 'absolute',
          top: 'calc(100% + 12px)',
          right: 'clamp(16px, 4vw, 40px)',
          width: 'min(320px, calc(100vw - 32px))',
          opacity: menuOpen ? 1 : 0,
          transform: menuOpen ? 'translateY(0)' : 'translateY(-8px)',
          pointerEvents: menuOpen ? 'auto' : 'none',
          transition: 'opacity 0.3s ease, transform 0.3s cubic-bezier(.2,.8,.2,1)',
          zIndex: 200,
        }}
      >
        <div style={{
          border: `1.5px solid ${pillBorder}`,
          background: pillBg,
          color: pillInk,
          borderRadius: 14,
          padding: 8,
          display: 'flex',
          flexDirection: 'column',
          transition: 'background 0.4s ease, color 0.4s ease, border-color 0.4s ease',
          boxShadow: '0 20px 40px -20px rgba(0,0,0,0.35)',
        }}>
          {(isMobile
            ? ['Press', 'Projects', 'Research', 'About', 'Papers', 'Careers', 'Contact', 'Twitter ↗']
            : ['About', 'Papers', 'Careers', 'Contact', 'Twitter ↗']
          ).map((item) => (
            <button
              key={item}
              type="button"
              style={{
                textAlign: 'left',
                padding: '12px 14px',
                background: 'transparent',
                border: 'none',
                borderRadius: 8,
                color: 'inherit',
                fontFamily: 'inherit',
                fontSize: 14,
                fontWeight: 600,
                letterSpacing: '0.02em',
                cursor: 'pointer',
              }}
            >
              {item}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
};

const NavLink = ({ children }) => (
  <a
    href="#"
    onClick={(e) => e.preventDefault()}
    className="nav-link"
    style={{
      padding: '0 14px',
      height: '100%',
      display: 'flex',
      alignItems: 'center',
      borderRadius: 10,
      color: 'inherit',
      textDecoration: 'none',
      fontSize: 12,
      fontWeight: 600,
      letterSpacing: '0.14em',
      textTransform: 'uppercase',
    }}
  >
    {children}
  </a>
);

const Hamburger = ({ open }) => (
  <div style={{ width: 18, height: 14, position: 'relative' }}>
    <span style={{
      position: 'absolute', left: 0, right: 0, height: 2, background: 'currentColor', borderRadius: 2,
      top: open ? 6 : 1, transform: open ? 'rotate(45deg)' : 'none',
      transition: 'transform 0.28s ease, top 0.28s ease',
    }} />
    <span style={{
      position: 'absolute', left: 0, right: 0, height: 2, background: 'currentColor', borderRadius: 2,
      top: 6, opacity: open ? 0 : 1,
      transition: 'opacity 0.2s ease',
    }} />
    <span style={{
      position: 'absolute', left: 0, right: 0, height: 2, background: 'currentColor', borderRadius: 2,
      top: open ? 6 : 11, transform: open ? 'rotate(-45deg)' : 'none',
      transition: 'transform 0.28s ease, top 0.28s ease',
    }} />
  </div>
);

/* ---------- Sticky canvas background ---------- */

const AnimationBackground = ({ sceneRef, pastWave }) => {
  const canvasRef = useRef_(null);
  const stageRef = useRef_(null);

  useEffect_(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const stage = window.InversionEngine.createStage(canvas);
    stageRef.current = stage;
    const scene = window.createHeroScene();
    sceneRef.current = scene;
    stage.addScene(scene);
    stage.start();
    return () => { stage.destroy(); };
  }, []);

  // Canvas base is permanently ink. The wave shape, drawn white with
  // 'difference' compositing, becomes paper where it covers — creating
  // the upper-ink / lower-paper visual divide WITHIN the same frame.
  // Circles drawn on top: white against ink → inverts to paper (white
  // circles in upper half), white against paper → inverts to ink (black
  // circles in lower half). One scene, two zones, zero scroll triggers.
  useEffect_(() => {
    const stage = stageRef.current;
    if (!stage) return;
    stage.setBaseColor('#0a0a0a');
  }, []);

  return (
    <canvas
      ref={canvasRef}
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        display: 'block',
        pointerEvents: 'none',
        zIndex: 0,
        // iOS Safari fix: position:fixed canvases jitter during momentum /
        // rubber-band scroll because iOS doesn't repaint fixed layers while
        // inertial scroll is active. Promoting the canvas to its own GPU
        // compositor layer via translate3d + will-change tells Safari to
        // treat it as a standalone layer that survives scroll without
        // repainting. Result: smooth canvas during iOS scroll, zero impact
        // on desktop browsers (they already do this implicitly).
        transform: 'translate3d(0, 0, 0)',
        WebkitTransform: 'translate3d(0, 0, 0)',
        willChange: 'transform',
        // Backface visibility hidden reinforces the GPU layer promotion on
        // older iOS versions where will-change isn't fully respected.
        backfaceVisibility: 'hidden',
        WebkitBackfaceVisibility: 'hidden',
      }}
    />
  );
};

/* ---------- Hero text ----------
 * Source-of-truth for the headline. Easy to swap for i18n later.
 */
/**
 * HERO_TITLE_LINES
 *
 * Each entry is one rendered line. The `align` field controls horizontal
 * alignment WITHIN the h1's left-aligned block — default 'left', override
 * to 'center' for the isolated em-dash line. Breaking the em-dash onto its
 * own centered line gives it a rhythm-beat role instead of reading as
 * trailing punctuation on line 1.
 *
 * Source-of-truth for the headline. Easy to swap for i18n later.
 */
const HERO_TITLE_LINES = [
  { text: "It's what we can't see", align: 'center' },
  { text: "—", align: 'center' },
  { text: "the answer is always", align: 'center' },
  { text: "under the surface.", align: 'center' },
];

// WebKit detection — covers ALL browsers that use WebKit and therefore
// share Safari's mix-blend-mode-across-stacking-contexts limitation:
//   • Desktop Safari (macOS)
//   • iOS Safari
//   • Chrome / Firefox / Edge / Brave on iOS (all forced to WebKit by Apple)
//   • iPadOS Safari (which masquerades as MacIntel + touch)
// Explicitly excludes Chromium-on-Mac (UA contains "Safari" because of
// historical reasons but it's actually Blink, which blends correctly).
const isWebKit = (() => {
  if (typeof navigator === 'undefined') return false;
  const ua = navigator.userAgent || '';
  const iOS =
    /iPad|iPhone|iPod/.test(ua) ||
    (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
  const desktopSafari =
    /Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR/.test(ua);
  return iOS || desktopSafari;
})();

const Hero = () => {
  // Hero h1 inversion strategy varies by engine:
  //
  //   Blink / Gecko (Chrome, Firefox, Edge desktop):
  //     mix-blend-mode: difference does true per-pixel inversion against
  //     the canvas circles below. Cross-stacking-context blending works.
  //
  //   WebKit (Safari + all iOS browsers):
  //     mix-blend-mode refuses to blend across stacking contexts. Rather
  //     than fight it, we keep the text plain white and add a thin INK
  //     STROKE (text-stroke). The stroke gives the headline enough edge
  //     definition to stay legible against any backdrop the canvas might
  //     paint underneath: white wave, white circles, or ink zones — the
  //     stroke separates the white text from white shapes, the white fill
  //     keeps it readable on ink. Pragmatic fallback, no JS, no perf cost.
  const h1Style = {
    margin: 0,
    // Responsive sizing that REALLY fits on small screens.
    //   • Floor (22px): low enough that ~390-414px portrait phones still
    //     fit the longest line ("the answer is always") without overflow.
    //     Previous floor of 36px was overriding the vw-based value on
    //     narrow viewports and forcing horizontal overflow.
    //   • Slope (7vw): tracks viewport width so the headline scales
    //     smoothly up through mid-size screens.
    //   • Ceiling (80px): caps growth on wide desktops.
    fontSize: 'clamp(22px, 7vw, 80px)',
    fontWeight: 700,
    letterSpacing: '-0.03em',
    lineHeight: 1.02,
    color: '#ffffff',
    // No textWrap: 'balance' — lines are now explicitly split per entry in
    // HERO_TITLE_LINES with their own textAlign, so we don't want the
    // browser redistributing words across block spans.
    ...(isWebKit
      ? {
          // Stroke entirely OUTSIDE the glyph so the letterform itself stays
          // pixel-identical to the unstroked version. Mechanism:
          //   • -webkit-text-stroke is centered on the path by default
          //     (half inside, half outside the glyph outline).
          //   • paint-order: stroke fill paints the stroke FIRST, then the
          //     fill ON TOP — so the inner half of the stroke is hidden by
          //     the fill, leaving only the outer half visible.
          //   • Doubling the stroke width (10px → visual 5px outside)
          //     compensates for that hidden inner half.
          WebkitTextStroke: '10px var(--ink)',
          paintOrder: 'stroke fill',
        }
      : {
          mixBlendMode: 'difference',
        }),
  };

  return (
    <section
      style={{
        minHeight: '100vh',
        position: 'relative',
        // Center the headline block horizontally on the page. Previously
        // the h1 lived in a left-anchored 900px column inside 6vw side
        // padding. Now the section is a centered flex container: the inner
        // div sizes to its widest line and sits dead-center horizontally,
        // while per-line textAlign inside the h1 still controls whether
        // each line is left-flush or centered within that block.
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        padding: '0 6vw',
      }}
    >
      <div style={{ position: 'relative' }}>
        {/*
          Each line is rendered as its own block inside the h1 so that
          per-line textAlign works. Default align is 'left' (the headline's
          natural orientation — text reads left-to-right within the
          centered block); override to 'center' for the em-dash line so it
          floats on its own axis. Line-break handling is implicit via
          block layout — no <br /> needed.
        */}
        <h1 style={h1Style}>
          {HERO_TITLE_LINES.map((line, i) => (
            <span
              key={i}
              style={{
                display: 'block',
                textAlign: line.align || 'left',
                // Prevent soft-wrap on any title line. Font size is already
                // tuned to fit the longest line in-viewport; whiteSpace:
                // nowrap is a safety belt against edge-case zoom / font
                // substitution reflows.
                whiteSpace: 'nowrap',
              }}
            >
              {line.text}
            </span>
          ))}
        </h1>
      </div>
    </section>
  );
};

/* ---------- Annotation sections ---------- */

const AnnotationRow = ({ children, align = 'left', offset = 0, margin = 200, fullBleed = false }) => (
  <section
    style={{
      minHeight: fullBleed ? 'auto' : '70vh',
      position: 'relative',
      display: 'flex',
      alignItems: 'center',
      justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start',
      padding: fullBleed ? `${margin}px 0` : `${margin}px clamp(16px, 6vw, 80px)`,
      transform: `translateX(${offset}px)`,
    }}
  >
    <div style={{ position: 'relative', zIndex: 2, width: fullBleed ? '100%' : 'auto', maxWidth: '100%' }}>{children}</div>
  </section>
);

const AnnotationsLayer = () => (
  <div style={{ position: 'relative', zIndex: 2 }}>
    <AnnotationRow align="left" margin={80}>
      <QuoteFragment seed={3} rotate={-2.4} width="min(480px, 92vw)"
        quote="If we cannot see how a model reasons, we cannot claim it reasons at all."
        cite="Internal note, Feb 2026" />
    </AnnotationRow>

    <AnnotationRow align="right">
      <PostFragment seed={11} rotate={2.8} width="min(460px, 92vw)"
        tag="Research" title="Sparse features, dense meaning"
        excerpt="We trained a dictionary over a 7B-parameter model's residual stream. What fell out wasn't noise — it was a surprisingly legible map."
        date="APR 2026" />
    </AnnotationRow>

    <AnnotationRow fullBleed margin={60}>
      <ManifestoRibbon seed={21} rotate={-0.6} index={1}
        text="Interpretability is not a feature. It is the floor." />
    </AnnotationRow>

    <AnnotationRow align="right">
      <PostFragment seed={42} rotate={-3.2} width="min(470px, 92vw)"
        tag="Notebook" title="What the circuits hide"
        excerpt="A walkthrough of induction heads in miniature, and why the tricks that scale are the ones we least understand."
        date="MAR 2026" />
    </AnnotationRow>

    <AnnotationRow fullBleed margin={50}>
      <QuoteRibbon seed={101} rotate={0.3}
        quote="Every black box is a deferred cost. We are paying it down."
        cite="Merilum, mission brief" />
    </AnnotationRow>

    <AnnotationRow align="left" offset={20}>
      <QuoteFragment seed={58} rotate={-1.4} width="min(520px, 92vw)"
        quote="If we cannot see how a model reasons, we cannot claim it reasons at all."
        cite="Internal note, Feb 2026" />
    </AnnotationRow>

    <AnnotationRow fullBleed margin={60}>
      <ManifestoRibbon seed={77} rotate={0.4} index={2}
        text="We publish what we find. Especially when it's inconvenient." />
    </AnnotationRow>

    <AnnotationRow fullBleed margin={50}>
      <PostRibbon seed={133} rotate={-0.4}
        tag="Paper" date="FEB 2026"
        title="Model welfare, operationally"
        excerpt="A draft framework for decisions we'd rather not leave to folklore." />
    </AnnotationRow>

    <AnnotationRow align="right" offset={-20}>
      <PostFragment seed={91} rotate={1.1} width="min(480px, 92vw)"
        tag="Paper" title="The shape of refusal"
        excerpt="What model abstentions look like geometrically — and why they cluster differently than we expected."
        date="JAN 2026" />
    </AnnotationRow>
  </div>
);

/* ---------- Footer ---------- */
//
// Two visual modes, branched on engine:
//
//   • Blink/Gecko: white text in a wrapper with mix-blend-mode: difference,
//     which inverts against the underlying canvas (whatever its color
//     happens to be where the footer sits). Includes the logo squircle.
//
//   • WebKit (Safari + all iOS): difference-blend doesn't reach across to
//     the canvas in the same way, so on iOS the footer text was rendering
//     as gray-on-near-white (illegible). Switch to plain ink-on-transparent
//     (no blend, no logo squircle) — content reads straight against the
//     ink page background. Logo is dropped because it becomes redundant
//     with the header logo glyph on this short-circuit path.

const Footer = () => {
  const webkit = isWebKit;
  const textColor = webkit ? 'var(--ink)' : '#ffffff';
  const linkColor = webkit ? 'var(--ink)' : '#fafaf7';
  const dividerColor = webkit ? 'rgba(0,0,0,0.12)' : 'rgba(255,255,255,0.12)';

  return (
    <footer
      style={{
        position: 'relative',
        marginTop: 120,
        padding: '80px 6vw 48px',
        color: textColor,
        fontFamily: 'var(--font-mono)',
        // WebKit: paper-colored backplate so ink text has a guaranteed
        // legible substrate regardless of what the canvas is doing.
        background: webkit ? 'var(--paper)' : 'transparent',
      }}
    >
      <div
        style={{
          maxWidth: 1200,
          margin: '0 auto',
          color: textColor,
          // Difference blend only on engines that handle it correctly here.
          ...(webkit ? null : { mixBlendMode: 'difference' }),
        }}
      >
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
          gap: 48,
          paddingBottom: 64,
          borderBottom: `1px solid ${dividerColor}`,
        }}>
          {/* Logo + tagline cell. On WebKit, drop the squircle entirely —
              the header already shows the logo, repeating it here adds
              visual noise without aiding orientation. Tagline still spans
              two grid columns to preserve the original column rhythm. */}
          <div style={{ gridColumn: 'span 2', minWidth: 0 }}>
            {!webkit && (
              <div style={{
                width: 52, height: 52, borderRadius: 14, background: '#fafaf7',
                color: 'var(--ink)',
                display: 'grid', placeItems: 'center', marginBottom: 24,
              }}>
                <MerilumGlyph size="30px" />
              </div>
            )}
            <div style={{ fontSize: 14, opacity: 0.7, lineHeight: 1.6, maxWidth: 340 }}>
              An interpretability research lab. We study what's inside models so
              the people who use them can choose knowingly.
            </div>
          </div>

          <FooterCol title="Work" items={['Research', 'Papers', 'Projects', 'Press']} linkColor={linkColor} />
          <FooterCol title="Company" items={['About', 'Careers', 'Contact', 'Mission']} linkColor={linkColor} />
          <FooterCol title="Connect" items={['Twitter ↗', 'GitHub ↗', 'arXiv ↗', 'Newsletter']} linkColor={linkColor} />
        </div>

        <div style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          gap: 24,
          marginTop: 40,
          fontSize: 12,
          letterSpacing: '0.14em',
          textTransform: 'uppercase',
          opacity: 0.6,
          flexWrap: 'wrap',
        }}>
          <div>© Merilum 2026. All rights reserved.</div>
          <div style={{ display: 'flex', gap: 28 }}>
            <a href="#" onClick={(e) => e.preventDefault()} style={{ color: 'inherit', textDecoration: 'none' }}>Privacy</a>
            <a href="#" onClick={(e) => e.preventDefault()} style={{ color: 'inherit', textDecoration: 'none' }}>Terms</a>
            <a href="#" onClick={(e) => e.preventDefault()} style={{ color: 'inherit', textDecoration: 'none' }}>Responsible Disclosure</a>
          </div>
        </div>
      </div>
    </footer>
  );
};

const FooterCol = ({ title, items, linkColor }) => (
  <div>
    <div style={{
      fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase',
      opacity: 0.55, marginBottom: 20, fontWeight: 700,
    }}>{title}</div>
    <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
      {items.map((i) => (
        <li key={i}>
          <a
            href="#"
            onClick={(e) => e.preventDefault()}
            style={{
              color: linkColor, textDecoration: 'none', fontSize: 15, fontWeight: 500,
              opacity: 0.85, transition: 'opacity 0.2s ease',
            }}
            onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
            onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.85')}
          >{i}</a>
        </li>
      ))}
    </ul>
  </div>
);

/* ---------- App ---------- */

// Shared scroll state — TWO independent thresholds:
//   - pastWave (45% viewH): logo glyph flips color (paper → ink) so it
//     stays readable as content scrolls into view. Earlier trigger because
//     the visual need (logo contrast) hits as soon as paper content
//     reaches the header zone.
//   - pillRevealed (85% viewH): nav pill fades in. Later trigger because
//     it's a discrete UI element and shouldn't appear until the user has
//     clearly committed to scrolling past the hero.
const useScrollState = () => {
  const [pastWave, setPastWave] = useState_(false);
  const [pillRevealed, setPillRevealed] = useState_(false);
  useEffect_(() => {
    const onScroll = () => {
      const y = window.scrollY;
      const vh = window.innerHeight;
      setPastWave(y > vh * 0.45);
      setPillRevealed(y > vh * 0.70);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  return { pastWave, pillRevealed };
};

const App = () => {
  const sceneRef = useRef_(null);
  const { pastWave, pillRevealed } = useScrollState();

  // Body background ALWAYS matches the canvas base (ink). The visual
  // Body background ALWAYS matches the canvas base (ink). The visual
  // black/white divide on the page is created entirely by the wave silhouette
  // inside the canvas — not by toggling page bg with scroll. This way the
  // hero zone (above wave = ink) and content zone (below wave = paper-via-
  // difference) coexist in every frame, scroll-independently.
  useEffect_(() => {
    document.body.style.background = 'var(--ink)';
  }, []);

  return (
    <div>
      <Header pastWave={pastWave} pillRevealed={pillRevealed} />
      <AnimationBackground sceneRef={sceneRef} pastWave={pastWave} />
      <main>
        <Hero />
        <AnnotationsLayer />
      </main>
      <Footer />
    </div>
  );
};

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