/* ============================================================================
   HYDRO2050 - HOME
   ----------------------------------------------------------------------------
   Source of truth: Paper artboard 5SN-0 (1440 x 7577) and 9WU-0 (375 x 6849).
   Every number below traces to a measured value in DESIGN-NOTES.md. The
   artboard is a stack of six fixed-height sections; this file reproduces the
   composition of each one and lets height follow content, because a fixed
   height that is 42px short of its own contents is a defect, not a spec.
   ========================================================================== */

/* ===== 1. HERO, 1440 x 900 ================================================
   The navy bar is the page header and Paper puts it INSIDE the hero at
   top 15, inset 64, so it is 1312 wide here against 1376 on every inner
   page. The header is written into the document before <main>, so the hero
   pulls up under it by exactly the header's own height. ================== */

.page-home .site-header { padding: 15px var(--gutter) 0; }
/* The stuck state has to be restated here. Home's resting override is
   `.page-home .site-header`, the same 0,2,0 specificity as chrome.css's
   `.site-header.is-stuck`, and home.css loads second, so the override was
   winning and the stuck bar kept a 15px gap above it with the hero video
   sliding through. Restated at 0,3,0 rather than reordered, because the
   resting 15px is a real measured value from the artboard and belongs here. */
.page-home .site-header.is-stuck { padding: 0; }

/* THE HERO IS A GRID, NOT A SET OF COORDINATES.
   Paper draws five things at absolute 1440 coordinates: the lockup at y235,
   the tagline at y571, the intro card at y579, the partner column at y566,
   inside a 900px frame. Those numbers do not shrink, so between 900 and 1440
   the centred tagline ran under the partner column and measured 1.94:1.

   The same five things are now a two-row grid: the lockup across the top, then
   the card, the tagline and the partner column side by side, which is what the
   artboard's near-identical y566 / y571 / y579 actually describe. Every offset
   below is a row gap or a margin against that grid, and at 1440 each element
   lands within 1px of its measured coordinate. The arithmetic is in
   DESIGN-NOTES under the fourth pass. */
.hero {
  position: relative;
  /* The hero stacks three layers, the media at --z-art, the veil at --z-media
     and the stage at --z-content, and until this isolation was added it was
     not a stacking context, so all three competed against the document rather
     than against each other. `overflow: clip` looks like it should contain
     them and does not: clipping is not a stacking context. */
  isolation: isolate;
  margin-top: -99px;                 /* 15 top offset + 84 bar */
  display: flex;
  min-height: clamp(700px, calc(366.67px + 37.037vw), 900px);   /* 700 -> 900 */
  overflow: clip;
  /* Head session, at the client's request: was --surface (#EBF7FD). The hero
     media stops 27px above the hero's own bottom edge, so that tint showed as
     a pale strip meeting the white page below and read as a seam. White here
     means the hero and the section under it are the same ground and the join
     is invisible. The tint is still doing its job everywhere the art covers. */
  background: var(--page);
}
.hero__media { position: absolute; inset: 0; z-index: var(--z-art); }
.hero__media picture { position: absolute; inset: 0; }
.hero__media video,
.hero__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.85;                     /* Paper 5SP-0 */
}
/* A white radial veil, brightest at the optical centre, so the lockup has a
   ground to sit on without a panel behind it. */
.hero__veil {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  background: radial-gradient(circle farthest-corner at 50% 48%,
              rgba(255,255,255,0.46) 0%, rgba(255,255,255,0.16) 100%);
}

.hero__stage {
  position: relative;
  z-index: var(--z-content);
  display: grid;
  /* The two outer tracks are a FRACTION of the stage, not a pixel ceiling.

     They used to be minmax(200px, 240px) and minmax(220px, 280px), which meant
     the intro card and the partner column hit their ceilings at about 1100 and
     then never moved again: every pixel the stage gained between there and
     1440 went into the middle track, so the two panels held their size and
     drifted apart while the hero video behind them kept growing edge to edge.
     That is the defect the client reported by dragging his window, and no
     harness saw it because nothing overflows, nothing escapes its parent, and
     at 1440 exactly the composition is correct.

     240 and 280 are the artboard's own measurements against its 1312 column,
     so they are written here as what they always were, 18.293% and 21.341% of
     it. At 1440 each resolves to its measured pixel value within a rounding
     error, and either side of 1440 the panels now take their proportional
     share of the change instead of standing still.

     THE FLOORS ARE GONE, AND THAT WAS THE REMAINING HALF OF THE SAME DEFECT.
     They read minmax(200px, 18.293%) and minmax(220px, 21.341%), and a floor
     above the fraction wins, so between the band's start and the width at
     which each fraction overtakes its own floor the two panels were frozen at
     a pixel size again. Measured across the sweep, the intro card sat at
     exactly 200px from 1120 to 1220 while the viewport grew 100px, and the
     partner column at exactly 220px from 1120 to 1180: every pixel of that
     growth went into the middle track, which is the same thing the panels did
     before against a ceiling instead of a floor. It is the gesture the client
     reported, dragging the window and watching the two panels stand still
     while the composition opened up around them.

     No floor is needed here. This band starts at 1100, where the fractions are
     already 178 and 207, so the band's own lower edge is the floor and it
     cannot be undercut. Below 1100 a different composition takes over, and it
     is the one that decides what happens there. */
  grid-template-columns:
    minmax(0, 18.293%)          /* 240 of the artboard's 1312 column */
    minmax(0, 1fr)
    minmax(0, 21.341%);         /* 280 of the artboard's 1312 column */
  grid-template-areas:
    'lock lock lock'
    'card tag  partners';
  column-gap: clamp(20px, 2.78vw, 40px);          /* 40 at 1440 */
  align-items: start;
  /* start, not the grid default. A stretched auto row shares the frame's
     spare height between the two rows and drifts every element in the
     composition off its measured y by the same amount. */
  align-content: start;
  justify-items: center;
  /* Head session, at the client's request: the band used to cap at a hard
     1312px, so past 1440 the two panels stopped spreading and the window just
     grew dead margin either side. 91.112vw is exactly 1312 at 1440, so the
     reviewed composition is untouched at and below the design width, and the
     panels now keep moving outward above it up to a 1600 ceiling. Margins are
     a constant 4.444 per cent of the viewport rather than a gutter that
     plateaus at 64. */
  width: min(1600px, 91.112vw);
  margin-inline: auto;
  /* 235 above the lockup and 21 below the card at 1440, easing to 120 / 56 at
     the breakpoint, so the frame breathes rather than stepping. */
  padding-block: clamp(120px, calc(21.2963vw - 71.667px), 235px)
                 clamp(21px, calc(114.333px - 6.4815vw), 56px);
}

/* The lockup, 774 x 362 at the optical centre, 235 from the hero's top. */
/* The client's own lockup, exported from the artboard and shipped whole
   rather than re-set in Montserrat: the wordmark is drawn, not typed, and no
   available face matches it. The keyed animation is wired to the header and
   footer marks instead, where a square mark is the right shape for it. */
.hero__lockup {
  grid-area: lock;
  width: min(774px, 100%);
  /* Paper sets the lockup at x345 in a 1440, which is 12px right of the
     content column's centre. A 24px left margin on a centred grid item moves
     its centre by exactly half that.

     The exported lockup is 774 x 257. It sits at y235, so it ends at y492 and
     the second row's 74px offset puts that row on the artboard's y566. */
  margin: 0 0 74px 24px;
}
.hero__lockup img { display: block; width: 100%; height: auto; }

/* Paper sets this 88px in from the lockup's own left edge. Expressed against
   the lockup's centring so it holds below 1440 as well. */
.hero__tagline {
  grid-area: tag;
  width: min(100%, max-content);
  text-align: center;
  font-family: var(--font-display);
  font-size: clamp(20px, 2.36vw, 34px);        /* 34 at 1440 */
  font-weight: var(--wt-bold);
  line-height: 1.235;
  color: var(--brand-navy);          /* 9.60:1 on the veiled ground */
}
/* Paper sets this word in #019CE9, which measures 3.03:1 on white and lower
   over the veil. --azure-ink is the same hue at 4.94:1 on white.

   OVER THE VIDEO IT IS NOT ON WHITE. Paper's veil is a single radial across
   the whole hero at 46 percent falling to 16, and the tagline sits at 63
   percent down, well outside its bright centre. Measured from real pixels
   with the wave running behind it, "Naturally" came out at 1.05:1 and the
   navy half of the line at 2.38:1. This is the site's tagline, so it is the
   last thing that can be unreadable.

   The plate below is the same idea as the veil, localised: white, soft, and
   sized to the words rather than to the frame, so the wave still runs the
   full width of the hero either side of it. */
/* The plate is the element's own background with padding, not a z-index:-1
   pseudo-element. A negative z-index child paints into the nearest stacking
   context's negative layer, and neither .hero__tagline nor .hero__stage makes
   one, so the plate went behind the video and changed nothing. The negative
   margins put the text back on its measured x433 / y571. */
.hero__tagline {
  padding-block: 24px;
  padding-inline: clamp(20px, 5.28vw, 76px);
  /* -19 against a row that starts at 566 puts the first line of type on the
     artboard's y571 once the plate's own 24px of padding is counted. */
  margin-top: -19px;
  background: radial-gradient(ellipse at center,
              rgba(255,255,255,0.96) 0%, rgba(255,255,255,0.92) 46%,
              rgba(255,255,255,0.50) 74%, rgba(255,255,255,0) 100%);
}
.hero__tagline em { font-style: normal; color: var(--azure-ink); }

/* The 240 x 300 intro card at the left gutter. */
.hero__card {
  grid-area: card;
  justify-self: start;
  margin-top: 13px;                  /* 566 + 13 = the artboard's y579 */
  display: flex;
  flex-direction: column;
  width: 100%;
  min-height: 300px;
  padding: 24px;
  background: #F2F7FB;
  border-radius: 22px;
  position: relative;
}
/* The 324px dot ring that orbits behind the card's type, overflowing it on
   every side and clipped by the hero. */
.hero__card-ring {
  position: absolute;
  left: -12.5%;                      /* -30 of the card's 240 */
  top: 8%;                           /* 24 of the card's 300  */
  width: 135%;                       /* 324 of the card's 240 */
  height: auto;
  aspect-ratio: 1;
  z-index: var(--z-art);
  opacity: 0.15;
  pointer-events: none;
}
.hero__card > *:not(.hero__card-ring) { position: relative; z-index: var(--z-content); }
.hero__card-name {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: var(--wt-bold);
  line-height: 21.6px;
  color: var(--brand-navy);
}
.hero__card-line { padding-top: 9px; }
.hero__card-body { padding-top: 16px; }
.hero__card-line,
.hero__card-body { font-size: 14px; line-height: 21px; color: var(--ink); }
.hero__card-thumb { display: none; }
.hero__card .btn-round { margin-top: auto; }

.btn-round {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--brand-blue-cta);
  box-shadow: 0 6px 20px #0A63AE57;
  color: #F3F7FB;
  flex-shrink: 0;
  transition: background-color var(--dur-hover) var(--ease-out),
              transform var(--dur-hover) var(--ease-out);
}
.btn-round:hover { background: var(--brand-blue-cta-h); transform: translateY(-2px); }
.btn-round:active { transform: translateY(0); }
.btn-round { position: relative; }

/* The partner column, 280 wide at the right gutter. */
.hero__partners {
  grid-area: partners;
  justify-self: end;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
}
/* 600, not 700. DESIGN-NOTES records exactly two 700s in this build, the hero
   tagline and the intro card name, and this caption is neither. A 700 on 15px
   supporting type is the weight the system's own ceiling exists to stop: it
   made a three-word label heavier than the 500-weight headings it sits beside.
   Nothing else about it moves. */
