/* ============================================================================
   NOVA · THE FIELD — homepage layout for the particle scene
   ----------------------------------------------------------------------------
   The important decision in here is that the copy is REAL DOM in normal flow,
   and the scroll runway is the sections themselves.

   The sketch this came from put all four captions in fixed-position divs at
   opacity 0, revealed only from inside the rAF loop — which sat behind a check
   for a CDN copy of three.js. On a network where that CDN is blocked, and a
   school network is exactly that, the page was a wordmark and nothing else: no
   copy, no call to action, and nothing for a search engine to read. <main> was
   empty, so there was no document underneath it either.

   Here the sections carry their own height and their own text. Sticky does the
   "hold, then release" that the JS was doing, so the sequencing survives with
   scripting off. The canvas behind is decoration in the strict sense — remove
   it and the page still says everything it needs to.
   ========================================================================= */

/* ---- The two grounds ------------------------------------------------------
   Apple's white is #FFFFFF exactly and its ink is #1D1D1F — not an off-white
   and not pure black, which is why their pages read as paper rather than as a
   screen turned all the way up. Both are set here rather than taken from the
   token ramp because this page is the one surface that quotes those values
   deliberately; the app keeps its own calmer light theme.

   Every colour below that used to be a bare hex for the dark ground now comes
   from these two, so adding a theme cannot leave one rule behind. */
