/* =========================================================================
   whspr-v3 — style.css
   Tokens from design-spec.md sections 2 and 8a. Geometry from layout-map.md.
   Ground is painted once on <body>. No section sets a background colour.
   NOTE: --ink is the LIGHT value on this build. Type on --accent is --accent-ink.
   ========================================================================= */

@font-face{
  font-family:'Geist';
  src:url('assets/fonts/geist-var.woff2') format('woff2');
  font-weight:100 900;
  font-style:normal;
  font-display:swap;
}

/* The detail layer's own voice. Same superfamily as the sans, SIL OFL, no licence
   cost. It is confined to indices, timecodes and measurement strings, a number
   with its unit; every word on the page stays in the sans. */
@font-face{
  font-family:'Geist Mono';
  src:url('assets/fonts/geist-mono-var.woff2') format('woff2');
  font-weight:100 900;
  font-style:normal;
  font-display:swap;
}

:root{
  /* base */
  --paper:       #171B1C;
  --paper-dim:   #7C8688;
  --paper-faint: #3A4244;
  /* ink */
  --ink:         #E8E6DE;
  --ink-2:       #B9B7AE;
  /* accent */
  --accent:      #CCFF00;
  --accent-ink:  #171B1C;
  /* rules */
  --line:        rgba(232,230,222,.20);
  --line-alt:    rgba(232,230,222,.34);
  /* type */
  --font:      'Geist', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, 'SF Mono', monospace;

  /* rhythm */
  --gutter:  max(5vw, 24px);
  --pad-sec: clamp(80px, 9svh, 140px);

  /* type scale, measured against Geist 500 advance widths */
  --fs-display: clamp(4.8rem, 0.6rem + 9.75vw, 12rem);
  --fs-h2:      clamp(2.6rem, 1.1rem + 4vw, 5rem);
  --fs-h3:      clamp(2.2rem, 0.9rem + 3.4vw, 4rem);
  --fs-num:     clamp(2rem, 0.9rem + 3vw, 3.6rem);
  --fs-lead:    20px;
  --fs-body:    17px;
  --fs-ui:      15px;
  --fs-micro:   13px;

  /* easing, the closed set */
  --ease-out:    cubic-bezier(.16,1,.3,1);
  --ease-inout:  cubic-bezier(.76,0,.24,1);
  --ease-drawer: cubic-bezier(.32,.72,0,1);
  --ease-soft:   cubic-bezier(.22,.61,.36,1);

  /* durations. Two, and both are read. GSAP owns every long duration at its own
     call site on this build, so a CSS token for one would be a number nothing
     obeys. Two real tokens beat four aspirational ones. */
  --d-fast: .28s;
  --d-mid:  .5s;

  /* weather radial, driven by main.js */
  --wx: 30%;
  --wy: 20%;
}

/* ------------------------------------------------------------------ reset */
*,*::before,*::after{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; scroll-behavior:auto; }
body,h1,h2,h3,p,figure,ol,ul,li,form{ margin:0; padding:0; }
ol,ul{ list-style:none; }
img,svg,video,canvas{ display:block; max-width:100%; }
button,input{ font:inherit; color:inherit; }
a{ color:inherit; text-decoration:none; }

/* ------------------------------------------------- the ground, four layers */
body{
  background-color:var(--paper);          /* layer 1 */
  color:var(--ink);
  font-family:var(--font);
  font-size:var(--fs-body);
  font-weight:400;
  line-height:1.52;
  /* clip, NEVER hidden. `overflow-x:hidden` forces overflow-y to compute to
     auto, which makes <body> a scroll container, and every `position:sticky`
     descendant then sticks to a scrollport that does not scroll, so it does not
     stick at all. That silently killed the drain, geluid and regen devices and
     left their sections reading as empty space. `clip` bounds the same overflow
     without establishing a scroll container. */
  overflow-x:clip;
  -webkit-font-smoothing:antialiased;
}

body::before{                              /* layer 2, the journey */
  content:'';
  position:fixed; inset:0; z-index:-2;
  background:linear-gradient(180deg,#1B2022 0%,#171B1C 38%,#121516 74%,#0D1011 100%);
  background-attachment:fixed;
  pointer-events:none;
}

body::after{                               /* layer 3, the weather */
  content:'';
  position:fixed; inset:0; z-index:-1;
  background:radial-gradient(60vw 60vh at var(--wx) var(--wy),
             rgba(140,160,165,.055), transparent 70%);
  pointer-events:none;
}

/* --------------------------------------------------------------- layer 4
   Film grain, and it has to be film grain rather than a texture: a still noise
   tile reads as a dirty screen, a moving one reads as emulsion. Two things do
   that here. The tile itself is stepped through eight positions at 12fps by a
   CSS animation, which is the frame rate hand-drawn animation uses for exactly
   this reason and is slow enough to read as grain rather than static. And the
   grain sits ABOVE the flat ground and BELOW anything on ground F, so a section
   that is carrying footage keeps its own texture and a section that is paper
   gets the emulsion instead.

   .03 was invisible. It is .055 over the paper and lifted per ground below,
   which is still under the point where it starts eating type. */
.grain{
  position:fixed; inset:0; z-index:1;
  pointer-events:none;
  opacity:.055;
  mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='260' height='260' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:260px 260px;
  animation:grain-shift 1.1s steps(1) infinite;
  will-change:background-position;
}
@keyframes grain-shift{
  0%   { background-position:  0px   0px; }
  12.5%{ background-position:-38px  22px; }
  25%  { background-position: 26px -44px; }
  37.5%{ background-position:-52px -18px; }
  50%  { background-position: 44px  36px; }
  62.5%{ background-position:-14px  58px; }
  75%  { background-position: 60px -30px; }
  87.5%{ background-position:-46px  -6px; }
}
@media (prefers-reduced-motion:reduce){
  .grain{ animation:none; }
}

/* Per-section emulsion. The root layer carries the page; each section adds its
   own on top of its own background, so the grain is IN the section rather than
   only over it, and a section on paper is grainier than one carrying footage
   which already has grain of its own.

   SCOPED TO `[data-ground]`, and that attribute is doing real work here rather
   than documenting. It used to read `section::before`, which caught every
   `<section>` in the document, including the four that are semantic
   subdivisions inside another section and not grounds of their own: `.spec`
   inside the specification block, and `.ft__plate`, `.ft__certs` and `.ft__ask`
   in the footer. Each of those was laying a second emulsion over the one its
   parent already carries, and `mix-blend-mode:overlay` on a doubled layer
   lightens rather than textures, so they read as rectangular patches of a
   different colour sitting behind the footer's text blocks. Reported on client
   review 2026-07-29. A ground is a thing a section stands on, and only the
   twelve sections that declare one own an emulsion. */
section[data-ground]::before{
  /* 15, between the footage at 10 and the type at 20. Below 10 the emulsion
     would sit under the very frames it is supposed to be part of, which is
     precisely the mistake the note at `main{}` further down records. */
  content:''; position:absolute; inset:0; z-index:15;
  pointer-events:none;
  opacity:.05;
  mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23m)'/%3E%3C/svg%3E");
  background-size:180px 180px;
  /* Deliberately still. It scrolls with its section, which is motion enough,
     and animating a layer this tall in eleven places at once buys a repaint
     budget for texture the moving root layer already supplies. */
}
section[data-ground="F"]::before{ opacity:.03; }
section[data-ground="P"]::before{ opacity:.07; }

/* ------------------------------------------------------------------- intro
   A held frame before the page starts, not a spinner, and the loading state is
   the site's own subject rather than a bar borrowed from an installer.

   The readout counts DOWN from 60,0 dB(A), the noise of an ordinary heat pump,
   to 50,8, the number KIWA measured. That descent IS the progress: the fonts
   and the hero's first frame resolving are what drive it. By the time the page
   is ready to be looked at, the reader has already been told the one thing the
   site is about, and has watched it happen instead of reading it.

   The hand-over is not a wipe. The plate is two halves and they part along the
   horizon, so the hero's footage arrives through a widening slit and the
   headline rises out of the seam. A plate that lifts as one block says the
   loader is finished; a plate that opens says the film is starting.

   It is removed from the DOM as soon as it has played, and under reduced motion
   it is never shown at all, so there is no state in which it can sit in front
   of the page.
   -------------------------------------------------------------------------- */
.intro{
  position:fixed; inset:0; z-index:90;
  overflow:hidden;
  pointer-events:none;
}
/* The two halves are the plate. They carry the ground, so the inner block can
   sit above both and stay whole while they part underneath it. */
.intro__half{
  position:absolute; left:0; right:0; height:50.2%;
  background:var(--paper);
  z-index:0;
}
.intro__half--t{ top:0; }
.intro__half--b{ bottom:0; }
.intro__inner{
  position:absolute; inset:0; z-index:1;
  display:grid; place-content:center; justify-items:center;
  gap:22px;
}
.intro__mark{
  font-size:clamp(2.6rem, 1.2rem + 4vw, 5rem);
  font-weight:500; letter-spacing:.5em; margin-right:-.5em;
  overflow:hidden;
}
.intro__mark-i{ display:block; }
/* The readout is display-sized on purpose: it is the headline of the loader. */
.intro__read{
  display:flex; align-items:baseline; gap:.4em;
  font-variant-numeric:tabular-nums;
}
.intro__n{
  font-size:clamp(3.4rem, 1.4rem + 6vw, 7.5rem);
  font-weight:500; line-height:.9; letter-spacing:-.03em;
  color:var(--accent);
}
.intro__u{
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.22em;
  color:var(--ink-2);
}
.intro__meta{
  font-size:var(--fs-micro); font-weight:500; letter-spacing:.2em;
  color:var(--ink-2);
}
/* The axis, not a bar. It is the same graduated rail #geluid plots its five
   measurements against, carrying the two numbers the whole site is about at its
   two ends: 60,0 where an ordinary heat pump sits, 50,8 where this one does. The
   fill travels between them and a square rides its leading edge, so what the
   reader watches during the load is the measurement being taken, and the loader
   is a preview of the instrument rather than an installer widget with the site's
   colours on it. */
.intro__axis{
  display:flex; align-items:center; gap:14px;
  width:min(38vw, 320px);
}
.intro__axis-l{
  font-family:var(--font-mono); font-size:10px; font-weight:500;
  letter-spacing:.14em; color:var(--ink-2);
  font-variant-numeric:tabular-nums;
}
.intro__axis-l--b{ color:var(--accent); }
.intro__axis-track{
  position:relative; flex:1; height:1px;
  background:var(--line);
}
/* the graduations: one hairline at each endpoint, the far one in accent because
   that is the value the count is travelling to */
.intro__axis-track::before,
.intro__axis-track::after{
  content:''; position:absolute; top:-4px;
  width:1px; height:9px; background:var(--line-alt);
}
.intro__axis-track::before{ left:0; }
.intro__axis-track::after{ right:0; background:var(--accent); }
.intro__fill{
  position:absolute; inset:0;
  background:var(--accent);
  transform:scaleX(0); transform-origin:0 50%;
}
.intro__axis-sq{
  position:absolute; top:50%; left:calc(var(--p, 0) * 100%);
  width:7px; height:7px;
  background:var(--accent);
  transform:translate(-50%, -50%);
}
.intro.is-done{ pointer-events:none; }
/* Set by main.js for the length of the plate and taken off at the hand-over.
   On the root element only: `overflow:hidden` on <body> is what made every
   sticky pin on this page stick to a scrollport that does not scroll, and that
   bug is not worth reintroducing for two seconds of loader. */
.is-intro{ overflow:hidden; overscroll-behavior:none; }
@media (prefers-reduced-motion:reduce){
  .intro{ display:none; }
}

/* --------------------------------------------------------------- utilities */
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
}
.skip{
  position:absolute; left:-9999px; top:0; z-index:100;
  background:var(--accent); color:var(--accent-ink);
  padding:12px 20px; font-size:var(--fs-ui); font-weight:600;
}
.skip:focus{ left:0; }
/* The two inside the long pins. Same link, moved to where the reader's eye
   already is: the bottom left of the frame they are being offered a way out of.
   Above the film and the type, and only ever on screen while it has focus. */
.skip--pin{ z-index:30; }
.skip--pin:focus{ left:var(--gutter); top:auto; bottom:24px; }

:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }

/* No z-index here. `z-index:0` on main and on every section made each one a
   stacking context, which trapped their z-index:10 footage and z-index:20 type
   underneath the root-level .grain at z-index:1. The grain then painted OVER
   the hero clip, all three drains and the takeover, which is the exact inverse
   of the spec: grain above the flat ground, below anything on ground F. */
main{ position:relative; }

section,footer{ position:relative; }


/* the wordmark */
.mark{
  font-size:26px; font-weight:500; letter-spacing:.62em;
  margin-right:-.62em;
}

/* masked line reveal: overflow wrapper + inner span */
/* The reveal mask clips at the padding box, not the line box. Geist's content
   area is 1.300em against an ascent of 1.005em, and `g` drops to -0.162em, so a
   mask cut at a line-height of .92 slices the tail off every g, j, p and y. At
   1440 that was 8.6px off the h1 and 14.2px off the footer wordmark, which is
   half its descender on the largest object on the page. The negative margin
   gives the space straight back, so no baseline moves and the yPercent:110
   from-state still parks the line out of sight. */
.ln{ display:block; overflow:hidden; padding-bottom:.16em; margin-bottom:-.16em; }
.ln > span{ display:block; will-change:transform; }

/* the accent square, the one shape the accent is allowed to be */
.sq{
  display:inline-block; width:.62em; height:.62em;
  background:var(--accent);
  vertical-align:baseline;
  margin-right:.34em;
}

/* the button, and it is an instrument
   ---------------------------------------------------------------------------
   Four review rounds rejected this control, and every one of those rounds was
   answered with a different easing on the same idea: a fill crossing a pill
   while the label swaps colour. That is the most common button on the award
   sites this page is going to sit next to, and it cannot be tuned into being
   its own. So the shape stops being a pill with a hover and becomes the thing
   the rest of the page already is.

   The page's detail language is axes: the loader plots 60,0 against 50,8 on a
   graduated track, #geluid plots five measurements against a second one, and
   #besparing collapses a hundred ticks. The button is the only object on the
   page a reader can touch that was not written in that language. It gets a
   graduation rail along its bottom edge, on its own dark plinth so the rail
   reads the same over lime, over bone and over the dark, and the hover is a
   level rising along it to a value rather than a rectangle being coloured in.

   The rail resolves as it fills. At rest it carries the axis and a major tick
   every fifth graduation, which is a quiet four or five marks on a 170px
   button. The level that rises under the pointer carries the FINE graduation
   as well, in accent, so the instrument gains resolution exactly where it has
   taken a reading. That is the difference between an axis and a progress bar.
   ------------------------------------------------------------------------- */
.btn{
  /* the rail's geometry, overridable per button. --rail-g is the fine pitch and
     the major pitch is five of them, the way a ruler is built. */
  --rail-h:7px;                 /* band height, and the major tick height */
  --rail-t:4px;                 /* the fine tick height */
  --rail-g:9px;                 /* the fine graduation pitch */
  --rail-c:rgb(232 230 222 / .54);   /* the resting axis, 4.93:1 on the plinth */
  --rail-lc:var(--accent);           /* the level, 14.77:1 on the plinth */

  display:inline-block;
  font-size:var(--fs-ui); font-weight:600; letter-spacing:.01em;
  padding:13px 26px;
  background:var(--accent); color:var(--accent-ink);
  border:0; border-radius:0;
  cursor:pointer;
  position:relative; overflow:hidden; isolation:isolate;
  /* --fill is the whole hover gesture: 0 at rest, 1 under the pointer, and the
     fill, the recoloured label and the level are all functions of it. It is the
     only thing on this control that transitions. */
  --fill:0;
  transition:transform 140ms var(--ease-out),
             --fill var(--d-mid) var(--ease-drawer);
}
/* G-c. The fill knows which way the hand came in. It used to rise from below
   whichever side you approached from, which is a control that has one answer
   for every question; it now opens from the edge the pointer actually crossed,
   on the same `--eo` fromEdge() writes for #bewijs's facts. Approach from the
   right and the ink arrives from the right. A translate or a scale, never a
   colour transition: a background swap has no travel and reads as a state
   change rather than a response. `--ink` is the LIGHT token here.

   The slant does not resolve. At rest-hover the wedge leaves a sliver of accent
   across the top left, which is the same cut the CTA blocks carry, so the
   button ends the gesture as a shape rather than as a filled rectangle. Under
   scaleX the wedge opens sideways and the slope arrives with it, so the shape
   is drawn by the gesture instead of being carried in already finished.

   --ease-drawer, not --ease-inout. This is the "unsmooth" the client named four
   rounds running, and it was never the travel: cubic-bezier(.76,0,.24,1) holds
   still, lurches through the middle and stops dead, which on a 0.5s hover is
   read as a stutter rather than as an ease. The drawer curve leaves under the
   hand immediately and settles, which is what a control answering a pointer
   should do. Same four named curves, no new token.

   ONE animated scalar, --fill, and everything reads it. This is not a
   refinement, it is the only construction that works, and the reason is worth
   the paragraph: `--eo` is written by JS on pointerenter, which is the same
   moment `:hover` starts. A transition captures its start value when the hover
   rule applies, so a hover-swapped clip-path had already committed to the OLD
   edge before the pointer event handler ran. Measured: the fill correctly
   opened from the right, because transform-origin is not transitioned and
   re-anchors a running animation, while the recoloured label kept sweeping in
   from the left. Two halves of one gesture disagreeing about which way the
   hand came in is worse than not being directional at all.

   With --fill as the only thing that transitions, --eo is read fresh on every
   frame by all three layers, exactly the way transform-origin already was. The
   edge can be rewritten mid-gesture and the whole control simply re-anchors.
   The pair also makes the leave direction free: fromEdge() rewrites --eo on
   pointerleave and the fill collapses out through the side the hand left by. */
