/* ═══════════════════════════════════════════════════════════════════════════
   The wave: a band of luminous points across the foot of the landing screen.

   Layering, back to front, and all of it decided by paint order rather than by
   a stack of magic numbers:

     .hero-mark      z-index 0, first positioned element   the studio's mark
     .wave-canvas    z-index 0, after it                    THIS, the floor
     .swarm-vignette z-index 1                              the frame's falloff
     .section__pin   z-index 5 (styles/hero.css)            the copy

   In front of the mark so it reads as the ground the mark stands on, and behind
   the vignette so the same atmospheric falloff closes over it as closes over
   everything else. Since the particle field stopped being built, this is the
   only thing on the page still drawn from particles. It never goes above the
   copy, and it is never hit-tested: the pointer it answers is the one
   scripts/pointer.js already tracks for the whole page, so this layer takes no
   events of its own.

   Compositing is plain source-over, for the reason styles/swarm.css sets out
   at length: the canvas is additive internally and genuinely transparent
   between its points, so there is nothing for a blend mode to improve.
   ═════════════════════════════════════════════════════════════════════════ */

.wave-canvas {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* The band. Tall enough to hold a horizon and the air above it, short enough
     that the wave stays where the brief puts it, very low in the frame, under
     a composition that belongs to the mark. */
  height: min(52svh, 560px);
  width: 100%;
  z-index: 0;
  display: block;
  pointer-events: none;
  background: transparent;
  opacity: 0;
  transition: opacity 1200ms cubic-bezier(0.22, 1, 0.36, 1) 200ms;
}

/* Lit once the opening is up, and dimmed by one scroll-driven variable,
   `--field-fade`, written by `fadeField` in scripts/main.js off the build
   stage's approach: so the wave leaves on the lattice's terms: it is the
   water the object is built from, and it goes over the last of its own fall,
   not when the reader leaves the hero. It used to step down on the chapter as well
   (`html:not([data-chapter='signal'])`); that rule is gone, because the
   handover happens two screens after the hero and the wave has to still be
   there for it. Held under full: this is the floor, and a floor as bright as
   the subject is a floor you are looking at. */
.has-swarm .wave-canvas { opacity: calc(var(--field-fade, 1) * 0.85); }

/* The terrain broadens under the journey. `--terrain` is written on the canvas
   by scripts/process/journey.js as the reader travels from the hero into the
   process section, 0 at rest and 1 by the time the composition has settled,
   and it stays at 1 under the rail and into the build stage's approach, the same
   water, wider and taller, the floor of the next screen as well as the first.
   A transform rather than a resize: a new canvas height re-projects twenty-
   four thousand points, and the wave has to keep rolling through this. The
   origin is the foot of the frame, so the horizon lifts and the foreground
   stays anchored where it always was. */
.has-swarm .wave-canvas {
  transform-origin: 50% 100%;
  transform: scale(calc(1 + 0.06 * var(--terrain, 0)), calc(1 + 0.18 * var(--terrain, 0)));
}

/* `field-live` takes the transition off once
   the first fade has run, so a value rewritten on every scrolled frame is
   painted on that frame. `!important` because the universal reduced-motion
   damping in styles/experience.css sets a duration on `*`. */
.has-swarm.field-live .wave-canvas { transition: none !important; }

/* Under reduced motion the wave is drawn once and never stepped (see
   mountWave), so what is left is a still. Keep it, but quieter: a static band
   of points is decoration, and decoration gets less of the frame. */
@media (prefers-reduced-motion: reduce) {
  .has-swarm .wave-canvas { opacity: calc(var(--field-fade, 1) * 0.55); }
}

/* Narrow frames do not get the wave at all.
   On one column the hero's copy runs the full width and down past where the
   band would sit, so the wave ends up behind the call to action at an opacity
   that measured all but invisible: it earns nothing there, and it is a second
   WebGL context and twenty-four thousand points on the devices least able to
   spare either. scripts/main.js gates the MOUNT at this same boundary, because
   a `display: none` canvas that is still being drawn still costs the frame. */
@media (max-width: 720px) {
  .wave-canvas { display: none; }
}

/* Forced colours: the page's rule everywhere is that decorative layers do not
   survive it, because there is no palette left to be decorative in. */