.hero__partners-head {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: var(--wt-semi);
  line-height: 24.3px;
  color: var(--brand-navy);
}
.hero__chip {
  display: grid;
  place-items: center;
  height: 86px;
  background: var(--page);
  border-radius: 10px;
  box-shadow: 0 1px 2px #00477B0F, 0 6px 16px #00477B1A;
  transition: transform var(--dur-hover) var(--ease-out),
              box-shadow var(--dur-hover) var(--ease-out);
}
.hero__chip:hover { transform: translateY(-2px); box-shadow: 0 2px 4px #00477B14, 0 10px 24px #00477B26; }
.hero__chip img { max-width: 150px; max-height: 56px; width: auto; height: auto; object-fit: contain; }

/* Paper carries this link on the 375 artboard only. It stays on desktop as
   the one route from the hero to the explainer, which otherwise has none. */
/* Paper carries this link on the 375 artboard only, so it is mobile-only
   here. The desktop hero has no route to the explainer and that is the
   design's own call, not an omission. */
.hero__watch { display: none; }

/* ===== 2. EVIDENCE FIRST, 1440 x 1154 ==================================== */

/* Head session, at the client's request: the gap between the hero and this
   section was --rhythm-lg, 56 to 120. Closed to roughly half, still fluid, so
   the second section sits up against the hero instead of floating away from
   it. Bottom spacing is unchanged. */
/* Head session, at the client's request. The gap went 144 -> 84 -> 44 while we
   were chasing the wrong thing: what he was seeing was a colour seam, not
   spacing. With the hero ground now white the join is invisible, so this goes
   back up to give the eyebrow and headline air. 48 at 375, 88 at 1440. */
.evidence { padding: clamp(48px, calc(35.4px + 3.36vw), 88px) 0 var(--rhythm-row-end); background: var(--page); }

.evidence__top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 40px;
  align-items: start;
}
.evidence__head { display: flex; flex-direction: column; gap: 28px; }
.evidence__head h2 { max-width: 606px; }
.evidence__head .measure { max-width: 660px; }

.stat { display: flex; flex-direction: column; gap: 12px; padding-top: var(--rhythm-lg); }
.stat__row { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.stat__to { display: none; }
.stat__arrow { color: var(--brand-turq); flex-shrink: 0; align-self: center; }
.stat__unit {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: var(--wt-medium);
  line-height: 24px;
  color: var(--ink-muted);
}
.stat__note {
  max-width: 560px;
  font-size: 16px;
  line-height: 24px;
  color: var(--ink-muted);           /* 5.51:1 on page */
}

/* The 924 plate and the 351 CTA sit side by side with a 37px gutter, and the
   CTA's rule aligns to the bottom of the plate. */
.evidence__bottom {
  display: grid;
  grid-template-columns: minmax(0, 924fr) 351fr;
  gap: 37px;
  align-items: end;
  margin-top: var(--rhythm-open);
}
.explainer {
  position: relative;
  margin: 0;
  aspect-ratio: 924 / 503;
  overflow: hidden;
  background: var(--surface);
}
.explainer__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE PLAY CONTROL.
   A solid disc, not a glyph floating on the frame. Two reasons, and the second
   one is the load-bearing one. It is the same object as .btn-round in the hero
   card, so the page has one round control rather than two. And a disc puts the
   only piece of the control a reader has to see, the triangle, on a flat colour
   this project can compute a ratio for: white on --brand-blue-cta is 6.55:1 at
   every width, whatever frame the poster happens to show. A bare triangle over
   a photograph cannot be measured without adding a selector to
   tools/over-image-contrast.mjs, which is another lane's file this week.

   Hidden until the script has confirmed the encode is on disk, so the plate is
   a still frame rather than a dead button while the film is still in flight,
   and gone again once the film is running and the native controls have it. */
.explainer__play {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  display: none;
  place-items: center;
  width: clamp(64px, 6.1vw, 88px);
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--brand-blue-cta);
  box-shadow: 0 6px 20px #0A63AE57;
  color: #F3F7FB;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease-out),
              scale var(--dur-hover) var(--ease-out);
}
.explainer.is-ready:not(.is-playing) .explainer__play { display: grid; }
/* Translate lives on `translate` and the hover lift on `scale`, so the two do
   not share one transform and the hover cannot drop the centring. */
.explainer__play:hover { background: var(--brand-blue-cta-h); scale: 1.06; }
.explainer__play:active { scale: 1; }
/* The disc sits over a photograph, so the site's single --focus ring has no
   guaranteed ground. A white ring against the disc's own navy, with navy again
   outside it, reads on any frame the poster can show. */
.explainer__play:focus-visible {
  outline: 3px solid var(--page);
  outline-offset: 3px;
  box-shadow: 0 0 0 6px #00477BD9, 0 6px 20px #0A63AE57;
}
/* A triangle's optical centre is right of its bounding box centre. */
.explainer__play svg { width: 34%; height: auto; margin-left: 8%; }

.explainer__note {
  position: absolute;
  left: 32px;
  top: 32px;
  font-family: var(--font-display);
  font-size: 24px;
  line-height: 26px;
  color: var(--ink);                 /* 14.68:1, and the plate is near white */
}
.evidence__cta { padding-top: var(--rhythm-row-end); border-top: 1px solid var(--rule); }
.evidence__cta .btn { width: 100%; }

/* ===== 3. INDUSTRIES, 1440 x 1492 ======================================== */

.industries {
  position: relative;
  isolation: isolate;
  padding: var(--rhythm-section) 0;
  background: var(--surface);
  overflow: clip;
}
/* Paper runs a live water shader here (@paper-design/shaders-react Water).
   Its BACKING is a vertical neutral gradient at 20 to 50 percent, and the
   shader then paints white highlights and caustics over it. A static build
   can reproduce the backing but not the highlights, and the backing alone at
   50 percent reads as a grey wash rather than as water, which is further from
   the artboard than a lighter stop is. Held at 34 percent, which is where the
   shaded section's own mid-tone sits. Flagged as a difference from the
   declared value rather than as a match. */
.industries__wash {
  position: absolute;
  inset: 0;
  z-index: var(--z-art);
  background: linear-gradient(180deg, rgba(221,221,221,0.20) 0%, rgba(164,164,164,0.34) 100%);
}
/* Paper draws this at left 253, top -43, width 1643 inside a 1440 artboard,
   so its right edge sits 456px past the page edge. Anchored to the section's
   own right edge at that same fraction, it holds the artboard's placement at
   1440 and keeps it at every other width instead of sliding leftward.
   456 / 1440 = 31.67 percent, 1643 / 1440 = 114.1 percent. */
.industries__glyph {
  position: absolute;
  inset: -3% -31.67% auto auto;
  width: 114.1%;
  z-index: var(--z-art);
  opacity: 0.2;
  pointer-events: none;
  /* The edge fade every decorative layer carries. The glyph is drawn arcs, so
     without it the arcs end at the section boundary mid-curve. */
  -webkit-mask-image: radial-gradient(farthest-side at 50% 50%, #000 0%, #000 46%, transparent 88%);
          mask-image: radial-gradient(farthest-side at 50% 50%, #000 0%, #000 46%, transparent 88%);
}
.industries__glyph svg { display: block; width: 100%; height: auto; }

.industries > .wrap { position: relative; z-index: var(--z-content); }
.industries__head { display: flex; flex-direction: column; gap: 18px; padding-bottom: var(--rhythm-row-end); }

.tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.tile {
  position: relative;
  aspect-ratio: 648 / 536;
  border-radius: 32px;
  overflow: clip;
  background: var(--brand-navy);
}
/* THE HOVER IS AN OPACITY, NOT A ZOOM.
   `Section, Industries on hover` differs from the resting homepage artboard in
   exactly one property across all four tiles: the photograph goes from 0.6 to
   1.0. The scrim, the title colour, the body, the link, the padding, the radius
   and the tile size are identical in both artboards, and nothing moves.

   What shipped before this was `transform: scale(1.04)` over 700ms, which is
   not drawn anywhere in the Paper file. It was also the only thing the resting
   0.6 could not coexist with: a photograph already at full strength has no
   headroom for the designed state, so the zoom had to stand in for it.

   700ms and --ease-out are kept from the zoom rather than dropped to the 200ms
   the other hovers use. The design file states no timing for any of its eight
   states, so this is a proposal, and the reasoning is that a photograph
   resolving is a different gesture from a control answering: the tile is not
   reporting that it heard the pointer, it is coming up to strength. */
.tile__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.6;
  transition: opacity var(--dur-slow) var(--ease-out);
}
/* :focus-within, so tabbing to "Learn more" brings its own tile up exactly as
   pointing at it does. The tile reveals no content either way, so the keyboard
   route is parity rather than access. */
.tile:hover .tile__photo,
.tile:focus-within .tile__photo { opacity: 1; }
.tile__scrim { position: absolute; inset: 0; }
/* The type is anchored to the TOP of the tile, not centred in it. */
.tile__body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 40px;
  text-align: center;
}
.tile__body p { max-width: 416px; padding-top: 10px; font-size: 15px; line-height: 22.5px; }
/* Drawn at 22.5px leading 14px under the body, and given a real 44px box
   around that so the element itself clears the tap floor rather than only its
   hit area doing so. The negative margin puts the text back where the design
   has it. */