@property --fill{ syntax:'<number>'; initial-value:0; inherits:true; }
/* The fill is the DARK, on every button on the page, and this is the answer to
   the complaint that has run longest in this build: four rounds of "the button
   animations are mid", against four rounds of easing and travel tuning that
   could never have fixed it.

   It was never the motion. Measured on the built page: the fill was `--ink`,
   #E8E6DE, sweeping across a pill whose resting colour is `--accent`, #CCFF00.
   Relative luminance 0.78 against 0.83. **1.06:1.** The single most prominent
   gesture on the page was a near-invisible tonal move, executing perfectly, on
   the correct curve, over the correct distance, and arriving as almost no
   change at all. The one button the client picked out unprompted as the best on
   the page, best animation and best colour, was `#cta-2`'s, and the only thing
   different about it was a local override sweeping `--paper` instead: 14.5:1
   against the same pill.

   An earlier version of this comment concluded from that coincidence that he
   was not describing a preference, he was reading a contrast ratio. He was
   asked, and he was not: "the color was not really the issue, it was the kind
   of animation ... also the higher contrast made a better impact." Two separate
   things, and the measurement only ever explained the second one. The note
   stays as written because inventing a motive for a person and filing it as a
   finding is the more expensive mistake of the two. See _lib/EXEMPLARS.md.

   That override is now the law and the local rule is gone. Four buttons stand
   on dark ground and one on bone, so the old pair of rules looked like a
   ground-aware law and behaved like one; it was aimed at the wrong surface. The
   fill's job is to be seen crossing THE BUTTON, whose colour is the same lime
   everywhere, not to separate from the section behind it. The button already
   separates from the section: it is lime.

   What is left on dark ground when the fill has fully crossed was measured in
   composited pixels before this was accepted, not reasoned about. The hero
   button, 170x49, samples 78.9% accent at rest and 11.1% at full hover, and
   that remainder is in the two places it has to be: the slant's wedge across
   the top band, x 0 to 152, and the rail's lit level along the bottom band, x 0
   to 169, the full width. The label is `--ink` at 13.89:1 on the fill. So the
   button empties out to reveal its own instrument, held by lime on two edges,
   and does not dissolve into the section behind it. */
.btn::after{
  content:''; position:absolute; inset:0; z-index:0;
  background:var(--paper);
  clip-path:polygon(0 18%, 100% 0, 100% 100%, 0 100%);
  transform:scaleX(var(--fill));
  transform-origin:var(--eo, 0%) 50%;
}
/* Two whole labels, stacked in register, one in ink and one in paper. The paper
   copy is clipped to the SAME slanted edge as the fill and on the same clock, so
   the letters change colour exactly where the dark crosses them: the word is
   recoloured by the sweep passing through it rather than swapped out for
   another word. Nothing moves letter by letter, which is the version of this
   that reads as a template.

   `inset:0` resolves against the button's padding box, so `padding:inherit`
   lands the overlay's text on the same baseline as the label underneath it, and
   the clip percentages are percentages of the button itself. Both copies are
   built by main.js and are aria-hidden; the accessible name moves onto the
   control. Without JS the button is a plain readable button. */
.btn__t{ display:block; }
/* The sizer is the only copy in flow, and it is the reason the label is allowed
   to change weight at all. Both visible copies are out of flow now, so the
   600 to 660 the word gains under the pointer cannot widen the control, cannot
   move the dock's centre and cannot relayout the form column on a hover. It
   holds the box the way .btn__t1 used to, at a weight that never moves. */
.btn__sz{ visibility:hidden; font-weight:600; }
/* Both copies now sit in the same absolute box, which is what makes the
   register exact rather than nearly exact: one padding box, one line box, two
   colours. */
.btn__t1,
.btn__t2{
  position:absolute; inset:0;
  padding:inherit; line-height:inherit; text-align:inherit;
  white-space:nowrap;
  font-weight:600;
  transform:translate3d(0,0,0);
  transition:transform var(--d-mid) var(--ease-drawer),
             font-weight var(--d-mid) var(--ease-drawer);
}
.btn__t1{ z-index:2; }
/* The paper copy is revealed by the SAME edge the fill opens from, and by the
   same value: `--eo` is 0% or 100%, so the rest clip is inset(0 100% 0 0) when
   the hand came from the left and inset(0 0 0 100%) when it came from the
   right. Both edges are vertical, which is exactly the fill's leading edge
   under scaleX, so the colour change happens on the ink's own boundary and
   nowhere else. One property drives both, so they cannot drift apart.

   The fill's slant is not mirrored here any more, and does not need to be: it
   reaches 18% of the control's height at the very left edge and tapers to
   nothing, while the label starts a full 26px of padding inside that. The
   slant has never crossed a letter. */
.btn__t2{
  z-index:3;
  /* The swept copy is the LIGHT one, because the fill under it is the dark.
     These two move together and always have; see the note on .btn::after.

     It is light only once there is dark underneath it to be light against. At
     --fill:0 this copy is clipped to zero width and nobody can see it, but its
     computed colour is still a fact about the page, and a bone label whose
     computed background is lime is 1.06:1. The probe read exactly that and
     failed 11 stops at all three widths the moment the fill went dark. The
     honest fix is not to teach the probe about clip-path, it is for the resting
     colour to be the one that is true at rest: `--accent-ink`, the same dark the
     unswept copy carries, 14.5:1 on the pill.

     REVERTED, and the reason is worth keeping so nobody tries it again. The
     resting colour was briefly driven off --fill through
     `color-mix(in oklab, var(--ink) calc(var(--fill) * 1000%), var(--accent-ink))`,
     which measures perfectly in Chrome: oklab L 0.218 at --fill 0 and 0.924 at
     --fill 1, a clean step by a tenth of the gesture. It also silently produces
     a DARK label on the dark fill anywhere that construction is not fully
     supported, because an invalid computed value falls back to the inherited
     colour, and the button's own colour is `--accent-ink`. Dark on dark on the
     most prominent control on the page. Reported from a real browser within
     minutes of it shipping.

     A registered custom property, animated, inherited across an element
     boundary, and consumed inside a color-mix percentage through a calc that
     multiplies a number by a percentage, is four things that each have to hold.
     The contrast line item it was buying is not worth one of them failing.

     So the label is plainly `--ink`, always, and the probe's complaint about it
     is a documented false positive rather than a defect: at --fill 0 this copy
     is clipped to `inset(0 100% 0 0)`, zero width, and no reader has ever seen
     bone on lime here. The probe reads computed colour against computed
     background and cannot see a clip-path. Verified by measurement, not
     asserted: `.btn__t2`'s painted width at rest is 0. */
  color:var(--ink); pointer-events:none;
  /* The reveal is a function of --fill and --eo, never a hover swap. At
     --eo:0% this is inset(0 100% 0 0) closing to inset(0), and at --eo:100% it
     is inset(0 0 0 100%) closing to inset(0): a vertical edge either way,
     which is exactly the fill's leading edge under scaleX, so the letters
     change colour on the ink's own boundary and nowhere else. */
  clip-path:inset(0 calc((1 - var(--fill)) * (100% - var(--eo, 0%)))
                  0 calc((1 - var(--fill)) * var(--eo, 0%)));
  transition:transform var(--d-mid) var(--ease-drawer),
             font-weight var(--d-mid) var(--ease-drawer);
}

/* The rail. Its own plinth in the page's dark, so the axis reads identically on
   the lime ground at rest and on the dark fill under the pointer, everywhere,
   with no per-section colour to keep in sync. That was already true when the
   fill was still bone on four of the five buttons, which is why making the fill
   uniformly dark cost the rail nothing. */
.btn__rail{
  position:absolute; left:0; right:0; bottom:0; z-index:4;
  height:var(--rail-h);
  pointer-events:none;
  background-color:var(--accent-ink);
  background-image:
    linear-gradient(var(--rail-c), var(--rail-c)),                                                 /* the end tick */
    repeating-linear-gradient(90deg, var(--rail-c) 0 1px, transparent 1px calc(var(--rail-g)*5)),  /* major */
    linear-gradient(var(--rail-c), var(--rail-c));                                                 /* the axis */
  background-repeat:no-repeat;
  background-position:100% 100%, 0 100%, 0 100%;
  background-size:1px var(--rail-h), 100% var(--rail-h), 100% 1px;
}
/* The level. Same four marks plus the fine graduation the resting rail does not
   carry, in accent, revealed left to right on the fill's own clock and the
   fill's own curve so the two are one gesture. clip-path rather than a width or
   a scaleX: a width would relayout and a scale would stretch the pitch, and a
   graduation whose spacing changes is not a graduation. */
.btn__lvl{
  position:absolute; inset:0;
  background-image:
    linear-gradient(var(--rail-lc), var(--rail-lc)),
    repeating-linear-gradient(90deg, var(--rail-lc) 0 1px, transparent 1px calc(var(--rail-g)*5)),
    repeating-linear-gradient(90deg, var(--rail-lc) 0 1px, transparent 1px var(--rail-g)),
    linear-gradient(var(--rail-lc), var(--rail-lc));
  background-repeat:no-repeat;
  background-position:100% 100%, 0 100%, 0 100%, 0 100%;
  background-size:1px var(--rail-h), 100% var(--rail-h), 100% var(--rail-t), 100% 1px;
  /* The level enters from the crossed edge too, off the same --fill and the
     same --eo, so the fill and the reading are one gesture arriving from one
     side rather than two things starting from opposite corners of the same
     control. */
  clip-path:inset(0 calc((1 - var(--fill)) * (100% - var(--eo, 0%)))
                  0 calc((1 - var(--fill)) * var(--eo, 0%)));
}
/* No JS, no rail element, so the rail is painted into the button's own ground
   instead. The resting state is identical, which is the whole point: a button
   that only looks designed once a script has run does not look designed. */
.btn:not(:has(.btn__rail)){
  background-image:
    linear-gradient(var(--rail-c), var(--rail-c)),
    repeating-linear-gradient(90deg, var(--rail-c) 0 1px, transparent 1px calc(var(--rail-g)*5)),
    linear-gradient(var(--rail-c), var(--rail-c)),
    linear-gradient(var(--accent-ink), var(--accent-ink));
  background-repeat:no-repeat;
  background-position:100% 100%, 0 100%, 0 100%, 0 100%;
  background-size:1px var(--rail-h), 100% var(--rail-h), 100% 1px, 100% var(--rail-h);
}
@media (hover:hover) and (pointer:fine){
  /* One declaration is the entire hover. The fill, the label recolour and the
     level are all functions of --fill, so there is nothing else to swap and
     nothing that can disagree about the direction. */
  .btn:hover{ --fill:1; }
  /* both copies lift together, or they come out of register and the colour edge
     stops reading as one surface passing behind the letters */
  .btn:hover .btn__t1,
  .btn:hover .btn__t2 { transform:translate3d(0,-2px,0); }
  /* D6. The word firms as the level passes under it, as one word on one clock,
     600 to 660 on the wght axis Geist already carries. Both copies or neither:
     a weight difference across the slanted colour edge would ghost the letters
     exactly where the edge is supposed to be invisible. */
  .btn:hover .btn__t1,
  .btn:hover .btn__t2 { font-weight:660; }
  /* no-JS fallback: without the duplicate there is nothing to recolour, so the
     single label needs to survive the fill on its own */
  .btn:not(:has(.btn__t)):hover{ color:var(--paper); }
}
/* The press law is a reading, not a scale. The whole rail snaps to accent at
   once and the level opens to full with no transition at all, so the button
   answers with a value rather than with the 1.5 percent squeeze every second
   page ships. The scale stays underneath it for the pointer that expects it,
   and where GSAP has left an inline transform on the control the scale is dead
   and the rail is the whole of the response, which is the more robust half of
   the law. --rail-c is an UNregistered custom property, so it does not
   interpolate, which is exactly what a snap wants. --fill above is registered
   and does interpolate; that difference is deliberate, one is a gesture and the
   other is a reading, and registering --rail-c would turn the snap into a
   fade. */
.btn:active .btn__rail{ --rail-c:var(--accent); }
.btn:active .btn__lvl { clip-path:inset(0 0 0 0); transition:none; }
.btn:active{ transform:scale(.985); }

/* D4, the rest of the press law. The button answers with a reading because it
   has a rail to read; everything else that can be pressed on this page has no
   instrument on it, so it gives the 3 percent instead. One block, and it names
   what it excludes rather than leaving the exclusion to be rediscovered:
   `.db__row` and `.pr__st` both carry a scrub-owned transform, and a press scale
   would replace the whole declaration rather than compose with it, which is bug
   class 1 exactly. Their press response is the rule and the child translate they
   already have.

   The dock's own link is ruled at the end of the dock section rather than here,
   because the entry stagger leaves a transition-delay of up to .41s on those
   four anchors and a press that answers four tenths of a second late is not a
   press. It needs to sit after those rules and cancel the delay. */
.ft__nav a:active,
.ft__url a:active,
.ft__ask-l a:active,
.fm__consent-l:active{
  transform:scale(.97);
  transition-duration:.12s;
}
.ft__nav a,
.ft__url a,
.ft__ask-l a{ transition:transform var(--d-fast) var(--ease-out); }
/* From its own left edge, where the box is. A wide flex row scaled from its
   centre reads as a paragraph shrinking; scaled from the control it reads as
   the control being pressed. */
.fm__consent-l{ transform-origin:0 50%; transition:transform var(--d-fast) var(--ease-out); }

/* =============================================================== the dock
   A squared pill, floating, centred, and small: 40px of height, a hairline, a
   translucent ground that lets the footage read through it, and the CTA living
   inside the same shape rather than pinned to the far corner. The full-width
   bar it replaces was a browser chrome impression of a nav, and a full-width
   bar over a full-bleed frame is the single loudest way to say "template".

   The one thing it is NOT is a rounded capsule. Every corner on this page is
   square or cut, and a 999px radius here would be the only curve on the site.
   ------------------------------------------------------------------------- */
#dock{
  position:fixed; top:18px; left:50%; z-index:50;
  display:flex; align-items:center; gap:6px;
  height:40px;
  padding:0 5px 0 16px;
  /* No glass. The blur was doing exactly one job, holding four small words legible
     over moving footage, and a .9 ink ground does that job without the frosted
     panel that every second page ships as its default surface, and without the
     per-frame paint of blurring whatever is scrolling underneath. */
  background:rgb(13 16 17 / .9);
  border:1px solid rgb(232 230 222 / .14);
  /* The entry is not a bar sliding down. The pill draws itself: it arrives as a
     single square at the centre of the top edge and opens outward from that
     square to its full width, and only then do the mark, the four links and the
     CTA rise into the shape, each a beat after the last. A nav that travels in
     as one finished rectangle is a browser chrome impression; a nav that is
     built in front of you is the page introducing its own furniture.

     Two transforms on one property, the centring and the entry travel, have to
     live in the same declaration or the later one deletes the other. */
  transform:translate3d(-50%, -8px, 0) scaleY(.72);
  clip-path:inset(0 calc(50% - 20px) 0 calc(50% - 20px));
  opacity:0;
  pointer-events:none;
  /* The drawer curve belongs to the things that travel, the shape and its entry.
     The ground and the hairline do not travel, they only change value, and with
     the blur gone the ground fade is the whole of what happens when the pill
     gives itself up over the takeover. That is what --ease-soft is for. */
  transition:transform .54s var(--ease-drawer), clip-path .62s var(--ease-drawer),
             opacity .3s var(--ease-drawer),
             background .42s var(--ease-soft), border-color .42s var(--ease-soft),
             gap .42s var(--ease-drawer), padding .42s var(--ease-drawer);
}
#dock[data-state="docked"]{
  transform:translate3d(-50%, 0, 0) scaleY(1);
  clip-path:inset(0 0 0 0);
  opacity:1; pointer-events:auto;
}
/* The contents wait for the shape. They are staggered by hand rather than by a
   timeline, because the state is a CSS attribute and nothing here needs GSAP. */
#dock .dock__mark,
#dock .dock__nav a,
#dock .dock__cta{
  opacity:0;
  transform:translate3d(0, 9px, 0);
  transition:opacity .34s var(--ease-out), transform .46s var(--ease-out),
             max-width .42s var(--ease-drawer);
}
#dock[data-state="docked"] .dock__mark,
#dock[data-state="docked"] .dock__nav a,
#dock[data-state="docked"] .dock__cta,
#dock[data-state="reduced"] .dock__nav a{
  opacity:1; transform:translate3d(0, 0, 0);
}
#dock[data-state="docked"] .dock__mark      { transition-delay:.20s; }
#dock[data-state="docked"] .dock__nav a:nth-child(1){ transition-delay:.26s; }
#dock[data-state="docked"] .dock__nav a:nth-child(2){ transition-delay:.31s; }
#dock[data-state="docked"] .dock__nav a:nth-child(3){ transition-delay:.36s; }
#dock[data-state="docked"] .dock__nav a:nth-child(4){ transition-delay:.41s; }
#dock[data-state="docked"] .dock__cta       { transition-delay:.46s; }
/* Over the takeover the pill gives up its ground and its CTA and becomes four
   words floating on the film, which is what the frame is for. */