html { --field-ink: #F4F6F8; --field-ink-2: #C9CFD7; --field-rule: rgba(255,255,255,.20);
       --field-hair: rgba(255,255,255,.34); --field-ground: #000; }
html[data-theme="light"] {
  --field-ink: #1D1D1F; --field-ink-2: #4B4E54; --field-rule: rgba(29,29,31,.18);
  --field-hair: rgba(29,29,31,.30); --field-ground: #FFF;
}
html, body { background: var(--field-ground); }

/* ---- Hero ---------------------------------------------------------------- */
.field-hero {
  min-height: 100svh;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  padding: var(--s-16) var(--s-6) var(--s-20);
}

.word {
  margin: 0;
  font-family: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  font-weight: 700;
  font-size: min(23vw, 300px);
  line-height: .88;
  letter-spacing: -.045em;
  text-transform: uppercase;
  background: linear-gradient(105deg, #DCE9FF 0%, #7FB0FF 26%, #3B82F6 62%, #1D4ED8 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  user-select: none;
}

/* A visitor who never scrolls should still learn what this is. The sketch's
   hero was the wordmark alone, which tells a first-time reader nothing at all
   — and it is the one screen every visitor sees. */
.word-sub {
  margin: var(--s-6) 0 0;
  max-width: 30ch;
  font-size: clamp(15px, 1.5vw, 18px);
  line-height: 1.5;
  color: var(--text-secondary);
}

.field-cta { margin-top: var(--s-8); display: flex; gap: var(--s-3);
             justify-content: center; flex-wrap: wrap; }

.scrollcue {
  margin-top: var(--s-12);
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10.5px; letter-spacing: .24em; text-transform: uppercase;
  /* --text-tertiary is documented in tokens.css as "meta and large only", and
     at 10.5px uppercase this is neither: on white it measures 3.21:1, under
     the 4.5 a label this small needs. Secondary clears it in both themes —
     7.0:1 on the white ground and 7.6:1 on the black one. */
  color: var(--text-secondary);
}
/* Only hide it once we know the scene is live and can react to scrolling. */
.gl-live .scrollcue { transition: opacity .5s ease; }
.gl-live.is-scrolled .scrollcue { opacity: 0; }

/* ---- The wordmark hand-off ----------------------------------------------
   When the scene is live the PARTICLES are the wordmark, and the DOM copy of
   it steps aside. Cross-fading the two instead means matching a canvas glyph
   to a CSS one across every viewport, and they do not match — measured, the
   cloud spans about 67% of the frame where the type spans 57%, so the hand-off
   visibly jumps. Space is kept, not collapsed, so the tagline underneath does
   not move when the swap happens.

   Without JS or without WebGL the class is never set, and the real text simply
   stays — which is also the accessible name for the <h1>. */
.gl-live .word { opacity: 0; transition: opacity .3s ease; }

/* ---- Beats --------------------------------------------------------------- */
/* Height here IS the scroll runway the morph needs. Each beat is long enough
   that its state is held and read before the next one begins. */
/* 150svh x 4 beats plus the hero was seven screens of scrolling to get through
   one page, which is its own kind of slow however fast the frames are. */
.beat { min-height: 110svh; position: relative; }

/* A COLUMN, NOT A BANNER.
   Centred copy sticking to the bottom edge is the default treatment for every
   scroll-driven page ever built, and it puts the words directly on top of the
   one thing they are describing. Set as a column against the left margin
   instead, the page gets a composition: a typographic spine down one side and
   the field open on the other, which is how an editorial spread is organised
   and why it does not read as captions pasted over a video.

   It shares its left margin with the chapter marker, so the page has one
   vertical alignment rather than two competing ones. Left-aligned and
   ragged-right, because a 34-character measure centred is a shape, not a
   paragraph. */
.cap {
  position: sticky;
  /* LOWER than it was. At clamp(96px, 20vh, 210px) the copy sat about twenty
     pixels under the bottom line of the particle text, so the cloud's softest,
     most defocused points landed right on the eyebrow. Dropping it opens
     roughly a hundred pixels of clear ground between the two. */
  bottom: clamp(48px, 9vh, 104px);
  padding: 0 var(--s-6) 0 clamp(20px, 4vw, 44px);
  max-width: 40ch;
  margin-inline: 0 auto;
  text-align: left;

  /* Driven continuously by scroll from js/chrome.js: 1 while this caption owns
     the middle of the screen, easing to 0 either side. It is what stops two
     captions being on screen at a handover — see the note there — and it is
     also the copy's own animation, arriving and leaving with the scroll rather
     than snapping on once and staying.

     Defaults to 1, so with no JS every caption is simply present. */
  opacity: var(--in, 1);
  transform: translateY(calc((1 - var(--in, 1)) * 20px));
  will-change: opacity, transform;
}
@media (prefers-reduced-motion: reduce) {
  .cap { opacity: 1; transform: none; }
}

/* ---- Caption greys, lifted for this background ---------------------------
   The token ramp is calibrated against a flat near-black page. Over the cloud
   it does not survive: dark --text-secondary at #9BA1A9 needs a background
   below 0.034 to clear 4.5:1, and --text-tertiary needs one below zero, which
   is not a background any amount of attenuation can produce.

   Both lifted to a single value here. The three-step grey ramp collapses to
   two on this page, and the hierarchy comes from size, tracking and case
   instead — which is where it should come from on a caption this small. */
.word-sub { color: var(--field-ink-2); }

.cap__eyebrow {
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
  margin: 0 0 var(--s-3);
}
.cap__eyebrow i { color: var(--text-secondary); font-style: normal; margin: 0 6px; }

.cap h2 {
  font-size: clamp(24px, 3.2vw, 40px);
  font-weight: 600; letter-spacing: -.03em; line-height: 1.12;
  margin: 0; max-width: 17ch;
}
/* Set HERE rather than in an earlier override — this rule is later in the
   file and was quietly winning on source order, so the captions kept the
   token grey while only .word-sub took the lifted one. */
.cap p { margin: var(--s-3) 0 0; max-width: 38ch; font-size: 15px;
         line-height: 1.55; color: var(--field-ink-2); }
.cap .field-cta { margin-top: var(--s-6); justify-content: flex-start; }

/* ---- Legibility ---------------------------------------------------------
   There is deliberately NO panel behind the copy.

   A translucent black ground under each caption works, and it looks like what
   it is: a box laid over the artwork. However soft you make the gradient, it
   is still a second surface sitting between the reader and the picture, and
   on a field of moving points it is the one element that never moves.

   The cloud handles it instead. Every particle knows where it lands on screen
   and quietly fades if it falls behind the copy — see uCopy in
   js/gl/scenes/particles.js. There is no edge to see, because the thing being
   attenuated is the artwork itself rather than something drawn on top of it,
   and it feathers with the cloud's own density instead of against a shape. */

/* One hairline over each eyebrow. The whole ornament budget for the page. */
.cap__eyebrow::before {
  content: ""; display: block;
  width: 56px; height: 1px; margin: 0 0 var(--s-4);
  background: linear-gradient(90deg, var(--field-hair), transparent);
}

/* ---- The hero copy waits for the mark ------------------------------------
   The wordmark takes about two seconds to assemble out of the dark, and for
   that whole time the particles are mid-flight across the entire frame — which
   is both a mess to read over and a measured contrast failure on the tagline.

   So the words arrive after the mark does. It also happens to be the better
   order: the thing forms, then it tells you what it is. Only under .gl-live,
   so a page without the scene shows its copy immediately and unconditionally. */
@keyframes hero-in { to { opacity: 1; transform: none; } }

.gl-live .word-sub,
.gl-live .field-hero .field-cta,
.gl-live .scrollcue {
  opacity: 0; transform: translateY(8px);
  animation: hero-in .7s cubic-bezier(.2,.7,.3,1) forwards;
}
.gl-live .word-sub { animation-delay: 1.55s; }
.gl-live .field-hero .field-cta { animation-delay: 1.72s; }
.gl-live .scrollcue { animation-delay: 1.95s; }

@media (prefers-reduced-motion: reduce) {
  .gl-live .word-sub,
  .gl-live .field-hero .field-cta,
  .gl-live .scrollcue { opacity: 1; transform: none; animation: none; }
}

/* ---- Chapter marker ------------------------------------------------------
   A number, a rule and a name, bottom left. Small, permanent, and the only
   persistent interface on the page besides the nav.

   It is here because it is the cheapest signal there is that something was
   made deliberately rather than assembled — it tells you the page has parts,
   how many, and which one you are in, before you have read a word of it. It
   also replaces "Scroll", which said less and stopped being true after the
   first gesture. Hidden entirely until the scene is live, so a page without
   scripting never shows a control that does nothing. */
.chapter {
  position: fixed; z-index: 5;
  left: clamp(20px, 4vw, 44px); bottom: clamp(20px, 4vh, 36px);
  display: none; align-items: center; gap: 12px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, monospace;
  font-size: 10.5px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--field-ink-2);
  pointer-events: none;
  opacity: 0; transition: opacity .45s ease;
}
.gl-live .chapter { display: flex; }
.gl-live.is-scrolled .chapter { opacity: 1; }

.chapter__n { font-variant-numeric: tabular-nums; opacity: .95; }

/* The rule fills as you go. One element, one transform, so it animates on the
   compositor and the scroll handler writes a single custom property. */
.chapter__rule {
  position: relative; display: block;
  width: clamp(48px, 7vw, 92px); height: 1px;
  background: var(--field-rule);
  overflow: hidden;
}
.chapter__rule i {
  position: absolute; inset: 0;
  background: var(--field-ink-2);
  transform: scaleX(var(--f, 0)); transform-origin: 0 50%;
  will-change: transform;
}

.chapter__label { opacity: .72; }

@media (max-width: 760px) { .chapter { display: none !important; } }

/* ---- Footer -------------------------------------------------------------- */
/* Opaque, because the canvas is fixed behind the whole document and a footer
   of small links over a moving point cloud is unreadable. */
.footer { background: var(--field-ground); }

@media (max-width: 860px) {
  /* No room for a column here: the copy takes the full measure and the object
     sits behind it, which is what the attenuation is for. */
  .cap { max-width: none; padding-inline: var(--s-6); bottom: clamp(64px, 13vh, 132px); }
  .cap h2 { max-width: 20ch; }
}

@media (max-width: 760px) {
  .word { font-size: 30vw; }
  .cap p { font-size: 14px; }
  .beat { min-height: 100svh; }
}

@media (prefers-reduced-motion: reduce) {
  /* No sticky hold and no runway: the page becomes an ordinary short document
     with the same words in the same order. */
  .beat { min-height: auto; padding-block: var(--s-20); }
  .cap { position: static; }
}