.tile__more {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: -11px;
  padding-top: 14px;
  font-size: 15px;
  font-weight: var(--wt-medium);
  line-height: 22.5px;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.tile .t-tile { max-width: 456px; }

.tile--light .t-tile,
.tile--light .tile__more { color: var(--brand-navy); }
.tile--light .tile__body p { color: var(--ink); }
/* CORRECTION, WITH THE ARITHMETIC, AND THE STOPS ARE IN PIXELS ON PURPOSE.

   Paper's stops are percentages: 88 / 72 at 28 / 0 at 60 on the light tiles,
   72 / 58 at 28 / 0 at 60 on the dark ones. The tile is `aspect-ratio: 648/536`
   so its height tracks the viewport, but the type block inside it is a fixed
   number of pixels tall. Percentage stops therefore slide out from under the
   type as the window narrows: at 1440 "Learn more" sits 202px down a 536px
   tile, and at 900 the same words sit 190px down a 313px tile, which is past
   the point where Paper's gradient has released to nothing. Measured from real
   rendered pixels, that put the link at 2.19:1 on a dark tile and 2.61:1 on a
   light one, against a 4.5 floor, and this project has WCAG AA as a hard
   requirement because government procurement is in scope.

   In pixels the scrim covers the same type at every width. It holds to 230px,
   which is past the deepest the link can sit (40 padding + a two-line title +
   10 + a three-line body), and releases at 340px, which at 1440 is 63 percent
   of the tile against Paper's own 60. tools/over-image-contrast.mjs measures
   the result from the pixels rather than from a model. */
.tile--light .tile__scrim {
  background: linear-gradient(180deg, rgba(255,255,255,0.92) 0,
              rgba(255,255,255,0.86) 230px, rgba(255,255,255,0) 340px);
}
.tile--dark .t-tile,
.tile--dark .tile__body p,
.tile--dark .tile__more { color: var(--on-navy); }
.tile--dark .tile__scrim {
  background: linear-gradient(180deg, rgba(0,30,55,0.80) 0,
              rgba(0,30,55,0.74) 230px, rgba(0,30,55,0) 340px);
}
.tile--light .tile__more:hover { color: var(--brand-blue-cta); }
.tile--dark .tile__more:hover { color: var(--brand-turq); }   /* 5.89:1 on navy */

/* ===== 4. HARDWARE BAND, 1440 x 414 ====================================== */

/* 1440 x 414 on the artboard, held as a ratio so the band keeps its
   proportion at every width rather than staying 414 tall inside a 375 page.
   The floor is the height at which the photograph still reads as a scene. */
.hardware {
  position: relative;
  isolation: isolate;
  aspect-ratio: 1440 / 414;
  min-height: 240px;
  overflow: clip;
  background: #0E0F11;
}
/* Head session, at the client's request: this band is now a parallax layer.
   The overscan is sized to the travel, not the other way round. motion.js
   moves it yPercent -8, which is 8 per cent of the element's own 118 per cent
   height, so 9.44 per cent of the container. Starting at top -4% the bottom
   edge sits at 114% and finishes at 104.6%, so the frame is covered for the
   whole scrub with room to spare. The old inset of -6px / -4px gave 10px of
   overscan, which any real parallax travel would have torn straight through.
   Under reduced motion or a coarse pointer motion.css forces transform: none
   and the band simply sits still, still fully covered. */
.hardware__photo {
  position: absolute;
  top: -4%;
  left: 0;
  right: 0;
  z-index: var(--z-media);
  width: 100%;
  height: 118%;
  object-fit: cover;
}
/* 0deg, so 0% is the BOTTOM edge: opaque at both edges, near clear across
   the middle third, which is what holds the frame together with no type. */
.hardware__scrim {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  background: linear-gradient(0deg, rgba(14,15,17,0.95) 0%, rgba(14,15,17,0.62) 30%,
              rgba(14,15,17,0.16) 58%, rgba(14,15,17,0.52) 86%, rgba(14,15,17,0.94) 100%);
}

/* ===== 5. RESEARCH AND TESTING =========================================== */

.research { position: relative; isolation: isolate; padding-top: var(--rhythm-section); background: var(--page); overflow: clip; }
/* 673 wide at left 5, top 5 on the artboard. Expressed against the section
   so it scales with it, and masked so the artwork does not end at a corner. */
.research__artwork {
  position: absolute;
  inset: 0.5% auto auto 0.35%;
  width: 46.7%;
  z-index: var(--z-art);
  opacity: 0.2;
  pointer-events: none;
  -webkit-mask-image: radial-gradient(farthest-side at 40% 40%, #000 0%, #000 40%, transparent 86%);
          mask-image: radial-gradient(farthest-side at 40% 40%, #000 0%, #000 40%, transparent 86%);
}
.research__artwork img { width: 100%; height: auto; }
.research > .wrap { position: relative; z-index: var(--z-content); display: flex; flex-direction: column; gap: var(--rhythm-row-end); }
.research__head { display: flex; flex-direction: column; gap: 18px; }

.uni {
  position: relative;
  display: grid;
  grid-template-columns: 460px minmax(0, 1fr);
  gap: 56px;
  padding: var(--rhythm-row) 48px;
  background: var(--surface);
  border-radius: 20px;
}
.uni__identity { display: flex; flex-direction: column; gap: 14px; }
.uni__logo { display: block; width: 359px; max-width: 100%; }
.uni__logo img { width: 100%; height: auto; }
.uni__centre { font-size: 16px; font-weight: var(--wt-semi); line-height: 24px; color: var(--ink-muted); }
.uni__detail { display: flex; flex-direction: column; gap: 24px; }
.uni__lead { max-width: 700px; font-size: 19px; line-height: 31px; color: var(--ink); }
.uni__areas { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; list-style: none; }
.uni__areas li {
  padding-top: 16px;
  border-top: 2px solid var(--brand-azure);
  font-size: 17px;
  font-weight: var(--wt-semi);
  line-height: 26px;
  color: var(--brand-navy);
}
.uni__cta { grid-column: 2; justify-self: end; align-self: end; }

.labs { display: flex; flex-direction: column; gap: 28px; padding-top: var(--rhythm-tight); }
.labs__cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; }
.lab {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding-top: var(--rhythm-tight);
  border-top: 1px solid var(--rule);
}
/* THE COLUMN TINT, from `Section, Research and testing on hover`.
   Paper draws the hovered column as a card filled azure to turquoise, both at
   20 percent, in oklab, with a 20px radius. The two literal stops in the file,
   oklab(66.7% -0.080 -0.119) and oklab(82.4% -0.134 -0.028), convert to #02A0E0
   and #23E1DC, which are --brand-azure and --brand-turq to within a unit, so
   the tokens are used rather than the literals.

   TWO DEPARTURES FROM THE ARTBOARD, both so that nothing moves.

   The artboard's hovered column carries padding-left: 10px that the resting one
   does not, which slides every line of type 10px right the moment a pointer
   arrives. Nothing else in the eight states moves, and a column of running text
   stepping sideways under the cursor is the kind of motion this build refuses
   elsewhere. The 10px is how the card was drawn, so it is spent on the card
   instead: the tint is a layer inset 10px outside the column on three sides,
   and the text stays exactly where it is at rest.

   Second, the tint is a pseudo-element faded on opacity rather than a
   background-image swapped on the column. Gradient interpolation is not
   portable, so a transition on background-image cuts rather than fades outside
   Chrome, and this is a fade.

   The top edge is flush at inset 0 so the tint starts under the 1px rule and
   the rule itself is untouched, which is what the artboard shows. */
.lab::before {
  content: '';
  position: absolute;
  inset: 0 -10px -10px;
  z-index: var(--z-art);
  border-radius: 20px;
  /* THE TINT PLUS A COLOUR STEP, AND THE ARTBOARD'S 20 PER CENT IS KEPT.
     A 20 percent azure-to-turquoise wash lifts a white ground toward cyan, and
     the two greys that sit on it do not survive it. Measured from real pixels
     with the state pinned on, "Dairy farm wastewater, South Korea" and "Mount
     Pleasant, South Carolina, 2018" came out at 4.19 and 4.07:1, and three of
     the 13px figure labels at 4.19, 4.36 and 4.36:1, all against a 4.5 floor.
     They are 15px and 13px --ink-muted, which clears at 5.51:1 on the plain
     page and has no slack to spend on a tint.

     Weakening the tint was tried first and does not pay. At 12 percent the same
     five pairs came back at 4.40, 4.47, 4.49, 4.49 and 4.40:1, still under, so
     clearing them that way costs most of the state and buys a card nobody can
     see. The tint is what the artboard draws, and the greys are muted only
     because there is nothing behind them; the moment there is, they should
     firm up. So the wash stays at 20 percent and the supporting type steps from
     --ink-muted to --ink on the same 200ms, which takes it to 14.68:1 on white
     and clears the tint by a wide margin. See the rule under this one.

     sRGB first, so a browser without color-mix or an interpolation hint still
     gets the tint rather than dropping the declaration and the whole state. */
  background-image: linear-gradient(180deg, rgba(0,160,224,0.20), rgba(35,225,220,0.20));
  background-image: linear-gradient(in oklab,
    color-mix(in oklab, var(--brand-azure) 20%, transparent),
    color-mix(in oklab, var(--brand-turq) 20%, transparent));
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-hover) var(--ease-out);
}
/* :focus-within costs nothing today, because the column holds no focusable
   element, and it is the state that carries the hover the moment one is added.
   See the note on .cap__row for why neither block is made focusable now. */
.lab:hover::before,
.lab:focus-within::before { opacity: 1; }
/* The supporting greys firm up with the card. Not a decoration: it is what
   keeps the 15px place line and the 13px figure labels above 4.5:1 once the
   tint is under them, and it is the only change in these eight states that
   moves a text colour. The heading and the figures are --brand-navy and clear
   the tint on their own, so they are left alone. */
.lab__where,
.lab__label,
.lab__note { transition: color var(--dur-hover) var(--ease-out); }
.lab:hover .lab__where,
.lab:hover .lab__label,
.lab:hover .lab__note,
.lab:focus-within .lab__where,
.lab:focus-within .lab__label,
.lab:focus-within .lab__note { color: var(--ink); }
/* The laboratory marks are watermarks in the design, at a tenth opacity
   behind their own column. They are identification, not decoration. */
/* Fixed boxes with object-fit, because Paper clips each mark to a window
   rather than fitting it. Left uncropped, the Korea mark is 1109px wide and
   the Babcock mark runs into the note under it. */
/* 288 x 284 at left 44 inside a 416 column, so the offsets are fractions of
   the column rather than of the page: 44/416 and 288/416. */
.lab__watermark {
  position: absolute;
  inset: 5px auto auto 10.6%;
  z-index: var(--z-art);
  width: 69.2%;
  height: auto;
  aspect-ratio: 288 / 284;
  max-width: calc(100% - 10.6%);
  object-fit: cover;
  object-position: left top;
  /* Paper sets these at 0.1. Measured behind the NOAA column's own note, that
     leaves --ink-muted at 4.45:1 and its 13px labels at 4.47:1, both a hair
     under the floor. 0.06 clears it and the mark still reads. */
  opacity: 0.06;
  pointer-events: none;
}
.lab__watermark--low { top: 28%; left: 0; width: 100%; height: auto; aspect-ratio: 416 / 187; }
.lab__id { position: relative; display: flex; flex-direction: column; gap: 6px; }
.lab__id h3 { font-size: 19px; font-weight: var(--wt-semi); line-height: 27px; color: var(--brand-navy); }
.lab__where { font-size: 15px; line-height: 22px; color: var(--ink-muted); }
.lab__figs { position: relative; display: flex; gap: 22px; flex-wrap: wrap; }
.lab__figs p { display: flex; flex-direction: column; gap: 2px; }
.lab__figs--four p { min-width: 88px; }
.lab__label { font-size: 13px; line-height: 16px; color: var(--ink-muted); }
.lab__note { position: relative; font-size: 15px; line-height: 23px; color: var(--ink-muted); }

/* Paper leaves 128px of white above the footer, which the original defect
   list called a gap because it truncated the tail of a decorative wave that
   crossed the seam. That wave is not in this build, so there is nothing to
   truncate.

   Raised to 160. The enquiry card is lifted 64px and sits at right 80 in a
   420px box, which puts it directly over the third laboratory column. At the
   measured 128 the card's turquoise top rule cuts through the NOAA footnote.
   160 clears it. This is a legibility correction, not a redesign: the card,
   its lift and its position are all unchanged. */
/* The footer is written after </main>, so it is a sibling of the sections
   rather than a child of .research: a .research .site-footer selector matches
   nothing. Scoped to the page instead. */
.page-home .site-footer { margin-top: var(--rhythm-lg); }

/* ===== THE HERO BETWEEN 900 AND 1100 ====================================
   Not an escape from coordinates: there are none left. Below about 1100 the
   content column is 972 or less, and a 240 card plus a 280 partner column
   leaves under 400 for the tagline between them, which wraps the site's own
   tagline to three lines. The five elements keep their order and their sizes
   and stack into three rows instead of two. Above 1100 the artboard's own
   two-row composition returns untouched. ================================ */

@media (min-width: 900px) and (max-width: 1099px) {
  .hero__stage {
    /* Two tracks here, not three with the middle one left empty.

       The bottom row used to read 'card . partners' against the desktop's
       three tracks, so the middle track stayed in the grid with nothing in it
       and absorbed every pixel the stage gained: the two panels sat against
       opposite edges of the row with a hole between them that grew from about
       220px at 900 to about 390px at 1099. Two objects at opposite ends of an
       empty row do not read as a composition, they read as a layout that has
       come apart, which is the horizontal form of the unexplained gap the
       rhythm tool already fails vertically.

       240fr and 280fr are the artboard's own ratio between these two panels,
       so the row keeps the proportion the design draws at 1440 and simply
       fills the width it is given. */
    grid-template-columns: minmax(0, 240fr) minmax(0, 280fr);
    grid-template-areas:
      'lock lock'
      'tag  tag'
      'card partners';
    row-gap: var(--rhythm-block);
  }
  .hero__lockup { margin-bottom: 0; width: min(620px, 84%); }
  .hero__tagline { margin-top: 0; justify-self: center; }
  .hero__card { margin-top: 0; }
}

/* ===== MOBILE, from artboard 9WU-0 ======================================= */

@media (max-width: 899px) {

  .page-home .site-header { padding: 0; }

  .hero { margin-top: 0; min-height: 662px; }
  .hero__media video, .hero__media img { opacity: 1; }
  .hero__veil { display: none; }

  /* The mobile still is object-fit: cover inside a clipped box, so it stops
     dead at the hero's floor. At 375 the crop lands where the particle field
     has already thinned and the cut does not read. At 768 the same image is
     cropped wider, the cut lands mid-field, and the hero ends on a hard
     horizontal line with the dots sliced through. Same defect the .art
     treatment carries a mask for, on the one piece of hero artwork that is an
     <img> rather than an .art layer, so it gets the same treatment.

     The fade resolves to the hero's own --surface ground, which measures
     1.09:1 against the white page below it, so the seam is a fade to nothing
     rather than a fade to a different colour. Only the last 14 percent, so the
     composition behind the card and the partner column is untouched. */
  .hero__media {
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 86%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 86%, transparent 100%);
  }

  /* The 375 artboard is the same composition one column narrower: the lockup
     across the top, the tagline under it, then the card and the partner
     column on the hero's floor. Three rows, the third one elastic, so the
     photograph keeps its open middle at every width from 320 to the
     breakpoint instead of holding five numbers measured at exactly 375. */
  .hero__stage {
    width: 100%;
    padding-inline: var(--gutter);
    /* The same fractions-not-pixels rule the desktop stage carries, for the
       same reason. A 177px card and a 120px partner column are the 375
       artboard's measurements against its own 335 row, so they are written
       as their share of it. Held as pixels they were correct at 375 and
       frozen everywhere above it: by 899 the row is 520 wide and the two
       panels still measured 177 and 120, so the dead middle track had grown
       to 199px and the pair sat at opposite ends of a hole.

       52.836% and 35.821% of 335 are 177.0 and 120.0, so 375 is unchanged to
       the pixel, gaps and all, and every width above it now shares the growth
       between the two panels instead of banking it in the middle. */
    grid-template-columns:
      minmax(0, 52.836%)          /* 177 of the 375 artboard's 335 row */
      minmax(0, 1fr)
      minmax(0, 35.821%);         /* 120 of the 375 artboard's 335 row */
    grid-template-areas:
      'lock lock lock'
      'tag  tag  tag'
      'card .    partners';
    grid-template-rows: auto auto 1fr;
    column-gap: 12px;
    row-gap: 0;
    padding-block: 136px 114px;      /* 375 artboard: y136 to a 662 floor */
    align-items: end;
  }

  /* 276 wide on the 375 artboard, and the 37px below it puts the tagline on
     the artboard's y265 without naming y265.

     Both are written as their share of the stage rather than as the pixels
     they measure at 375, for the same reason the two panels below are. The
     276px was a ceiling, and a ceiling on a fraction is a freeze: 82 percent
     of the 375 stage is 274.7, so the cap did nothing at 375 and everything
     above about 340, where it locked the lockup at 276 and held it there
     while the stage ran on to 760. Set free it reaches 623 at 899, against the
     620 the next band up gives it at 900, so the lockup crosses the breakpoint
     without a step. 37 / 335 is 11 percent, and a percentage margin resolves
     against the containing block's inline size, which here is the stage. */
  .hero__lockup { width: 82%; margin: 0 0 11%; align-self: start; }
  /* Paper sets this 27px in from the lockup's own left edge, so it is offset
     against the lockup rather than pinned to a number that holds at 375 only. */
  /* THE PLATE STAYS AT MOBILE. It was dropped here, and the desktop rule's own
     comment explains why it exists: over the running hero video, measured from
     real pixels, the tagline is unreadable without it. At 375 the same
     measurement gave 1.59:1 on "Naturally" against a turquoise frame of the
     wave, on the site's own tagline, which is the last line on the site that
     can be unreadable. The correction was made on desktop and never carried
     down. The plate's core is held solid further out than the desktop one
     because 12px glyphs sit closer to the edge of a small box, and the em
     takes --azure-hover rather than --azure-ink for the same reason: at 12px
     there is no slack to spend. Measured after the change by
     tools/over-image-contrast.mjs, not estimated.

     The negative margins keep the type on its measured x54 and its measured
     baseline: the plate's own padding would otherwise push it down and right. */
  /* Every number in this rule is the 375 artboard's own, written as a ramp
     that starts on it. At 375 each clamp is on its floor and resolves to the
     artboard value to within a rounding error, so the design width is
     untouched and provably so; from there they climb with the stage so the
     tagline stays in proportion to a lockup that is no longer frozen, and
     arrive at 899 within a few pixels of what the 900 band hands them.

       font-size    12 at 375  ->  21.0 at 899   (900 gives 21.2)
       padding      12 / 22    ->  24 / 52.8     (900 gives 24 / 47.5)
       left offset  32         ->  9.55% of the stage, 32 at 375

     The negative top margin is tied to the padding rather than restated, so
     the plate can grow without the type drifting off its measured baseline. */
  .hero__tagline {
    --plate-y: clamp(12px, 3.2vw, 24px);
    justify-self: center;
    align-self: start;
    margin: calc(var(--plate-y) * -1) 0 0 9.55%;
    padding: var(--plate-y) clamp(22px, 5.87vw, 53px);
    background: radial-gradient(ellipse at center,
                rgba(255,255,255,0.97) 0%, rgba(255,255,255,0.95) 62%,
                rgba(255,255,255,0.55) 84%, rgba(255,255,255,0) 100%);
    font-size: clamp(12px, calc(5.56px + 1.717vw), 21px);
    line-height: 1.333;
  }
  .hero__tagline em { color: var(--azure-hover); }
  /* The artboard puts this 14px below the card's own top edge, so it is
     offset from the card's floor rather than from the elastic row's. */
  .hero__watch {
    display: block;
    grid-area: card;
    align-self: end;
    justify-self: start;
    z-index: var(--z-content);
    /* CORRECTED, head session 024, reported by the client as a broken mobile
       hero. The offset is right and its arithmetic was stale.

       This label shares the card's grid cell on purpose and is pushed up off
       the row's floor so it lands 14px inside the card's top edge, where the
       artboard puts it. The old 100px was derived as 134 card - 14 inset - 20
       line, and the line is not 20px: this is an anchor carrying the project's
       44px tap-target floor, so it is 44px tall. The label therefore landed 10
       above the card rather than 14 below its top, a 24px error, which reads as
       "Watch Video" floating loose over the hero with the card starting under
       it. Measured at -10px on every width from 360 to 899, the card holding
       134 throughout.

       Written as the subtraction rather than as its result, because the two
       numbers it depends on are exactly the two somebody will change next. */
    margin-bottom: calc(134px - 14px - 44px);   /* card - inset - tap floor */
  }
  .hero__card {
    grid-area: card;
    justify-self: start;
    align-self: end;
    margin-top: 0;
    width: 100%;
    min-height: 134px;
    height: 134px;
    padding: 14px;
    border-radius: 15px;
  }
  .hero__card-name, .hero__card-line, .hero__card-body { display: none; }
  /* Paper stacks three things on this card: the Watch Video link at its top
     edge, a 152 x 82 still of the explainer 44px below that, and the round
     button overlapping the still's lower right. */
  .hero__card-thumb {
    display: block;
    position: absolute;
    left: 6px;
    top: 44px;
    width: 152px;
    height: 82px;
    object-fit: cover;
    border-radius: 6px;
  }
  .hero__card .btn-round { position: absolute; right: 6px; bottom: 6px; margin: 0; }
  /* The artboard draws a 31px disc. The button is 44px so it clears the tap
     floor; the disc is drawn by the pseudo-element at its measured size. */
  .hero__card .btn-round {
    width: 44px;
    height: 44px;
    background: none;
    box-shadow: none;
    margin-top: auto;
  }
  .hero__card .btn-round::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 31px;
    height: 31px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: var(--brand-blue-cta);
    box-shadow: 0 6px 20px #0A63AE57;
  }
  .hero__card .btn-round svg { position: relative; width: 15px; height: 15px; }
  /* The artboard leaves 46px under the partner column and 114 under the card,
     so the column sits 68px lower than the card's own floor. */
  .hero__partners { align-self: end; margin-bottom: -68px; z-index: var(--z-content); width: 100%; gap: 10px; }
  /* The 375 hero photograph runs dark navy dots straight under this line,
     which measured 1.34:1. Same answer as the tagline: a plate sized to the
     words, not a recolour. */
  .hero__partners-head {
    align-self: flex-start;
    padding: 2px 8px;
    font-size: 12px;
    line-height: 20px;
    background: rgba(255,255,255,0.92);
    border-radius: 4px;
  }
  .hero__chip { height: 36px; border-radius: 10px; }
  .hero__chip img { max-width: 78px; max-height: 26px; }

  .evidence { padding: 28px 0 0; }
  .evidence__top { grid-template-columns: 1fr; gap: 0; }
  .evidence__head { gap: 14px; }
  .stat { gap: 8px; padding-top: 24px; border-top: 2px solid var(--brand-turq); margin-top: 24px; }
  .stat__row { gap: 10px; }
  .stat__arrow { display: none; }
  .stat__to { display: inline; font-family: var(--font-display); font-size: 34px;
    font-weight: var(--wt-medium); line-height: 42px; letter-spacing: -0.03em; color: var(--brand-navy); }
  .stat__unit { font-family: var(--font-body); font-size: 15px; line-height: 18px; }
  .stat__note { font-size: 13px; line-height: 20px; }
  .evidence__bottom { grid-template-columns: 1fr; gap: 24px; margin-top: 24px; }
  .evidence__cta { padding-top: 0; border: 0; }
  .evidence__cta .btn { width: 100%; height: 56px; justify-content: center; font-size: 17px; line-height: 22px; }

  .industries { padding: 56px 0 0; background: var(--page); }
  .industries__glyph, .industries__wash { display: none; }
  .industries__head { gap: 10px; padding-bottom: 24px; }
  /* Edge to edge, capped height, and no type over any photograph: the tile
     splits into a band and a text block rather than insetting. */
  .tiles { grid-template-columns: 1fr; gap: 28px; }
  .tile { aspect-ratio: auto; border-radius: 0; background: none; margin-inline: calc(var(--gutter) * -1); }
  .tile__photo { position: relative; inset: auto; height: 152px; opacity: 1; }
  .tile__scrim { display: none; }
  .tile__body { align-items: flex-start; padding: 20px var(--gutter) 0; text-align: left; }
  .tile--dark .t-tile { color: var(--brand-navy); }
  .tile--dark .tile__body p { color: var(--ink); }
  .tile--dark .tile__more, .tile--light .tile__more { color: var(--azure-ink); }
  .tile__body p { font-size: 17px; line-height: 26px; }
  .tile__more {
    align-self: flex-start;
    margin-top: 20px;
    min-height: 44px;
    padding-top: 0;
    display: flex;
    align-items: center;
    font-family: var(--font-display);
    font-size: 17px;
    line-height: 22px;
    text-decoration: none;
    border-bottom: 1.5px solid var(--brand-turq);
  }

  .hardware { display: none; }   /* no mobile artboard carries this band */

  .research { padding-top: 52px; }
  .research__artwork { display: none; }
  .research > .wrap { gap: 22px; }
  .research__head { gap: 10px; }

  .uni { grid-template-columns: 1fr; gap: 16px; padding: 24px var(--gutter); border-radius: 0; }
  .uni__logo { width: 150px; }
  .uni__centre { font-size: 15px; line-height: 22px; color: var(--brand-navy); }
  .uni__lead { font-size: 17px; line-height: 26px; }
  .uni__areas { grid-template-columns: 1fr; gap: 12px; }
  .uni__areas li { padding-top: 8px; border-top-width: 1.5px; font-size: 17px; line-height: 22px; }
  .uni__cta { grid-column: 1; justify-self: stretch; }
  .uni__cta .btn { width: 100%; height: 52px; }

  .labs { gap: 18px; padding-top: 44px; }
  .labs__cols { grid-template-columns: 1fr; gap: 32px; }
  .lab { gap: 4px; padding-top: 18px; border-top-width: 1.5px; }
  /* Same reason as the capability rows: the 375 artboard draws no state for
     these columns, and a tint that a tap leaves stuck on is worse than none. */
  .lab::before { content: none; }
  .lab__watermark { display: none; }
  .lab__id h3 { font-size: 17px; line-height: 22px; }
  .lab__where { font-size: 15px; line-height: 18px; }
  .lab__figs { flex-direction: column; gap: 16px; padding-top: 18px; }
  .lab__figs--four { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
  .lab__note { padding-top: 16px; }

  /* The credits block already closes with 40px of its own, so a 56 here made
     96 against a 72 mobile ceiling. The two are one boundary, not two. */
  .page-home .site-footer { margin-top: var(--rhythm-block); }
}