#dock[data-state="reduced"]{
  transform:translate3d(-50%, 0, 0) scaleY(1);
  clip-path:inset(0 0 0 0);
  opacity:1; pointer-events:auto;
  background:transparent;
  border-color:transparent;
  padding:0;
}
#dock[data-state="reduced"] .dock__mark,
#dock[data-state="reduced"] .dock__cta{
  opacity:0; pointer-events:none;
  max-width:0; margin:0; padding-inline:0; overflow:hidden;
}

.dock__mark{
  font-size:10px; font-weight:600; letter-spacing:.34em; margin-right:.2em;
  color:var(--ink); white-space:nowrap;
}
/* the hairline that separates the wordmark from the nav inside the pill */
.dock__mark::after{
  content:''; display:inline-block; vertical-align:middle;
  width:1px; height:12px; margin-left:14px;
  background:rgb(232 230 222 / .2);
}
#dock[data-state="reduced"] .dock__mark::after{ opacity:0; }
.dock__nav{ display:flex; gap:4px; }
.dock__nav a{
  font-size:11px; font-weight:500; letter-spacing:.02em;
  position:relative; padding:0 10px; line-height:38px; white-space:nowrap;
}
/* Directional underline: it grows in from the left edge and, on leave, keeps
   travelling the same way and collapses out through the right, because the
   origin flips with the hover state. A single origin makes the line retrace its
   own path backwards, which is the tell of a default transition. */
/* Inside a pill the underline has nowhere to sit, so the hover fills the item's
   own square instead. It still travels: the fill wipes up from the baseline. */
.dock__nav a::after{
  content:''; position:absolute; inset:2px 0; z-index:-1;
  background:rgb(232 230 222 / .1);
  transform:scaleY(0); transform-origin:50% 100%;
  transition:transform .26s var(--ease-out);
}
.dock__nav a{ z-index:0; }
@media (hover:hover) and (pointer:fine){
  .dock__nav a:hover::after{ transform:scaleY(1); }
}
/* the pill's own CTA: full accent, square, and the same height as the pill's
   inner box so the shape reads as one object with a filled end. The rail comes
   with it, at a finer pitch because the control is a third of the size. */
.dock__cta.btn{
  height:30px; padding:0 14px; line-height:30px;
  font-size:11px; letter-spacing:.04em;
  --rail-h:4px; --rail-t:2px; --rail-g:6px;
}
/* G-b. The pill used to carry a graduated axis of its own, `.dock__ln`, along
   the inside of its bottom edge at a 30px pitch. The CTA in the pill is a .btn
   and carries the page's rail at 6px, and with the CTA centred in a 40px pill
   the two sat 4px apart: two parallel rails, which is what the client saw and
   named. Both were defended in a comment as "a scale and its detail"; on the
   built page they were the same ruler printed twice, 4px apart.

   The button's rail is the one that stays, and not by coin toss. It is the
   thing the hover level rises along and the press snaps to accent, so it takes
   a reading; removing it would have made the dock CTA the only button on the
   page that answers differently from every other button, which is a second
   button law to keep in sync forever. The pill's axis took no reading at all.
   It arrived, and that was its whole behaviour. It also restated the opening
   gesture the pill's own clip-path already performs, half a second later.
   An unearned detail layer, by the standard in _lib/AI-TELLS.md section 0. */

/* D4, the dock's share of the press law, and it has to live here.
   The four links carry a transition-delay of .26s to .41s so they arrive in
   sequence behind the pill, and that delay is on the same declaration the press
   would use: measured, the scale started 260ms after the finger and finished at
   380. The delay is cancelled for the duration of the press and the rule is
   placed after the stagger it is cancelling, because at equal specificity the
   later rule is the one that means it. */
#dock[data-state] .dock__nav a:active{
  transform:scale(.97);
  transition-duration:.12s;
  transition-delay:0s;
}

/* ============================================== 01 #hero — ground F — 100svh */
#hero{ height:100svh; min-height:560px; overflow:hidden; }

.hero__film{ position:absolute; inset:0; z-index:10; }
.hero__video{
  width:100%; height:100%; object-fit:cover; object-position:50% 50%;
  opacity:1;
}
.hero__grade{
  position:absolute; inset:0;
  background:
    linear-gradient(to top,    rgba(10,13,14,.55) 0%, rgba(10,13,14,0) 26%),
    linear-gradient(to bottom, rgba(10,13,14,.42) 0%, rgba(10,13,14,0) 20%),
    linear-gradient(to right,  rgba(10,13,14,.82) 0%, rgba(10,13,14,.48) 30%, rgba(10,13,14,0) 66%),
    rgba(10,13,14,.18);
}

#hero .mark{ position:absolute; z-index:20; top:3.4%; left:50%; transform:translateX(-50%); }

.hero__type{
  position:absolute; z-index:20;
  left:var(--gutter); right:var(--gutter); bottom:13.2%;
  text-align:center;
}
.hero__h1{
  font-size:var(--fs-display); font-weight:500;
  line-height:.92; letter-spacing:-.02em;
}
.hero__lead{
  /* .7em of the LEAD, not of the display, so the gap stayed ~19px while the
     headline above it grew to 192px. Geist's content box overhangs its line box
     by .19em at that size, which put 20px of descender inside the lead's own
     rect at 1920 and 12px at 1440, one pixel under the probe's filter. Scaling
     the gap off --fs-display is what makes the clearance width-independent. */
  margin-top:calc(.20 * var(--fs-display));
  font-size:var(--fs-lead); font-weight:400; color:var(--ink);
  max-width:66ch; margin-left:auto; margin-right:auto;
}
.hero__cta{
  position:absolute; z-index:20; right:var(--gutter); top:90%;
}

/* ============================================= 02 #drain — ground D — 120svh */
/* 240svh, not 120. A sticky pin only scrubs for (section height - pin height),
   so a 120svh section around a 100svh pin gave the drain 20svh of travel: the
   whole dissolve finished inside 180px and the rest of the section was the
   post-drain emptiness. The section height is the pin height PLUS the travel
   the device needs, never the travel on its own. Measured with
   `_tools/scrollcheck.mjs --sticky`, which reports travel in viewport heights. */
#drain{ height:240svh; }
.drain__pin{
  position:sticky; top:0; height:100svh; overflow:hidden;
}
.drain__stage{ position:absolute; inset:0; z-index:10; }
.drain__img,
.drain__gl{ position:absolute; inset:0; width:100%; height:100%; }
.drain__gl{ pointer-events:none; }
.drain__img{ object-fit:cover; }
.drain__gl{ display:none; }           /* main.js shows it when WebGL is live */
.drain__stage.is-gl .drain__img{ visibility:hidden; }
.drain__stage.is-gl .drain__gl{ display:block; }

/* ---------------------------------------------- the drain without WebGL
   Not an opacity fade. A fade is precisely what device 2 exists to replace,
   and it is the reflex every thin build in this repo reached for, so the
   machine that cannot run the shader still has to see a frame come apart in
   grain-sized bites rather than dim.

   Two mask layers, intersected. Layer 1 is a 4px grid of hard-edged dots whose
   radius shrinks from full coverage to nothing, which is the erosion. Layer 2
   is a threshold sweeping top to bottom, which is the shader's (1 - vUv.y)
   bias. Because layer 1 is binary the intersection never dims anything: a
   pixel is either film or ground, exactly as in the shader.

   Applied only where the compositing operator resolves, tested in JS, because
   a mask that half-applies would leave the film on screen forever. Shared by
   all three drains, so `.is-mask` names no section. */