@media (forced-colors: active) {
  .wave-canvas { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE CLOSING WAVE: the same water, at the last screen.

   The page opens on water and closes on it. The contact section is the page's
   last control on a near-black ground, and a form on black is a form; so the
   landing screen's own water is brought to the end of the document: the same
   module, the same lattice, the same five-stop ramp, the same bloom gate,
   quieter, slower, and a different seed, which makes it a different stretch of
   one sea rather than the hero's band pasted in twice.

   Layering inside .contact, and all of it paint order rather than numbers:

     .contact background + ::before   the section's own ground and its horizon glow
     .closing-wave                    THIS, no z-index
     .contact::after                  the horizon filament
     .contact-inner   z-index 1       the statement, the email, the CTA, the lanes

   Every word and control on the screen is in front of it. Nothing here is
   ever hit-tested.

   ── The box, and where the horizon lands ─────────────────────────────────
   scripts/wave/surface.js puts the horizon at 10% of the canvas HEIGHT and
   fills downward by ~0.30 × the canvas WIDTH (the focal length is F = W·0.62
   and the nearest row lands F·EYE/NEAR_Z below the horizon). The aspect ratio
   is therefore not a taste: at 1:3.4 the band exactly fills its box, and any
   taller box is dead space under a wave that has already run out.

   Anchored bottom-right, and every number cut from the width so the whole
   composition scales as one. The band it draws is about 0.24 × W tall on
   screen, which is what decides the width: the field has to fit between the
   headline above it and the three lanes below it, and at 1780×860 that gap is
   320px. Hence the lift, `bottom` is POSITIVE. Measured on the built page at
   1780×860, water runs from 168px to 464px above the section's bottom edge:

     the headline ends 486px up, so it never has water behind it;
     the crest line lands around 330px, just above the CTA at 245-294, which it
       therefore frames rather than crosses;
     the three lanes at 96-162 are BELOW the field entirely, so the one place
       on this screen with small type over the corner has nothing behind it. */
.closing-wave {
  --closing-w: min(78vw, 1400px);
  position: absolute;
  right: 0;
  bottom: calc(var(--closing-w) * 0.079);
  width: var(--closing-w);
  height: calc(var(--closing-w) / 3.4);
  display: block;
  pointer-events: none;
  background: transparent;
  opacity: 0;
  /* The same fade the page's other lit layers take, so it arrives as one of
     them rather than switching on. */
  transition: opacity 1400ms var(--ease-soft) 200ms;
  /* The lattice already thins its own left and right margins (the edge
     envelope in surface.js) and its own foreground, so this is not what stops
     the layer being a rectangle: that is already handled. What this does is
     WEIGHT it: full strength up on the right and along the crest,
     falling away toward the bottom left, which is where the three lanes and
     the email are. Elliptical and centred just under the horizon on the right,
     so the falloff follows the composition rather than the box. */
  -webkit-mask-image: radial-gradient(85% 150% at 80% 42%, #000 0%, #000 44%, transparent 100%);
  mask-image: radial-gradient(85% 150% at 80% 42%, #000 0%, #000 44%, transparent 100%);
}

/* Lit by scripts/main.js on the frame the first one is drawn. */
.closing-wave.is-lit { opacity: 1; }

/* Tablet: the water is lifted a little further clear of the three lanes, which
   are proportionally closer to the CTA on a shorter frame. */
@media (max-width: 1279px) {
  .closing-wave { bottom: calc(var(--closing-w) * 0.105); }
}

/* It leaves at a boundary of the contact section's own: at 1024
   `.contact-channels` becomes one column (styles/editorial.css), the three
   lanes go from a 66px row at the foot of the section to a 249px block up its
   middle, and the band of clear air this water occupies is simply gone. Water
   behind three stacked links is water in front of the reader's next sentence.
   `liftClosingWave()` refuses to mount below the same boundary, because a
   `display: none` canvas that is still being drawn still costs the frame. */
@media (max-width: 1024px), (max-height: 639px), (max-aspect-ratio: 1/1) {
  .closing-wave { display: none; }
}

@media (forced-colors: active) {
  .closing-wave { display: none; }
}