/* ===== 6. CAPABILITIES, 1440 x 1754 ======================================
   A navy gradient band, opened by two 122px display lines and closed by four
   rows that run flush to the left page edge and round on the right corners
   only. Each row carries its own photograph at a fifth opacity.

   Paper sets the row titles in Archivo 100 and the bodies in Schibsted
   Grotesk. Neither is a Hydro2050 brand face; both are left over from the
   superseded mockup at the repository root. Montserrat 300 and Source Sans 3
   carry the same sizes and leading here. The body colour #AAAAAA measures
   3.05:1 on this ground and fails the 4.5 floor; --on-navy-mute is 6.69:1.
   ========================================================================== */

.capabilities {
  position: relative;
  isolation: isolate;
  /* 200 on the artboard, and 200 is not a rhythm step: 57 of it is the wave
     that rides the band's top edge and the rest is the rhythm under it. */
  padding-top: calc(var(--rhythm-lg) + 80px);
  padding-bottom: var(--rhythm-row);
  background: linear-gradient(180deg,
              #266399 4.52%, #15558A 12.26%, #00477B 50.4%,
              #00487C 90%, #15558A 98.33%, #266399 100%);
}
/* The wave is the transition INTO the band, so it rides its top edge. */
.capabilities__wave {
  position: absolute;
  inset: -57px 0 auto;
  z-index: var(--z-art);
  height: 114px;
  pointer-events: none;
}
.capabilities__wave svg { display: block; width: 100%; height: 100%; }

/* Three concentric arcs, rotated 180 degrees, riding the band's lower right.
   Paper places this at left 2049 with the rotation about its own top-left
   corner, which lands it back over the canvas at 808. Rotated about its
   centre here, which is the same result and does not depend on the origin. */
/* Three concentric arcs at left 808, width 1241, in a 1440 artboard, so the
   arcs run 609px past the right page edge. Anchored to the section's own
   right edge at that fraction: 609/1440 = 42.3 percent, 1241/1440 = 86.2. */
.capabilities__orbit {
  position: absolute;
  inset: -53px -42.3% auto auto;
  z-index: var(--z-art);
  width: 86.2%;
  height: auto;
  aspect-ratio: 1241 / 567;
  transform: rotate(180deg);
  pointer-events: none;
}
.capabilities { overflow: clip; }
.capabilities__head { position: relative; z-index: var(--z-content); padding-inline: var(--gutter); }
.cap__rows, .cap__all { position: relative; z-index: var(--z-content); }
.cap__giant {
  font-family: var(--font-display);
  font-size: 122px;
  font-weight: var(--wt-light);
  line-height: 107px;
  letter-spacing: -0.065em;
  color: var(--on-navy);
}
.cap__giant + .cap__giant { margin-bottom: 50px; }
.cap__head-mob { display: none; }

.cap__rows { display: flex; flex-direction: column; gap: 50px; list-style: none; }
.cap__row {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: 24px;
  width: min(1376px, 100vw - var(--gutter));
  min-height: 210px;
  padding: var(--rhythm-row) var(--gutter);
  border-radius: 0 14px 14px 0;
  overflow: clip;
  transition: border-radius var(--dur-drawer) var(--ease-out);
}
.cap__photo {
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.2;
  transition: opacity var(--dur-drawer) var(--ease-out);
}
/* The row photograph at 0.2 lifts the ground under the body column, which sits
   on the bright right-hand third. Measured, --on-navy-mute came out at 3.92,
   3.94 and 4.05:1 there against a 4.5 floor. A navy scrim weighted to the
   right darkens the body column and leaves the title's half of the row alone,
   because the title is white and already clears with room. */
.cap__row::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  background: linear-gradient(90deg, rgba(0,44,80,0) 0%, rgba(0,44,80,0) 38%,
              rgba(0,44,80,0.52) 60%, rgba(0,44,80,0.62) 100%);
  pointer-events: none;
}
.cap__photo--split { position: absolute; inset: 0; z-index: var(--z-media); display: flex; opacity: 0.2;
  transition: opacity var(--dur-drawer) var(--ease-out); }
