/* ============================================================
   SECTIONS: shared rhythm
   ============================================================ */
/* The top padding was 8px, fitted so that an ANCHORED section put its first
   line directly under the header band, reference2's eyebrow measures y=125
   against a 124px header. That was right for a page read by jumping between
   anchors, and wrong for this one, which is read by scrolling: an eyebrow with
   8px above it passes under the fixed header and is never seen. It now matches
   the rhythm the editorial sections already had, so the two halves of the page
   breathe at the same rate, and anchored arrivals simply land a little lower.

   Horizontal padding stays at zero: each of these sections hands the gutter to
   its own inner blocks (.section-intro, .pipeline-flow and the rest), which is
   what lets their backgrounds run full-bleed underneath. */
.pipeline, .capabilities, .studio, .contact {
  position: relative;
  padding: clamp(88px, 11vw, 170px) 0 clamp(90px, 12vh, 150px);
}

/* ...with one exception. The studio section is laid out with the editorial
   half's inner column, which expects its gutter from the section, every other
   inner block here brings its own. Without this the statement ran to both
   edges of the window. */
.studio > .static-section__inner { padding-inline: var(--pad-section); }

/* One measure for the whole page.

   These sections were authored full-bleed against a 1672px frame and kept
   growing past it; the editorial sections that came from the other repository
   cap their column at --content and centre it. On a 2560px display that read
   as two different websites stacked. They now agree: the backgrounds stay
   full-bleed, and everything that carries type is capped and centred on the
   same measure, with the same gutter either side of it. */
.build > *, .pipeline > *, .capabilities > *,
.studio > *, .contact > * {
  max-width: calc(var(--content) + 2 * var(--pad-section));
  margin-inline: auto;
}

/* ...with a second exception, and it is the whole point of the section below.
   The measure is for things that carry type. The travel box carries a stage,
   and a stage capped at the page measure is a stage with two black bars down
   it on any window wider than --content. The cap is re-applied to the copy
   INSIDE the stage instead, so the statement keeps the page's one measure
   while the drawing keeps the whole window, and the object still stands in
   the statement's own right-hand column, because scripts/lattice/geometry.js
   measures that column rather than the window. */
.build > .lattice-travel { max-width: none; }

/* ============================================================
   BUILD
   ============================================================ */
/* What the statement stands on. The section this block used to close was much
   taller (three exhibit slabs and a rail above it) and its ground was a
   three-layer gradient whose cyan wash sat far above this band. What reached
   the statement was the bottom of that gradient and nothing else, so that is
   what is kept: page ground falling to the void, with the pools on the stage
   below supplying the one lit horizon this composition ever had. */
.build {
  /* `.static-section` sets a section's own padding and `.work` used to override
     it here. Neither applies now: `.work-cta` below brings the whole band's
     rhythm, and either of the other two on top of it would open a screen of
     empty black where the exhibits used to be. */
  padding: 0;
  /* THE HANDOVER. The first screen is transparent, and what shows through it
     is the wave: a fixed canvas at the foot of the viewport, the floor the
     landing screen stood on, still there two screens later. As this section
     slides up over it the water flattens, flares and falls (`collapse` in
     scripts/wave/surface.js, driven by `fadeField` in scripts/main.js off
     this box's approach), and the frame the stage pins, the lattice's canvas
     picks the same hot points up low in its own frame and carries them up
     onto the edges of the object (scripts/lattice/geometry.js). So the object
     is the water's, built out of the opening screen rather than of nothing.

     For that screen everything in here is see-through: this ground and the
     pools under the object (`--lattice-glow`, 0 until the structure exists);
     the canvas draws nothing until its box has pinned, because at playhead 0
     every point is still below the foot of the frame. */
  margin-top: 0;
  background: linear-gradient(180deg, transparent 0, transparent 100svh, var(--black-near) 100svh, var(--black-abs));
  /* Both `.section` and `.static-section` clip, and this section carries both.
     A clipping ancestor is a scroll container, and a sticky box measures itself
     against its nearest one: so with either of them in force the stage below
     has nothing to stick to and scrolls past like any other block. That failure
     is silent: the markup is right, the maths is right, and the shot simply
     never holds still. Nothing escapes by opening it, because `.lattice-stage`
     clips on its own account and this section's ground is a background rather
     than a full-bleed child. This one declaration IS the pin, and work.css
     loading after both of them is what lets a single class win. */
  overflow: visible;
}