.is-mask{
  --cut:0;
  -webkit-mask-image:
    radial-gradient(circle, #000 calc((1 - var(--cut)) * 104%), transparent 0),
    linear-gradient(to bottom,
      transparent calc(var(--cut) * 132% - 32%), #000 calc(var(--cut) * 132%));
  mask-image:
    radial-gradient(circle, #000 calc((1 - var(--cut)) * 104%), transparent 0),
    linear-gradient(to bottom,
      transparent calc(var(--cut) * 132% - 32%), #000 calc(var(--cut) * 132%));
  -webkit-mask-size:4px 4px, 100% 100%;
  mask-size:4px 4px, 100% 100%;
  -webkit-mask-repeat:repeat, no-repeat;
  mask-repeat:repeat, no-repeat;
  -webkit-mask-composite:source-in;
  mask-composite:intersect;
}

/* The payoff of the drain: the statement carries the section at h2 scale and the
   film is what uncovers it, rather than one 20px line floating in the middle of
   a section whose whole job is to empty itself.

   The measure is in px, not ch. `ch` on this container resolves against the
   INHERITED body size, roughly 9px, so `max-width:20ch` was about 190px of box
   around an 80px headline: the sentence broke into four ragged lines, and once
   each line was told not to wrap, `.ln{overflow:hidden}` clipped the glyphs
   horizontally and the heading came out garbled. A ch measure only means what
   you think it means on an element that is set at the size it measures. */
.drain__type{
  position:absolute; z-index:20;
  left:var(--gutter); right:var(--gutter); bottom:16%;
  max-width:min(1180px, 100%);
  margin-inline:auto;
  text-align:center;
}
.drain__line{
  font-size:var(--fs-h2); font-weight:500; line-height:1.02; letter-spacing:-.02em;
  color:var(--ink);
}
/* One written line is one rendered line, but only where 41.6px of Geist and a
   twenty-character sentence actually fit. Below that the balance is the fix. */
@media (min-width:1000px){
  .drain__line .ln > span{ white-space:nowrap; }
}
.drain__line .sq{ width:.44em; height:.44em; margin-right:.3em; }
.drain__sub{
  margin-top:1.1em; max-width:46ch; margin-inline:auto;
  font-size:var(--fs-lead); font-weight:400; color:var(--ink-2);
}

/* ============================================ 03 #geluid — ground P — 324svh */
/* 100svh pin + 224svh of travel for the five-row scrub, same arithmetic as #drain.
   G3. It was 240svh, so 140svh of travel, and a sticky pin scrubs for section
   height minus pin height and not one pixel more: five levels shared 140svh and
   each of them got 28svh, which at 900 is 252px and is less than a third of a
   wheel gesture per measurement. The travel is 224svh now, 1.6x, so a level is
   403px at 900 and 378px at 844. The clock inside it is shaped rather than
   linear; main.js holds that arithmetic. */
#geluid{ height:324svh; }
.geluid__pin{
  position:sticky; top:0; height:100svh;
  padding:var(--pad-sec) var(--gutter);
}
.geluid__h{
  /* pinned for the whole section, so the traced 5.3% (48px at 900) would keep
     this heading two thirds under the docked bar the entire time, not for a
     moment in passing. Same 88px floor as the timecode. */
  position:absolute; right:var(--gutter); top:max(5.3%, 88px);
  max-width:34ch; text-align:right;
  font-size:var(--fs-ui); font-weight:500; letter-spacing:0;
  color:var(--ink-2);
}
.db{
  /* The stack clears the rail rather than sitting against it: the rail is set
     in from the page edge and the list is set in again from the rail, so the
     axis reads as an axis with air on both sides of it. */
  position:absolute; left:calc(var(--gutter) + 44px); right:var(--gutter);
  top:16%; height:66%;
  --spread:1;                                   /* CSS holds the END state */
  --stack:.66svh;                               /* one percent of this stack */
  --prog:1;                                     /* and the END of the rail */
}

/* ------------------------------------------------------------- the rail
   A progress line for the section that is also the axis the five figures are
   plotted against, which is the only reason it earns its place: a bar that
   only reports how far you have scrolled is a scrollbar with a coat of paint.
   The fill grows, a square marker rides the fill, and the marker carries the
   value of whichever row the reader is on. Set in the gutter, left of the
   stack, so it frames the list rather than sitting in it. */
.db__rail{
  /* --prog lives here, not on .db: the rail is .db's SIBLING, so a custom
     property set on the list would never reach it. */
  --prog:1;                                     /* CSS holds the END state */
  position:absolute; left:max(30px, calc(var(--gutter) - 6px));
  top:16%; height:66%;
  width:1px; background:var(--line);
  z-index:2;
}
.db__rail-fill{
  position:absolute; inset:0;
  background:var(--accent);
  transform:scaleY(var(--prog, 0)); transform-origin:50% 0;
}
/* the ticks: one hairline per true dB position, so the axis is graduated */
.db__rail::after{
  content:''; position:absolute; left:0; top:0; width:9px; height:100%;
  background:
    linear-gradient(var(--line), var(--line)) 0 0    / 9px 1px no-repeat,
    linear-gradient(var(--line), var(--line)) 0 50%  / 9px 1px no-repeat,
    linear-gradient(var(--line), var(--line)) 0 62.5%/ 9px 1px no-repeat,
    linear-gradient(var(--line), var(--line)) 0 77%  / 9px 1px no-repeat,
    linear-gradient(var(--line), var(--line)) 0 100% / 9px 1px no-repeat;
}
.db__rail-mark{
  position:absolute; left:0; top:calc(var(--prog, 0) * 100%);
  display:flex; align-items:center; gap:10px;
  transform:translate(-50%, -50%);
  will-change:top;
}
.db__rail-sq{
  display:block; width:9px; height:9px; flex:0 0 9px;
  background:var(--accent);
}
.db__rail-v{
  font-style:normal; white-space:nowrap;
  font-family:var(--font-mono); font-weight:450;
  font-size:10px; letter-spacing:.14em;
  color:var(--accent); font-variant-numeric:tabular-nums;
  transition:opacity .18s var(--ease-out);
}
.db__rail.is-swapping .db__rail-v{ opacity:0; }
.db__row{
  position:absolute; left:0;
  display:flex; align-items:baseline; gap:.6em;
  /* --e = evenly spaced start, --t = true relative dB position, both % of stack.
     The scrub used to drive `top`, which relaid out five rows on every frame of
     a 224svh scrub. It rides transform now, so the stack composites. The unit is
     a fraction of svh rather than a percentage because a percentage on translate
     resolves against the row's own height, not the stack's.
     --in is the entry travel, folded into the same transform: two transforms on
     one element is one of them winning, and GSAP writing y here would have
     silently deleted the scrub. */
  --in:0px;
  top:calc(var(--e) * var(--stack));
  transform:translateY(calc((var(--t) - var(--e)) * var(--spread) * var(--stack) + var(--in)));
}
.db__row:nth-child(1){ --e:0;   --t:0;    --s:.62; }
.db__row:nth-child(2){ --e:25;  --t:50;   --s:.72; }
.db__row:nth-child(3){ --e:50;  --t:62.5; --s:.86; }
.db__row:nth-child(4){ --e:75;  --t:77;   --s:1;   }
.db__row:nth-child(5){ --e:100; --t:100;  --s:1;   }

.db__n{
  font-size:calc(clamp(2.6rem, 1rem + 4.2vw, 5.2rem) * var(--s));
  font-weight:500; line-height:1; letter-spacing:-.015em;
  font-variant-numeric:tabular-nums;
}
.db__l{ font-size:var(--fs-ui); font-weight:400; color:var(--ink-2); }

.db__row--hit .db__n{
  background:var(--accent); color:var(--accent-ink);
  padding:.06em .18em;
  clip-path:inset(0 0 0 0);              /* JS wipes this open; scaleX would distort the glyphs */
}
.db__row--hit .db__l{ color:var(--ink); }

/* The rows are the measurement; the aside is what the measurement means. It
   answers whichever row is hovered, focused or nearest the scrub, so the
   section stops being a list of numbers you have to interpret yourself. */
/* Narrowed from 56% to make room for the readout, which is now the largest thing
   on the page and needs a column it cannot reach out of. Below 1360 the pair
   swaps to a narrower readout instead, because at that width there is no
   arrangement where a 200px numeral and this list both fit. */
.db{ width:min(52%, 640px); }
@media (min-width:1360px){ .db{ width:min(50%, 640px); } }
.db__row{
  /* Full width, or the only hoverable thing on the row is the glyphs themselves
     and the pointer falls through the gaps between them. */
  width:100%;
  cursor:pointer;
  padding-block:.2em;
  transition:opacity .24s var(--ease-out);
}
/* An accent rule draws in from the left edge under the attended row. It is the
   thing that says "this one", and it costs one pseudo-element. */
.db__row::before{
  content:''; position:absolute; left:-12px; top:0; bottom:0;
  width:2px; background:var(--accent);
  transform:scaleY(0); transform-origin:50% 100%;
  transition:transform .3s var(--ease-out);
}
.db__row .db__n,
.db__row .db__l{ transition:transform .26s var(--ease-out), color .24s var(--ease-out); }
/* the un-attended rows step back so the attended one reads as chosen */
.db.is-attending .db__row{ opacity:.34; }
.db.is-attending .db__row.is-on{ opacity:1; }
.db__row.is-on::before{ transform:scaleY(1); }
.db__row.is-on .db__n{ transform:translateX(18px); }
.db__row.is-on .db__l{ transform:translateX(18px); color:var(--ink); }
.db__row:focus-visible{ outline:2px solid var(--accent); outline-offset:6px; }

.db__aside{
  position:absolute; right:var(--gutter); bottom:16%;
  width:min(32%, 400px);
  text-align:right;
}
@media (min-width:1360px){ .db__aside{ width:min(38%, 560px); } }
.db__aside-k{
  font-size:var(--fs-micro); font-weight:500; letter-spacing:.16em;
  text-transform:lowercase; color:var(--ink-2);
}

/* ---------------------------------------------------------- the readout
   DEVICE. Every decibel value is typeset at the volume it describes. Scale and
   weight are mapped to loudness, so 20 dB is small and thin, 60 dB is the
   largest and heaviest object on the page, and 50,8 is the only value that
   arrives in accent and the only one that stands perfectly still. The quietest
   machine on the page is the quietest object on the page, which is the entire
   argument of the product said in type rather than in a sentence.

   Scale rides a transform on the inner span, NEVER font-size, so the swap is
   composited and the box never reflows. The number sits in a lane of its own
   (`min-width`, tabular figures, right aligned) and the unit sits outside it, so
   a value resolving from four glyphs to two cannot move anything on the line.
   JS writes --rs, --rw and data-level and nothing else: the transform itself is
   declared once, here, so no inline transform ever lands on this element and the
   scrub and the hover can never overwrite each other. */
.db__aside-v{
  margin-top:.1em;
  font-size:clamp(2.2rem, .4rem + 7.4vw, 4.4rem);
  font-weight:500; line-height:.86; letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;
  color:var(--accent);
  --rs:.885; --rw:450;                        /* CSS holds the END state, and it is 50,8 */
}
/* Three tiers, and they are sized off the column rather than off the viewport:
   the numeral's lane is 2.2em wide and the unit's is 65px, so the ceiling at every
   width is what the aside can hold without the value wrapping. 1360 is where the
   readout takes the page over; below it the list still needs the width more. */
@media (min-width:700px) { .db__aside-v{ font-size:clamp(4rem, -1rem + 9.4vw, 8rem); } }
@media (min-width:1360px){ .db__aside-v{ font-size:clamp(6rem, -.4rem + 14.5vw, 13rem); } }
/* min-width, not width: the numeral gets a lane of its own so the box is the same
   width whether the value is two glyphs or four, and text-align keeps its right
   edge nailed to the same pixel. The unit therefore never moves while the value
   resolves, which is the difference between a meter settling and a layout jumping.
   pointer-events:none because the readout is not interactive and, being a
   transformed layer around 200px glyphs, its paint bounds reach well past the
   0.86 line box and would otherwise sit in front of the note under it. */
.db__ro{
  display:inline-block; min-width:2.2em; text-align:right;
  pointer-events:none;
  transform:scale(var(--rs)); transform-origin:100% 100%;
  font-variation-settings:'wght' var(--rw);
  transition:transform .5s var(--ease-inout),
             font-variation-settings .4s var(--ease-soft),
             color .3s var(--ease-soft);
  will-change:transform;
}
.db__ro-u{
  display:inline-block; margin-left:14px;
  font-family:var(--font-mono); font-size:var(--fs-micro);
  font-weight:500; letter-spacing:.18em; color:var(--ink-2);
}
/* only the whspr level is accent; every other level reads in ink */
.db__aside-v:not([data-level="50.8"]){ color:var(--ink); }
.db__aside-v:not([data-level="50.8"]) .db__ro{ transition-duration:.4s; }

/* 60 dB is the one value that cannot hold still. A weight-axis square wave, one
   element, no translation and no rotation, so it reads as instability under load
   rather than as a wobbling logo. Delayed past the weight transition, or the
   animation would win the moment the level lands and the 300-to-740 climb that
   carries the meaning would never be seen. */
.db__aside-v[data-level="60"] .db__ro{ animation:ro-loud .14s steps(2) .45s infinite; }
@keyframes ro-loud{
  0%  { font-variation-settings:'wght' 740; }
  50% { font-variation-settings:'wght' 768; }
}
@media (prefers-reduced-motion:reduce){
  .db__aside-v .db__ro{ animation:none; }
}

.db__aside-n{
  margin-top:1.1em;
  font-size:var(--fs-ui); line-height:1.55; color:var(--ink-2);
}
/* The swap: the old note leaves upward, the new one is set from JS. The readout
   is deliberately NOT in this group. It resolves through its own values instead
   of blinking out and back, which is what makes it read as a meter settling. */
.db__aside.is-swapping .db__aside-k,
.db__aside.is-swapping .db__aside-n{ opacity:0; transform:translateY(-8px); }
.db__aside-k,.db__aside-n{
  transition:opacity .18s var(--ease-out), transform .18s var(--ease-out);
}

/* ============================================== 04 #cta-1 — ground P — 70svh */
.cta-block{ padding:var(--pad-sec) 0; }
/* No min-height. 70svh forced 630px around 433px of content at 1440, and the
   200px left over sat between this block and the takeover as dead page. The
   section is now the height of what is in it plus the section rhythm. */
#cta-1{ display:flex; align-items:center; }
/* Was a 72vw box whose content only filled its left half, so a quarter of the
   viewport sat empty to the right of a block that was itself mostly empty. Full
   bleed now, edge to edge, with the gutter as the box's own padding: 92vw left
   115px of nothing outside the right edge on top of the 72px inside it.
   The bottom cut is shallower too. A 18% wedge on a 415px box is 75px of
   deliberately empty box directly above the section's own bottom padding. */
/* The one solid accent field on the page. A 5 percent tint of --ink behind a
   heading and three notes is what a wireframe looks like, and it is what this
   block was: nothing in it was louder than the section above it, so the page's
   own question arrived quieter than the copy that led to it. The block is now
   #CCFF00 edge to edge with #171B1C type on it, and it is the only place the
   accent is ever used as a field rather than as a mark. Once, so it lands. */
.cta-block--a .cta-block__box{
  position:relative;
  width:100%;
  padding:clamp(52px,7.5svh,104px) var(--gutter) clamp(56px,7svh,92px);
  background:var(--accent);
  color:var(--accent-ink);
  clip-path:polygon(0 0, 100% 0, 100% 100%, 0 90%);
  display:grid; grid-template-columns:minmax(0,1.05fr) minmax(0,.8fr);
  column-gap:clamp(40px,7vw,140px); align-items:start;
}
/* The rule across the top edge is the first thing to arrive and the thing the
   rest of the block is built off. On the lime field it is drawn in the ink. */
.cta-block__rule{
  position:absolute; left:0; right:0; top:0; height:3px;
  background:var(--accent);
  transform-origin:0 50%;
}
.cta-block--a .cta-block__rule{ background:var(--accent-ink); height:4px; }
.cta-block__eyebrow{
  display:flex; align-items:center; gap:14px;
  margin-bottom:clamp(20px,3svh,36px);
  font-size:var(--fs-micro); font-weight:600; letter-spacing:.2em; color:var(--ink-2);
}
.cta-block--a .cta-block__eyebrow{ color:var(--accent-ink); opacity:.62; }
.cta-block__eyebrow .sq{ width:9px; height:9px; }
.cta-block--a .cta-block__eyebrow .sq{ background:var(--accent-ink); }
.cta-block__main{ min-width:0; }

/* ---- de specificatieplaat ------------------------------------------------
   G9. The right half of this block was a ledger of three key-value rows, and
   on a solid #CCFF00 field beside a question set at 80px a ledger in body copy
   is the small print under the ask. The facts are the right facts. The
   treatment was the wrong treatment.

   It is a plate now, and specifically it is the SAME plate the footer stamps
   at the end of the page, one rank larger and inverted onto the lime: a
   graduated axis across the top, a hairline per cell, a mono key, a value at a
   size that can stand beside the headline, and a note under it. Three
   measurements taken off the aanvraag, read the way the page reads every other
   measurement it has taken. The index sits at the right edge, where a drawing
   sheet puts it, which is also the one difference from the footer's version:
   there the cells run four across, here they run down a column.

   Contrast is computed against #CCFF00, never estimated. L(#CCFF00) is
   0.84357 and L(#171B1C) is 0.01053, so solid ink is 14.76:1. Composited in
   sRGB the alphas used here measure: .5 -> 3.24:1, rules only; .66 -> 5.27:1;
   .74 -> 6.84:1. Nothing below 4.5 carries a word.
   ------------------------------------------------------------------------- */
.spec{ position:relative; min-width:0; padding-top:clamp(18px,2.4svh,30px); }
/* The same graduated edge the loader, the dB rail, every button and the footer
   plate carry. Drawn in the ink here, because on this block the accent is the
   ground. 28px pitch: the column is a third of the page wide, so the footer's
   46px would read as three marks and the button's 9px as hatching. */
.spec__ax{
  --rail-c:rgb(23 27 28 / .5);
  position:absolute; left:0; right:0; top:0; height:9px;
  display:block; pointer-events:none;
  background-image:
    repeating-linear-gradient(90deg, var(--rail-c) 0 1px, transparent 1px 28px),
    linear-gradient(var(--rail-c), var(--rail-c));
  background-repeat:no-repeat;
  background-position:0 0, 0 0;
  background-size:100% 9px, 100% 2px;
  transform-origin:0 50%;
}
.spec__h{
  margin-bottom:clamp(22px,3.2svh,38px);
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.24em;
  color:rgb(23 27 28 / .66);
}
.spec__cells{ display:flex; flex-direction:column; gap:clamp(22px,3.2svh,40px); }
.spec__c{ position:relative; padding-top:18px; }
.spec__l{
  position:absolute; left:0; top:0; display:block;
  width:100%; height:1px; background:rgb(23 27 28 / .5);
  transform-origin:0 50%;
  transition:background .3s var(--ease-out);
}
/* z-index, and it is load-bearing rather than tidy. The scrub writes a clip-path
   on .spec__k, which makes that element a stacking context painted as if it were
   z-index:0, and it comes AFTER this index in tree order. Without a z-index of
   its own the sheet number is painted over by the empty right half of the key's
   line box: still legible, but the probe reads it as occluded and it is. */
.spec__i{
  position:absolute; right:0; top:18px; z-index:2;
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.14em;
  color:rgb(23 27 28 / .66); font-variant-numeric:tabular-nums;
}
/* The body is a box of its own for one structural reason: the cell answers the
   pointer with a translate, and the scrub writes opacity and y on the key, the
   value and the note. A hover transform and a scrub transform on the same
   element is bug class 1, and the inline one wins. Two levels, two owners. */
.spec__b{ transition:transform .3s var(--ease-out); }
.spec__k{
  display:block;
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.2em; color:var(--accent-ink);
}
.spec__v{
  display:flex; align-items:baseline; flex-wrap:wrap; gap:.42em;
  margin-top:.4em;
  font-size:clamp(2.6rem, 1rem + 3.4vw, 4.6rem);
  font-weight:500; line-height:.96; letter-spacing:-.035em;
  color:var(--accent-ink); font-variant-numeric:tabular-nums;
}
.spec__u{
  font-family:var(--font-mono); font-style:normal; font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.14em;
  color:rgb(23 27 28 / .66);
}
.spec__n{
  display:block; margin-top:.7em; max-width:34ch;
  font-size:var(--fs-micro); line-height:1.55;
  color:rgb(23 27 28 / .74);
}
@media (hover:hover) and (pointer:fine){
  .spec__c:hover .spec__l{ background:var(--accent-ink); }
  .spec__c:hover .spec__b{ transform:translateX(9px); }
}
/* The block answers the pointer before it reaches the button: the tinted ground
   lifts a little and the cut corner opens. Only the fill and the clip move, both
   composited, and the geometry of the shape is the thing that changes, so it
   reads as the block responding rather than a card doing a hover trick. */
.cta-block__box{
  transition:background .3s var(--ease-out), clip-path .3s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .cta-block--a .cta-block__box:hover{
    clip-path:polygon(0 0, 100% 0, 100% 100%, 0 80%);
  }
}
/* Was clamped to 4.4rem, which put the page's own question at 70px next to a
   150px hero. A call to action set smaller than the copy around it is a call to
   action nobody hears. */
.cta-block__h{
  font-size:var(--fs-h2);
  font-weight:500; line-height:.92; letter-spacing:-.025em;
}
.cta-block__h .ln{ display:block; }
/* On the lime field the question is the only thing competing with the colour,
   so it goes up to display scale. Anything smaller loses to its own ground. */
.cta-block--a .cta-block__h{
  font-size:clamp(3.6rem, 0.9rem + 6.4vw, 8.4rem);
  line-height:.88; letter-spacing:-.035em;
}
.cta-block__p{
  margin-top:.9em; font-size:var(--fs-body); color:var(--ink-2); max-width:52ch;
}
.cta-block--a .btn{ margin-top:clamp(28px,4svh,48px); }

/* ---- the inline capture --------------------------------------------------
   A link to a form 4000px further down is a promise to fill something in
   later, and it was the only thing this block asked of anyone. One field on
   the page instead: what is typed here is written into the real form at
   #aanvraag and the page travels there with the field already filled and
   focused, so the ask is answered where it is made. Mechanically it is the
   same field as the main form, inverted: floating label, a rule that fills
   from the left on focus, a typed error under it, no browser chrome. */
.cap{ position:relative; margin-top:clamp(30px,4.5svh,54px); max-width:min(560px, 100%); }
.cap__label{
  display:block; margin-bottom:10px;
  font-size:var(--fs-micro); font-weight:600; letter-spacing:.18em;
  color:rgb(23 27 28 / .66);
  transition:color .26s var(--ease-out);
}
.cap:focus-within .cap__label{ color:var(--accent-ink); }
.cap__row{ position:relative; display:flex; align-items:stretch; gap:12px; }
.cap__input{
  flex:1 1 auto; min-width:0;
  padding:14px 2px 16px;
  font-family:inherit; font-size:clamp(1.05rem, .8rem + .55vw, 1.5rem);
  font-weight:500; letter-spacing:-.01em;
  color:var(--accent-ink);
  background:transparent; border:0; border-radius:0;
  border-bottom:2px solid rgb(23 27 28 / .3);
  transition:border-color .3s var(--ease-out),
             opacity .32s var(--ease-out), transform .32s var(--ease-out);
}
.cap__input::placeholder{ color:rgb(23 27 28 / .52); }
.cap__input:focus{ outline:none; border-bottom-color:rgb(23 27 28 / .16); }
/* the rule that fills the underline from the left, the one moving part */
.cap__line{
  position:absolute; left:0; bottom:0; display:block; height:2px;
  width:calc(100% - clamp(52px,5.4vw,64px) - 12px);
  background:var(--accent-ink);
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform var(--d-mid) var(--ease-inout);
  pointer-events:none;
}
.cap:focus-within .cap__line{ transform:scaleX(1); }
/* the send square. Two arrows in one window: the resting one leaves to the
   right as the next one arrives from the left, so the control moves the way
   the thing it does moves. */
.cap__go{
  position:relative; overflow:hidden; flex:0 0 auto;
  width:clamp(52px,5.4vw,64px); height:auto; min-height:56px;
  display:grid; place-items:center;
  background:var(--accent-ink); color:var(--accent);
  border:0; border-radius:0; cursor:pointer;
  font-size:1.35rem; line-height:1;
  transition:background .3s var(--ease-out);
}
.cap__go-a,.cap__go-b{
  grid-area:1 / 1; display:block;
  transition:transform .46s var(--ease-inout), opacity .3s var(--ease-out);
}
.cap__go-b{ transform:translateX(-140%); opacity:0; }
@media (hover:hover) and (pointer:fine){
  .cap__go:hover .cap__go-a{ transform:translateX(140%); opacity:0; }
  .cap__go:hover .cap__go-b{ transform:translateX(0); opacity:1; }
}
.cap__go:active{ transform:scale(.97); }
.cap__err{
  min-height:1.2em; margin-top:10px;
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.04em;
  color:#5B1A12;
  opacity:0; transform:translateY(-4px);
  transition:opacity .26s var(--ease-out), transform .26s var(--ease-out);
}
.cap.is-error .cap__err{ opacity:1; transform:translateY(0); }
.cap.is-error .cap__input{ border-bottom-color:#5B1A12; }
/* D3, the armed state. The control answered focus and it answered failure, and
   it had nothing at all to say about the one thing that actually matters to the
   person typing: that what they have written is now enough. The moment the value
   becomes reachable the underline completes on its own, without waiting for the
   caret to leave, and the second arrow slides into the window the way it does
   under the pointer. The control says it will accept before anyone commits.

   Same travel as the hover, deliberately: an armed control and a control under
   the pointer are both "this will go", and inventing a third gesture for the
   same statement is how a form ends up with four vocabularies. Rest state,
   invalid and unfocused, is untouched. */
.cap.is-valid .cap__line{ transform:scaleX(1); }
.cap.is-valid .cap__go-a{ transform:translateX(140%); opacity:0; }
.cap.is-valid .cap__go-b{ transform:translateX(0); opacity:1; }
/* the label stops being a prompt and becomes a heading over an answer */
.cap.is-valid .cap__label{ color:var(--accent-ink); }
/* the handoff: the field empties upward once the value is on its way down */
.cap.is-sent .cap__input{ opacity:0; transform:translateY(-10px); }

/* ================================= 05 #regen — ground D>F>D — 320svh — PINNED */
/* 100svh pin + 320svh of travel, the figure §8d specifies for the takeover.
   Same arithmetic as #drain: a sticky pin scrubs for section height MINUS pin
   height, so 320svh of section only ever bought 220svh of device. */
/* 360, down from 420. The height is fixed; how the travel is SPENT is in
   main.js, in the T_* constants at the top of INITS.regen, and that is where
   this section's pacing complaints get answered. Measured at 1440x900: 2340px
   of travel, of which the takeover now takes 1076, the film holds the whole
   screen for 889, and the dissolve takes the last 374 and finishes exactly as
   the pin releases. Do not retune those by changing this number. */
#regen{ height:360svh; }
.regen__pin{
  /* positioned, so the absolute children below resolve against the pin and not
     against the 420svh section. Without it, a page with no JS parks the video
     at 160svh and paints the veil across the whole section. */
  position:relative; height:100svh; overflow:hidden; }
.regen__veil{
  position:absolute; inset:0; z-index:9;
  background:#0D1011;
  opacity:var(--regen-veil, 1);          /* CSS holds the END state: the page has left */
  pointer-events:none;
}
.regen__stage{
  position:absolute; z-index:10;
  /* CSS holds the END state: full bleed. Placed by inset, NOT by
     left/top:50% plus translate(-50%,-50%), and that is the whole fix for
     "the video only half shows".

     The scrub writes `yPercent` on this element. Centring with a translate
     meant GSAP held that centring AS xPercent -50 / yPercent -50, so the very
     first scrub frame overwrote yPercent -50 with the rise value and the stage
     dropped exactly half a viewport. Measured on the built page: at full bleed
     the stage box sat at y=450 in a 900px viewport, so the bottom half of the
     picture was below the fold at every scroll position in the section, for
     the whole life of this build. x and y were free lanes, as the note in
     main.js said, but yPercent never was. Positioned by inset there is no
     transform to collide with and all four lanes are free. */
  inset:0;
  /* The poster sits behind the video. Between the clip-path opening and the
     decoder handing over its first picture there were a few frames of nothing,
     and a few frames of nothing at the start of a full-bleed takeover is the
     whole difference between a cut and a glitch. */
  background:#0D1011 url("assets/plate-regen.jpg") center/cover no-repeat;
}
.regen__video{
  display:block; width:100%; height:100%; object-fit:cover;
  transition:opacity .45s var(--ease-out);
}
/* The from-state, added by JS and taken off at the first decoded frame. This
   way round rather than the reverse, because a page with no JS must still get
   the clip: an `opacity:0` default would have hidden it for good. */
.regen__stage.is-waiting .regen__video{ opacity:0; }
/* The video only goes once the canvas has fully faded in over it. Hiding it the
   instant the drain existed made a visible step in brightness, because a WebGL
   canvas and a <video> element do not resolve colour identically even when they
   are showing the same frame. */
/* `.is-gl` is load-bearing, not decoration. The mask drain has no canvas at
   all: it erodes the stage itself, video and all, so the clip IS the picture on
   that path. Unscoped, this rule hid the running video at 0.82 of the travel
   and left the poster background to dissolve on its own, which is a hard freeze
   for every phone and every machine without WebGL, the second half of what the
   client kept reporting as "the video pauses". */
.regen__stage.is-gl.is-drain .regen__video{ visibility:hidden; }
.regen__stage .drain__gl{
  position:absolute; inset:0; width:100%; height:100%; display:none;
  opacity:var(--gl-in, 1);
}
.regen__stage.is-gl .drain__gl{ display:block; }

.regen__lines{
  position:absolute; z-index:20;
  left:var(--gutter); right:var(--gutter); bottom:8.5%;
  font-weight:500; letter-spacing:-.02em; line-height:1.05;
  text-align:right;
}
/* Both lines occupy one grid cell, so the second does not arrive somewhere the
   first never was. They were a centred 20px line and a right-aligned 64px one,
   which made the first read as a caption and the swap read as two unrelated
   events instead of one sentence turning over. */
.regen__lines{ display:grid; }
.regen__ln{
  grid-area:1 / 1;
  /* shrink-wrapped and pushed to the right edge, not a full-width block. Both
     lines are masked, so the one waiting its turn sits 110% below its own box,
     and a full-width box put that waiting line straight across the caption in
     the opposite corner. Clipped is not the same as absent: the probe measures
     rects, and so does a screen reader's reading order. */
  justify-self:end;
  display:block; overflow:hidden;
  padding-bottom:.16em; margin-bottom:-.16em;
}
.regen__ln > span{ display:block; }
.regen__ln--a,
.regen__ln--b{ font-size:clamp(2.2rem, .9rem + 3.4vw, 4rem); font-weight:500; letter-spacing:-.02em; }
.regen__ln--b .sq{ width:22px; height:22px; margin-right:18px; }
/* Opposite corner from the statement, on the same baseline: the sentence is
   the claim, the caption is who measured it, and putting them on one side made
   the second read as a third line of the first. */
.regen__cap{
  position:absolute; z-index:20; left:var(--gutter); bottom:8.5%;
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.16em;
  color:var(--ink-2); overflow:hidden;
}
.regen__cap > span{ display:block; }

/* ============================ 06 #techniek — ground D then P — 100 + 120 svh */
#techniek{ padding-bottom:var(--pad-sec); }
.tk__media{
  position:relative; height:100svh; overflow:hidden;
}
.tk__media .drain__img,
.tk__media .drain__gl{ position:absolute; inset:0; width:100%; height:100%; }
.tk__media .drain__img{ object-fit:cover; }
.tk__media .drain__gl{ display:none; }
.tk__media.is-gl .drain__img{ visibility:hidden; }
.tk__media.is-gl .drain__gl{ display:block; }

.tk__body{ padding:clamp(48px,7svh,96px) var(--gutter) 0; margin-top:-14svh; position:relative; z-index:20; }
.tk__h{
  font-size:var(--fs-h2); font-weight:500; line-height:.95; letter-spacing:-.02em;
}
.tk__row{
  margin-top:clamp(48px,7svh,96px);
  display:grid; grid-template-columns:repeat(3,1fr); gap:var(--gutter);
  align-items:start;
}
/* Three stats, read the way the dB rows and the ledger at #cta-1 are read: the
   one under the pointer steps forward off its own hairline and the other two
   step back. One grammar for attention across the whole page is most of what
   makes twelve sections feel like one hand. */
.tk__b{ position:relative; padding-top:16px; }
.tk__fl{
  position:absolute; left:0; top:0; display:block;
  width:100%; height:1px; background:var(--line);
  transform-origin:0 50%;
  transition:background .3s var(--ease-out);
}
.tk__n{
  font-size:var(--fs-num); font-weight:500; line-height:1;
  letter-spacing:-.02em; font-variant-numeric:tabular-nums;
  display:inline-block;
}
.tk__n--hit{
  background:var(--accent); color:var(--accent-ink);
  padding:.1em .28em;
}
.tk__p{ margin-top:1.1em; font-size:var(--fs-body); color:var(--ink-2); max-width:30ch; }
.tk__n,
.tk__p{
  transition:transform .3s var(--ease-out), color .26s var(--ease-out),
             opacity .26s var(--ease-soft);
}
@media (hover:hover) and (pointer:fine){
  .tk__row:hover .tk__b:not(:hover) .tk__n,
  .tk__row:hover .tk__b:not(:hover) .tk__p{ opacity:.38; }
  .tk__b:hover .tk__n{ transform:translateX(10px); }
  .tk__b:hover .tk__p{ transform:translateX(10px); color:var(--ink); }
  .tk__b:hover .tk__fl{ background:var(--accent); }
}

/* The elevation. A fourth column at the widths that have room for one, measured
   rather than assumed: at 1280 the three numerals are already within a few
   pixels of their columns, and 60x60x90 is the widest string on the row. */
.tk__dimfig{ display:none; }
.tk__dim{ display:block; width:100%; height:auto; color:var(--ink-2); }
.tk__dim-t{
  font-family:var(--font-mono); font-size:11px; font-weight:450;
  letter-spacing:.14em; fill:var(--ink-2);
}
@media (min-width:1400px){
  .tk__row{ grid-template-columns:repeat(3,1fr) minmax(160px,220px); }
  .tk__dimfig{ display:block; }
}

/* ============================= 07 #proces — ground P — 500svh — PINNED, deck */
/* 100svh pin + 500svh of travel for the five-stage deck, per §8d. */
#proces{ height:600svh; }
.pr__pin{
  /* same containing-block fix as .regen__pin: .pr__h and the 250vw .pr__row are
     absolute and would otherwise resolve against the 600svh section. */
  position:relative; height:100svh; overflow:hidden; }
.pr__head{
  position:absolute; z-index:2; left:var(--gutter); top:max(11%, 112px);
  width:min(46%, 620px);
}
.pr__intro{
  margin-top:1.5em; max-width:44ch;
  font-size:var(--fs-ui); line-height:1.6; color:var(--ink-2);
}
.pr__h{
  font-size:var(--fs-h3); font-weight:500; line-height:.96; letter-spacing:-.02em;
}
.pr__row{
  position:absolute; left:var(--gutter); top:46%;
  display:flex; gap:8vw; align-items:flex-start;
  will-change:transform;
}
.pr__st{ width:38vw; flex:0 0 38vw; }
.pr__i{
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.18em;
  color:var(--ink-2); font-variant-numeric:tabular-nums;
  /* The numeral below grows 18% upward off its own baseline, which at 1920 is
     18px of glyph arriving in space that was not reserved for it. The headroom
     is derived from the same clamp the numeral uses, so it holds at every
     width instead of being a number that happened to work at one. */
  margin-bottom:calc(1.1em + .18 * clamp(3rem, 1.4rem + 5vw, 6.4rem));
}
/* Set from JS as each stage crosses the middle of the viewport, so the numeral
   is largest exactly when the stage is the one you are reading. The variable
   carries the whole effect, which keeps it composited and keeps the resting
   value in CSS where the contract wants it. */
.pr__st{ --grow:0; }
.pr__d{
  margin-top:1.1em; max-width:34ch;
  font-size:var(--fs-ui); line-height:1.6; color:var(--ink-2);
}
.pr__n{
  font-size:clamp(3rem, 1.4rem + 5vw, 6.4rem);
  font-weight:500; line-height:1; letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;
  display:inline-block;
  transform-origin:0 100%;
  transform:scale(calc(1 + .18 * var(--grow)));
}
.pr__n--hit{ background:var(--accent); color:var(--accent-ink); padding:.06em .18em; }
.pr__l{ margin-top:.9em; font-size:var(--fs-lead); max-width:24ch; }
.pr__m{
  margin-top:1.2em; font-size:var(--fs-ui);
  font-family:var(--font-mono); font-weight:450;
  letter-spacing:.06em; color:var(--ink-2); font-variant-numeric:tabular-nums;
}
/* The deck answers the pointer the same way it answers the scrub: the stage you
   are on comes forward, the rest step back. Same grammar as the dB list, which
   is what makes two very different sections feel like one hand made them. */
.pr__st{ transition:opacity .3s var(--ease-out); }
.pr__i,.pr__l,.pr__d,.pr__m{ transition:color .28s var(--ease-out); }
.pr__st .pr__m{ position:relative; display:inline-block; }
.pr__st .pr__m::after{
  content:''; position:absolute; left:0; right:0; bottom:-8px; height:1px;
  background:var(--accent);
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform .38s var(--ease-out);
}
/* The deck also attends without a pointer. --grow already knows which stage is
   nearest the middle of the frame; is-c spends that same measurement a second
   time, so the stage being read draws its measurement rule off the scrub itself
   instead of waiting for a cursor that may never arrive. At most one stage can
   carry it: the pitch is 46vw against a 38vw stage, so two neighbours can never
   both be inside half a stage width of centre. */
.pr__st.is-c .pr__m{ color:var(--ink); }
.pr__st.is-c .pr__m::after{ transform:scaleX(1); }
@media (hover:hover) and (pointer:fine){
  .pr__row:hover .pr__st{ opacity:.4; }
  .pr__row:hover .pr__st:hover{ opacity:1; }
  .pr__st:hover .pr__m{ color:var(--ink); }
  .pr__st:hover .pr__m::after{ transform:scaleX(1); }
  .pr__st:hover .pr__d{ color:var(--ink); }
  /* The deck dims every card but the one under the hand, and a form you are
     typing into is a card under the hand whether or not the pointer is still
     resting on it. Without this the ask fades to .4 the moment the caret is in
     it and the reader is filling in something the page has stopped attending
     to. */
  .pr__row:hover .pr__st--cta:focus-within{ opacity:1; }
}
/* ---- 06, the ask, on the neon -------------------------------------------
   Five cards are the chronology and the sixth is what the chronology was for,
   and the only thing that separated them was the presence of a field. 06 is the
   one card in the deck that is not on the page's dark: #CCFF00 edge to edge with
   #171B1C on it, the same inversion #cta-1 makes and the second and last time
   the accent is spent as a field rather than as a mark. The deck travels five
   viewports through the story of how it got quiet and arrives on the ask, and
   the ask is a different colour of paper.

   PADDING, NOT A BLEED, and the negative margin is load-bearing. `.pr__st` is
   `flex:0 0 38vw` on a border-box element, so padding narrows the content column
   and leaves the flex basis, the 8vw pitch and `row.scrollWidth` at the values
   the scrub in main.js reads: the deck's travel, its pin and its proximity
   scaling are untouched arithmetic. The negative top margin then pulls the card
   back up by exactly its own top padding, so 06's index still sits on the same
   line as 01 through 05 rather than a padding lower than the deck it belongs to.
   The narrowed column costs nothing: `.pr__l` is capped at 24ch, `.pr__d` at
   34ch and `.bel` at 24rem, all of which fit inside 38vw minus the padding at
   every width above the deck's 1000px floor.

   Contrast is computed against #CCFF00, never estimated, and these are the same
   figures already established on the lime field at #cta-1. L(#CCFF00) is 0.84357
   and L(#171B1C) is 0.01050, so solid ink measures 14.77:1. Composited in sRGB:
   .74 -> 6.84:1, .66 -> 5.27:1, and .5 -> 3.23:1, which carries hairlines only
   and never a word. The failure colour is #5B1A12, 11.16:1 on the lime, the same
   one the capture at #cta-1 fails in.
   ------------------------------------------------------------------------- */
.pr__st--cta{
  --cta-pt:clamp(26px,3.4svh,42px);
  padding:var(--cta-pt) clamp(24px,2.2vw,38px) clamp(30px,4svh,48px);
  margin-top:calc(-1 * var(--cta-pt));
  background:var(--accent);
  color:var(--accent-ink);
}
/* the index and the running copy, both one step down from solid so the numeral
   and the lead stay the loudest things on the card */
.pr__st--cta .pr__i{ color:rgb(23 27 28 / .66); }   /* 5.27:1 */
.pr__st--cta .pr__d{ color:rgb(23 27 28 / .74); }   /* 6.84:1 */
/* The deck's attend law recolours the description to `--ink` under the pointer,
   and `--ink` is the LIGHT token: bone on lime is 1.06:1. On this card attending
   goes the other way, to solid ink. */
@media (hover:hover) and (pointer:fine){
  .pr__st--cta:hover .pr__d{ color:var(--accent-ink); }
}

/* The deck's own field. It is the third control on the page and it is written
   in the same three parts as the other two, a mono prompt, a rule that fills
   from the left, and a typed failure, so the page has one form language at
   three sizes rather than three forms. What is different is the ask: one
   mobile number, validated as a Dutch mobile number and nothing else. Every
   colour below is the dark-ground version of this control mirrored onto the
   lime; not one new idiom appears. */
.bel{ position:relative; margin-top:1.3em; max-width:24rem; }
.bel__label{
  display:block; margin-bottom:8px;
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.2em; color:rgb(23 27 28 / .66);   /* 5.27:1 */
  transition:color .26s var(--ease-out);
}
.bel:focus-within .bel__label{ color:var(--accent-ink); }           /* 14.77:1 */
.bel__row{ position:relative; }
.bel__input{
  display:block; width:100%;
  padding:8px 2px 11px;
  appearance:none; -webkit-appearance:none;
  font-family:inherit; font-size:var(--fs-lead); font-weight:500;
  letter-spacing:.02em; color:var(--accent-ink);                    /* 14.77:1 */
  background:transparent; border:0; border-radius:0;
  border-bottom:1px solid rgb(23 27 28 / .5);                       /* hairline */
  transition:border-color .3s var(--ease-out),
             opacity .32s var(--ease-out), transform .32s var(--ease-out);
}
.bel__input:focus:not(:focus-visible){ outline:none; }
/* .66, not the .52 the capture at #cta-1 uses. A placeholder is a word, and the
   rule on this ground is that nothing under 4.5:1 carries one. */
.bel__input::placeholder{ color:rgb(23 27 28 / .66); }              /* 5.27:1 */
/* the rule travels along the underline rather than appearing on it, the same law
   the two fields at #founders and the capture at #cta-1 are ruled by. In the ink
   here, because on this card the accent is the ground. */
.bel__line{
  position:absolute; left:0; bottom:0; display:block;
  height:1px; width:100%; background:var(--accent-ink);
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform var(--d-mid) var(--ease-inout);
  pointer-events:none;
}
.bel:focus-within .bel__line,
.bel.is-valid .bel__line{ transform:scaleX(1); }
.bel__err{
  min-height:1.3em; margin-top:8px;
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.04em; color:#5B1A12;   /* 11.16:1 */
  opacity:0; transform:translateY(-4px);
  transition:opacity .26s var(--ease-out), transform .26s var(--ease-out);
}
.bel.is-error .bel__err{ opacity:1; transform:translateY(0); }
.bel.is-error .bel__input{ border-bottom-color:#5B1A12; }

/* ---- the control, mirrored ----------------------------------------------
   Every `.btn` on the page is a lime pill that a dark fill crosses. On a lime
   card a lime pill has no ground to stand on, so this one is the same button
   with every value swapped and nothing else changed: the pill is the ink, the
   label is the accent, the fill that crosses it is the accent, the swept copy
   under the fill is the ink, and the rail's plinth is the accent with its
   graduation in the ink. The gesture, the direction, the `--fill` scalar, the
   `--eo` edge, the press law and the four curves are the ones `.btn` already
   owns; only the two colours trade places.

   Measured, not assumed. At rest the pill is #171B1C on the lime card and
   `.btn__t1` on it is #CCFF00: 14.77:1. Under the pointer the fill is #CCFF00
   and `.btn__t2` on it is #171B1C: 14.77:1. Both ends of the one gesture read
   at the same ratio, which is what the fill going dark bought every other
   button on the page.

   `.btn__t2` is the one element the probe will now fail here, and it is the
   same documented false positive it already reports on every other button, with
   the colours the other way round: at `--fill:0` this copy computes
   `clip-path: inset(0px 100% 0px 0%)`, zero painted width, so the ink-on-ink it
   measures at rest is a colour pair no reader can ever see. Do not "fix" it.
   ------------------------------------------------------------------------- */
.bel__go{
  margin-top:1.1em;
  background:var(--accent-ink); color:var(--accent);
  --rail-c:rgb(23 27 28 / .54);        /* the resting graduation, on the lime plinth */
  --rail-lc:var(--accent-ink);         /* the lit level, 14.77:1 on it */
}
.bel__go::after{ background:var(--accent); }
.bel__go .btn__t2{ color:var(--accent-ink); }
.bel__go .btn__rail{ background-color:var(--accent); }
/* the press law reads a value rather than scaling: the whole rail snaps, and on
   this button it snaps to the ink because the plinth is already the accent */
.bel__go:active .btn__rail{ --rail-c:var(--accent-ink); }
/* The two no-JS fallbacks, mirrored with the rest. The first paints the rail
   into the button's own ground when buttonRails() never ran; only the fourth
   layer, the plinth, differs from `.btn`, but background-image is one list and
   has to be restated whole. The second is the single-label hover, which without
   the duplicate has nothing to recolour and would otherwise leave an accent word
   on the accent fill. */
.bel__go:not(:has(.btn__rail)){
  background-image:
    linear-gradient(var(--rail-c), var(--rail-c)),
    repeating-linear-gradient(90deg, var(--rail-c) 0 1px, transparent 1px calc(var(--rail-g)*5)),
    linear-gradient(var(--rail-c), var(--rail-c)),
    linear-gradient(var(--accent), var(--accent));
}
@media (hover:hover) and (pointer:fine){
  .bel__go:not(:has(.btn__t)):hover{ color:var(--accent-ink); }
}
/* the field empties upward once the value is on its way to #aanvraag, the same
   hand-off gesture the capture at #cta-1 makes */
.bel.is-sent .bel__input{ opacity:0; transform:translateY(-10px); }

/* =========================================== 08 #besparing — ground P — 90svh */
/* +44px of top padding, not a bigger --pad-sec: the extra is headroom for the
   timecode above the heading, not a change to the section rhythm. */
#besparing{ min-height:90svh; padding:var(--pad-sec) var(--gutter); }
.bs__h{
  font-size:var(--fs-h2); font-weight:500; line-height:.93; letter-spacing:-.02em;
}
.bs__h .ln{ display:block; }
/* The figure the section is named after is the only accent word on the page's
   paper ground. Colour, not a filled box: `.ln` clips at the line box, so a
   background on an inline run this large loses its top and bottom edges. */
.bs__hit{ font-style:normal; color:var(--accent); }

/* ----------------------------------------------------------- the gas comb
   A hundred hairlines, one per percent of a year's gas. They all stand at full
   height, and on the scroll the eighty on the right collapse away from the far
   end inward until only the twenty that Whspr still burns are left standing,
   and those twenty light up in accent.

   It is the section's own sentence drawn instead of written, and it is the only
   place on the site where a number is demonstrated rather than stated. A filled
   progress bar would have said the same thing in the shape every other site
   uses; this says it in a shape that is about gas. */
.bs__meter{
  position:relative;
  margin-top:clamp(44px,7svh,96px);
  padding-top:clamp(30px,4svh,48px);
}
.bs__ticks{
  display:flex; align-items:flex-end; gap:2px;
  height:clamp(64px,11svh,140px);
}
.bs__tick{
  flex:1 1 0; min-width:0; height:100%;
  background:var(--line-alt);
  transform-origin:50% 100%;
}
.bs__tick--keep{ background:var(--accent); }   /* CSS holds the END state */
.bs__meter-a,
.bs__meter-b{
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.14em;
  font-variant-numeric:tabular-nums; white-space:nowrap;
}
.bs__meter-a{
  position:absolute; right:0; top:0;
  color:var(--ink-2);
}
/* pinned to the boundary the comb is about: exactly one fifth across */
.bs__meter-b{
  position:absolute; left:20%; top:0;
  padding-left:12px;
  color:var(--accent);
}
.bs__meter-t{
  position:absolute; left:0; top:2px;
  display:block; width:1px; height:calc(clamp(30px,4svh,48px) + clamp(64px,11svh,140px) - 2px);
  background:var(--accent);
}
/* The comb answers the hand, and the chip is what it answers with. It rides the
   baseline the hundred ticks stand on, which is where an instrument puts a
   cursor readout, rather than floating over the comb where the two axis labels
   already live. Real arithmetic, not a decoration: one percent of the 1.200 m3
   year named in the copy above is 12 m3.

   Empty and visibility:hidden until a pointer asks, so the page at rest has no
   stray measurement in it and nothing to collide with at 390. */
.bs__chip{
  position:absolute; top:100%; left:var(--cx,0); margin-top:10px;
  transform:translate(-50%,0);
  font-family:var(--font-mono); font-size:10px; font-weight:500;
  letter-spacing:.12em; color:var(--ink); white-space:nowrap;
  font-variant-numeric:tabular-nums;
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .18s var(--ease-out), visibility .18s var(--ease-out);
}
.bs__chip.is-on{ opacity:1; visibility:visible; }
.bs__p{
  margin-top:1.6em; max-width:62ch;
  font-size:var(--fs-lead); line-height:1.6; color:var(--ink-2);
}
.bs__row{
  /* G4 pays for the comb's longer scrub here. The comb's window now ends with
     the meter at 36% of the viewport instead of 52%, and the six figures below
     it must still finish arriving AFTER the comb has finished collapsing, which
     is the fix the end value carries in main.js. That ordering is a distance
     between two elements, so it is bought with layout: 16svh more air between
     the comb and the figures restores the 7svh of clearance the shorter window
     used to have. It is the section getting taller, which is the one freeze
     lifted for #besparing, and the whitespace is not a side effect worth
     apologising for either. */
  margin-top:calc(clamp(48px,8svh,110px) + 16svh);
  display:flex; justify-content:space-between; gap:clamp(16px,2vw,40px);
  flex-wrap:wrap;
}
.bs__f{ display:flex; flex-direction:column; gap:.5em; }
.bs__n{
  font-size:clamp(1.8rem, .8rem + 2.6vw, 3.2rem);
  font-weight:500; line-height:1; letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
  display:inline-block;
}
.bs__l{ font-size:var(--fs-ui); font-weight:400; color:var(--ink-2); }
.bs__f--hit .bs__n{ background:var(--accent); color:var(--accent-ink); padding:.06em .2em; }
/* The figures answer the pointer by underscoring themselves in accent and
   bringing their own label up to full ink. Nothing moves but a scale and a
   colour, so the row never reflows under the cursor. */
.bs__f{ position:relative; padding-bottom:14px; }
.bs__f::after{
  content:''; position:absolute; left:0; right:0; bottom:0; height:2px;
  background:var(--accent);
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform .34s var(--ease-out);
}
.bs__n{ transition:transform .3s var(--ease-out); transform-origin:0 100%; }
.bs__l{ transition:color .26s var(--ease-out); }
@media (hover:hover) and (pointer:fine){
  .bs__f:hover::after{ transform:scaleX(1); }
  .bs__f:hover .bs__n{ transform:scale(1.06); }
  .bs__f:hover .bs__l{ color:var(--ink); }
}

/* ============================================== 09 #bewijs — ground P — 90svh */
#bewijs{
  min-height:90svh;
  padding:var(--pad-sec) var(--gutter);
  display:grid;
  grid-template-columns:20% 1fr 22vw;
  grid-template-rows:auto auto auto;
  column-gap:var(--gutter); row-gap:clamp(48px,7svh,96px);
}
.bw__h{
  grid-column:1 / 3; grid-row:1;
  font-size:var(--fs-h2); font-weight:500; line-height:.95; letter-spacing:-.02em;
}
.bw__marg{
  grid-column:1; grid-row:2;
  text-align:right;
  display:flex; flex-direction:column; gap:clamp(24px,3svh,44px);
}
.bw__n{
  display:block;
  font-size:clamp(1.6rem, .7rem + 2.2vw, 2.6rem);
  font-weight:500; line-height:1; letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
}
.bw__l{
  display:block; margin-top:.4em;
  font-size:var(--fs-micro); font-weight:500; letter-spacing:.06em; color:var(--ink-2);
}
.bw__p{
  grid-column:2; grid-row:2;
  font-size:var(--fs-lead); line-height:1.55; max-width:52ch;
}
.bw__p .sq{ width:22px; height:22px; margin-right:16px; }
/* The Red Dot was one clause at the end of a paragraph, which is where a claim
   goes to be missed. It gets its own object, and the accent fills it, because
   this is the one fact on the page awarded by somebody other than us. */

/* The rule shares the award's grid cell and its top margin, so it draws itself
   along the exact edge the block then wipes down from. It is the same accent as
   the block, which means it is not a decoration sitting on top of the finished
   object: once the block arrives the rule has been absorbed into it. */
.award__rule{
  grid-column:1 / 3; grid-row:3;
  align-self:start;
  margin-top:clamp(56px,8svh,110px);
  height:3px; background:var(--accent);
  /* The stylesheet holds the END state and the scrub writes the opening one at
     init. The other way round leaves a reduced-motion visitor, who never gets
     an init, looking at a rule that is permanently scaled to nothing. */
  transform-origin:0 50%;
  z-index:1;
}
.award{
  grid-column:1 / 3; grid-row:3;
  margin-top:clamp(56px,8svh,110px);
  position:relative; z-index:2;
  background:var(--accent); color:var(--accent-ink);
  padding:clamp(40px,5.5svh,72px) clamp(32px,4vw,72px);
  clip-path:polygon(0 0, 100% 0, 100% 100%, 0 100%);
  display:grid; grid-template-columns:minmax(0,.9fr) minmax(0,1.3fr);
  column-gap:clamp(32px,5vw,96px); row-gap:clamp(24px,3svh,40px);
  align-items:start;
}
.award__eyebrow{
  grid-column:1; grid-row:1;
  font-size:var(--fs-micro); font-weight:600; letter-spacing:.18em;
}
.award__year{ grid-column:1; grid-row:2; align-self:start; }
.award__digits{
  display:block;
  font-size:clamp(4rem, 2rem + 6vw, 9rem);
  font-weight:500; line-height:.86; letter-spacing:-.04em;
  font-variant-numeric:tabular-nums;
}
/* One mask per digit, so the year is set rather than faded in: four columns
   that fill from below, left to right, off the same scrub that opens the block.
   `overflow:hidden` needs a block child or the descender clipping lands on the
   wrong box, hence the inner span. */
.award__d{ display:inline-block; overflow:hidden; vertical-align:bottom; }
.award__d > span{ display:block; }
.award__h{
  grid-column:2; grid-row:1;
  font-size:var(--fs-h3); font-weight:500; line-height:1; letter-spacing:-.02em;
  max-width:18ch;
}
.award__p{
  grid-column:2;
  font-size:var(--fs-body); line-height:1.55; max-width:60ch;
}
.award__p + .award__p{ margin-top:-.4em; }
.award__facts{
  grid-column:1 / 3; grid-row:4;
  margin-top:clamp(16px,2svh,28px);
  display:flex; gap:clamp(32px,6vw,110px); flex-wrap:wrap;
}
.award__facts > li{ position:relative; padding-top:18px; }
.award__fl{
  position:absolute; left:0; top:0; display:block;
  width:100%; height:1px; background:rgb(23 27 28 / .34);
  transform-origin:0 50%;
}
.award__fv{
  display:block;
  font-size:var(--fs-num); font-weight:500; line-height:1; letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
}
.award__fk{
  display:block; margin-top:.5em;
  font-size:var(--fs-ui); font-weight:500;
  color:rgb(23 27 28 / .68);
  transition:color .26s var(--ease-out);
}
/* The three facts answer from the edge the pointer crossed: the tint grows out
   of the side you came in from, so the block responds to the direction of the
   hand rather than switching on. --eo is written on pointerenter and is the
   only thing JS touches here.

   Nothing on the fact carries a transform. The entry timeline is a SCRUB and it
   owns the mask these figures rise through, so a hover translate would be an
   inline transform arguing with a live one, which is bug class 1 exactly. The
   fill and the label colour say the same thing without that fight. */
.award__facts > li::after{
  content:''; position:absolute; inset:-10px -14px; z-index:-1;
  background:rgb(23 27 28 / .08);
  /* --eo is the scalar fromEdge() writes, 0% or 100%, shared with .btn. */
  transform:scaleX(0); transform-origin:var(--eo, 0%) 50%;
  transition:transform .26s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .award__facts > li:hover::after{ transform:scaleX(1); }
  .award__facts > li:hover .award__fk{ color:var(--accent-ink); }
}

/* overflow:hidden is what makes the still a window rather than a picture: the
   image inside it is oversized and drifts against the scroll, so the crop moves
   while the frame does not. */
.bw__still{ grid-column:3; grid-row:1 / 3; align-self:end; overflow:hidden; position:relative; }
.bw__still img{ width:100%; height:auto; object-fit:cover; will-change:transform; }
/* The caption is the window's own label and it opens on a diagonal, the same
   cut the two CTA blocks and the button fill carry, on the curve those already
   use. The ground is --paper at three alphas rather than a flat panel, so the
   type has a dark to sit on where it is and the picture keeps its top edge.
   The crop tween targets the img; this is a sibling, so the drift never moves
   it. */
.bw__cap{
  position:absolute; left:0; right:0; bottom:0;
  padding:16px 18px;
  font-size:var(--fs-micro); line-height:1.45; color:var(--ink);
  background:linear-gradient(to top,
    rgb(23 27 28 / .94) 0%, rgb(23 27 28 / .84) 62%, rgb(23 27 28 / 0) 100%);
  clip-path:polygon(0 100%, 100% 60%, 100% 100%, 0 100%);
  transition:clip-path .45s var(--ease-inout);
}
@media (hover:hover) and (pointer:fine){
  .bw__still:hover .bw__cap{ clip-path:polygon(0 0, 100% -40%, 100% 100%, 0 100%); }
}
/* No pointer to open it with, so it stands open. */
@media (max-width:999px){ .bw__cap{ clip-path:none; } }

/* ============================================== 10 #cta-2 — ground P — 80svh */
#cta-2{ min-height:80svh; display:flex; align-items:center; padding:0; }
/* The page runs dark from the first pixel to here. The closing block turns the
   lights on: full-bleed bone with near-black type, the one inversion on the
   site, and the pair to the lime slab at #cta-1. Two blocks break the ground
   and both of them are the two places the page asks for something. The tinted
   5 percent slab it replaces was the same dark as everything around it, which
   is why the closing argument read as one more section rather than as the end. */
.cta-block--b .cta-block__box{
  position:relative;
  width:100vw; margin-left:calc(50% - 50vw);
  min-height:62svh;
  display:flex; flex-direction:column; align-items:stretch;
  justify-content:flex-end; gap:clamp(30px,5svh,64px);
  padding:clamp(96px,16svh,200px) var(--gutter) clamp(48px,7svh,96px);
  background:var(--ink);
  color:var(--paper);
  clip-path:polygon(0 18%, 100% 0, 100% 100%, 0 100%);
}
/* The last block draws its own frame. Four accent edges, inset from the box, run
   in sequence off one scrub: across the top, down the right, back across the
   bottom, up the left, so the rectangle closes exactly as the last line of the
   headline lands. It is the page putting a box around its closing argument, and
   it is a shape no other section on the site draws. On bone the accent would be
   invisible, so the frame is drawn in the page's own dark instead. */
.cta-block__e{
  position:absolute; display:block; background:var(--paper); z-index:1;
  pointer-events:none;
}
.cta-block__e--t{ left:var(--gutter);  right:var(--gutter); top:calc(18% + 24px); height:2px; transform-origin:0 50%; }
.cta-block__e--r{ right:var(--gutter); top:calc(18% + 24px); bottom:24px; width:2px; transform-origin:50% 0; }
.cta-block__e--b{ left:var(--gutter);  right:var(--gutter); bottom:24px; height:2px; transform-origin:100% 50%; }
.cta-block__e--l{ left:var(--gutter);  top:calc(18% + 24px); bottom:24px; width:2px; transform-origin:50% 100%; }
/* The mirrored block opens its cut the other way, so the pair rhymes on hover
   the same way it rhymes at rest. */
@media (hover:hover) and (pointer:fine){
  .cta-block--b .cta-block__box:hover{
    clip-path:polygon(0 26%, 100% 0, 100% 100%, 0 100%);
  }
}
/* The last words on the page, at the largest size on the page after the hero.
   6.4rem beside a 150px hero made the closing line read as a caption for the
   section above it. It gets the full width now, three lines, and the note and
   the button sit under it on their own row instead of competing for it. */
.cta-block--b .cta-block__h{
  font-size:clamp(3.4rem, 0.5rem + 7vw, 9.4rem);
  line-height:.86; letter-spacing:-.04em;
  position:relative; z-index:2;
  padding-left:clamp(20px,2.4vw,44px);
  padding-right:clamp(20px,2.4vw,44px);
}
.cta-block__foot{
  position:relative; z-index:2;
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:clamp(28px,5vw,90px);
  padding:0 clamp(20px,2.4vw,44px);
}
.cta-block__note{
  max-width:44ch;
  font-size:var(--fs-ui); line-height:1.5;
  color:rgb(23 27 28 / .68);
}
.cta-block--b .btn{
  flex:0 0 auto; position:relative; z-index:2;
  margin-bottom:clamp(4px,1svh,10px);
}
/* The local override that used to live here, the dark fill and the light swept
   label, is the page-wide default now. It was the best button on the page and
   the reason was measurable, so it stopped being a local exception. Nothing is
   needed here. See the note on .btn::after. */

/* ---- de plattegrond ------------------------------------------------------
   G11. The section's argument is a measurement and it was written out as a
   sentence, which is the one thing this page does not do with a measurement
   anywhere else. It is drawn now, in plan, at true relative scale: the unit is
   --u wide and stands calc(--u / 2) off the wall, because 30 is half of 60 and
   the ratio should be a property of the drawing rather than a number that
   happened to look right at 1440. Change --u and the drawing stays honest.

   All of it is built out of the page's own parts. The wall is a hatched
   hairline, the two dimension lines carry the same end caps the loader's axis
   does, and the readout is the typeplaat's key-value-unit stack. Nothing here
   is an SVG: a drawing whose proportions are enforced by calc() cannot drift
   out of scale the way a viewBox scaled to a column can.

   Contrast is measured against the bone, #E8E6DE, not against the page's dark.
   Solid --paper on it is 13.88:1. The mono labels run at .68, which composites
   to 5.40:1; .62 measures 4.49 and is one hundredth short, so it is not used.
   ------------------------------------------------------------------------- */
.fp{
  position:relative; z-index:2; min-width:0;
  padding:0 clamp(20px,2.4vw,44px);
}
.fp__plan{
  --u:clamp(92px, 9.5vw, 150px);
  position:relative;
  height:calc(var(--u) * 1.5 + 46px);
}
/* the wall: a solid line with the hatch a section drawing puts on the side you
   are not standing on */
.fp__wall{
  position:absolute; left:0; right:0; top:0; height:10px; display:block;
  background-image:
    linear-gradient(var(--paper), var(--paper)),
    repeating-linear-gradient(45deg, var(--paper) 0 1px, transparent 1px 6px);
  background-repeat:no-repeat;
  background-position:0 100%, 0 0;
  background-size:100% 2px, 100% 8px;
  transform-origin:0 50%;
}
.fp__wk,
.fp__gv,
.fp__dv,
.fp__wv{
  position:absolute; display:block;
  font-family:var(--font-mono); font-weight:450;
  font-size:10px; letter-spacing:.14em; line-height:1;
  color:rgb(23 27 28 / .68);
  white-space:nowrap;
}
.fp__wk{ right:0; top:16px; }
/* the standoff, dimensioned off the centre line of the unit */
.fp__gap{
  position:absolute; left:calc(var(--u) / 2); top:10px;
  width:1px; height:calc(var(--u) / 2);
  background:var(--paper); display:block;
  transform-origin:50% 0;
}
.fp__gap::before,
.fp__gap::after,
.fp__dd::before,
.fp__dd::after,
.fp__dw::before,
.fp__dw::after{
  content:''; position:absolute; background:var(--paper);
}
.fp__gap::before,
.fp__gap::after,
.fp__dd::before,
.fp__dd::after{ left:-3px; width:7px; height:1px; }
.fp__gap::before,
.fp__dd::before{ top:0; }
.fp__gap::after,
.fp__dd::after{ bottom:0; }
.fp__dw::before,
.fp__dw::after{ top:-3px; width:1px; height:7px; }
.fp__dw::before{ left:0; }
.fp__dw::after{ right:0; }

.fp__gv{
  left:calc(var(--u) / 2 + 9px);
  top:calc(10px + var(--u) / 4 - 5px);
}
/* the footprint itself. An outline with a light hatch, not a filled square: a
   plan shows an area, and a solid block would read as an object in elevation. */
.fp__unit{
  position:absolute; left:0; top:calc(10px + var(--u) / 2);
  width:var(--u); height:var(--u); display:block;
  border:2px solid var(--paper);
  background-image:repeating-linear-gradient(45deg,
    rgb(23 27 28 / .17) 0 1px, transparent 1px 8px);
}
.fp__dd{
  position:absolute; left:calc(var(--u) + 14px);
  top:calc(10px + var(--u) / 2); width:1px; height:var(--u);
  background:var(--paper); display:block;
  transform-origin:50% 0;
}
.fp__dv{
  left:calc(var(--u) + 23px);
  top:calc(10px + var(--u) - 5px);
}
.fp__dw{
  position:absolute; left:0; top:calc(10px + var(--u) * 1.5 + 13px);
  width:var(--u); height:1px;
  background:var(--paper); display:block;
  transform-origin:0 50%;
}
.fp__wv{
  left:0; width:var(--u); text-align:center;
  top:calc(10px + var(--u) * 1.5 + 24px);
}

.fp__k{
  margin-top:clamp(18px,2.4svh,28px);
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.22em;
  color:rgb(23 27 28 / .68);
}
.fp__v{
  display:flex; align-items:baseline; gap:.4em; margin-top:.34em;
  font-size:clamp(2.2rem, 1rem + 1.9vw, 3.4rem);
  font-weight:500; line-height:.98; letter-spacing:-.03em;
  color:var(--paper); font-variant-numeric:tabular-nums;
}
.fp__u{
  font-family:var(--font-mono); font-style:normal; font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.12em;
  color:rgb(23 27 28 / .68);
}
/* At 1200 and up the drawing takes a column of its own beside the closing
   sentence and bottom-aligns with its last line, which is where a drawing sheet
   puts its title block. Below that the box stays the flex column it already
   was and the plan sits under the headline, full width. 1200 rather than the
   page's own 1000: at 1000 the three headline lines are still ~530px wide and
   a second column would squeeze "waarschijnlijk" into a wrap, which would put
   two rendered lines inside one .ln mask. */
@media (min-width:1200px){
  .cta-block--b .cta-block__box{
    display:grid;
    grid-template-columns:minmax(0,1fr) minmax(0, clamp(200px, 20vw, 340px));
    column-gap:clamp(48px,6vw,110px);
    align-content:end; align-items:end;
  }
  .cta-block--b .cta-block__h{ grid-column:1; grid-row:1; }
  .fp{ grid-column:2; grid-row:1; padding-left:0; }
  .cta-block__foot{ grid-column:1 / -1; grid-row:2; }
}

/* =========================================== 11 #founders — ground P — 160svh */
/* 160svh was 1.6 viewports of box for about one viewport of content, and the
   surplus all fell after the form, which is the slab of empty page sitting
   between the last thing you can do and the footer. The content sets the
   height now; the padding sets the breathing room. */
#founders{
  padding:var(--pad-sec) var(--gutter) clamp(96px,14svh,190px);
  display:grid; grid-template-columns:39% 1fr; column-gap:8%;
  align-content:start;
}
.fo__left{ position:relative; padding-top:6svh; }
.fo__h{
  font-size:var(--fs-h3); font-weight:500; line-height:.96; letter-spacing:-.02em;
}
.fo__p{ margin-top:1.6em; font-size:var(--fs-lead); line-height:1.55; max-width:46ch; }
.fo__sig{
  width:100%; height:auto; margin-top:clamp(40px,6svh,84px);
  color:var(--ink);
  overflow:visible;
}
.fo__names{
  margin-top:1.2em;
  font-size:var(--fs-micro); font-weight:500; letter-spacing:.06em; color:var(--ink-2);
}

/* the form: no native chrome anywhere.
   The signature is 634px of column and the form is 324px, so the form sat at
   the top of its track with 311px of nothing under it and the page appeared to
   stop working well before the footer arrived. Distributing the form's own
   parts down the track puts the submit on roughly the signature's baseline,
   which is what makes the two halves read as one spread. */
.fo__right{
  padding-top:6svh; scroll-margin-top:84px;
  display:flex; flex-direction:column;
}
.fo__right .fm{ flex:1 1 auto; display:flex; flex-direction:column; }
.fo__right .fm__submit{ margin-top:auto; }
/* D1. The form attends the way the dB list attends and the way the deck attends:
   while one thing is being read, the rest of the block steps back. Three
   sections, one grammar, one hand. The dimming is written against the form's own
   direct children, so the intro, both fields, the consent line and the submit all
   answer, which is correct: a form looking at one field is not also asking you
   the other question.

   `is-attending` is written by main.js and never by CSS, on purpose. Without the
   script the form has no attend state at all rather than a half of one, and a
   `:focus-within` version would dim the block for a keyboard reader whose focus
   ring is the only thing telling them where they are. */
.fm > *{ transition:opacity .24s var(--ease-soft); }
.fm.is-attending > *:not(.is-focus):not(:focus-within){ opacity:.45; }

.fm__intro{
  font-size:var(--fs-ui); font-weight:500; letter-spacing:.06em; color:var(--ink-2);
  margin-bottom:clamp(40px,6svh,72px);
}
.fm__field{ position:relative; margin-bottom:44px; padding-top:26px; }
.fm__label{
  position:absolute; left:0; top:26px;
  font-size:var(--fs-ui); font-weight:600; letter-spacing:.02em;
  color:var(--ink-2);
  transform-origin:left top;
  transition:transform var(--d-fast) var(--ease-out), color var(--d-fast) var(--ease-out);
  pointer-events:none;
}
/* Every state below is mirrored in pure CSS, and the JS classes only ride
   alongside. Before this, `is-lifted` and `is-focus` existed nowhere but in
   main.js, so with JS off the label never left the line the input text sits on
   and a keyboard visitor typed straight over it. */
.fm__field.is-lifted .fm__label,
.fm__field:focus-within .fm__label,
.fm__field:has(.fm__input:not(:placeholder-shown)) .fm__label{
  transform:translateY(-26px) scale(.8); color:var(--ink);
}
.fm__field.is-focus .fm__label,
.fm__field:focus-within .fm__label{ color:var(--accent); }

.fm__input{
  display:block; width:100%;
  appearance:none; -webkit-appearance:none;
  background:transparent; border:0; border-radius:0;
  /* Narrowed from a blanket `outline:none`, which sat 490 lines after the global
     :focus-visible rule at equal specificity and so removed the only focus
     indicator on the two inputs. Now it only suppresses the mouse-click ring. */
  padding:0 0 14px;
  font-size:var(--fs-lead); font-weight:400; color:var(--ink);
  position:relative; z-index:1;
}
.fm__input:focus:not(:focus-visible){ outline:none; }
.fm__input::placeholder{ color:var(--paper-dim); opacity:0; transition:opacity var(--d-fast) var(--ease-out); }
.fm__field.is-focus .fm__input::placeholder,
.fm__field:focus-within .fm__input::placeholder{ opacity:1; }

.fm__line{
  position:absolute; left:0; right:0; bottom:0;
  height:1px; background:var(--line-alt);
  transform-origin:left bottom;
  transition:transform var(--d-fast) var(--ease-out), background var(--d-fast) var(--ease-out);
}
/* The accent does not appear on the line, it travels along it. A background
   swap tells you the field is focused; a wipe from the caret's own edge shows
   the field accepting focus, which is the difference the form is here for. */
.fm__line::after{
  content:''; position:absolute; inset:0;
  background:var(--accent);
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform .28s var(--ease-out);
}
.fm__field.is-focus .fm__line,
.fm__field:focus-within .fm__line{ transform:scaleY(2); }
.fm__field.is-focus .fm__line::after,
.fm__field:focus-within .fm__line::after{ transform:scaleX(1); }
/* the field answers the pointer before it is clicked */
@media (hover:hover) and (pointer:fine){
  .fm__field:hover .fm__line{ background:var(--ink-2); }
}
/* an error fills the whole line at once: it is a state, not a gesture */
.fm__field.is-error .fm__line{ background:var(--accent); }
.fm__field.is-error .fm__line::after{ transform:scaleX(1); }

.fm__err{
  min-height:1.4em; margin-top:10px;
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-ui); color:var(--accent);
}
/* The consent line. A real checkbox is still in the DOM, still focusable, still
   keyboard-operable; only the browser's drawing of it is gone. The box fills
   from its own bottom edge and the tick draws itself along its path, so
   consenting is the same gesture as the field lines above rather than a second
   idiom bolted onto the same form. */
.fm__consent{ position:relative; margin-top:clamp(20px,3svh,34px); }
.fm__check{
  position:absolute; left:0; top:2px;
  width:22px; height:22px; margin:0;
  appearance:none; -webkit-appearance:none;
  opacity:0; cursor:pointer;
}
.fm__consent-l{ display:flex; align-items:flex-start; gap:14px; cursor:pointer; }
.fm__box{
  position:relative; flex:0 0 22px;
  width:22px; height:22px; margin-top:1px;
  border:1px solid var(--line-alt);
  color:var(--accent-ink);
  transition:border-color var(--d-fast) var(--ease-out);
}
.fm__box::before{
  content:''; position:absolute; inset:0;
  background:var(--accent);
  transform:scaleY(0); transform-origin:50% 100%;
  transition:transform .3s var(--ease-out);
}
.fm__box svg{ position:relative; z-index:1; display:block; width:100%; height:100%; }
.fm__tick{
  stroke-dasharray:30; stroke-dashoffset:30;
  transition:stroke-dashoffset .34s var(--ease-out) .1s;
}
.fm__check:checked + .fm__consent-l .fm__box{ border-color:var(--accent); }
.fm__check:checked + .fm__consent-l .fm__box::before{ transform:scaleY(1); }
.fm__check:checked + .fm__consent-l .fm__tick{ stroke-dashoffset:0; }
.fm__check:focus-visible + .fm__consent-l .fm__box{ outline:2px solid var(--accent); outline-offset:3px; }
@media (hover:hover) and (pointer:fine){
  .fm__consent-l:hover .fm__box{ border-color:var(--ink-2); }
}
.fm__consent-t{
  font-size:var(--fs-ui); line-height:1.5; color:var(--ink-2); max-width:46ch;
}
.fm__consent.is-error .fm__box{ border-color:var(--accent); }
.fm__consent .fm__err{ margin-left:36px; }

/* G7, the size. `float` is ignored on a flex item, so `float:right` did nothing
   at all and the submit stretched to the full 687px of the form column while
   every other control on the page is 170px. That is the whole of "giant": not a
   type size, an alignment that never applied. `align-self:flex-end` is what
   `float:right` was reaching for, and it shrink-wraps the control at the same
   time.

   It stays the largest button on the page and it should: it is the only one that
   commits anything. But it is now the same species as the other three, at
   4px more height and about 60px more width than the hero CTA, which is emphasis
   rather than an outlier. */
.fm__submit{
  margin-top:clamp(24px,4svh,44px);
  align-self:flex-end;
  padding:15px 34px;
}
/* The press wipe sits ABOVE the shared hover fill and BELOW both label copies:
   fill 0, wipe 1, ink label 2, paper label 3, rail 4. It used to be at 2 with a
   `.fm__submit > span` rule lifting the labels over it, and that rule was
   written for the single <span> in the markup, which main.js replaces. Once it
   was replaced, `.fm__submit > span` (0,1,1) outranked `.btn__t2` (0,1,0) and
   forced the second label copy back into flow, so this button rendered at 106px
   of double-stacked label instead of 53px of register. That is the whole of
   F10's "giant, and its animation does not work": the copies were never on top
   of each other here, so there was nothing for the fill to recolour. */
.fm__submit::before{
  content:''; position:absolute; inset:0; z-index:1;
  background:var(--accent-ink);
  clip-path:inset(0 100% 0 0);
  transition:clip-path .35s var(--ease-inout);
}
.fm__submit:active::before{ clip-path:inset(0); }
/* both label copies, since either can be the visible one mid-hover */
.fm__submit:active .btn__t{ color:var(--accent); }

/* D2. The control answers in its own body. The word leaves upward through the
   mask the button already has, the same dark that answers a press closes behind
   it, and the tick is drawn in the space the word left. No text swap: a button
   that says "Verzonden" is a button telling you what happened, and a button that
   becomes a tick is the thing happening.

   `is-ok` is written by main.js for the two seconds between the click and the
   form leaving, and it is the only state in which the tick exists. The resting
   end state, which the stylesheet owns, is a button with an invisible tick in
   it, because the tick is a state and not a reveal. */
.fm__ok{
  position:absolute; inset:0; margin:auto; z-index:3;
  width:24px; height:24px;
  color:var(--accent);
  opacity:0;
  pointer-events:none;
}
.fm__submit.is-ok .fm__ok{ opacity:1; }
.fm__submit.is-ok::before{ clip-path:inset(0); }
/* the rail reads the same as a press does: the level full, the axis in accent */
.fm__submit.is-ok .btn__rail{ --rail-c:var(--accent); }
.fm__submit.is-ok .btn__lvl{ clip-path:inset(0 0 0 0); }

.fm__done{
  font-size:var(--fs-h3); font-weight:500; line-height:1.1; letter-spacing:-.02em;
  max-width:18ch;
}

/* ============================================== 12 #footer — ground P, dark */
/* `isolation:isolate` is here for the corner plot at the bottom of this file and
   for nothing else. Without it `#footer` is position:relative with z-index auto,
   which is not a stacking context, so the plot's `z-index:-1` would resolve
   against the ROOT and it would paint underneath the page's own ground layers.
   With it the plot sits behind every word in the footer and in front of nothing
   else. The footer itself still paints below the fixed `.grain` at z-index 1, so
   the emulsion runs over the plot exactly as it runs over everything. */
#footer{
  position:relative; isolation:isolate;
  min-height:70svh; padding:var(--pad-sec) var(--gutter) clamp(48px,7svh,90px);
  display:grid; grid-template-columns:1fr auto; align-content:end; gap:0;
}
/* the line the footer is built off, drawn edge to edge before anything below it
   arrives, the same opening move as the award and the first CTA */
.ft__rule{
  position:absolute; left:var(--gutter); right:var(--gutter);
  top:clamp(48px,7svh,90px); height:1px;
  background:var(--line);
  transform-origin:0 50%;
}
.ft__mark{
  grid-column:1; grid-row:1;
  font-size:clamp(4rem, 1rem + 12vw, 16rem);
  font-weight:500; line-height:.86; letter-spacing:.04em;
}
/* Five letters, five masks. The wordmark is the largest type on the page after
   the hero and it is the last thing anyone sees, so it is set letter by letter
   out of the floor rather than faded up as one block.

   Three nested spans, not two, and the reason is the bug class this build keeps
   hitting: the scrub writes an inline transform on `.ft__mi`, and an inline
   transform outranks any stylesheet rule, so a hover that also wrote transform
   on that element could never fire. The scrub owns `.ft__mi`, the pointer owns
   `.ft__mc`, and neither can delete the other. */
.ft__chars{ display:flex; }
.ft__m{ display:block; overflow:hidden; }
.ft__mi{ display:block; }
.ft__mc{
  display:block;
  transition:transform .42s var(--ease-out), color .3s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .ft__m:hover .ft__mc{ transform:translateY(-.1em); color:var(--accent); }
}
/* The claim's reveal comes in from x:34. At 1440 the 72px gutter absorbs that;
   at 390 the gutter is 24 and the claim is the full column, so the from-state
   put its right edge 10px past the viewport and the whole page could be dragged
   sideways by 10px until the footer had revealed. `clip` rather than `hidden`:
   it does not create a scroll container, so nothing about the sticky pins
   further up the page changes. Measured with `probe.mjs` at 390. */
#footer{ overflow-x:clip; }
.ft__claim{
  grid-column:2; grid-row:1; align-self:end;
  text-align:right; font-size:var(--fs-lead); color:var(--ink-2);
  padding-bottom:.35em;
}
.ft__claim .sq{ width:22px; height:22px; margin-left:16px; margin-right:0; }
/* ---- de typeplaat -------------------------------------------------------
   G12. The footer was a wordmark, a nav, a URL and a legal line: four items and
   about a viewport of ground under them, which is a page that stops rather than
   a page that ends. What a manufacturer puts at the end of its own site is the
   plate off the back of the machine, and this page has spent twelve sections
   producing exactly those numbers. Nothing here is asserted for the first time.
   Every value is stated somewhere above it and the footer is where they are
   finally read together, which is a different act from reading them one at a
   time across 20.000 pixels.

   The plate hangs off a graduated edge, the same axis the loader plots 60,0
   against 50,8 on, the same rail every button carries and the same one the dock
   is ruled with. One accent value on the whole plate, 50,8, which is the number
   the site exists to state.

   Four cells across one row, down from eight across two. See the note in
   index.html for which four went and why. The grid is `repeat(4, 1fr)` and no
   rule in this file selects a cell by index, so the second row simply stops
   existing; `row-gap` and the two-column fallbacks below still hold for the
   widths where four cells wrap.
   ------------------------------------------------------------------------- */
.ft__plate{
  position:relative;
  grid-column:1 / 3; grid-row:2;
  margin-top:clamp(56px,9svh,112px);
  padding-top:clamp(26px,3.4svh,44px);
}
.ft__ax{
  --rail-c:rgb(232 230 222 / .34);
  position:absolute; left:0; right:0; top:0; height:7px;
  display:block; pointer-events:none;
  /* ticks hanging down off the axis, 46px apart: the coarsest graduation on the
     page, because this rail runs the full width of the footer and the dock's own
     30px pitch would read as hatching at this length */
  background-image:
    repeating-linear-gradient(90deg, var(--rail-c) 0 1px, transparent 1px 46px),
    linear-gradient(var(--rail-c), var(--rail-c));
  background-repeat:no-repeat;
  background-position:0 0, 0 0;
  background-size:100% 7px, 100% 1px;
  transform-origin:0 50%;
}
.ft__plate-h,
.ft__sub{
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.24em; color:var(--paper-dim);
}
.ft__plate-h{ margin-bottom:clamp(30px,4.4svh,56px); }
.ft__sub{ margin-bottom:clamp(22px,3svh,36px); }

.ft__cells{
  display:grid; grid-template-columns:repeat(4, minmax(0,1fr));
  column-gap:clamp(20px,3vw,44px); row-gap:clamp(30px,4.4svh,52px);
}
.ft__cell{ position:relative; padding-top:16px; }
/* the same hairline the two ledgers upstream hang off, so a cell here and a fact
   at #cta-1 are the same object at two sizes */
.ft__cl{
  position:absolute; left:0; top:0; display:block;
  width:100%; height:1px; background:var(--line);
  transform-origin:0 50%;
  transition:background .3s var(--ease-out);
}
.ft__ck{
  display:block;
  font-family:var(--font-mono); font-weight:450;
  font-size:11px; letter-spacing:.16em; color:var(--paper-dim);
  transition:color .26s var(--ease-out);
}
.ft__cv{
  display:flex; align-items:baseline; gap:.42em; margin-top:.55em;
  font-size:clamp(1.6rem, .7rem + 2.1vw, 2.8rem);
  font-weight:500; line-height:.94; letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
}
.ft__cu{
  font-family:var(--font-mono); font-style:normal; font-weight:450;
  font-size:var(--fs-micro); letter-spacing:.14em; color:var(--ink-2);
  white-space:nowrap;
}
.ft__cn{
  display:block; margin-top:.85em;
  font-size:var(--fs-micro); line-height:1.5; color:var(--ink-2);
}
.ft__cell--hit .ft__cv{ color:var(--accent); }
.ft__cell > *:not(.ft__cl){ transition:transform .3s var(--ease-out), color .26s var(--ease-out); }
@media (hover:hover) and (pointer:fine){
  .ft__cell:hover .ft__cl{ background:var(--accent); }
  .ft__cell:hover .ft__ck{ color:var(--accent); }
  .ft__cell:hover > *:not(.ft__cl){ transform:translateX(9px); }
}

/* the two columns under the plate: what certified it, and how to reach the two
   people who built it. Same ledger, one rank smaller. */
.ft__cols{
  grid-column:1 / 3; grid-row:3;
  margin-top:clamp(52px,8svh,96px);
  display:grid; grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);
  column-gap:clamp(40px,7vw,140px); row-gap:clamp(44px,6svh,72px);
}
.ft__list{
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
  column-gap:clamp(28px,4vw,64px); row-gap:clamp(26px,3.6svh,42px);
}
.ft__list li{ position:relative; padding-top:14px; }
.ft__lk{
  display:block;
  font-size:var(--fs-ui); font-weight:600; letter-spacing:.01em; color:var(--ink);
}
.ft__lv{
  display:block; margin-top:.5em;
  font-size:var(--fs-micro); line-height:1.55; color:var(--ink-2); max-width:36ch;
}
.ft__list li > *:not(.ft__cl){ transition:transform .3s var(--ease-out), color .26s var(--ease-out); }
@media (hover:hover) and (pointer:fine){
  .ft__list li:hover .ft__cl{ background:var(--accent); }
  .ft__list li:hover > *:not(.ft__cl){ transform:translateX(9px); }
}
/* The ask. Not a fifth button: the page already carries four, and a footer that
   ends on a control competing with the form 800px above it is asking the same
   question twice. A line at heading scale with a rule under it that draws on the
   pointer, in the nav's own underline grammar. */
.ft__ask-l{
  font-size:clamp(1.35rem, .7rem + 1.4vw, 2.1rem);
  font-weight:500; line-height:1.18; letter-spacing:-.02em;
}
.ft__ask-l a{ position:relative; display:inline-block; padding-bottom:.16em; }
/* the rule is there at rest, in the page's hairline; the accent travels along it
   under the pointer rather than replacing its colour, which is the same
   distinction the field lines in the form are built on */
.ft__ask-l a::after,
.ft__ask-l a::before{
  content:''; position:absolute; left:0; right:0; bottom:0; height:1px;
}
.ft__ask-l a::after{ background:var(--line-alt); }
.ft__ask-l a::before{
  background:var(--accent); z-index:1;
  transform:scaleX(0); transform-origin:0 50%;
  transition:transform .42s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .ft__ask-l a:hover::before{ transform:scaleX(1); }
}
.ft__ask-p{
  margin-top:1.25em;
  font-size:var(--fs-micro); line-height:1.6; color:var(--ink-2); max-width:38ch;
}

.ft__nav{
  grid-column:1; grid-row:4; margin-top:clamp(48px,7svh,88px);
  display:flex; gap:40px; flex-wrap:wrap;
}
.ft__nav a{ font-size:var(--fs-ui); font-weight:500; position:relative; padding-bottom:2px; }
.ft__nav a::after{
  content:''; position:absolute; left:0; right:0; bottom:0; height:1px;
  background:currentColor; transform:scaleX(0); transform-origin:100% 50%;
  transition:transform .42s var(--ease-out);
}
@media (hover:hover) and (pointer:fine){
  .ft__nav a:hover::after{ transform:scaleX(1); transform-origin:0% 50%; }
}
.ft__url{
  grid-column:2; grid-row:4; align-self:end; text-align:right;
  margin-top:clamp(48px,7svh,88px);
  font-size:var(--fs-ui); font-weight:500;
}
/* the anchor holds a block-level mask now, so it has to be a block box itself */
.ft__url a{ display:inline-block; }
/* The gap above the copyright line was clamp(40px,7svh,90px), 63px at 1440x900,
   which is the SAME interval as the one above the nav and the url. Read down the
   footer, its four vertical intervals were 81, 72, 63, 63: the last line of the
   page was separated from the page by as much air as the nav block that precedes
   it, so the footer did not close, it just stopped one row later.

   It is the smallest interval in the footer now, by a clear margin rather than by
   a pixel: 81 / 72 / 63 / 23 at 1440x900. The line is no longer a block of its
   own with air around it, it is the underscore beneath the one above it. */
.ft__legal{
  grid-column:1 / 3; grid-row:5; margin-top:clamp(18px,2.6svh,34px);
  font-family:var(--font-mono); font-weight:450;
  font-size:var(--fs-micro); color:var(--paper-dim);
}

/* The polar directivity plot that stood in the bottom right corner was removed
   on client review 2026-07-29: the bloom read as a colour wash behind the
   footer's text blocks and the lobe read as a ball, neither of which is what a
   measurement drawing is supposed to look like. Its markup, its CSS and its
   `INITS.footer` sweep are all gone. The footer ends on type and the graduated
   axis, with no colour behind it. */

/* ==========================================================================
   Breakpoints. Four, each with a stated reason in design-spec.md 8c.
   ========================================================================== */

/* 700 to 999: pins released, deck stacks, takeover shortens, shader off */
@media (max-width:999px){
  /* released pin, so the whole height IS the travel. 180svh gave the takeover,
     the line swap and the drain-out 80svh between them. */
  #regen{ height:280svh; }
  /* Full width and centred here, so the caption cannot sit beside it the way it
     does on desktop. It goes below instead, on its own band. */
  .regen__lines{ text-align:center; justify-self:stretch; bottom:15%; }
  .regen__cap{ left:0; right:0; text-align:center; bottom:6%; }

  #proces{ height:auto; padding:var(--pad-sec) var(--gutter); }
  .pr__pin{ height:auto; overflow:visible; }
  /* The head is the absolutely positioned thing here, not the h2 inside it.
     Leaving it absolute stacked the whole stage list underneath the intro. */
  .pr__head{ position:static; width:100%; margin-bottom:clamp(48px,7svh,96px); }
  .pr__h{ position:static; }
  /* No horizontal scrub under 1000px, so --grow never moves and the headroom
     reserved for the numeral's growth is 17px of nothing. */
  .pr__i{ margin-bottom:1.1em; }
  .pr__row{
    position:static; flex-direction:column; gap:clamp(56px,9svh,110px);
    transform:none !important;
  }
  .pr__st{ width:100%; flex:0 0 auto; }
  /* The deck is a column here, so there is no shared index line for the negative
     margin to restore; left in, it would eat 42px of the gap between 05 and 06
     and hang the lime card off the bottom of the card above it. The padding
     stays, the pull does not. */
  .pr__st--cta{ margin-top:0; }

  /* Two columns put "2026" at 64px into a 120px track beside a paragraph. */
  .award{ grid-template-columns:1fr; row-gap:clamp(18px,2.4svh,32px); }
  .award__eyebrow,.award__year,.award__h,
  .award__p,.award__facts{ grid-column:1; grid-row:auto; }
  .award__facts{ margin-top:clamp(8px,1.5svh,20px); }

  #bewijs{ grid-template-columns:1fr; }
  .bw__h,.bw__marg,.bw__p,.bw__still{ grid-column:1; grid-row:auto; }
  /* The award kept `grid-row:3` here, which in a one-column grid pins it to the
     third row and pushes the paragraph past it: on mobile the block landed
     between the margin figures and the prose that sets it up. Auto-placed now,
     and the rule is dropped rather than left floating a row-gap above nothing. */
  .award{ grid-column:1; grid-row:auto; margin-top:clamp(40px,6svh,80px); }
  .award__rule{ display:none; }
  .bw__marg{ text-align:left; flex-direction:row; flex-wrap:wrap; gap:clamp(28px,5vw,64px); }
  .bw__still{ max-width:100%; }

  #founders{ grid-template-columns:1fr; row-gap:clamp(72px,12svh,140px); min-height:0; }

  /* Four measurement columns need 200px each to hold "60x60x90" without a wrap,
     and below 1000 there are not 800 of them left after the gutter. Two, so the
     plate stays a plate rather than becoming a list. */
  .ft__cells{ grid-template-columns:repeat(2, minmax(0,1fr)); }
  .ft__cols{ grid-template-columns:1fr; }

  .cta-block--a .cta-block__box{ grid-template-columns:1fr; row-gap:clamp(36px,5svh,56px); }
  /* the note and the button stop competing for one line and take two */
  .cta-block__foot{ flex-direction:column; align-items:flex-start; gap:clamp(24px,4svh,36px); }

  .dock__nav{ gap:22px; }
  .dock__mark{ display:none; }
}

/* under 700: three-line hero, one-column founders, figure row wraps to 3+3 */
@media (max-width:699px){
  :root{ --fs-body:16px; --fs-lead:18px; }
  .hero__type{ bottom:16%; }
  .hero__lead{ max-width:32ch; }
  .hero__cta{ top:auto; bottom:5%; right:var(--gutter); }
  .hero__video{ object-position:62% 50%; }

  .db{ top:14%; height:62%; --stack:.62svh; }
  .geluid__h{ max-width:16ch; font-size:var(--fs-micro); }
  /* One narrow column, so the unit takes its own line under the number rather
     than eating half the width the numeral needs. The lane and the still right
     edge survive; only the stacking changes. */
  .db__ro-u{ display:block; margin-left:0; margin-top:.5em; }

  .tk__row{ grid-template-columns:1fr; gap:clamp(40px,7svh,72px); }
  .tk__body{ margin-top:-8svh; }

  .bs__row{ gap:clamp(28px,6vw,44px); }
  .bs__f{ flex:0 0 calc(33.333% - 24px); }

  #dock{ height:56px; }
  .dock__nav{ display:none; }
  /* One column means five rows. Moving the right-hand pair to column 1 without
     re-assigning grid-row drops each of them onto the cell already occupied by
     its row partner, which is the mark under the claim and the nav under the url. */
  #footer{ grid-template-columns:1fr; }
  .ft__mark { grid-column:1; grid-row:1; }
  .ft__claim{ grid-column:1; grid-row:2; text-align:left; margin-top:1.2em; padding-bottom:0; }
  /* One column, so a `grid-column:1 / 3` span would open a second implicit column
     and drag the whole footer off its own gutter. Both full-width blocks are
     re-placed rather than left to span into nothing. */
  .ft__plate{ grid-column:1; grid-row:3; }
  .ft__cols { grid-column:1; grid-row:4; }
  .ft__nav  { grid-column:1; grid-row:5; gap:24px 32px; }
  .ft__url  { grid-column:1; grid-row:6; text-align:left; margin-top:1.4em; }
  .ft__legal{ grid-column:1; grid-row:7; }
  .ft__list { grid-template-columns:1fr; }
  /* the plate's own notes wrap to three lines at this width, so the cells get
     their vertical rhythm back rather than colliding */
  .ft__cells{ row-gap:clamp(34px,5svh,52px); }
  .fm__submit{ float:none; width:100%; align-self:stretch; }
}

/* ==========================================================================
   Reduced motion. Keeps opacity and colour, drops travel, scale, every scrub.
   Not inert: every element is already at its final state, which lives in CSS.
   ========================================================================== */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
  #dock{ transform:translateY(0); opacity:1; pointer-events:auto; }
  .db{ --spread:1; }
}