.cap__photo--split img { width: 50%; height: 100%; object-fit: cover; }

/* ---- THE ROW ON HOVER, from `Capabilities on hover` --------------------
   Four of the five properties in that artboard are here. The fifth is not, and
   the paragraph after this one is the reason.

   WHAT IS BUILT
     photograph      0.2 -> 0.7
     ring            none -> 3px of #FFFFFF80 on all four sides
     radius          0 14px 14px 0 -> 20px on all four corners
     description     --on-navy-mute -> #FFFFFF, and 370px -> 490px of measure

   WHAT IS NOT, AND WHY
   The artboard also grows the row from 240px to 465px. That is the state's
   headline gesture and it is the one thing here that cannot ship as a hover.
   A row that nearly doubles in height pushes every row below it down while the
   pointer is resting on it, and the pointer does not move, so it can leave the
   row it just entered, which closes the row, which puts it back, which opens
   it again. DESIGN-NOTES already refuses this pattern for the sticky header in
   those words. The design's intent, the hovered row opening up and showing its
   picture, is carried here by the photograph coming up to 0.7 and the ring
   closing around the row, neither of which touches the layout.

   The 16px -> 24px description is built, but only because it is measurably
   free: every row's height is set by its 60px title at 76px leading over two
   lines, and the hovered description at 24px/26px in a 490px measure is
   shorter than that in all four rows at all forty widths from 360 to 1920. If
   a fifth row is ever added with a longer body, check it again, because the
   moment the description sets the row height this becomes the height animation
   above by another route.

   THE RING IS A PSEUDO-ELEMENT, NOT A BORDER, for the same reason. A real 3px
   border is 3px of box on every side, so it would reflow the row and shift its
   own contents 3px on hover. Drawn inside, nothing moves.

   THE RING CARRIES A SCRIM, AND THIS IS A CORRECTION TO THE ARTBOARD.
   The photograph at 0.7 puts white 60px titles and white 13px numbers on
   whatever the picture happens to show, and three of the four rows fail WCAG
   AA there. Measured from real pixels by tools/over-image-contrast.mjs with the
   state pinned on: the titles came out at 1.76, 1.97 and 2.66:1 against a floor
   of 3, and the "02" and "04" numbers at 4.34 and 1.88:1 against a floor of
   4.5. The worst ground under "04" measured rgb(168,192,214), which is very
   nearly white. This project has AA as a hard requirement because government
   procurement is in scope, so the artboard loses.

   THE EXISTING SCRIM CANNOT DO THIS JOB, and the reason is worth writing down
   because it is invisible in the source. .cap__row::before and .cap__photo are
   both at --z-media, and a pseudo-element is the first child, so the photograph
   paints OVER the scrim, not under it. That works at rest only because the
   photograph is 20 percent opaque and the scrim shows through the other 80. At
   0.7 the same scrim is down to 30 percent of its effect, which is why raising
   it there does almost nothing: candidate values of 0.55 on the left still left
   two titles at 1.97 and 1.99:1.

   So the hover scrim goes on ::after, which is the last child and therefore
   paints above the photograph and below the type, which is at --z-content. It
   is 0 at rest, so the resting row is untouched to the pixel, and it is one
   layer with the ring rather than two, so they fade together.

   The alphas are measured, not chosen: 0.52 across the title's half of the row
   and 0.66 at the right edge, which clears every pair with margin. Net effect,
   the picture is present at 0.7 x 0.48 = 0.34 on hover against 0.2 at rest, so
   it does come up by about two thirds, which is the state's intent, and the
   type stays readable, which is not negotiable.

   Timing is --dur-drawer, 320ms. The design file states none. 200ms across a
   photograph lifting three and a half stops reads as a flash rather than a
   fade, and 320 is the token this build already spends on the drawer, which is
   the nearest thing to it in weight.

   :focus-within is written alongside :hover throughout and matches nothing
   today: the rows are <li> with no link inside them, and the artboard draws
   none. Giving them tabindex would put four stops in the tab order that lead
   nowhere and do nothing but repaint, which is worse for a keyboard visitor
   than no state at all, and nothing in the state is content: the description
   is fully present and fully legible at rest. When case-studies.html gains
   per-study anchors these rows should become links to them, and the keyboard
   state is then already here. */
.cap__row::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: var(--z-media);
  border: 3px solid #FFFFFF80;
  border-radius: inherit;
  background: linear-gradient(90deg, rgba(0,44,80,0.52) 0%, rgba(0,44,80,0.52) 38%,
              rgba(0,44,80,0.60) 60%, rgba(0,44,80,0.66) 100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-drawer) var(--ease-out);
}
.cap__row:hover,
.cap__row:focus-within { border-radius: 20px; }
.cap__row:hover::after,
.cap__row:focus-within::after { opacity: 1; }
.cap__row:hover .cap__photo,
.cap__row:hover .cap__photo--split,
.cap__row:focus-within .cap__photo,
.cap__row:focus-within .cap__photo--split { opacity: 0.7; }
.cap__n {
  position: relative;
  z-index: var(--z-content);
  flex: 0 0 40px;
  font-size: 13px;
  line-height: 16px;
  color: var(--on-navy);
}
/* CORRECTION, WITH THE ARITHMETIC. Paper lays this out as 40 index + 520
   title + 640 body inside a 1248 row, and sets the title in Archivo 100,
   where every one of the four fits on two lines and the row computes to 240.
   Montserrat 300 is wider at the same 60px, so in a 520 box three of the four
   wrapped to three lines and the band grew 195px past the artboard. The
   columns are re-cut to 620 / 540, which still sums to 1248, and all four
   titles come back to two lines and 240px rows. Stepping the type down
   instead would have broken the band's one piece of scale contrast. */
/* 40 index + 620 title + 540 body inside the row's own 1248, with two 24px
   gaps, so the three columns are declared as flex bases that sum to the row
   rather than as three widths that only add up at 1440. */
.cap__title {
  position: relative;
  z-index: var(--z-content);
  flex: 1 1 620px;
  font-family: var(--font-display);
  font-size: 60px;
  font-weight: var(--wt-light);
  line-height: 76px;
  letter-spacing: -0.065em;
  color: var(--on-navy);
}
.cap__col { position: relative; z-index: var(--z-content); display: flex; justify-content: flex-end; flex: 0 1 540px; }
/* The column is justified to its right edge, so widening the description opens
   it leftward into the column's own slack. The title's flex basis is untouched,
   so nothing on the left of the row moves. */
.cap__desc {
  width: min(370px, 100%);
  font-size: 16px;
  line-height: 26px;
  color: var(--on-navy-mute);
  transition: width var(--dur-drawer) var(--ease-out),
              font-size var(--dur-drawer) var(--ease-out),
              color var(--dur-drawer) var(--ease-out);
}
/* 24px on a 26px leading is nearly solid set. That is deliberate in the file
   and is kept: the hovered paragraph reads as a block, not as a list of lines. */
.cap__row:hover .cap__desc,
.cap__row:focus-within .cap__desc {
  width: min(490px, 100%);
  font-size: 24px;
  color: var(--on-navy);
}