/* The screen the water falls through, for the observer in scripts/main.js
   that decides whether the wave can be seen. Nothing is drawn here. */
.build-approach {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100svh;
  pointer-events: none;
}

/* ── the travel ────────────────────────────────────────────────────────────
   The section is taller than what it shows, and that difference IS the shot.

   Three and a bit screens of box against a one-screen sticky stage gives the
   playhead two and a bit screens of scrolling to run in: the water is written
   into the statement over the first third, the letters hold and the type
   comes up over them, the light leaves the type for the cube over the middle,
   the unfold and the turn take the last third, and the last few per cent
   hold a still frame so the release hands the next section a composition
   rather than a motion. Nothing here traps the wheel, the page scrolls exactly as it always
   did, and scripts/lattice/mount.js simply reads how far into this box the
   reader has come. Scroll back and the object un-builds, because the scene is
   a function of that number and of nothing else.

   svh, not vh: on a phone the URL bar slides and `vh` would restate the length
   of the shot every time it did. scripts/lattice/geometry.js's TRAVEL is this
   number, and tests/lattice.mjs holds the two together. */
.lattice-travel {
  position: relative;
  height: 360svh;
}

/* Held at the top for the length of the travel above. `overflow: clip` is on
   THIS element rather than on its parent on purpose, a clipping ancestor
   between a sticky box and the scroller is the classic way to break sticky,
   and the parent above must stay unclipped for that reason. */
.lattice-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  align-items: center;
  overflow: clip;
}

/* ── the statement ─────────────────────────────────────────────────────────
   The statement is the dominant mass on the left; the object answers it on
   the right; the control is anchored under the statement instead of floating
   alone in an accidental void.

   The grid keeps its two columns even though the object is not one of them:
   the second track is what holds the statement off the right-hand half of the
   screen, which is where the object stands, and it is the track the object
   is measured against, so the two are one composition rather than two layers
   that happen to agree at one window size. */
.work-cta {
  position: relative;
  /* Above the canvas, and re-capped to the page measure the stage opted out
     of, so the statement sits on the same column as every other section. */
  z-index: 1;
  max-width: calc(var(--content) + 2 * var(--pad-section));
  margin-inline: auto;
  width: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  align-items: center;
  column-gap: clamp(30px, 5vw, 90px);
  padding: 0 var(--pad-section);
}
.work-cta-heading {
  grid-column: 1;
  font-size: clamp(48px, 6.4vw, 108px);
  max-width: 9.5em;
  margin: 0 0 clamp(26px, 3.4vh, 44px);
  /* The object's light reaching the document. `--lattice-glow` is written on
     the stage by scripts/lattice/mount.js, 0 while the points are still
     rising, 1 once the structure has unfolded, so as the object comes into
     existence the type beside it sits in a faintly cooler room. It is a GLOW
     and never a colour: the heading's own fill is untouched, nothing moves,
     nothing re-lays-out, and at 0 this rule computes to no shadow at all
     rather than a cheap one turned down.

     Deliberately weaker than it looks written down. A reader should not be
     able to name what changed, only notice that the drawing and the type are
     in the same space: which is the difference between a scene on this page
     and a video playing behind it. */
  text-shadow:
    0 0 calc(46px * var(--lattice-glow, 0)) rgba(0, 172, 214, calc(0.26 * var(--lattice-glow, 0))),
    0 0 calc(14px * var(--lattice-glow, 0)) rgba(120, 220, 250, calc(0.11 * var(--lattice-glow, 0)));
}
.work-cta .btn-outline { grid-column: 1; justify-self: start; }