/* ---- THE WHOLE ROW IS CLICKABLE, with one real link ----------------------
   The long note on `.cap__row::after` above ends by saying that :focus-within
   is written throughout and matches nothing, because the rows are <li> with no
   link inside them, and that when case-studies.html gains per-study anchors
   these rows should become links to them. That is what this is. The keyboard
   state was already here and needed nothing added.

   Same mechanism as `.tile__more`: one real <a> wrapping the title, dropped to
   position:static so its ::after resolves against the row rather than against
   the anchor. Two things differ from the tile, and both are forced by this
   section's z-index budget rather than chosen.

   `.cap__title` gives up `position: relative`. It only ever carried it to hold
   a z-index above the photograph, and it is a flex item of `.cap__row` (a grid
   item below 900), so the z-index works without it. While it stayed positioned
   it was the nearest positioned ancestor, and a stretched ::after inside it
   covered the title instead of the row.

   It also moves from --z-content to 3. A z-index makes the title a stacking
   context, and a descendant cannot paint above a sibling its own context sits
   below: at --z-content the title tied with `.cap__col` and lost on source
   order, so the right-hand third of the row, which is the description, stayed
   unclickable. At 3 the hit area is the whole row, which is the point.

   Nothing here touches `.cap__row::after` or `::before`. The new rule is on
   the link, and both of those pseudo-elements are pointer-events:none, so the
   resting scrim and the hover scrim and ring are unaffected in both states. */
.cap__title { position: static; z-index: 3; }
/* HEAD SESSION 024. The verification run caught this as a regression against
   the parent commit: `.study__link` on the case studies page carries a 44px
   floor and a flex box for it, and `.cap__link` here was given the stretched
   `::after` without them. The audit measures the anchor's own rect rather than
   the stretched hit area, so one or two of the homepage rows failed the tap
   target minimum between 440 and 899, where the title sets on a single line.
   Same fix as its sibling, and centring keeps the title optically where the
   shorter box had it. */
.cap__link {
  position: static;
  display: flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
}
.cap__link::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 20px;
}
/* The ring is the row, not the title the anchor wraps. 20px matches the radius
   the row itself takes under :focus-within, so the outline and the row's own
   corners agree. Turquoise because base.css switches the focus colour over
   `.capabilities`, where it measures 4.34:1 against the gradient's lightest
   stop, the worst ground in the set. */
.cap__link:focus-visible { outline: none; }
.cap__link:focus-visible::after {
  outline: 3px solid var(--brand-turq);
  outline-offset: -6px;
}

@media (max-width: 899px) {
  /* The mobile row is a 76px thumbnail over a 1px divider with square corners,
     so the ring is square there too. */
  .cap__link::after { border-radius: 0; }
}

.cap__all { padding-inline: var(--gutter); padding-top: var(--rhythm-row-end); }

/* The 60px title needs its own 620 to hold two lines. Below 1260 the row has
   less than that, so the type steps down rather than the row growing. Same
   band the header and the footer lanes use, so the site has one tablet
   breakpoint rather than three. */
@media (max-width: 1259px) {
  .cap__giant { font-size: 11vw; line-height: 0.88; }
  .cap__title { flex: 1 1 auto; font-size: 40px; line-height: 50px; }
  .cap__col { flex: 0 1 340px; }
}

@media (max-width: 899px) {
  /* The 375 artboard opens this band with an eyebrow and a sentence, and
     turns the four rows into thumbnail rows over a 1px divider. */
  .capabilities { margin-top: var(--rhythm-row-end); padding: var(--rhythm-row) var(--gutter) var(--rhythm-block); background: var(--brand-navy); }
  .capabilities__wave, .capabilities__orbit { display: none; }
  .capabilities__head { padding-inline: 0; }
  .cap__giant { display: none; }
  .cap__head-mob { display: flex; flex-direction: column; gap: 10px; }
  .cap__head-mob .t-section { font-size: 22px; line-height: 27px; letter-spacing: -0.02em; color: var(--on-navy); }

  .cap__rows { gap: 0; margin-top: 26px; }
  .cap__row {
    display: grid;
    grid-template-columns: 76px minmax(0, 1fr);
    grid-template-areas: 'photo n' 'photo title';
    align-content: start;
    column-gap: 16px;
    row-gap: 6px;
    width: 100%;
    min-height: 0;
    padding: 22px 0 0;
    margin-top: 20px;
    border-top: 1px solid #BFDCEA4D;
    border-radius: 0;
  }
  .cap__photo, .cap__photo--split {
    position: relative;
    inset: auto;
    grid-area: photo;
    width: 76px;
    height: 76px;
    opacity: 1;
    align-self: start;
  }
  .cap__n {
    grid-area: n;
    width: auto;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 18px;
    color: var(--on-navy-mute);
  }
  .cap__title {
    grid-area: title;
    width: auto;
    font-size: 22px;
    font-weight: var(--wt-medium);
    line-height: 27px;
    letter-spacing: -0.02em;
  }
  .cap__col { display: none; }
  .cap__all { padding: 0; margin-top: 30px; }
  .cap__all .btn { width: 100%; height: 56px; }

  /* THE DESKTOP HOVER DOES NOT COME DOWN HERE.
     `Capabilities on hover` is drawn against the 1440 row, and this is not that
     row: it is a 76px thumbnail over a 1px divider with no body column at all.
     Applied unchanged, the ring would draw 3px of white around a divider and
     the thumbnail would drop from full strength to 0.7, which is the state
     running backwards. A tap also leaves :hover stuck on a touch screen, so it
     would drop and stay dropped. Keyed to width rather than to (hover: hover)
     because it is the composition that differs, not the input device. */
  .cap__row:hover,
  .cap__row:focus-within { border-radius: 0; }
  .cap__row::after { content: none; }
  .cap__row:hover .cap__photo,
  .cap__row:hover .cap__photo--split,
  .cap__row:focus-within .cap__photo,
  .cap__row:focus-within .cap__photo--split { opacity: 1; }
}

/* ===== THE BAND BETWEEN THE TWO DESIGN WIDTHS ============================ */

/* Paper draws Home at 375 and at 1440 and says nothing about 768. The mobile
   composition puts the intro card hard left and the partner column hard right
   with the photograph showing through an elastic middle, which is the right
   answer at 375 where the two are 12px apart. Stretched to 899 the same grid
   pulls them to opposite edges and the middle stops reading as an open middle
   and starts reading as a hole.

   Capping the stage holds the 375 composition together as the viewport grows
   instead of letting it come apart. Scoped from 600 so the 375 artboard is
   provably untouched: this block cannot apply at either design width.

   THE CAP IS A RAMP NOW, NOT A NUMBER, and that is the third face of the one
   defect this pass was sent to fix. A flat 560 held the composition together
   and then held it exactly there for the next 299px: from 600 to 899 the
   window grew by half again and not one element inside the hero moved. At 880
   the result is a 560px composition adrift in an 880px frame with the wave
   running out from under it on both sides, which is the same reading the
   client gave of the two panels at 1200, arrived at from the other direction.

   The ramp runs from 560 at 600, where the composition is still effectively
   full width, to 760 at 899. It is written as the line through those two
   points and clamped at both ends, so nothing here can apply outside the band.

   760 at 899 is chosen against the width on the other side of the breakpoint,
   not picked for looks: at 900 the desktop stage is min(1312, 100vw - 128),
   which is 772. So the composition now arrives at the 900 boundary 12px short
   of what the next band gives it, and the breakpoint stops being a step. */
@media (min-width: 600px) and (max-width: 899px) {
  .hero__stage {
    max-width: clamp(560px, calc(158.6px + 66.89vw), 760px);
    margin-inline: auto;
  }
}

/* ===== HEAD SESSION 024, LIVE WITH THE CLIENT ============================
   Four changes made in chat, all client-directed. Where one of them overrides
   a decision the artboards or an earlier lane made, that is said out loud
   below rather than quietly reversed. ==================================== */

/* ---- 1. THE STAT ROTATOR ----------------------------------------------
   The figure now cycles six published results instead of stating one forever.
   Every value comes from case-studies.html, which is the only place on this
   site where a number carries its study and its laboratory. */
.stat__label {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: var(--wt-semi);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--brand-blue-cta, #00A0E6);
}
/* Opacity only, no translate. This block shares a baseline with the display
   heading beside it, and anything that moves vertically reads as the section
   twitching every five seconds. */
.stat__row,
.stat__note { transition: opacity 200ms var(--ease-out); }
.stat__row.is-swapping,
.stat__note.is-swapping { opacity: 0; }
.stat__unit[hidden] { display: none; }

.stat__dots { display: flex; gap: 8px; padding-top: 4px; }
.stat__dot {
  width: 28px;
  height: 20px;
  padding: 8px 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
/* The 28 x 20 button is the tap target; the 3px bar inside it is the mark.
   Sizing the visible dot to the tap floor would have put six fat lozenges
   under a display figure. */
.stat__dot::before {
  content: '';
  display: block;
  height: 3px;
  border-radius: 2px;
  background: #00477B33;
  transition: background var(--dur-hover) var(--ease-out);
}
.stat__dot:hover::before { background: #00477B66; }
.stat__dot[aria-current="true"]::before { background: var(--brand-turq); }

/* ---- 2. THE GAP FILL ---------------------------------------------------
   The right column ended at the note and left roughly 400px of white above the
   explainer. This is the only place on the homepage that names who did the
   measuring. */
.stat__proof {
  margin-top: 28px;
  padding-top: 24px;
  border-top: 1px solid #00477B1F;
  max-width: 560px;
}
.stat__proof-head {
  font-size: 13px;
  font-weight: var(--wt-semi);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.stat__proof-list {
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.stat__proof-list li {
  font-size: 16px;
  line-height: 26px;
  color: var(--ink);
}
.stat__proof-list li::before {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 10px;
  border-radius: 50%;
  background: var(--brand-turq);
  vertical-align: middle;
}
.stat__proof-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  margin-top: 6px;
  font-size: 15px;
  font-weight: var(--wt-medium);
  color: var(--brand-navy);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.stat__proof-link svg { transition: transform var(--dur-hover) var(--ease-out); }
.stat__proof-link:hover { color: var(--brand-blue-cta, #00A0E6); }
.stat__proof-link:hover svg { transform: translateX(3px); }

/* ---- 3. THE DOCKED MINI PLAYER ----------------------------------------
   Only the <video> is moved. The <figure> keeps its place and its height, so
   nothing below the section shifts when the player docks. */
.explainer.is-docked .explainer__video {
  position: fixed;
  right: 24px;
  bottom: 24px;
  width: min(380px, 40vw);
  height: auto;
  z-index: 90;
  border-radius: 14px;
  box-shadow: 0 12px 40px #00477B40, 0 2px 8px #00477B26;
  background: var(--brand-navy);
}
.explainer__dock-close {
  display: none;
  position: fixed;
  z-index: 91;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 50%;
  color: var(--on-navy, #fff);
  background: #003B66E6;
  cursor: pointer;
}
.explainer.is-docked .explainer__dock-close {
  display: grid;
  right: 12px;
  bottom: calc(24px + min(380px, 40vw) * 0.5625 - 12px);
}
.explainer.is-docked .explainer__dock-close:hover { background: var(--brand-navy); }
@media (max-width: 599px) {
  .explainer.is-docked .explainer__video { width: min(200px, 52vw); right: 12px; bottom: 12px; }
  .explainer.is-docked .explainer__dock-close {
    right: 4px;
    bottom: calc(12px + min(200px, 52vw) * 0.5625 - 10px);
  }
}

/* ---- 4. THE INDUSTRY TILES POP, AND THE WHOLE CARD IS THE LINK --------
   This deliberately overrides the note above `.tile__photo`, which argued that
   the hover is an opacity and nothing moves, because the Paper hover artboard
   changes exactly one property. The client asked for the tile to pop out on
   hover. The opacity change is kept and the lift is added on top of it.

   The lift is 6px and 1.015, not the 1.04 that shipped before: at 648 x 536 a
   4 per cent scale moves the tile edge 13px and the photograph visibly
   resamples. 6px of travel and a shadow read as depth without either. */
.tile {
  transition: transform var(--dur-slow) var(--ease-out),
              box-shadow var(--dur-slow) var(--ease-out);
  will-change: transform;
}
.tile:hover,
.tile:focus-within {
  transform: translateY(-6px) scale(1.015);
  box-shadow: 0 18px 44px #00477B33, 0 4px 12px #00477B1F;
}
/* THE WHOLE CARD IS CLICKABLE, with one real link, not an overlay anchor and
   not a click handler on the article. `.tile__more` drops to position:static so
   its ::after resolves against `.tile`, which is the positioned ancestor, and
   covers it. Keyboard, middle click, copy link address and the accessible name
   all keep working, because there is still exactly one <a>. */
.tile { cursor: pointer; isolation: isolate; }

/* CORRECTED, HEAD SESSION 024, after measuring it rather than assuming it.
   A stretched `::after` resolves against the nearest POSITIONED ancestor, and
   `.tile__body` is `position: relative`. So the first version of this covered
   the text block at the top of the tile and nothing else: probing the card at
   12, 50 and 90 per cent of its height returned the link, then `.tile__scrim`,
   then `.tile__scrim`. The top third was clickable and the photograph, which
   is most of the card, was not.

   The fix is to take the positioning off `.tile__body` so `.tile` becomes the
   containing block. That alone would drop the text behind the photograph,
   because a static in-flow block paints below a positioned sibling, so the
   photo and scrim go to z-index -1 and `.tile` gets `isolation: isolate` to
   keep them inside it: -1 then means above the tile's own navy ground but
   below the type, which is exactly the order the design draws. */
.tile__photo,
.tile__scrim { z-index: -1; }
.tile__body { position: static; }
.tile__more { position: static; }
.tile__more::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 32px;
}
.tile__more:focus-visible::after {
  outline: 3px solid var(--brand-turq);
  outline-offset: -4px;
}

@media (prefers-reduced-motion: reduce) {
  .tile,
  .stat__row,
  .stat__note,
  .stat__proof-link svg { transition: none; }
  .tile:hover,
  .tile:focus-within { transform: none; }
}

/* ---- 5. THE ROTATOR STOPS THE PAGE MOVING, AND THE VIDEO COMES UP ------
   Client, on the rotator: "fix the framing when the stats rotate. As the
   allignent of text moves around due to text changing."

   He is right and it is two separate movements, not one. The label swaps
   between one and two words, the figures swap between one and five glyphs, and
   the note swaps between one and two lines. Each of those re-flowed everything
   under it every 5.2 seconds.

   The fix is to reserve the tallest state rather than to shorten the copy, so
   the block occupies one fixed box and only the ink inside it changes. The
   values below are the measured tallest of the six entries, not round numbers:
   one line of label, one line of figures, two lines of note. */
.stat__label { min-height: 24px; }
.stat__row   { min-height: 88px; align-content: center; }
.stat__note  { min-height: 48px; }

/* The figures are lining by default in this face, so 5,700 and 10 have
   different widths and the arrow moved with them. Tabular figures hold the
   glyph advance constant, which stops the arrow and the second number sliding
   left and right between entries. */
.stat .t-stat {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* The proof block now lives beside the video, so it loses the top margin it
   carried when it hung under the note. */
.evidence__bottom { align-items: stretch; }
.evidence__aside {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 32px;
  min-width: 0;
}
.evidence__aside .stat__proof {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  max-width: none;
}
.evidence__aside .evidence__cta { padding-top: 0; }

@media (max-width: 899px) {
  /* Stacked, the reserved boxes are the wrong shape: the note runs to three
     lines in the narrow column and the figures drop to a smaller ramp. */
  .stat__row  { min-height: 0; }
  .stat__note { min-height: 0; }
  .stat__label { min-height: 0; }
  .evidence__aside { gap: 24px; }
}

/* ---- 6. THE FOOTER ENQUIRY CARD ---------------------------------------
   Lives in css/chrome.css. Nothing here. */

/* Tabular figures fixed the glyph advance but not the glyph COUNT: "5,700" is
   five characters and "10" is two, so the arrow and the second figure still
   slid 200px between entries. Reserving the widest state on each side pins the
   arrow to one x for all six results.

   Left-aligned inside the reserved box, not centred or right-aligned, so the
   first digit stays on the same left edge as the label and the note above and
   below it. The widths are the measured widest entries, 5,700 and 23.3, plus a
   little slack for the comma, which is narrower than a digit even in tnum. */
.stat__row [data-stat-from],
.stat__row [data-stat-to] { display: inline-block; text-align: left; }
.stat__row [data-stat-from] { min-width: 4.9ch; }
.stat__row [data-stat-to]   { min-width: 3.9ch; }

@media (max-width: 899px) {
  .stat__row [data-stat-from],
  .stat__row [data-stat-to] { min-width: 0; }
}

/* THE ACTUAL CAUSE OF THE MOVING FRAME, and it was not the arrow.
   `.evidence__top` sized its second track as `auto`, which means the track is
   as wide as its content. Every rotation changed that content, so the whole
   stat block changed width, slid sideways by up to 64px, re-wrapped its note,
   and changed the height of the row, which moved the explainer plate below it
   by over 100px. Reserving the tallest state inside the block could never fix
   that, because the box the block sits in was itself elastic.

   The track is now held as the same fraction of the content column that the
   design draws, 560 of 1312, in the pattern this build already uses for the
   enquiry card and the hero band: a fraction so it stays fluid, with a floor
   so it cannot collapse in the narrow stretch above the 899 breakpoint. */
.evidence__top {
  grid-template-columns: minmax(0, 1fr) max(320px, calc(var(--content) * 0.42683));
}
@media (max-width: 899px) {
  .evidence__top { grid-template-columns: 1fr; }
}

/* ---- 7. THE LABORATORY CARDS LINK THROUGH ------------------------------
   Client: the evidence block "currently just brings us to the partners page.
   But we need a whole page on the studies/ laboritoes themselves."

   Each card now carries one real link to its own laboratory page, and that
   link stretches over the card, same pattern as the industry tiles and the
   footer enquiry card. `.lab` was already position:relative with an isolate,
   so it is already the containing block and the tint layer already sits at
   z-index 0 behind the text; the hit area goes above both. */
.lab { cursor: pointer; }
.lab__link {
  position: static;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: auto;
  font-size: 15px;
  font-weight: var(--wt-medium);
  line-height: 22.5px;
  color: var(--brand-navy);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color var(--dur-hover) var(--ease-out);
}
.lab:hover .lab__link { color: var(--brand-blue-cta); }
.lab__link::after { content: ''; position: absolute; inset: -10px; z-index: 3; }
.lab__link:focus-visible::after {
  outline: 3px solid var(--brand-turq);
  outline-offset: -3px;
  border-radius: 20px;
}

/* ---- 8. THE HERO CARD REDIRECTS, AND THE WHOLE CARD IS THE TARGET ------
   Client: "make this panel actually mean something and re-direct somewhere."
   The arrow pointed at `#evidence`, a scroll to a section two screens down on
   the same page, which is not a redirect and was the least useful thing the
   only control in the hero could have done. It goes to how-it-works.html.

   The containing-block trap again, and this time it was already set: line 231
   gives every direct child of the card `position: relative`, so a stretched
   `::after` on `.btn-round` would have covered the 44px button and nothing
   else. Same fix as the tiles, and for the same reason. */
.hero__card { isolation: isolate; }
.hero__card-ring { z-index: -1; }

/* DESKTOP ONLY, and that scoping is a correction, not caution. Unscoped, the
   `position: static` below beat the mobile card's own
   `.hero__card .btn-round { position: absolute }`: same specificity, later in
   the file. The button fell out of its corner into the card's flex flow, landed
   at 14,94 instead of bottom right, and pushed 4px past a card whose height the
   artboard fixes at 134. Reported by the client as a broken mobile hero.

   Below 900 the arrow keeps its absolute corner and is the tap target on its
   own, at the full 44px floor. The whole-card hit area is not carried down
   there: a stretched ::after resolves against its own element once that element
   is positioned, so on an absolutely positioned button it would cover the
   button and nothing more, and the honest options are a 44px target that works
   or a full-card one that silently does not. */
@media (min-width: 900px) {
  .hero__card { cursor: pointer; }
  .hero__card > .btn-round { position: static; }
  .hero__card > .btn-round::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 22px;
  }
  .hero__card > .btn-round:focus-visible::after {
    outline: 3px solid var(--brand-turq);
    outline-offset: 3px;
  }
}

/* THE MOBILE THUMBNAIL WAS NEVER ABSOLUTE, and this one predates tonight.
   The mobile rule is written `.hero__card-thumb`, one class, and it is trying
   to override `.hero__card > *:not(.hero__card-ring)` further up this file,
   which is two. Specificity beats source order, so the thumbnail stayed
   `position: relative` and its measured `left: 6px; top: 44px` were simply
   discarded: it sat in flow at the card's padding edge instead, 14px lower
   than drawn, and spilled 4 to 6px out of the card's floor at every width from
   375 up. Same selector, one class heavier, so it now wins on its own terms
   rather than by being moved. */
@media (max-width: 899px) {
  .hero__card > .hero__card-thumb { position: absolute; }
}

/* ---- 9. TWO DEFECTS THE TOOLING SWEEP FOUND IN TONIGHT'S OWN WORK ------
   Both were introduced by the head session earlier this session, and both were
   caught by the verification run rather than by me.

   ONE, the rotator dots failed the 44px tap floor. The button is 28x20 and a
   CDP elementFromPoint probe put the true hit area at 28x21, so the visible
   bar was also the whole target. The bar stays 28 wide, because six 44px
   lozenges under a display figure is not the design; the target grows around
   it instead, centred, using a pseudo-element so nothing in the layout moves.
   ::before is already the bar, so this is ::after. */
.stat__dot::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  translate: -50% -50%;
}
.stat__dot { position: relative; }

/* TWO, the docked player sat at z-index 90 and 91, which is `--z-curtain`, the
   loading curtain's own level, and well over the 55 the audit treats as the
   ceiling for anything that is not the header. A floating video is chrome, so
   it belongs on the chrome scale, and at --z-drawer it sits below the sticky
   header rather than over it. The close button takes the same value and wins
   the tie on source order, being appended after the video. */
.explainer.is-docked .explainer__video { z-index: var(--z-drawer); }
.explainer__dock-close { z-index: var(--z-drawer); }

/* The 44px boxes above are adjacent, so the pitch is what caps the width: at
   an 8px gap the probe measured 36x44, because 28 + 8 is 36 and the next dot's
   box starts there. 16 makes the pitch exactly 44, so each target is a full
   44x44 with no dead space between them. The visible bars stay 28 wide and 8
   apart to the eye; only the space between them grew. */
.stat__dots { gap: 16px; }

/* ===== 9. THE RESEARCH VALIDATION PANEL ==================================
   index.html only, directly under the research section's own head and above
   the `.uni` band. The comment on the markup says why it sits there and why
   it is navy; this is the mechanism.

   THE WHOLE PANEL IS THE CLICK TARGET, same stretched-link pattern as
   `.tile__more`, `.lab__link` and `.enquiry-card__btn`: one real <a>, no
   overlay anchor and no click handler on the div, so keyboard, middle click
   and copy link address all keep working.

   THE CONTAINING BLOCK IS THE THING TO WATCH, and it is what the tile and the
   capability row both got wrong first. A stretched ::after resolves against
   the nearest POSITIONED ancestor, so every element between `.rv-panel__link`
   and `.rv-panel` has to stay static or the hit area collapses onto whichever
   one is not. `.rv-panel__copy` and `.rv-panel__marks` are therefore declared
   `position: static` explicitly rather than left to the default, so that a
   later edit adding `position: relative` to either one has to delete a line
   that says why it must not. Verified with elementFromPoint at nine points
   across the panel, corners included; every one resolves to the link. */
.rv-panel {
  position: relative;
  isolation: isolate;
  cursor: pointer;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 56px;
  align-items: center;
  padding: var(--rhythm-row) 48px;
  background: var(--brand-navy);
  border-radius: 20px;
  transition: transform var(--dur-hover) var(--ease-out),
              box-shadow var(--dur-hover) var(--ease-out);
}
.rv-panel:hover,
.rv-panel:focus-within {
  transform: translateY(-4px);
  box-shadow: 0 16px 40px #00477B33, 0 4px 12px #00477B1F;
}

.rv-panel__copy { position: static; display: flex; flex-direction: column; gap: 16px; }
.rv-panel__marks { position: static; display: flex; flex-direction: column; gap: 16px; }

.rv-panel__title {
  font-family: var(--font-display);
  font-size: 34px;
  line-height: 42px;
  letter-spacing: -0.02em;
  font-weight: var(--wt-medium);
  color: var(--on-navy);
}
/* --on-navy-mute, 6.69:1, not --on-navy: this is a 47 word paragraph and the
   headline above it needs to stay the brighter of the two. */
.rv-panel__body {
  max-width: 700px;
  font-size: 18px;
  line-height: 30px;
  color: var(--on-navy-mute);
}

.rv-panel__link {
  position: static;
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  min-height: 44px;
  margin-top: 4px;
  font-family: var(--font-display);
  font-size: 17px;
  line-height: 22px;
  font-weight: var(--wt-semi);
  color: var(--brand-turq);            /* 5.89:1 on navy */
  text-decoration: underline;
  text-underline-offset: 4px;
  transition: color var(--dur-hover) var(--ease-out);
}
.rv-panel:hover .rv-panel__link { color: var(--on-navy); }
/* z-index 2, not auto. The mark chips are LATER in source order than the link,
   so at auto they would have painted over the hit area and taken every click
   that landed on a logo. A positioned box at 2 beats a static sibling whatever
   the order. */
.rv-panel__link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: 20px;
}
.rv-panel__link:focus-visible::after {
  outline: 3px solid var(--brand-turq);
  outline-offset: -4px;
}

/* The marks sit on white plates, the same treatment `.hero__chip` gives the
   commercial partner marks. On navy directly, the Monash crest measures
   1.71:1 and half the mark vanishes; on white it is the artwork its own brand
   book specifies. Both plates are one height so neither institution reads as
   the larger, and the marks are capped on BOTH axes so the two different
   aspect ratios, 4.19 and 3.42, resolve to the same optical weight. */
.rv-panel__chip {
  display: grid;
  place-items: center;
  height: 78px;
  padding-inline: 20px;
  background: var(--page);
  border-radius: 10px;
}
.rv-panel__chip img { max-width: 180px; max-height: 44px; width: auto; height: auto; object-fit: contain; }

@media (max-width: 899px) {
  .rv-panel {
    grid-template-columns: 1fr;
    gap: 24px;
    padding: 24px var(--gutter);
    border-radius: 0;
    margin-inline: calc(var(--gutter) * -1);
  }
  /* No lift on touch. Same reason `.lab::before` is dropped below this
     breakpoint: a hover state a tap leaves stuck on is worse than none. */
  .rv-panel:hover, .rv-panel:focus-within { transform: none; box-shadow: none; }
  /* THE HIT AREA HAS TO GIVE UP ITS RADIUS TOO, and this was a real defect
     rather than a precaution. The panel goes square at this breakpoint but the
     stretched ::after kept the 20px radius it carries above it, so its four
     corners were rounded away from a panel whose corners are not. Probing the
     mobile panel at 4px in from each corner returned `.rv-panel` rather than
     the link, four dead slivers on the one layout where the panel runs to both
     page edges and a thumb is most likely to land near them. Measured in a
     375px frame before and after: 31/35 became 35/35. */
  .rv-panel__link::after { border-radius: 0; }
  .rv-panel__title { font-size: 26px; line-height: 32px; }
  .rv-panel__body { font-size: 17px; line-height: 26px; }
  /* THE ROW HAS TO WRAP NOW, session 030. This was two chips and it is four,
     and four in a non-wrapping row measured 447px inside a 375 viewport. The
     page did not visibly break, because html and body both carry
     overflow-x: hidden, so the panel was quietly clipping its own logos: the
     La Trobe mark was squeezed to 24px wide before this was caught. An overflow
     that is hidden rather than absent is still an overflow.

     Two by two, each chip taking half the row less half the 12px gap. The image
     cap comes down from 180 to 120 so the widest mark, Adelaide, fits its half
     without pushing the basis back out. */
  .rv-panel__marks { flex-direction: row; flex-wrap: wrap; gap: 12px; }
  .rv-panel__chip { flex: 1 1 calc(50% - 6px); min-width: 0; padding-inline: 12px; height: 66px; }
  .rv-panel__chip img { max-width: 120px; max-height: 38px; }
  .rv-panel__chip { flex: 1; height: 60px; padding-inline: 12px; }
  .rv-panel__chip img { max-width: 130px; max-height: 32px; }
  /* THE MARKS WERE SPILLING THEIR CHIPS AT 390, measured 25 September 2026:
     three equal chips of 108 x 60 leave 84px of content width, and the fixed
     130px cap let Adelaide render 130 wide and Monash 109, so both were cut
     at the plate's edge. The cap is now the chip's own content box, and the
     marks take the same equal-ink-area sizing as the hero partner chips, with
     --ink growing with the viewport so the 690 row keeps marks of a size a
     200px chip can carry. */
  .rv-panel__marks { --ink: clamp(40px, 10vw, 54px); }
  .rv-panel__chip img {
    width: auto;
    height: calc(var(--ink) * var(--k, 0.5));
    max-width: 100%;
    max-height: none;
  }
}

/* The hero partner chips are anchors now rather than spans. The rule above
   already gives them the grid, the height, the radius and the hover lift, and
   all of that applies unchanged; these three lines are only what an <a> brings
   with it that a <span> did not. */
.hero__chip { text-decoration: none; color: inherit; }
.hero__chip:focus-visible {
  outline: 3px solid var(--brand-turq);
  outline-offset: 3px;
}

/* OPTICAL SIZING FOR THE ADELAIDE MARK, which the shared cap gets wrong.
   `.hero__chip img` caps at 150 wide and 56 tall. The Adelaide lockup is
   800 x 191, an aspect of 4.19, so it meets the width cap first and lands at
   150 x 36: correct by the rule and visibly lighter than the squarer crests
   beside it. A wordmark that wide needs more width to carry the same weight,
   so it gets 184, which puts it at 184 x 44 and still 12px inside the height
   cap.
   Scoped to this one file by its own selector rather than raising the shared
   cap, which would let every future mark grow. */
.hero__chip img[src$="inst-adelaide-university.svg"] { max-width: 184px; }

/* Below the 375 artboard the thumbnail's own measurements stop fitting: it is
   drawn 152 wide at a 6px inset, which needs 158, and at 320 the card is 148.
   It spilled out of the right edge. Capped to what the card can actually give
   it, with the aspect ratio holding the height, so 375 and up are untouched to
   the pixel and only the narrow tail changes. */
@media (max-width: 899px) {
  .hero__card > .hero__card-thumb {
    max-width: calc(100% - 12px);
    height: auto;
    aspect-ratio: 152 / 82;
  }
}

/* ---- THE PARTNER STRIP IS FOUR CHIPS NOW, session 030 -------------------
   The client replaced one institution and two suppliers with four
   universities. Stacked in the column they were drawn for, four 86px chips
   plus their gaps came to 408px against the three-chip 278, which drove the
   hero from its designed 900 to a measured 996. Nothing was cut off, but a
   hero that no longer fits a 900px viewport is a real cost for a chip.

   So the four sit two by two inside the same 280px column. Measured: 2 rows
   of 86 plus one 10px gap is 182 against the old 278, so the column is
   SHORTER than it was with three, and the hero goes back to being driven by
   the intro card rather than by this strip.

   The chips keep their height, their radius and their shadow. Only the
   arrangement changes, and each mark now has 135px of width rather than 280,
   which is still wider than any of the four is tall. */
.hero__partners-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.hero__partners-grid .hero__chip { width: 100%; }

/* EVERY CHIP IS ONE SIZE, client request of 25 September 2026: "make all 3
   tiles here the same size". The rule this replaces sent an odd last chip
   across the full row, so with three marks UNSW sat on a plate twice the width
   of the other two and, capped only at 150 x 56, printed at 132 x 56 against
   113 x 27 and 113 x 33 beside it. The plate and the mark both read as the
   bigger institution.

   THE ODD CHIP KEEPS ONE COLUMN'S WIDTH AND SITS CENTRED under the pair, so
   the empty cell a plain two-column grid leaves never appears and no plate is
   larger than another. It still spans both tracks only so that it can centre;
   its width is one track, the same (100% - gap) / 2 the other two get. Written
   as a count test, so a fourth mark returning fills the row with no CSS
   change. */
.hero__partners-grid .hero__chip:last-child:nth-child(odd) {
  grid-column: 1 / -1;
  justify-self: center;
  width: calc((100% - 10px) / 2);
}

/* ONE INK AREA PER MARK, not one width and not one height. The four marks run
   from 2.36 (UNSW) to 4.19 (Adelaide) wide per unit of height. Held to one
   width the crest prints as the biggest institution; held to one height the
   Adelaide lockup does. Holding the AREA equal is the balance between the two:
   the height is --ink divided by the square root of the mark's aspect, which is
   the --k each file carries below. At the desktop --ink of 54px that is 110 x 26
   Adelaide, 100 x 29 Monash and 83 x 35 UNSW inside a 135 x 86 plate. The
   max-width stays as a guard, and `contain` on the base rule shrinks a mark
   that meets it rather than squashing it. */
.hero__partners-grid { --ink: 54px; }
.hero__partners-grid .hero__chip img {
  width: auto;
  height: calc(var(--ink) * var(--k, 0.5));
  max-width: 84%;
}
.hero__chip img[src$="inst-adelaide-university.svg"],
.rv-panel__chip img[src$="inst-adelaide-university.svg"] { --k: 0.4886; } /* 800 / 191 */
.hero__chip img[src$="inst-monash-university.svg"],
.rv-panel__chip img[src$="inst-monash-university.svg"] { --k: 0.5403; }   /* 394 / 115 */
.hero__chip img[src$="inst-la-trobe-university.svg"],
.rv-panel__chip img[src$="inst-la-trobe-university.svg"] { --k: 0.5818; } /* 452 / 153 */
.hero__chip img[src$="inst-unsw-sydney.png"],
.rv-panel__chip img[src$="inst-unsw-sydney.png"] { --k: 0.6513; }         /* 330 / 140 */

@media (max-width: 899px) {
  /* BACK TO ONE COLUMN AT 375, and this is measured rather than assumed. The
     mobile hero stage is `card . partners` on a 177 / 14 / 120 grid, so the
     partner column is 120px wide, not the 280 it has on desktop. Two by two
     inside 120 gave 56px chips carrying 47px marks, which is smaller than the
     body text beside them and is not a logo any more.

     Stacked at the mobile chip height of 36px, four chips plus their gaps come
     to 168 against the three-chip 124, so the strip grows 44px inside a 662px
     hero. That is the right trade: a mark that can be read, 44px lower. */
  .hero__partners-grid { grid-template-columns: minmax(0, 1fr); gap: 8px; --ink: 40px; }
  .hero__partners-grid .hero__chip img { max-width: 78%; }
  /* One column, so the odd chip is simply the third row at full width like
     the other two. --ink 40 puts the tallest mark, UNSW, at 26px inside the
     36px mobile chip, the height the old cap gave it. */
  .hero__partners-grid .hero__chip:last-child:nth-child(odd) { width: 100%; justify-self: stretch; }
}