/* The statement is made of light before it is made of type. `--lattice-text`
   is written on the stage by scripts/lattice/mount.js: 0 while the water is
   still being written into the heading's own glyphs, rising to 1 as the last
   points settle into their letters, and 1 from then on, so the type comes
   up over the letters of light exactly where they stand, and stays when the
   light leaves it for the object. The default is 1: a page with no canvas,
   no words for it, or a reader who asked for less motion has the statement
   outright, which is the composition this section had before the words. A
   variable rather than a transition, because the value is the scroll's and
   is rewritten on the frame it changes. */
.lattice-stage .work-cta-heading,
.lattice-stage .work-cta .btn-outline { opacity: var(--lattice-text, 1); }

/* ── the object ────────────────────────────────────────────────────────────
   The whole stage, full bleed, behind the copy: a frame cannot hold a thing
   that is built out of points arriving from the foot of the window, and a
   column with `overflow: hidden` would cut the rise off at its edge. The
   resting composition (statement left, object right) is framed by the
   geometry rather than by the layout, which measures the statement's own
   column and stands the object in it.

   No background, no border and no blend: the canvas clears to transparent and
   the section's own ground is what shows around the points, so there is no
   rectangle to see and no seam to hide. */
.lattice-object {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* The object is never the thing the hand is reaching for, the control
     above it is. The stage stays out of the way of every pointer event; the
     hand it answers is the page's own tracker (scripts/pointer.js). */
  pointer-events: none;
  opacity: 0;
  transition: opacity 1200ms var(--ease-soft) 120ms;
}
/* Lit by scripts/lattice/mount.js on the frame the canvas first draws. Until
   then, and forever on a machine with no canvas at all, this layer is empty
   and the statement is the composition, which is why the section still reads
   with the object entirely absent. */
.lattice-stage.is-lit .lattice-object { opacity: 1; }
.lattice-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The caption: what the object is, in the page's own mono, in the corner the
   stage keeps clear. It arrives with the structure (`--lattice-glow`) and it
   is aria-hidden because it labels a drawing that is itself hidden from
   assistive technology: the statement is the sentence this section makes.
   Default 0, not 1: without the script there is no object for it to name. */
.lattice-caption {
  position: absolute;
  right: var(--gutter);
  bottom: 3.6rem;
  z-index: 1;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-3);
  text-align: right;
  pointer-events: none;
  opacity: calc(var(--lattice-glow, 0) * 0.9);
}
.lattice-caption i { margin-inline: .55em; font-style: normal; opacity: .48; }

/* The lit horizon this composition has always had. Off through the rise, on
   as the structure unfolds: the pools are the object's resting light, and
   they come up with it. 1 when nothing writes the variable, so a page with no
   canvas keeps the ground it always had. */
.lattice-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background:
    radial-gradient(46% 52% at 74% 46%, rgba(0, 150, 190, 0.13), transparent 68%),
    radial-gradient(70% 34% at 30% 96%, rgba(0, 110, 145, 0.08), transparent 74%);
  /* A floor under the rise, and then the object's own light on top of it. The
     pools are what the object is standing in, and they used to be exactly 0
     until it had unfolded: so the frames between the water handing over and
     the first points arriving had nothing in them at all, not even a room. A
     fifth from the moment the stage pins reads as the space the thing is about
     to be built in; the rest still arrives with the structure, which is the
     part a reader notices. 1 where nothing writes the variable, so a page with
     no canvas keeps the ground it always had. */
  opacity: calc(0.2 + 0.8 * var(--lattice-glow, 1));
  pointer-events: none;
}

/* ---------- Build responsive ----------
   The phone gets the shot: the object needs a viewport, not a second column,
   and a thumb supplies the playhead as readily as a wheel. What it does not
   get is the hand (there is no pointer to carry it) so on a phone the scene
   is a function of the scroll and of nothing else. */
@media (max-width: 900px) {
  /* 3 screens of travel: geometry.js's TRAVEL_MOBILE. */
  .lattice-travel { height: 300svh; }
  .work-cta { padding-top: 0; }
  /* One column has no right-hand third to stand the object in, so the
     composition turns: the statement and the control hold the top of the
     stage, under the header, and the geometry stands the object in the lower
     half (`mobile` in scripts/lattice/geometry.js). At rest the two share the
     screen instead of the type standing on the drawing. */
  .lattice-stage {
    align-items: start;
    padding-top: calc(var(--header-h) + clamp(28px, 6svh, 64px));
  }
  /* The second track existed to hold the statement off the object's half of
     the screen. The object is below the statement here, not beside it, so the
     track is given back to the type: which was being set one word to a line
     in 57% of a phone. */
  .work-cta { grid-template-columns: minmax(0, 1fr); }
  .work-cta-heading { max-width: none; }
  .lattice-caption { right: auto; left: 50%; transform: translateX(-50%); bottom: 12px; width: max-content; max-width: calc(100% - 2 * var(--gutter)); text-align: center; }
}
/* No water, no writing. Below this width the page carries no wave at all
   (styles/wave.css hides the canvas and scripts/main.js refuses to mount it,
   both at 720), so there is nothing for the statement to be written out of and
   nothing to be gained by holding it back: the type is the composition from
   the stage's first frame, and the object still builds under it on the scroll.
   scripts/lattice/mount.js reads the same boundary and stops writing the
   variable; this is what holds before that first frame and on a page with no
   script at all. Measured at 390x844 without it: the stage pinned on a screen
   with 0.6% ink on it, no wave, no object yet, and no words. */
@media (max-width: 720px) {
  .lattice-stage { --lattice-text: 1; }
}

@media (max-width: 767px) {
  .work-cta-heading { font-size: clamp(52px, 13vw, 72px); }
}
/* A genuinely SHORT window has no room for a sticky stage, a landscape phone,
   or a desktop window dragged down to a strip. The travel collapses and the
   section becomes what it was before the shot: one screen, statement and
   control, with the object at rest beside them.
   The bound is height alone, and deliberately so. A portrait bound here would
   match every phone held upright: the entire mobile audience the shot is
   supposed to reach: and collapse the thing this section exists for on the
   one form factor that was explicitly in scope. */
@media (max-height: 520px) {
  .lattice-stage { --lattice-text: 1; }
  .lattice-travel { height: auto; }
  .lattice-stage { position: static; height: auto; padding: clamp(96px, 15vh, 190px) 0 clamp(40px, 6vh, 80px); }
  /* No travel, no handover: the section takes its ground from its first pixel. */
  .build { background: linear-gradient(180deg, var(--black-near), var(--black-abs)); }
  .lattice-stage::before { opacity: 1; }
}

/* A reader who asked for less motion is not given two screens of scrolling
   that do nothing. The travel collapses with the shot: the mount holds the
   resting frame (the object built, unfolded and turned) and subscribes to no
   loop, and the section becomes one screen of statement, control and a still
   object, which is a composition rather than a degraded one. Collapsing the
   height is the half that is easy to forget, and forgetting it leaves two dead
   screens under the copy. */
@media (prefers-reduced-motion: reduce) {
  /* No words written by scroll: the statement is type from the first frame. */
  .lattice-stage { --lattice-text: 1; }
  .lattice-travel { height: auto; }
  .lattice-stage { position: static; height: auto; min-height: 78svh; }
  /* No travel, no handover: the section takes its ground from its first pixel. */
  .build { background: linear-gradient(180deg, var(--black-near), var(--black-abs)); }
  .lattice-stage::before { opacity: 1; }
}

/* Forced colours asks for a page made of system colours; a drawing made of
   light is the opposite of that. It gets the same answer here as the wave and
   every other drawn layer on this page, and the section keeps its type. */
@media (forced-colors: active) {
  .lattice-stage { --lattice-text: 1; }
  .lattice-object, .lattice-caption { display: none; }
  .lattice-travel { height: auto; }
  .lattice-stage { position: static; height: auto; padding: clamp(96px, 15vh, 190px) 0 clamp(40px, 6vh, 80px); }
  .build { background: linear-gradient(180deg, var(--black-near), var(--black-abs)); }
}
