/* ============================================================
   THE ACTS — the scroll composition, as a stylesheet.

   THERE ARE FOUR OF THEM ON THE LANDING PAGE AND FIVE IN HERE. Act 5, the
   map, moved to patterns/expertise.html and stands under the four fields
   there; its CSS did not move with it, because the prototype below still
   draws it and two copies of a measured camera is the drift this whole file
   exists to prevent. So the expertise page loads this stylesheet for the
   .map* rules and nothing else reaches it: every other act is scoped to
   .sp-, .sp4-, .lp- or .act-rail, and that page carries none of them.

   IT LIVED IN ONE PAGE'S <style> FOR FIVE ACTS AND THAT IS ONE ACT TOO MANY.
   Two thousand lines of page-local CSS is not a prototype's working notes any
   more, it is the composition itself, and the moment a second page wants it
   the choice is a stylesheet or a copy. A copy of this would be two timings
   that cannot stay equal — the thing every generator in this repo exists to
   prevent — so it is a stylesheet, loaded by the prototype it was built in,
   by the landing page four of the acts ship on and by the expertise page the
   fifth does. One source, three consumers, and the lab and the shipped pages
   cannot drift.

   WHAT IS HERE: acts 1 and 2 (the field and the root, on --sp), act 3's
   page-local additions to .cf-pin, act 4 (the plates, on --sp4) and act 5
   (the map, on --map). What is NOT here is anything that names a prototype:
   the back-link chip and the lab's own page header stay where they were.

   THE BASE TIER COMES FIRST AND THE GATED TIER SECOND, throughout, and that
   order is load-bearing rather than tidy — see the note above the @supports
   block. At equal specificity the cascade decides on source order, and a base
   rule authored after the gated one wins in both tiers. It has cost two
   defects in this file already.
   ============================================================ */

  /* ============================================================
     STATEMENT -> PROZESS, REBUILT CLEAN. 2026-07-28.

     The first version of this page transplanted the landing page's
     2,174 lines of accreted choreography and inherited every layer
     of it: a fake hold built from translate ramps, anchor chains
     stretching one drawing between two sections, a dual trunk/stem
     tier, and windows in four different units. Daniel's recording
     of it showed a split field, orphan junction dots and a missing
     root. The layers were the bug. It is one mechanism now:

       ONE STAGE   .sp-track is the scroll budget (640vh), .sp-stage
                   is position: sticky for its whole length — a real
                   hold, not a translate ramp cancelling scroll.
       ONE
       TIMELINE    --sp, the track's own view timeline. Every part of
                   acts 1 and 2 reads it in `contain` percentages,
                   which map linearly to scroll pixels. No cover-of-
                   subject arithmetic, no per-viewport fitting.
       TWO
       LAYERS      the field is the stage's full-bleed backdrop
                   (gen-proto-field.py owns it); the root is a static
                   column above it, sized BY the stage (--sp-measure)
                   so it always fits the viewport it plays in. The two
                   meet at one measured point — the trunk's head,
                   published as --ux/--uy by the aim script. No
                   anchor(), no second trunk, no camera pan.

     THE SCRIPT, in contain % of the track:
        4 - 14    the lattice resolves, full bleed
        6 - 26    ACT 1a: the sensors stream in from the four corners
                  to their crossings (--m stagger, core first)
       26 - 36    ACT 1b: arrived, the field pulses twice in place
       32 - 40    the claim bows out with the field: the statement
                  dissolves into the drawing that answers it
       36 - 48    ACT 1c: the field converges on the trunk's head
                  (--ux/--uy, published at runtime), core first
       36 - 87    ACT 2: the root draws, front = head + l*c, exactly
                  the algebra check-flow-chain.py holds — the light
                  leaves the trunk as the first sensors arrive on it,
                  the contour lands 86.88, the light fades by 88.88
       89 -100    still: the finished drawing holds one beat
       then the stage releases and ACT 3 is the cards' own .cf-pin
       below, unchanged, with the lectern frame drawn on its entry.

     ACT 4 comes AFTER the cards, which is where the review put it, and
     runs on a third timeline, --sp4, 400vh, one plate to each half:

        t0+ 2 - 18   the four edges draw, clockwise from the top-left,
                     in the family's ramp
        t0+ 6 - 26   the ramp fades off each edge it has closed, leaving
                     the CF-Schwarz contour standing
        t0+12 - 30   the photograph sweeps in under the frame
        t0+ 4 - 45   the words type in the column beside it
        t0+45 - 50   the still
        t0+46 - 52   the plate leaves and the next one has the stage

     ACT 5 is a fourth timeline, --map, 500vh, three beats to a
     third each: the two offices, the markets, the assets, with the
     camera pulling back between them and holding while each is read.

     None of the five acts overlaps another: different sections,
     different timelines, and the rule over "Was wir machen" is
     ordinary flow content that never travels.
     ============================================================ */


  /* ---- the stage's own layout, valid at every tier ---- */
  .sp-stage__inner {
    display: grid;
    gap: var(--space-8);
    align-items: center;
  }
  .sp-drawing { min-width: 0; }
  .sp-drawing .lp-flow { display: block; width: 100%; height: auto; }
  /* THE GROUND LAYER. The field is the stage's backdrop, not the figure's
     first row: absolutely placed under the content (the container is the
     positioned sibling above it), stretched to the stage and cropped by its
     own `slice`, so the lattice reaches every corner at every admitted
     viewport. In the fallback tier the stage is ordinary flow and the layer
     simply papers its box — nothing depends on the pin.

     AND PAPERING ITS BOX IS THE THING THAT STOPPED BEING TRUE, one level up
     from where .lp-flow-data records the identical fault. That comment reads:
     `inset: 0` "was only ever true by coincidence … which held while the last
     child was the drawing and stopped holding the moment the copy below joined
     the flow". The numeral layer was fixed and the ground it sits on was not.
     Outside the gate the stage stacks — drawing, then .sp-say, then act 2's
     claim, then the rule — so `inset: 0` is 901 px of ground at 375 against a
     172 px drawing, and the surplus 729 px is act 2's copy with act 1's field
     behind it. Measured on the shipped render, beads and halos landing on the
     copy: five objects at 320, six at 375, four at 600, six at 768, five at
     900. The 104 px halo at 375 sits on "Von oben nach unten gelesen." and the
     two 84 px halos on "rechnen lässt" and "und entwickelt".

     CLIPPED AND NOT SHORTENED, because the box is the crop. `slice` throws the
     surplus axis away, so the box's ratio is what decides which units survive
     and at what scale: at 375 the present box is 375 x 901, height binds, and
     the lattice renders at 1.001 — a bead is the size a bead is. Give the box
     the drawing's band instead and width binds at 0.234, so every bead on a
     phone comes out a quarter of the size it has on a desktop, which is the
     opposite of what a full-bleed ground is for. check-slice-crop.py names this
     layer in UNREGISTERED_OK for the same reason — "no aspect-ratio and cannot
     have one" — so the fix must leave the box alone and take the ink.

     THE BAND IS THE DRAWING'S OWN HEIGHT, not a chosen number: .lp-flow is
     viewBox="0 0 1200 620" at width 100 % of the container's inner box, so its
     used height IS that width x 620/1200 — the same arithmetic, from the same
     ratio, that .lp-flow-data's note settles on for the numerals. Below
     --container-max the inner box is 100vw - 2 gutters. Verified against the
     rendered svg: 144.7 vs 145 at 320, 172.4 vs 172 at 375, 275.9 vs 276 at
     600, 353.1 vs 353 at 768, 413.8 vs 414 at 900.

     A HARD EDGE AND NOT A FADE, because there is nothing to fade over: .sp-say
     opens flush with the flow's foot at every one of those widths, so a band of
     air below the horizon would be a band of ink on the first line of the copy.
     The feet stand on the edge, which is where the gated tier's
     `align-content: end` puts them too.

     WHERE THE STACK IS NOT A STACK this changes nothing that was wrong. Above
     the 56rem container fold the copy sits BESIDE the drawing and shares its
     ground on purpose — the arrangement the gate itself uses — and there the
     band is generous rather than exact, because the drawing is 3fr of the row
     and not all of it. Nothing overlapped there before (measured 0 at 1023) and
     nothing does now; the clip only ever removes ground the stack put under
     prose. Inside the gate it is off altogether. */
  .sp-stage {
    position: relative;
    /* Declared on the stage and not on the layer, because ALL THREE inset
       layers read it — see .sp-annots-fig, whose notes end where this view
       does, and .sp-stage::before, which is the floor they stand on. */
    --sp-ground: calc((100vw - var(--gutter) * 2) * 620 / 1200);
    /* max(), so a band taller than the box never asks for a negative inset. */
    --sp-ground-clip: inset(0 0 max(0px, 100% - var(--sp-ground)) 0);
  }
  .sp-field {
    position: absolute; inset: 0; width: 100%; height: 100%; display: block;
    clip-path: var(--sp-ground-clip);
  }
  /* THE FLOOR, AND IT IS base.css's AND NOT THIS FILE'S. The markup carries
     .cf-ground on the stage now; everything that draws the lattice — the two
     rakes, the 96 x 48 cell, the 10 % ink ceiling and its theme flip — is the
     material's, declared once in base.css against tokens.css section 8b. What
     is left here is the one thing the material cannot know: WHERE THIS STAGE'S
     GROUND ENDS.

     IT IS A THIRD INK LAYER INSET TO .sp-stage, so it takes the band for
     exactly the reason the other two do. .cf-ground::before is
     `position: absolute; inset: 0` of its originating element, which is the
     stage — the same box, and the same box that grew to hold act 2's prose
     when the copy moved out from under the drawing. Unclipped, the floor ran
     under those four paragraphs at every width below the pin gate: the fault
     check-ground-band.py exists for, arriving through a new layer rather than
     through a new stylesheet. It is in that script's INK_LAYERS beside
     .sp-field and .sp-annots-fig, and the release inside the pin gate reaches
     it through the same custom property. */
  .sp-stage::before { clip-path: var(--sp-ground-clip); }
  /* THE HORIZON IS THE ONE-COLUMN STACK'S, AND 64rem IS ALREADY THE SYSTEM'S
     NAME FOR IT. The band above is exact while the drawing is the whole row —
     verified to the pixel at 320, 375, 600, 768 and 900 — and generous once
     .cf-statement takes its two-column form, because there the drawing is 3fr
     of the row and not all of it. Generous is not free: at 1023 the horizon
     falls 198 px below the drawing's foot, mid-field, and bisects two labels
     ("S07 95 %", "S06 17.7 bar") that were whole before. So the clip belongs to
     the stack and has to end where the stack does.

     Where the stack ends is a CONTAINER question — .cf-statement folds at a
     container of 56rem — and this layer is a sibling of .container, so it
     cannot ask. The register in tokens.css has already had this argument and
     recorded the answer: "64rem / 1024 … that fold is the container 56rem
     above, reached at a viewport of about 1007. A px gate cannot track a rem
     fold". The pin gate makes exactly this approximation and is registered
     making it. Reusing the number rather than deriving 62.921rem from the
     gutter keeps this a second consumer for an existing threshold instead of a
     new row, and it mis-serves precisely the 1007–1023 window the pin gate
     already mis-serves — the same 17 px, for the same reason, in the same
     direction. Two bisected labels in that window; the alternative was 107 px
     of viewport with a sensor field behind its prose.

     OUTSIDE THE @supports GATE ON PURPOSE, so a reader at 1280 with reduced
     motion — who is in the flow tier and whose copy sits beside the drawing,
     not under it — gets the same whole ground a reader at 1280 without it does.
     → tokens.css "THE BREAKPOINT REGISTER", 64rem */
  /* AND THE BAND IS RE-DERIVED ABOVE THE FOLD RATHER THAN SWITCHED OFF, because
     "the copy sits beside the drawing" is true of ONE of the two blocks of copy
     on this stage and this rule was written against that one.

     .cf-statement__text — act 1's claim — does sit beside the drawing above the
     fold, in grid column 2, and it was measured clear. .sp-say — act 2's four
     paragraphs — does not. Its absolute placement over the root is declared
     inside the @supports block (see THE OVERLAY, ~1400), so in every tier that
     does not pin it is ordinary flow INSIDE the figure column, directly under
     the drawing, at every width. `--sp-ground-clip: none` therefore handed the
     full-bleed field and all twenty-one callouts back to exactly the box that
     the clip below the fold exists to keep them out of.

     Measured under prefers-reduced-motion, the stage scrolled into view, count
     of .cf-annot__label boxes intersecting a .sp-say paragraph, and the largest
     single intersection:

                    before            after
       1024 x 900   6 labels, 1558 px²   0
       1024 x 700   6 labels, 1558 px²   0
       1280 x 900   6 labels, 1558 px²   0
       1440 x 900   7 labels, 1512 px²   0
       1920 x 1080  2 labels,  820 px²   0

     At 1440 "S16 2 565 rpm" lands on the lead "Tausende Sensoren, ein Strom."
     and "S06 17.2 bar" on "Von oben nach unten gelesen." — a mono readout
     through the middle of a bold line, at full opacity, in the tier a reader
     chose because they wanted less. The same six land on the same paragraphs in
     prototypes/statement-to-process.html, which shares this sheet.

     SO THE HORIZON MOVES TO WHERE THE DRAWING'S FOOT ACTUALLY IS. Above the
     fold .sp-stage__inner is minmax(0,3fr) minmax(0,2fr) with --space-8 between
     them (~727 in this file), so the drawing is 3/5 of the container's inner box
     less the gap, and its used height is that width x 620/1200 — the same
     ratio, from the same viewBox, that the one-column band is built on. The
     container's inner box is min(100vw - 2 gutters, --container-max), which is
     the .container rule's own arithmetic and not a new one. Verified against the
     rendered svg's height, reduced motion:

       1024   272.6 computed   273 rendered      1440   386.9   387
       1100   293.6            294               1600   386.9   387
       1280   343.2            343               1920   386.9   387

     — and .sp-say opens --space-12 below that foot at every one of them, so the
     band clears the copy by 48 px rather than by a rounding. The 1007–1023
     window this shares with the pin gate is unchanged: it is the same
     approximation in the same direction, and now BOTH sides of it clip.

     WHAT THIS INHERITS AND DOES NOT FIX, so the next reader does not re-derive
     it. The fold is a CONTAINER question and this is a MEDIA query, and the two
     disagree once the root font size moves: `rem` in a media prelude is always
     16 px, `rem` in the container query is the reader's own. At a 24 px root and
     1024 wide the container's inner box is 911 px against a 56rem fold that is
     now 1344, so .sp-stage__inner is ONE column and the drawing is the whole row
     again — while this rule, keyed on a prelude that still reads 64rem as 1024,
     hands it the two-column band. Measured, drawing's foot against the band:

       root 16   1024  273 / 272.6    1280  343 / 343.2    1440  387 / 386.9
       root 24   1024  471 / 267.6    1280  589 / 338.3    1440  601 / 382.4
       root 32   1024  471 / 262.7    1280  507 / 333.3    1440  507 / 377.5

     So above ~20 px the field and the notes stop short of the drawing's foot
     rather than past it, and the lower part of the root stands on unlatticed
     wash. That is the wrong edge of the same approximation, and it is strictly
     the better half of it: the copy is clear at every root size in the table
     (.sp-say opens at 543, 661, 673, 567, 603, 603 — below every band above),
     which is what this clip is for, and the release it replaces put twenty-one
     opaque callouts on that copy at all three root sizes.

     IT CANNOT BE FIXED WITH A CONTAINER QUERY FROM HERE, for the reason stated
     70 lines up: .sp-field and .sp-annots-fig are SIBLINGS of .container, which
     is the query container (base.css, `container: layout / inline-size`), so
     they are outside the box that knows where the fold is. Closing it means
     giving the stage a container of its own or moving the ink layers inside
     .container — a DOM change, not a declaration, and a bigger one than the
     defect above is worth. → for the gate-boundary lane. */
  @media (min-width: 64rem) {
    .sp-stage {
      --sp-ground: calc(
        (min(100vw - var(--gutter) * 2, var(--container-max)) - var(--space-8))
        * 3 / 5 * 620 / 1200);
    }
  }
  .sp-stage > .container { position: relative; }
  /* `.sp-field__lattice { opacity: .26 }` STOOD HERE AND IS GONE WITH THE
     GROUP IT DIMMED. It was the drawn lattice's whole answer to "how dark may
     a floor be", arrived at by eye against black dashes: check-authored-opacity
     .py's own header measured what it bought — 0.339 rendered luminance under
     the root's numerals, 3.39:1 from the figure standing on it — and had to be
     written to make it render at all, because a keyframe was overruling it.
     The material asks the question the other way round and answers it with a
     contrast floor: --field-ink is 10 % black because 12 % puts
     --text-secondary at 4.51:1 on CF-Grau and 16 % fails it outright
     (tokens.css 8b). One number, measured, in the file that owns the ink. */
  /* THE BEAD IS BUILT LIKE THE SOURCE IT RUNS INTO. .lp-flow__orb — the body
     at the trunk's head, two hundred lines below — is a body of the family's
     ramp with a contour on it and a spill around it. A sensor is the same
     object at a twentieth of the area, which is the truth of the drawing: act
     1 is twenty-one of these merging into that one. The contour is what makes
     the field legible at all (see the ramp's own note in the markup); the
     spill is the halo circle, generated at 2.6x the bead's own radius, so a
     hot bead at the focus reaches nearly three times as far as a cool one at
     the rim.

     THE BEAD IS A <circle> AGAIN. It was a <path> morphing its own `d` for
     one revision, on the review's ask to leave the sphere shape; the next
     review took it back — "go back to the spherical design, without border
     morphing, they are not following the design system". It was right. A
     lattice, a contour and one light is what an illustration in this system
     is, and an outline generated from two travelling harmonics is none of the
     three. The drawing is scrubbed by the reader now and nothing in it is
     driven by a clock; the only thing that moves on its own is the readout.

     The stroke is in USER UNITS, matching the lattice's own 1, so the beads
     and the ground they stand on keep one weight through every slice scale.

     AND THE CONTOUR IS THE ROOT'S BLACK, on the review's instruction — "the
     border should be black like the root system". --border-strong is
     var(--cf-schwarz), the same token .lp-flow__seg strokes with and
     .lp-flow__node fills with, so a bead and the drawing it feeds are now
     bounded in one weight and one colour. This reverses the argument the orb's
     note below makes for the quieter rgba(0,0,0,0.24) — that a black ring on a
     light reads as a washer sitting on it. That argument was about the ORB,
     which is 34 units of nearly white lime at the centre of an empty stage;
     a bead is a tenth of the area on a busy lattice, and there the quiet ring
     was losing the fight with the ground behind it. The orb keeps its grey,
     which is what the review asked for when it asked for the orb. */
  .cf-stmt-sensor__bead {
    stroke: var(--border-strong);
    stroke-width: 1;
    transform-box: fill-box;
    transform-origin: center;
  }

  /* ---- THE ORB'S GLOW, AND WHY IT IS NOT var(--glow-light) ----

     --glow-r DOES NOT REACH THE TOKEN, which is why the shadows are built
     here from the palette rather than read from it. tokens.css declares
       --glow-r: 12px;
       --glow-light: drop-shadow(… calc(var(--glow-r) * 0.45) …) …
     on the same :root rule, and says of the first "--glow-r is the reach, set
     per element". It is not: a custom property's var()s are substituted at
     computed-value time ON THE ELEMENT THAT DECLARES IT, so --glow-light's
     computed value on :root already has 12px baked into it, and every
     descendant inherits that string. Measured in Chromium 141 on this page,
     .lp-flow__orb declares --glow-r: 44px and renders
       drop-shadow(rgba(225,255,0,.5) 0 0 5.4px) drop-shadow(rgba(197,235,226,.6) 0 0 12px)
     — 5.4 is 12 x 0.45. The override has never had an effect, here or
     anywhere else in the system that sets --glow-r.

     Declaring the shadow on the ORB fixes it for the orb, because then the
     substitution happens where --glow-r is in scope. The ratios (0.45, and
     the two alphas) came from the token as literals, which was a duplication
     and was named as one. THAT PART IS DONE: tokens.css publishes --glow-core,
     --glow-lime and --glow-glas now, and the orb reads all three, so nothing
     here restates a number the palette owns. The rest of this note stands as
     the diagnosis it always was. The system-wide fix is a different change
     from this one: it is tokens.css publishing the two shadow COLOURS and
     every consumer composing its own reach, and it touches every glow on the
     site. The beads used to share this pair and no longer do — see the halo's
     own note below for why nothing in the field is filtered at all. */
  .lp-flow__orb {
    /* THE RATIO AND THE TWO COLOURS ARE THE PUBLISHED ONES NOW. This used to
       mix them out of --lime-500 and --glas-500 at 50 % and 60 %, and multiply
       the reach by a literal 0.45, because tokens.css published only the
       finished filter and there was nothing else to read. It publishes the
       parts — --glow-core, --glow-lime, --glow-glas — so the duplication the
       note above named as one is gone: this is the same light the system
       draws, at this element's own reach. */
    --sp-glow-glas: drop-shadow(0 0 var(--glow-r, 12px) var(--glow-glas));
    --sp-glow-lime:
      drop-shadow(0 0 calc(var(--glow-r, 12px) * var(--glow-core))
                  var(--glow-lime));
  }
  /* THE HALO IS A PAINTED CIRCLE AND THE BEAD CARRIES NO FILTER, which is a
     frame budget rather than a preference. A drop-shadow re-rasterizes when
     its source changes, and the source here is a path being remodelled every
     frame. Measured at 1600 x 900, twenty-one beads at rest:

       morph + filter   25 fps        filter, no morph   61 fps
       morph, no filter 61 fps        neither            61 fps

     Neither half is expensive; the pair is. So the spill went onto its own
     static circle — same radius the drop-shadow reached, same two colours,
     stops placed on the falloff it drew — and the element that morphs paints
     flat. It is also the better drawing: a Gaussian over a 1 px black contour
     smears the contour, and the bead's edge is the thing the review asked to
     make black. Nothing is filtered in the field now; the orb below still is,
     and can be, because the orb does not change shape. */
  .cf-stmt-sensor__halo { pointer-events: none; }

  /* ---- THE CALLOUT LAYER ----
     components/annotation.html's component, laid over a SLICED backdrop, which
     is the one thing it was not written for: its --annot-x / --annot-y are
     documented as fractions of a figure the drawing fills exactly, and this
     drawing is cropped by the stage rather than fitted to it.

     --sp-u IS THE CONVERSION AND IT NEEDS NO SCRIPT. `xMidYMid slice` scales
     by max(box width / 1600, box height / 900) and centres what is left, so
     one field unit is that scale as a CSS length and a crossing is the centre
     plus its own offset in those units. The generator writes that calc()
     straight into --annot-x, and the layer is pinned to the drawing at every
     viewport, in the markup, with nothing measuring anything at runtime.

     IT READS THE STAGE, NOT THE VIEWPORT, and the difference is three tiers.
     The first version wrote max(1vw / 16, 1vh / 9), which is exact — measured
     at six viewports from 1024x720 to 3440x1440 the pinned stage is the
     viewport, box (0,0), size (100vw, 100vh), every time. But it is only the
     viewport WHILE IT IS PINNED. Under reduced motion, below the gate, and in
     print the stage is ordinary flow and every one of those calc()s resolves
     somewhere else — so the layer had to be switched off in exactly the three
     tiers a reader is most likely to be in for a reason. Twenty-one readings,
     gone, because someone asked for less motion.

     .sp-annots-fig is `inset: 0` of the stage and carries `container-type:
     size`, so 1cqw and 1cqh are the stage's own box in every tier. An
     absolutely-positioned inset box has a definite size without needing its
     contents, which is the one case where `size` containment is free. The
     layer is now correct wherever the drawing is.

     THE LEADER IS IN FIELD UNITS AND THE LABEL IS NOT. --annot-u is set in
     --sp-u so the step scales with the drawing it points at — a leader that
     stayed 1rem while the lattice grew would be a different fraction of a cell
     at every viewport. The label is deliberately left alone: it is type, it is
     set at the system's own label size, and type that scales with a backdrop
     is type that is 11 px at one viewport only. */
  .sp-annots-fig {
    position: absolute;
    inset: 0;
    container-type: size;
    pointer-events: none;
    /* CLIPPED TO THE STAGE, because the drawing is. The slice crops the field
       to the stage's box and the notes are positioned in the same units, so a
       note whose sensor has been cropped away lands outside the stage — over
       the white below it, pointing at nothing. Measured in the flow tier at
       2000 x 650: the bottom row's six sensors are outside the crop and their
       six labels were drawn 25 px under the stage. The annotation specimen
       says it of its own ground lines — the ground does not end, the view
       does — and a note is part of the drawing, so it ends where the view
       does too. */
    overflow: hidden;
    /* AND THE VIEW MOVED, so this follows it. Outside the gate the ground now
       ends at the drawing's foot rather than at the stage's — the whole
       argument is over .sp-field — and this sentence is the reason the same
       clip belongs here: a note whose bead has been cropped away points at
       nothing. Measured on the shipped render at 375 before this line, with the
       field already clipped: five leaders and two labels ("S15", "S16") still
       stood on act 2's copy, pointing at beads that were no longer drawn.

       CLIPPED AND NOT INSET, for a second reason on top of the field's. This
       box is `container-type: size` and .sp-annots reads it — 1cqh is the
       stage's own height in --sp-u — so shrinking it would move every one of
       the twenty-one notes off the beads it names. The box stays the stage's;
       only the paint stops at the horizon. */
    clip-path: var(--sp-ground-clip);
  }
  /* A container-type:size BOX TAKES THE WHOLE SHEET DOWN WITH IT, not only
     its own field. Chromium's real print pipeline — page.pdf(), Ctrl+P, not
     emulateMedia('print') alone, which never leaves the screen compositor and
     so never shows this — drops the ENTIRE document to a blank raster once
     this element is anywhere in the tree. Measured on the shipped page: nine
     pages, zero extractable characters, from the headline down through the
     footer. Hiding only .sp-annots-fig restored all nine pages to real text —
     2 898 characters of it, every act's copy, the FAQ, the footer. Removing
     just `container-type: size` (keeping the clip-path and the overflow both
     in place) fixed it exactly as completely, which is what narrows the fault
     to that one declaration and clears the clip-path and the 21 children it
     sizes of blame. The readings are cycling mock data with nothing to say on
     a still sheet regardless — the same reasoning .cf-ground::before is
     withdrawn under (base.css) — so the field goes with it rather than
     shipping a page that prints nothing at all.

     LOOKS COVERED BY THE WITHDRAWAL BELOW, AND MUST NOT BE TIDIED INTO IT.
     A TIER WITH NO TIME AXIS (under .sp-annots) also sets this layer to
     `none`, unconditionally, so today this rule never decides anything. It
     stays because the two rules answer different questions: that one is a
     COMPOSITION decision — which moment of the act a tier draws — and is
     restored inside the pin gate; this one is the blank-raster guard the
     measurement above earned, and print must keep it even on the day the
     composition changes its mind. The gate's own @media names `screen`, so
     the restoration can never reach print past this rule — but that is the
     kind of coincidence this file has learned not to lean on twice.
     check-act-moment.py reads neither rule into the other: a `none` inside
     any conditional is invisible to its withdrawal clause on purpose. */
  @media print {
    .sp-annots-fig { display: none; }
  }
  /* THE SECOND CONTAINER IS THE ONE GECKO RE-READS, and it is declared for no
     other reason. The box above already is one — .sp-annots-fig is `inset: 0`
     of the stage and `container-type: size`, which is the whole argument of the
     note two screens up — and this list is `inset: 0` of THAT, so the two boxes
     are the same box to the pixel. Nothing about --sp-u changes; what changes
     is which element the twenty-one notes resolve their cqw/cqh against.

     WHAT FIREFOX SHIPS WITHOUT IT. Gecko resolves the units in --sp-u once,
     against the stage's height in an EARLIER pass — the stage in its stacked
     arrangement, before `@container (min-width: 56rem)` folds .cf-statement
     into two columns and takes ~390 px out of it — and never resolves them
     again. Every note is then placed as if the field were sliced at a scale it
     is not. Measured on the shipped page, Firefox 153, flow tier, all 21 notes
     wrong at every desktop width:

                    u used   u correct   worst note   labels cut by the frame
       1024 x 768   1.1428     0.8433      218.9 px    10 of 21   (7 after)
       1280 x 800   1.2359     0.9242      227.8       10         (3 after)
       1440 x 900   1.3167     0.9         304.6        9         (0 after)
       1920 x 1080  1.3167     1.2          85.3        0         (0 after)

     The u it keeps is max(w/1600, h/900) for a stage 1029, 1112, 1185, 1185 px
     tall; with the fold disabled the stacked stage measures 1008, 1092, 1165,
     1165, which is where those numbers come from. So the whole field of notes
     stands 46 % further from the centre than the beads it names — nine of them
     off the side of the screen at 1440 — and Chromium and WebKit, which are
     pinned here and re-resolve anyway, show none of it.

     THREE THINGS SAY IT IS THE STALE PASS AND NOT A DIFFERENT BOX. A clone of
     any note appended to the same parent gets the right number while the
     original keeps the wrong one; any resize of the window corrects all
     twenty-one; and replacing the fold's @container with an @media — decided in
     the first pass, so the height never moves — removes the drift entirely.
     None of those is a fix: the first two need a reader to do something, and
     the third is a viewport threshold standing in for a container fold, which
     is the trade tokens.css's register exists to refuse.

     WHY THE INNER BOX IS RE-READ AND THE OUTER ONE IS NOT IS GECKO'S BUSINESS
     and this note does not claim to know it. What is held here is what was
     measured: 0 of 21 notes off their bead, at 375, 768, 1024, 1280, 1440, 1920
     and 2560, with and without reduced motion, over three loads each. Chromium
     and WebKit are unchanged to the pixel — the units are the same units read
     off the same box — and the print withdrawal above still takes this list
     with it, because it removes the box that holds it. check-annot-container.py
     holds both declarations, so the inner one cannot be tidied away as the
     duplicate it looks like. */
  .sp-annots {
    container-type: size;
    --sp-u: max(1cqw / 16, 1cqh / 9);
    --annot-u: calc(22 * var(--sp-u));
    --annot-node: calc(9 * var(--sp-u));
    --annot-gap: calc(4.5 * var(--sp-u));
  }

  /* ==================================================================
     A TIER WITH NO TIME AXIS HAS TO PICK A MOMENT, and until now this one
     picked both. The field and the root are not two drawings on one stage;
     they are ONE drawing at two moments of the same act. Act 1 is the
     sensors: twenty-one beads with their readings, arriving. Act 1c
     converges every one of them onto the trunk's head. Act 2 is what is
     left when they have arrived — the root, drawn from that head down,
     with the beads gone. The pinned tier can show both because it has
     three hundred vh to put between them.

     THE FLOW TIER HAS NONE OF THAT, and it was drawing the lot at rest:
     the finished root, the field that becomes it, and the twenty-one
     callouts that name the field, in the same pixels. Reported from a
     Firefox window as "the tree and sensors are totally mashed and mixed
     up", and that is the right description — the leaders and labels cross
     the canopy, and the beads sit inside it, so the drawing appears to be
     annotating itself. Measured on the shipped render at 1850 x 950,
     objects of act 1 standing inside the root's own box: 5 beads and 4
     leaders, two of them crossing a branch; at 390 x 844, 2 beads and the
     label "S03 30.7 A" over the canopy.

     SO THE TIER THAT CANNOT SEQUENCE THEM DRAWS THE LATER MOMENT, which
     is the one the tier's own copy describes. .sp-say is directly under
     this drawing at every width outside the gate, and it is about the
     root: "Oben tritt jede Messung für sich ein … bis unten eine einzige
     Zahl steht." Nothing in that paragraph is about a bead. The claim
     beside the drawing keeps its picture too — 8 400 values a second is
     what the root is drawn out of, and the head carries the number.

     WHAT IS WITHDRAWN IS ACT 1's INK AND NOTHING ELSE. .sp-field is the
     sliced 1600 x 900 bead field; .sp-annots-fig is its callout layer. The
     lattice under them is .cf-ground, the material's, and it stays — it is
     the ground the root stands on in every tier, act or no act. The root
     keeps its head, its numerals and its own band.

     AND IT REVERSES A DECISION MADE HERE ON PURPOSE, so it says so. The
     note under .sp-field .cf-stmt-sensor argues the fallback tier into a
     field it had lost to an animation on an unresolvable timeline, and
     ends "an invisible backdrop looks like a design decision". This IS
     that design decision, made deliberately and in the open: not a field
     that fails to paint, but a moment that is not this tier's. The reason
     that note gives — "the drawing stands complete … nothing is hidden" —
     is what a double exposure defeats: two states of one drawing at once
     is not completeness.

     THE ALTERNATIVE WAS SEQUENCING THEM IN SPACE — a band of field above
     the root instead of behind it — and it costs the crop its subject.
     Both ink layers are `inset: 0` of the stage and read its box through
     `slice`; give the stage a band of its own at the top and the box grows
     by that band, so at 1440 the scale goes 0.9 -> 1.287 and the visible
     third is the field's sparse edge rather than the focus the generator
     places over the trunk. Shortening the box instead is the phone
     regression check-slice-crop.py and the band's own note already refuse.

     Held by check-act-moment.py, which reads the two layers out of the
     stylesheet and requires the withdrawal outside the gate and the
     restoration inside it — and requires the withdrawal to be
     UNCONDITIONAL: the `@media print` none on .sp-annots-fig ~110 lines up
     is the blank-raster guard, not this decision, and the check refuses to
     let one stand in for the other in either direction. Its own comment
     says the same. → foundations/motion.html, the acts' order */
  .sp-field,
  .sp-annots-fig { display: none; }
  /* The reading is a MEASURE, and the component says a measure goes inside the
     label so both stand on the one rule: S01 names the instrument, the value
     is what it is reporting. The label keeps the component's own --text-xs —
     11 px, mono, uppercase — because that is the size the whole system's
     labels are and a field of twenty-one is not the place to invent a
     twenty-second. */
  /* AND THE FALLBACK TIER GETS A FIELD, which it has not had. components.css
     animates .cf-stmt-sensor on the --cf-iso timeline, because on the landing
     page a sensor lives inside a .cf-iso scene that declares one. This page's
     sensors do not: the field is the STAGE's backdrop and the timeline it
     reads is --sp. Above the gate that never showed, because the block below
     re-declares animation-name wholesale. Below it — under 64rem or 45rem,
     where the stage does not pin — the inherited rule stood: `cf-iso-fade`
     with `fill-mode: both` on a timeline that does not resolve, which Chrome
     fills from its `from` keyframe. Every sensor computed opacity 0.

     So the promise three comments up — "the drawing stands complete … nothing
     is hidden" — was false for the whole field at small viewports, and it was
     false in the direction nobody checks, because an invisible backdrop looks
     like a design decision. Clearing the inherited animation here lets the
     opacity fall back to components.css's own `opacity: var(--iso-rest)`; the
     gated block re-states everything it needs on top.

     AND THE TIER THIS WAS WRITTEN FOR NO LONGER DRAWS THE FIELD AT ALL —
     see A TIER WITH NO TIME AXIS, under .sp-annots — so the line below now
     guards a box that is `display: none` everywhere it used to apply. It
     stays, one declaration, because the trap it disarms is not the field's
     visibility: it is an inherited animation on a timeline that does not
     resolve, filling every sensor to opacity 0 from its `from` keyframe, and
     that trap re-arms itself the moment anyone draws this layer outside the
     gate again. The argument above is kept in the past tense it was written
     in rather than deleted: it is the record of how the field was lost the
     first time, and the reason a withdrawal has to be stated out loud
     instead of arrived at. */
  .sp-field .cf-stmt-sensor { animation: none; }
  /* The root draws rails ON its viewBox boundary; SVG clips to its viewport
     by default, so without this the outermost strokes lose half their weight
     — the rule .cf-page-header__figure writes out in components.css. */
  .sp-drawing .lp-flow { overflow: visible; }
  /* ------------------------------------------------------------------
     THE ROOT AGAINST THE DESIGN SYSTEM — the audit, and the three places
     this drawing knowingly leaves it.

     "Does it make sense that the roots are black?" It does, and it is not
     a preference: foundations/illustration.html states the object in one
     line — "an object is a lattice, a contour and one light" — and then
     "the contour is the drawing. A fill is what stops you seeing through
     the object." The contour is --border-strong, which is CF-Schwarz on
     the light theme, and the nodes are "filled black dots". A grey root
     would be a root drawn in the colour reserved for FACES: CF-Grau is
     also the top of the page wash, so an unlit face "sits at exactly the
     page's own colour and the object would disappear entirely if the
     contour were not carrying it". Black is what carries it.

     WHAT THE ROOT FOLLOWS, item by item:
       angles      all five, and only those — 26 at 45, 20 at 63.43, 6 at
                   26.57, 13 vertical, recomputed by check-flow-axes.py
       contour     1 px, non-scaling, --border-strong, one weight
       line types  solid throughout: every stroke is a real route, which
                   is the "every edge of every real surface" row
       nodes       filled black, r 3/2/1, stepping down with the run from
                   the void, and only on divisions
       fills       one — the orb, which is a light rather than a surface and
                   is bounded like everything else (--border-default, see its
                   own note). No route carries a fill: "fill only to occlude",
                   and nothing here occludes
       ramp        one #lp-flow-ramp, href'd by all 83 paint servers, so
                   check-gradient-family.py has one stop list to recompute
       labels      the 11 px mono ramp, --text-xs, tabular

     AND THE THREE DEPARTURES, each with its reason:

     1. THE LIGHT IS A FRONT, NOT A FACE. Every .cf-iso in the system lights
        exactly one ELEMENT and lights it by filling it. This object has no
        faces to fill: it is a route diagram, and what the light means here
        is where the data has reached. So the ramp travels — every stroke
        carries it, one at a time, in the order the front arrives. The rule
        it keeps is the one underneath "one light per object": at any scroll
        position the lit region is one, and it is contiguous.

     2. THE FRINGE KEEPS ITS LIGHT. The front fades behind itself
        everywhere except the twenty-six strokes that land on the rail. A
        route is a thing data passed through and should go dark; a foot is
        where data is arriving from and has not stopped. That is a second
        lit region in the settled drawing, which is the plainest departure
        on this list, and it is the one the page is FOR — a still root with
        no light in it is a diagram of a tree, not of a stream.

     3. THE LEAF'S LIGHT IS 6 px, NOT 2. Illustration's rule is "1 px for
        everything except the trace, which is 2", and this drawing keeps it
        for every CONTOUR. The light is a second stroke over the contour,
        and at 2 px it covers it: half-opacity lime over black composites to
        olive, and the leaf reads as a dirty line rather than a lit one.
        Measured at both widths on the settled fringe. At 6 the black
        hairline stays crisp inside and the lime sits in the pixels either
        side, which is the only arrangement in which the contour and the
        light are both doing their own job.

     None of the three is available to a .cf-iso, and none of them should
     become one: they are what a ROUTE drawing needs and an object does
     not. If a second flow drawing ever wants them, that is the moment the
     three become a fourth layer contract rather than a note on a
     prototype. → foundations/illustration.html, foundations/colors.html
     ------------------------------------------------------------------ */
  /* The drawing's paint, static and tier-independent: the contour is the
     border colour, the light is 2 px over its own per-stroke ramp, junctions
     are filled points, and every stroke holds its weight under the stretch
     (non-scaling). These lived in the old page block and died with it — the
     first render of this rebuild drew forty invisible strokes. */
  .lp-flow :is(path, circle) { vector-effect: non-scaling-stroke; }
  .lp-flow__seg { stroke: var(--border-strong); }
  .lp-flow__light { stroke-width: 2; }
  .lp-flow__node { fill: var(--border-strong); }
  /* THE ORB IS BOUNDED, LIKE EVERY OTHER SHAPE HERE.
     It shipped without a contour on the argument that the source is the light
     the construction is made of rather than a thing the construction contains,
     and that an outline would draw it as an object. That was the wrong way
     round: foundations/illustration.html does not say "contour the objects",
     it says "an object is a lattice, a contour and one light" and "the contour
     is the drawing". An unbounded disc is the only shape in this figure whose
     edge is decided by a gradient stop, which is exactly the thing the contour
     rule exists to prevent — the light falls off and the reader has to guess
     where the source stops.

     --border-default AND NOT --border-strong, which is the one place this
     departs from the contour colour, and the reason is what the line is
     bounding. Every other contour here bounds a ROUTE — an edge of something
     that would be invisible without it, drawn in CF-Schwarz because it is
     carrying the whole drawing. This one bounds a LIGHT, which is already the
     brightest thing on the page: at full strength the black ring reads as a
     washer sitting on the glow and takes the source's own edge away from it.
     rgba(0,0,0,0.24) is the system's own quieter rule, the same token the
     lattice and the section header's hairline use, and it does what a contour
     has to do here — it says where the disc ends — without competing with
     what is inside it. */
  .lp-flow__orb {
    /* BLACK, ON THE REVIEW'S INSTRUCTION — "make the border of the big sphere
       black too". The note below argues for the quieter rgba(0,0,0,0.24) on
       the grounds that a black ring on a light reads as a washer sitting on
       it; that was a real observation and it is overruled, because the beads
       the orb absorbs are bounded in CF-Schwarz and a source bounded in a
       different weight from the twenty-one things that merge into it reads as
       a different KIND of thing. One drawing, one contour. */
    stroke: var(--border-strong);
    --glow-r: 44px;
    /* THE SAME COMPOSED SHADOW THE BEADS TAKE, and for the same reason: this
       rule has declared --glow-r: 44px since the source became a body, and
       until the beads were measured against it, it rendered at 12 — the
       :root default, baked into --glow-light where that token is declared.
       See the beads' own note for the substitution rule. Fixing the field and
       not the orb would have left the drawing with twenty-one lights that
       glow further than the one they all merge INTO. */
    filter: var(--sp-glow-lime) var(--sp-glow-glas);
    /* the circle grows about its own centre; fill-box on a circle is its own
       bounding box, so `center` is the centre of the disc rather than of the
       viewBox */
    transform-box: fill-box;
    transform-origin: center;
  }
  /* The drawing is the root alone now — the field moved to the stage's own
     backdrop — 620u on a 1200u basis: 0.5167 of its width in height. Sizing
     it off the viewport's height is what guarantees the whole of it fits the
     stage — the fault the landing page carried for a day was a drawing
     1.07x its viewport. */
  .sp-drawing { --sp-measure: min(100%, calc((100vh - 12rem) * 1.9)); width: var(--sp-measure); position: relative; }
  /* The root's numerals are placed over the flow box by the label law's own
     --x/--y (flow units of 1200 x 620) with --tx/--ty clearances. The flow
     box is the last child, so the values' container matches it exactly.

     AND IT IS THE QUERY CONTAINER FOR THEM, because the one thing that decides
     whether eighteen numerals fit is the width of the box they are placed in,
     and that box is this element: .lp-flow is `width: 100%` of it and the
     numeral layer takes the same ratio, so .sp-root's inline size IS the
     drawing's width to the pixel (measured: 280.00 at 320, 333.79 at 375,
     683.50 at 768, 911.40 at 1024, 1109.59 at 1280 — svg box and layer box
     identical at every one). Named, so the rule below cannot be captured by
     whatever container is added above it later. */
  .sp-root { position: relative; container-type: inline-size; container-name: sp-root; }
  /* THE ANNOTATION IS PLACED AT ONE SCALE AND THE DRAWING RENDERS AT SEVEN.
     The numerals hold their clearance off their own stroke in PIXELS — that is
     .lp-flow__val's whole argument and check-label-clearance.py holds it — but
     nothing held the distance from one numeral to the NEXT one, and that is
     the distance that closes. The positions are proportional (--x / 1200 *
     100%) and the type is a constant 11 px, so the lattice of points the
     eighteen hang off contracts with the box while the glyph boxes do not:
     at 375 the 60u between two branch points is 16.7 px on screen and each
     numeral is 53.7 px wide.

     The generator solved the placement AT THE GATE FLOOR and said so, eighteen
     times, in the markup beside every span: "nearest other 12.42 px at the
     gate floor". True there, and the drawing also renders below the gate,
     where it is a figure in a one-column stage. Measured on the shipped page,
     numeral-to-numeral, counting only pairs whose boxes actually intersect:

       viewport   drawing    overlapping pairs   numerals outside the drawing
        320        280.0 px        16                3
        375        333.8           11                2
        414        368.5            7                2
        480        427.2            6                1
        640        569.6            2                1
        768        683.5            0                1   (12.4 bar, 5.2 px out)
        900        801.0            0                0
       1024        911.4            0                0

     Nothing looks broken in the DOM and nothing scrolls sideways; on a phone
     "690 V", "4.2 mm/s" and "51 Hz" are printed on top of each other and the
     drawing's own claim, 8 400 /s, is under "3 200 /s".

     SO THE LAYER RENDERS WHERE ITS LAW HOLDS, AND THE DRAWING KEEPS ITS HEAD
     EVERYWHERE. 56rem of drawing clears the crowding floor — 746 px, the width
     below which the first pair intersects, recomputed from the markup by
     scripts/check-numeral-crowding.py — by 150 px, and holds 11.88 px between
     the nearest pair and 12.50 px to the nearest edge, against the 12.60 px the
     law was solved to at the gate floor's own 911.4. It is also the width at which .sp-stage__inner goes
     two-column above, so the annotation arrives with the layout that has room
     for it: a second consumer for an existing number, not a second number.

     The head is the exception and it is the one numeral act 2's copy names —
     "achttausendvierhundert Werte je Sekunde" is 8 400 /s, the value on the
     trunk. It sits on the vertical at --x:600, the viewBox's centre, and hangs
     left of it: at the narrowest admitted drawing width, 280 px, its box is
     74.30 -> 128.00 of 280 and 115.58 -> 129.88 of 144.67, clear of both edges
     with nothing else in the layer to collide with. So a phone gets a drawing
     that still states its own rate, and not seventeen numbers it cannot read.

     aria-hidden is already on .lp-flow-data, so nothing here changes what a
     screen reader is given: the readings were never in the accessibility tree.
     → scripts/check-numeral-crowding.py; check-label-clearance.py holds the
       other half of the placement, the clearance off the stroke itself. */
  .lp-flow-data > :not(.lp-flow__head) { display: none; }
  @container sp-root (min-width: 56rem) {
    .lp-flow-data > :not(.lp-flow__head) { display: revert; }
  }
  /* ---- THE COPY EITHER SIDE OF THE CONFLUENCE ----

     TWO TIERS, AND UNTIL NOW ONLY ONE OF THEM WAS WRITTEN. The placement this
     copy wants — two 35 % columns placed against the drawing's own box, at 74 %
     of it when this leaked and above its leaves since — is act 2's
     composition, and it is correct inside the pinned stage, where the
     drawing is fitted to the viewport and 471 px tall. It was authored HERE,
     in the base tier, where the drawing is whatever the flow makes it, and
     nothing ever took it back. Measured on the shipped page, act 1+2's stage:

         375 x 812     .sp-root 334 x 172     column 117 wide, 74 % = 127
         768 x 900     .sp-root 684 x 353     column 239 wide, 74 % = 261
        1023 x 900     .sp-root 527 x 272     column 184 wide, 74 % = 201
        1280 x 900     .sp-root 1109 x 573    reduced motion, the same tier

     Each column carries a bold lead and a forty-word paragraph — 2.3em plus
     7.5em of reserved height on its own, 220 px at 375 — inside a 172 px box,
     over a drawing already carrying nineteen numerals. All three layers shared
     one top edge (828 on the landing page at 375) and painted through each
     other: the lead across the fringe, the body through "1 840 /s" and
     "S16 2565 rpm", the paragraph out of the bottom of its own container and
     into the section header below it. Not a crop and not a near miss — four
     things in one place, none of them readable.

     WHO READS THIS TIER: every viewport under 64rem, every viewport under
     45rem tall, every reader with prefers-reduced-motion set, and every
     browser without animation-timeline. That is where most of this page is
     read and all of its degraded states.

     SO THE BASE TIER IS ORDINARY FLOW — the drawing, then the copy under it,
     as many equal columns as fit. The shape is base.css's .tiles--fit exactly,
     repeat(auto-fit, minmax(min(<tile>, 100%), 1fr)), restated rather than
     asked for by class because the gated tier takes this same box back for
     absolute placement: a utility class the stylesheet has to switch off is a
     worse dependency than the one declaration it saves. 20rem is the tile, and
     it is the only fold this needs — two columns fit .sp-root's 684 px at 768
     (2 x 320 + 32 = 672) and one fits its 334 at 375. The cap is the system's
     own --measure, for the single-column case at 1023 where the box is 527 px
     wide and 56ch is the line the reader is given everywhere else.

     THE COLUMNS KEEP THEIR TYPE AND LOSE THEIR PLACEMENT. Same lead, same
     body, same ink; the ragged inner edge and the pointer-events lift belong to
     the overlay and go with it. There is no axis to mirror when the drawing is
     above the copy rather than behind it, and prose the reader can select is
     worth more than a mirror they cannot see.
     → the gated placement is in the @supports block, under THE OVERLAY */
  .sp-say {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
    gap: var(--space-8);
    margin-block-start: var(--space-12);
  }
  .sp-say__act { display: none; }
  .sp-say__col { min-width: 0; max-inline-size: var(--measure); }
  .sp-say__lead {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--weight-bold);
    line-height: var(--leading-snug);
    color: var(--text-primary);
  }
  .sp-say__body {
    margin-block-start: var(--space-3);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
    color: var(--text-secondary);
  }
  /* THE NUMERAL LAYER IS THE DRAWING'S BOX, AND NOW IT SAYS SO. It was
     `inset: 0` of .sp-root and that was only ever true by coincidence: the
     comment above .sp-root says "the flow box is the last child, so the values'
     container matches it exactly", which held while the last child was the
     drawing and stopped holding the moment the copy below joined the flow. Both
     halves of every numeral's placement are percentages of this box —
     left: calc(--x / 1200 * 100%), top: calc(--y / 620 * 100%) — so a box one
     column of prose taller drags all nineteen of them down it.

     The drawing's own ratio is the honest containing block, and it is not a
     guess: the svg is viewBox="0 0 1200 620" at width 100 % / height auto, so
     its used height IS width x 620/1200 at every tier. Stated as the ratio,
     this layer and the drawing are the same box by construction rather than by
     whatever happens to be last in the flow. Identical to the pixel in the
     gated tier, where .sp-root is the drawing and nothing else — measured
     against the drawing's own origin, all 18 numerals unmoved to 0.01 px at
     1024, 1280, 1440 and 1920. */
  .lp-flow-data {
    position: absolute;
    inset: 0 0 auto 0;
    aspect-ratio: 1200 / 620;
    pointer-events: none;
  }
  /* THE SOURCES LAYER TAKES THE SAME BOX FOR THE SAME REASON — it places
     objects at the drawing's own coordinates, so its box has to be the flow
     box to the pixel. The markup argues the layer (patterns/landing-page.html,
     THE SOURCES); what is decided here is size, gate and light.

     THE SIZE IS A CLAMP AGAINST THE DRAWING, floored where an object stops
     being one. 1cqi rides .sp-root — the named container whose inline size
     IS the drawing's width — capped at 8px so the beads stay instruments
     rather than becoming act 1's field again, floored at 5px because below
     that a ring plus a core is a smudge. The floor is what sets the GATE: the
     tightest pair of entries stands 15 units apart (x=1117.5 and 1132.5, read
     out of the shipped paths), which is 6.0px of gap at a 30rem container —
     a 5.0px bead and 1.0px of air, the law met with nothing to spare. Below
     30rem the layer is withdrawn and the drawing is the bare root with its
     head, exactly what it was before this layer existed. 30rem is the
     register's ".cf-breadcrumb keeps only the last two crumbs" rung — a
     second consumer for an existing threshold, not a new row.

     THE FLOOR HAS BEEN WRONG TWICE, in the same direction, and the record is
     the argument for the arithmetic living in a check. The first draft gated
     at 44rem with a 1.1cqi slope: lawful, and it hid the beads from every
     folded drawing between 525 and 704px. The second gated at 34rem: lawful,
     and 1024 x 768 — the pin gate's own floor, whose reduced-motion reader
     is in this tier by definition — folds the drawing to 527.6px, sixteen
     pixels short of that gate. Both drafts were "the lowest rung the law
     admits" in prose and neither was in fact; 30rem is, because at 480px the
     inequality closes to equality. scripts/check-canopy-sources.py recomputes
     the pair, the gap, the clamp and the floor from the shipped markup and
     this file, so none of those numbers is trusted in prose again.

     THE BODY IS ACT 1's BEAD AT INSTRUMENT SIZE: Glas falling to the grey
     the contour closes, bounded in CF-Schwarz because the orb's own note
     settled that argument ("one drawing, one contour"), lit by the same
     composed pair every light on this stage carries. The reach scales with
     the disc — 1.5 diameters, which is the cap's own 12px at the cap — and
     the halos are ALLOWED TO POOL: at the tightest pair light meets light
     between the bodies, which is what light does and what act 1's field does
     at every density. It is the bodies the law keeps apart, and that law is
     check-canopy-sources.py's, not this sentence's. */
  .lp-flow-sources {
    position: absolute;
    inset: 0 0 auto 0;
    aspect-ratio: 1200 / 620;
    pointer-events: none;
    display: none;
  }
  @container sp-root (min-width: 30rem) {
    .lp-flow-sources { display: block; }
  }
  .lp-flow__src {
    --src-d: clamp(5px, 1cqi, 8px);
    position: absolute;
    left: calc(var(--x) / 1200 * 100%);
    top: calc(50 / 620 * 100%);
    translate: -50% -50%;
    width: var(--src-d);
    height: var(--src-d);
    border-radius: var(--radius-full);
    background: radial-gradient(circle, var(--cf-glas), var(--cf-grau));
    border: 1px solid var(--border-strong);
    --glow-r: calc(var(--src-d) * 1.5);
    box-shadow:
      0 0 calc(var(--glow-r) * var(--glow-core)) var(--glow-lime),
      0 0 var(--glow-r) var(--glow-glas);
  }
  /* THE CORE IS A LAYER AND NOT A STOP, which is how act 1 builds it too —
     "the field converges … core first". At instrument size the family ramp
     has no room to say anything: lime's leg of it is 6.1% of a radius that
     is four pixels here, a quarter of a pixel of the one colour the object
     is about. The field's beads solve this by drawing the core as its own
     body inside the disc, and this is that construction one size down: the
     body carries Glas falling to the contour's grey, and the core is solid
     source-light at --glow-core of the disc — the same share the composed
     glow gives lime of its reach, so the object and its light agree about
     how much of this thing is source. */
  .lp-flow__src::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    /* max(), because the share stops being visible before the disc stops
       being a disc: at the floor's 5px, 45% of the diameter is a 2.25px core
       that a 1x display rounds toward nothing, and a sensor whose one colour
       is unreadable is a dark speck. 3px is the smallest square of pixels
       that still reads as a lit centre; at the 8px cap the max() never
       binds and the share is the share. */
    width: max(3px, calc(var(--src-d) * var(--glow-core)));
    height: max(3px, calc(var(--src-d) * var(--glow-core)));
    border-radius: var(--radius-full);
    background: var(--cf-lime);
  }
  /* A VALUE AND A READING ARE PLACED THE SAME WAY AND SAY DIFFERENT THINGS.
     Both ride a stroke, both are put there by the label law in
     gen-flow-root.py, and both are real HTML at the label ramp's 11 px so
     they do not scale with the drawing. What separates them is arithmetic: a
     .lp-flow__val is a rate and rates DIVIDE, so check-flow-values.py holds
     every one of them to the sum of the ones below it. A .lp-flow__read is a
     measurement — 74 °C, 12.4 bar — and measurements do not: two 400 °C
     channels merging do not make 800 °C, and a drawing that let its units
     into the sum would be claiming exactly that. So the reading carries the
     unit, sits at the fringe where the sensors are, and is invisible to the
     check that adds things up. */
  /* --text-secondary AND NOT --text-muted, and the difference is whether the
     numbers can be read. components.css states the muted rule in prose four
     times — what sits on the page wash is never --text-muted, because #919191
     on CF-Grau is 2.0:1 — and check-contrast.py enumerates the four users that
     survive it, each one chrome whose information is carried at full contrast
     somewhere else. These are not that. A .lp-flow__val is the rate the
     drawing's whole arithmetic rests on and check-flow-values.py holds every
     one of them to the sum below it; a .lp-flow__read is an instrument
     reading with a unit. They ARE the information.

     Measured on the rendered page at 1280 x 800, the type hidden and the
     pixels behind each run sampled at the scroll offset where its own
     lp-frame-in has finished — opacity 1, nothing composited over it:

       .lp-flow__val   "1 360 /s"   2.48:1     .lp-flow__read  "74 °C"  2.25:1
       .lp-flow__val   "1 520 /s"   2.53:1     floor, 11 px non-large    4.5:1

     and 1.62:1 in the reduced-motion tier, where the drawing sits on the act's
     own darker ground (#B6BBBC) instead of the wash. --text-secondary is what
     the rule's own sentence offers as the remedy and it clears the floor at
     every stop of the wash: 5.88:1 at #CFCFD2 rising to 9.15:1 at white. */
  .lp-flow__val, .lp-flow__read {
    position: absolute;
    left: calc(var(--x) / 1200 * 100%);
    top: calc(var(--y) / 620 * 100%);
    translate: var(--tx, 0) var(--ty, 0);
    color: var(--text-secondary);
    white-space: nowrap;
    /* THE SHEET IS NOT HIT-TESTABLE AND THESE ARE, because they are the one
       thing on it that is not a sheet. .lp-flow-data is `pointer-events: none`
       for a good reason — it is a 1110 x 573 transparent layer lying over the
       whole drawing, and a layer that size taking a click is a layer that
       swallows the page. The eighteen numerals inherited that, and a numeral
       is TEXT: it is the rate the arithmetic rests on and the reading with its
       unit, at --text-secondary because the paragraph above insists they ARE
       the information — and not one of them could be selected, dragged across
       or copied. Measured at 1440 x 900 with the tree built, elementFromPoint
       at each numeral's own centre: 0 of 18 returned the numeral, 17 of them
       the <svg> under it. After this line, 17 of 18 return the numeral. The
       eighteenth is "74 °C", which has act 1's own claim standing over it —
       a pre-existing overlap of two layers this line neither makes nor mends,
       and the only one of the eighteen where the copy wins the hit test.

       The glyph boxes are 34 to 62 px wide and 14 px tall, 6 216 px² of the
       sheet's 635 730 — under 1 % — so the layer is as transparent to a
       pointer as it was: 0 of 121 points sampled over it return the sheet, at
       every viewport measured. Only the text answers.

       AND IT IS SELECTION, NOT INTERACTION. Nothing here gains a hover, a
       cursor change or a target: the numerals are inside .lp-flow-data's
       aria-hidden subtree, decorative to a screen reader by the same argument
       check-act-copy-exposed.py records, and this changes none of that. What
       it restores is the plainest thing a reader can do to a number on a
       page, which is take it. → scripts/check-numerals-selectable.py */
    pointer-events: auto;
  }
  /* A UNIT IS A SYMBOL, AND SYMBOLS ARE CASE-SENSITIVE. The label ramp
     uppercases, which is right for every label this system has had until now
     because they are all words — WAS WIR MACHEN, IHR VORTEIL, 01 / 04. A unit
     is not a word: kW is kilowatt and KW is nothing, Hz is hertz and HZ is
     nothing, mm/s is a speed and MM/S is not a symbol in any system. The first
     render of this shipped "246 KW", "51 HZ" and "4.2 MM/S" on a drawing whose
     whole subject is instrumentation, which is the one place that is not a
     typographic preference but a wrong reading.

     So the reading keeps every other property of the ramp — the mono face, the
     11 px, the tracking, the colour — and drops the case. It is not an
     exemption from the ramp's SIZE rule (check-label-ramp.py governs mono
     UPPERCASE rules off --text-xs / --text-sm, and this one is neither off the
     size nor uppercase any more), so nothing has to be added to that file's
     list: the rule stops being uppercase, which is the honest way out. */
  /* THE UNIT IS NOT A WORD, and now BOTH classes owe this declaration. The
     label ramp uppercases, which is right for a note and wrong for a measure:
     it prints M³/H for m³/h, KPA for kPa — and, since the rate took a unit on
     the review of 2026-07-29, "8 400 /S" for 8 400 /s. An SI symbol whose case
     has been changed is a different quantity, and a capital S is a siemens.
     .lp-flow__read has carried this since it was written; .lp-flow__val did not
     need it while it was digits and a space, and needed it the moment it was
     not. */
  .lp-flow__read, .lp-flow__val { text-transform: none; }

  /* NO SEPARATE OPACITY FOR THE READING, and the reason is a rule this system
     already checks for. The first pass gave it .82 to sit a shade under the
     values; lp-frame-in ends `opacity: 1`, an animation origin outranks an
     author declaration, and the .82 never reached a pixel —
     check-authored-opacity.py caught it, which is the finding it exists for.
     The distinction does not need a second colour anyway: a reading carries a
     unit and a value does not, and that is the whole of what the reader has
     to tell apart. */
  @container (min-width: 56rem) {
    .sp-stage__inner { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
    /* PLACED, NOT AUTO-FLOWED. The component's own tiers span the figure
       across the row, so without an explicit column each the figure took
       both tracks, the drawing resolved its min(100%, …) against the whole
       container, and the root's right branches ran 330 px through the
       claim's copy. One track each, one shared row. */
    .sp-stage__inner > .cf-statement__figure { grid-column: 1; grid-row: 1; }
    .sp-stage__inner > .cf-statement__text { grid-column: 2; grid-row: 1; }
  }

  /* ---- fallback: no timelines / reduced motion / under the gate ----
     The stage does not stick and nothing animates; the drawing stands
     complete (strokes ship drawn — the animations below only add the
     build), the cards are the stacked column — .cf-pin__steps on the row in
     the markup, the mechanism's own fallback since the stack moved to
     components.css with the rest of the scaffolding. Nothing is hidden. */
  .lp-proc-plate { display: none; }

  /* ---- THE READOUT STREAMS, AND IT IS THE ONLY CLOCK ON THE PAGE ----
     Everything in the stage is scrubbed: the beads arrive, pulse and merge on
     the track's own timeline, the leaders draw themselves on it, and a reader
     who stops scrolling sees a still. That is the pinned track's contract and
     the last revision broke it by putting twenty-one shapes on a timer.

     A NUMBER IS THE ONE THING THAT MAY MOVE ANYWAY, because a live instrument
     is exactly a number that changes while nothing else does — the review's
     "change like with the streaming (like in matrix)". Each reading holds
     STREAM_K generated values stacked in one grid cell, and one keyframe with
     a hold shows one at a time; the period is the sensor's own and the phase
     is its own, so twenty-one readouts tick out of step the way a rack of
     instruments does.

     THE UNIT DOES NOT CHANGE, only the magnitude. An instrument that changes
     what it MEASURES between frames is not an instrument, it is a slot
     machine, and the generator holds each sensor to one unit for that reason.

     Outside the reduced-motion gate the first value stands and the rest are
     not displayed — so a reader who asked for no motion gets a readout, not a
     blank rule and not six numbers stacked on top of each other. */
  .sp-stream {
    display: inline-grid;
    justify-items: start;
    /* THE UNIT IS NOT A WORD. .cf-annot__label uppercases, which is right for
       a note and wrong for a measure: it prints MM/S for mm/s and KPA for kPa,
       and an SI symbol whose case has been changed is a different quantity.
       The root's own readings carry this same declaration for the same
       reason — see .lp-flow__read. */
    text-transform: none;
  }
  .sp-stream > b {
    grid-area: 1 / 1;
    font-weight: inherit;
  }
  /* SAME SELECTOR ON BOTH SIDES OF THE GATE, and that is not redundancy. The
     first version paired `.sp-stream > b + b { display: none }` with
     `.sp-stream > b { display: block }` inside the media query and the second
     never won: (0,1,2) beats (0,1,1) whatever the source order, so five of
     every six readings were display:none at all times and the sixth was
     mid-cycle at opacity 0. Twenty-one labels rendered with a rule, a leader
     and no number on any of them. */
  .sp-stream > b + b { display: none; }
  @media screen and (prefers-reduced-motion: no-preference) {
    .sp-stream > b,
    .sp-stream > b + b {
      display: block;
      animation: sp-stream var(--sp-period, 3s) linear infinite;
      animation-delay: calc(var(--sp-period, 3s) * var(--sp-slot) / 6 * -1);
    }

    /* HELD WHILE NOBODY IS LOOKING AT THEM. These 126 animations — twenty-one
       labels, six readings each — are on the document clock and `infinite`,
       which means they run for the life of the tab whether act 1 is on screen
       or fifteen thousand pixels away. Measured at 1440 x 900 on a page loaded
       and then left completely alone: 1351 ms of style recalculation in five
       seconds, one recalc per frame at 60 Hz, 2246 ms of total task time — 45 %
       of the main thread, on a still page, for a readout nobody can see.
       Cancelling these 126 took it to 319 ms.

       PAUSED AND NOT STOPPED, which is why this is `animation-play-state` and
       not `animation: none`. Each reading is held at the value it had, so the
       field the reader comes back to is the field they left; and the phase
       offsets in the delay above — what makes twenty-one labels tick out of
       step instead of blinking in unison — survive, which they would not if
       the animations were dropped and re-created on arrival.

       The attribute is written by assets/js/cf-idle.js, an IntersectionObserver
       with a viewport of margin on each side, so the field has been ticking for
       a full screen before its first pixel arrives. With that file absent the
       attribute is never written and this rule never matches: the page runs
       exactly as it did. → scripts/check-idle-motion.py */
    [data-idle] :is(.sp-stream > b, .sp-stream > b + b) { animation-play-state: paused; }
  }

  /* THE OBSERVER NEVER SEES THIS FIELD LEAVE, for most of the track's own
     scroll. `data-cf-idle` is carried by .sp-track — 5760 px, NOT sticky — so
     the IntersectionObserver in cf-idle.js tracks THAT box against the
     viewport, and .sp-track intersects the viewport for the whole of its own
     640vh contain range: every scroll position a reader can be at while still
     inside act 1 or act 2. But .sp-annots, the box these 126 readouts live in,
     fades to opacity 0 by about contain 40% — its own crossfade into act 2's
     confluence — and stays there for the rest of the track. `[data-idle]`
     never arrives, because the box it is asked about (.sp-track) never
     geometrically leaves; the readouts just keep ticking, unseen, for
     whatever is left of the reader's time on this stage.

     Measured at 1440 x 900: ten small scroll steps at contain 70%, .sp-annots
     at opacity 0, data-idle on none of the 126:

       as shipped                     278.70 ms of style recalc
       the same 126 force-paused       72.19 ms                  (-74 %)

     A finer-grained mark does not reach this. Every descendant of .sp-stage
     shares .sp-track's own problem: fixed at the same screen position for the
     whole 640vh scroll, because the STAGE is what is sticky, not any one
     figure in it. IntersectionObserver reports box position against the
     viewport; it has no way to know one box now sits behind another that has
     gone opaque in front of it. Closing this needs a second gate driven by
     scroll position rather than geometry — the way act-rail.js and
     cf-stream.js already read contain% off this same timeline — not a
     smaller margin on the one this file already runs. Left open: it is a
     second mechanism beside the one this file documents at length, and that
     is a call for whoever owns the idle contract, not a drive-by CSS fix.
     check-idle-motion.py's contract only checks that the stylesheet pause
     rule and the JS file both exist, which they do; it has no way to see a
     box that is geometrically present but visually gone. */

  /* ============================================================
     WHY ACTS 4 AND 5 SIT HERE AND NOT BELOW THE GATE.

     A base tier is only a base if the cascade can see it that way, and at
     equal specificity the cascade decides on SOURCE ORDER. These two acts
     were authored after the @supports block that animates them, so every
     single-class rule they declared beat the single-class rule inside it —
     the base tier overriding the pinned tier, which is the opposite of what
     both were written to mean. It cost two defects that shipped:

       .sp4-lit   rest state opacity: 0 here, opacity: 1 in the pinned block.
                  The base won in BOTH tiers, so the frame's lit front never
                  painted and the review reported the gradients missing. They
                  were there; nothing could see them.
       .map       block-size: auto here, 100 % in the pinned block. The base
                  won, the svg took its 891 px intrinsic height, and the
                  legend was pushed to y 902 in a 900 px stage where the clip
                  ate it.

     Both were patched where they were found — one by driving the value from
     an animation rather than a declaration, one by going two classes deep —
     and neither patch addressed the reason. Moving the blocks above the gate
     does: a rule down there now wins on order, the way it reads as though it
     should, and the next act does not have to know this story.
     ============================================================ */

  /* ============================================================
     ACT 4 — the plates. Base tier first: no timeline, no gate, nothing
     hidden. Every rule that MOVES anything is inside the pinned block far
     below, so this is what a reduced-motion reader, a narrow viewport, a
     browser without view timelines and the printer all get — two framed
     photographs with their words beside them, whole, one under the other.
     ============================================================ */
  /* THE ACTS' HEADINGS CLEAR THE STICKY BAR. Each act names itself with a
     visually-hidden <h2> so its <section> has something to be labelled by, and
     an id is an id: check-arrival.py holds every addressable heading in
     patterns/ to landing under the nav rather than behind it. Invisible is not
     the same as unreachable — a skip link or a URL fragment can still put one
     of these at the top of the viewport. */
  .act-anchor { scroll-margin-top: calc(var(--nav-height) + var(--space-6)); }

  .sp4-track { position: relative; }
  .sp4-stage { display: grid; align-content: center; padding-block: var(--space-16); }
  /* THE PAGE MARGIN IS ONE NUMBER, AND --gutter IS IT. This band and .map-stage
     below are the two acts that stand OUTSIDE .container — they are full-bleed
     tracks with their own sticky stages, so each re-derives the content box
     rather than inheriting one. The arithmetic is .container's own, to the
     letter: base.css gives it `width: 100%; max-width: calc(--container-max +
     --gutter * 2); padding-inline: --gutter`, whose content box is
     min(100% - 2 * --gutter, --container-max) — the expression below.

     IT WAS --space-6, and a fixed rung cannot be a page margin. --gutter is
     `clamp(1.25rem, 5.5vw, 5rem)`: it tracks the viewport between 364 and 1454
     and pins at either end. A constant 24 px agrees with that ramp at exactly
     ONE viewport — 5.5vw = 24 at 436 px — and nowhere else. Measured, the left
     edge of act 5's legend against the left edge of the FAQ and partner
     headings on the same page:

       viewport   page margin   act 4/5   act stands
        320        20.0          24        4.0 px INSIDE the page
        436        24.0          24        0      (the one width it agrees)
        768        42.2          24       18.2 px OUTSIDE
        834        45.9          24       21.9 px OUTSIDE
       1024        56.3          24       32.3 px OUTSIDE
       1328        73.0          24       49.0 px OUTSIDE   <- worst
       1440        80.0          80        0      (both capped)

     So on every desktop and tablet width from 437 to 1439 the three beats of
     act 5's copy, its legend, and act 4's plates began left of every other
     line on the page, by an amount that changed continuously as the window was
     dragged — 0 to 49 px and back to 0, with no threshold anywhere in it,
     which is why no breakpoint sweep and no ladder stop could name it. It is
     the shape a page-local re-derivation of a shared measurement always takes:
     these rules were written in prototypes/statement-to-process.html, a lab
     page with no .container in it at all, where a self-contained inset is the
     only thing that CAN be written and 24 px looks like any other choice. The
     landing page has .container on every other section, and the substitute and
     the real thing agreed at one width out of the range.

     NOT A THRESHOLD AND NOT A NEW TOKEN — the register stays the length it
     was. --gutter is the token the page already keeps this number in, and the
     prototype gets the same ramp for free: it has no .container to disagree
     with, so the only thing that changes there is that its bands now breathe
     with the viewport the way every band on the site does.
     → scripts/check-band-inset.py, base.css .container, tokens.css --gutter */
  .sp4-set {
    display: grid;
    gap: var(--space-12);
    inline-size: min(100% - 2 * var(--gutter), var(--container-max, 76rem));
    margin-inline: auto;
  }

  .sp4-plate { margin: 0; display: grid; gap: var(--space-8); align-items: center; }

  /* ---- the words, in their own column ---- */
  .sp4-mark { margin: 0 0 var(--space-4); color: var(--text-secondary); }
  .sp4-say__lead {
    margin: 0;
    font-size: var(--text-h3);
    font-weight: var(--weight-bold);
    line-height: var(--leading-snug);
    color: var(--text-primary);
  }
  .sp4-say__body {
    margin-block-start: var(--space-4);
    max-inline-size: 42ch;
    font-size: var(--text-lg);
    line-height: var(--leading-relaxed);
    color: var(--text-secondary);
  }

  /* ---- the picture and its frame ---- */
  /* --mount IS THE PADDING, NAMED, because a second rule depends on the
     number: .sp4-tick runs the corner of the frame to the corner of the
     picture, so its length is this mount's DIAGONAL and nothing else. Typed
     as two independent tokens the two were free to disagree — and did, by
     1.37 px — so the tick reads its own length off the air it crosses. */
  .sp4-shot {
    position: relative;
    aspect-ratio: var(--ar);
    --mount: var(--space-4);
    padding: var(--mount);
  }
  .sp4-win {
    block-size: 100%;
    overflow: hidden;
    /* The wash, so an absent photograph is an empty mount rather than a
       broken-image glyph, and so the plate itself stays invisible until its
       act: the window is clipped shut before the sweep, and a wash on the
       shot would announce the second plate a whole stage early. */
    background: var(--surface-page, var(--grey-050));
  }
  .sp4-photo { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }

  /* overflow: visible, WHICH check-edge-stroke-clip.py ASKED FOR BY NAME. All
     eight paths lie ON the viewBox boundary — that is what a frame is — and an
     <svg> clips to its viewport by default, so each of them would paint at
     half its declared weight: the 1 px contour at 0.5 and the 2 px light at 1.
     Nothing looks broken, the frame is just quietly thinner than the tree's
     own strokes on the same page, which is the kind of drift a hairline never
     announces. Same reason and same fix as .cf-page-header__figure. */
  .sp4-frame {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    overflow: visible;
    pointer-events: none;
  }
  .sp4-frame path {
    fill: none;
    vector-effect: non-scaling-stroke;
    transform-box: fill-box;
    transform-origin: var(--o, 0 0);
  }
  .sp4-edge { stroke: var(--border-strong); stroke-width: 1; }
  /* THE LIGHT IS OFF UNTIL SOMETHING IS DRIVING IT. The ramp pass is a FRONT,
     not a frame: it exists while the edge is being drawn and goes out behind
     itself. Where there is no timeline to move it — reduced motion, below the
     gate, no view-timeline support, print — there is no front either, and a
     lime rectangle standing permanently around a photograph is not the
     settled state of this drawing. It is also the one place the settled state
     could contradict its own rule: "the contour is the drawing", and the
     contour is CF-Schwarz. So the base tier paints the black hairline alone
     and the pinned block below turns the light on for the acts that use it. */
  /* SIX, AND THE CONTOUR IS ONE. The light was 2 px — the tree's weight — and
     the tree's weight is wrong here for a reason that is about length, not
     about taste. On the root a lit stroke is 20 to 110 units long, so the
     ramp's coloured leg (lime to Glas is its first 32 %) is most of what you
     see. A plate's edge is the whole width of the picture, so the same ramp
     spreads its colour over a few dozen pixels at the tip and spends the rest
     of the run at #CFCFCF — which, painted at 2 px directly under a 1 px black
     contour, is invisible. Measured on the render: four black edges drawing,
     one lime pixel at each tip, and the review said the gradients were
     missing. They were there; there was nowhere for them to be seen.

     Six under one leaves two and a half px of ramp proud on each side of the
     contour. The lime tip reads, the Glas leg reads, and the tail — #CFCFCF
     against the CF-Grau wash — falls away on its own, which is the same
     falling-away the root's own light does at the end of every stroke. */
  .sp4-lit  { stroke-width: 6; opacity: 0; }

  /* THE TICK is the brand's one diagonal on an otherwise square frame: a 45°
     chamfer across the top-left corner, which is the angle the logo is cut on
     and one of the five the whole system draws. It is what stops the plate
     reading as a browser default border.

     IT HAS TWO ENDS AND BOTH ARE ADDRESSES: the corner where the frame's top
     and left strokes cross, and the corner of the picture inside the mount.
     Neither was stated. The tick started at -1px/-1px and ran --space-6, and
     the three numbers that fixed where it landed — the offset, the length and
     the rotation origin — each missed by under a pixel in a different
     direction, which is why it survived: the run measured 24 px against the
     22.63 the mount's diagonal actually is, so the line left 1.4 px OUTSIDE
     the corner as a burr in the wash and arrived 1.4 px past the picture's;
     and `transform-origin: 0 0` turns the bar about its top edge, not its
     centre, so the whole diagonal sat 0.35 px off the corner-to-corner axis.
     Sub-pixel each and plainly crooked together at any zoom — the review
     screenshot is one corner enlarged, and the burr is the first thing in it.

     THE THREE NUMBERS NOW STATE THE TWO CORNERS. Half the contour's own
     weight up and the origin half a weight down put the BAR'S CENTRELINE —
     not its top edge — on the crossing the two edge strokes make at the
     padding box's own corner, which is where they cross: both paths lie on
     the viewBox boundary and the frame is `overflow: visible`, so each stroke
     straddles that edge. From there the length is arithmetic and not taste:
     a 45° line closing a --mount square crosses --mount * sqrt(2), and the
     picture's corner is exactly --mount in on both axes. Measured after on
     both plates at 375 / 1024 / 1440 / 1920: the start lands on the crossing
     at 0.000 and the end on the picture's corner at 0.002 px, identical at
     every width because the mount is a token and the diagonal is its ratio.
     → .sp4-shot's --mount, which this length is read off */
  .sp4-tick {
    position: absolute;
    inset-block-start: calc(var(--stroke-1, 1px) * -0.5);
    inset-inline-start: 0;
    inline-size: calc(var(--mount) * 1.41421356);
    block-size: var(--stroke-1, 1px);
    background: var(--border-strong);
    transform: rotate(45deg);
    transform-origin: 0 calc(var(--stroke-1, 1px) * 0.5);
    pointer-events: none;
  }

  @media (min-width: 64rem) {
    /* WORDS LEFT, PICTURE RIGHT, four columns against eight. The review drew
       it that way — "text about the picture on the left" — and the split is
       the photographs' own: at the same height a 5:4 wants two thirds of the
       width a 2.55:1 does, so the picture takes the larger share and the
       words take a measure they can hold a line on. */
    .sp4-plate { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); }
  }

  /* ============================================================
     ACT 5 — the map. Base tier: no timeline, no camera, nothing hidden.
     The world framing is the viewBox, so this is what a reduced-motion
     reader, a narrow viewport, a browser without view timelines and the
     printer all get — one world map with everything on it and all three
     beats of copy under it, whole.
     ============================================================ */
  .map-track { position: relative; }
  /* THE SAME MARGIN AS EVERY OTHER LINE ON THE PAGE — see the note over
     .sp4-set for the measurements and for why a fixed rung cannot be one.
     This band carries the case that shows it: .map__legend and the three
     .map-beat runs are ordinary prose, set under the drawing at every width
     below the gate and under it inside the gate too, and at 834 they opened
     21.9 px left of "Partner & Technologien" two sections down. */
  .map-stage {
    display: grid;
    gap: var(--space-8);
    inline-size: min(100% - 2 * var(--gutter), var(--container-max, 76rem));
    margin-inline: auto;
    padding-block: var(--space-16);
    align-content: center;
  }
  /* THE CROP IS ON THE BOX AND overflow: visible STAYS ON THE SVG, which is
     two rules doing two different jobs and not one contradicting the other.
     The camera scales the map group fourteen times at the Germany framing, so
     everything outside that framing has to be cut — but cutting it at the
     <svg> would also halve every stroke lying on the viewBox boundary, which
     is the fault check-edge-stroke-clip.py exists to catch. Clipping the box
     that CONTAINS the svg crops the camera without touching the viewport the
     strokes are measured against. */
  .map-box { position: relative; overflow: clip; }
  .map { inline-size: 100%; block-size: auto; display: block; overflow: visible; }

  /* THE COASTLINE IS A CONTOUR, at the one weight the whole system draws in,
     and non-scaling so it stays a hairline while the camera moves. */
  .map__land {
    stroke: var(--border-default);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
  }
  /* THREE WEIGHTS, AND EACH IS ONE LAYER MORE THAN THE ONE BELOW IT. This is
     the lightest of them and the one the most countries wear: every country a
     later point stands on gets the strong contour. Europe adds the lattice on
     top of it, and the home market adds the ramp on top of that — so the act's
     "Deutschland und die ganze Welt" is drawn as a decay with Europe as its
     middle term, rather than as two countries at one weight and nine at none.
     Which country is in which set is derived in scripts/gen-world-map.py from
     the asset points act 5's third beat already carries. */
  .map__land--market { stroke: var(--border-strong); }
  /* fill-opacity AND NOT opacity, which check-authored-opacity.py insisted on
     and was right about. The ramp is meant to sit at half strength so the
     lattice and the contour stay readable through it — but lp-frame-in ends
     `opacity: 1`, the animation origin outranks the author origin, and the
     declaration never rendered at all: the fill arrived at full and the weave
     under it was gone. fill-opacity is a different property, so the arrival
     animates the element's opacity and the paint keeps its own strength. */
  .map__fill  { fill: url(#map-ramp); fill-opacity: 0.5; }
  .map__weave { fill: url(#map-weave); }

  /* A POINT IS A STROKE, NOT A CIRCLE. There is no vector-effect for a fill,
     so a <circle> would swell to a blot at the Germany framing and vanish at
     the world one. A zero-length path with a round linecap draws a dot whose
     radius IS a stroke width — constant at every camera scale, which is the
     only thing that lets one set of geometry serve three framings. */
  .map__dot {
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  /* A CONTOUR AND ONE LIGHT, on a point as on everything else here. Lime on
     the CF-Grau wash is the drawing's own colour and its own problem: at 6 px
     on a light ground it is a smudge, and twenty-seven of them scattered over
     a world map have to be findable. So an asset point is two strokes — the
     black contour under the lime — which is illustration.html's sentence at
     the smallest scale the page draws it. The office points need no ring:
     they are CF-Schwarz already. */
  .map__dot--office { stroke: var(--cf-schwarz); stroke-width: 9; }
  .map__dot--ring   { stroke: var(--border-strong); stroke-width: 8; }

  /* THE SIX KINDS, AND THE PALETTE IS THE BRAND'S OWN ARC. Lime -> Weiss ->
     Glas -> Sky -> Violett is the spectrum declared in tokens.css, and these
     are six steps taken along it in the order the drawing introduces the kinds.
     Nothing here is a new colour: every value is a rung of a ramp that already
     existed, which is what "seven, and no eighth" asks of anything that needs
     more than one accent.

     THE STEPS ZIGZAG IN LIGHTNESS ON PURPOSE -- .945, .44, .73, .58, .87, .62
     in OKLab L. Hue alone cannot separate six things in a four-hue palette;
     lightness can, and it is the channel that survives every kind of colour
     blindness. Walking the arc at one lightness was tried first and collapsed:
     the all-500 set put Sky against Glas at dE 8.9 for a normal-vision reader.

     MEASURED, NOT ASSERTED, against the wash the legend sits on (#E1E4E7):

       adjacent pairs   worst Sky 700 <-> Glas 700   dE 15.5 normal,
                        14.6 protan, 15.0 tritan            -- clears the 15 floor
       all pairs        worst Violett 500 <-> Sky 700 dE 10.6 normal,
                        8.3 deutan                          -- does NOT clear it
       on black         same adjacent worst, dE 15.5        -- clears it

     SO THE KEY IS DECODABLE AND THE DRAWING IS NOT, and the split is deliberate
     rather than overlooked. The key is an ordered list: a row's only neighbours
     are the rows above and below it, which is the adjacent-pair case, and every
     swatch has its name beside it -- so identity there never rests on colour.
     The map is the all-pairs case, because any two kinds can land next to each
     other, and no six-value set drawn from these four hue families clears it
     (the best found was 13.9). What the drawing therefore promises is weaker
     than the key's: that points differ, and roughly how -- not which is which
     at a glance. It is an illustrative map of asset TYPES, the copy says so,
     and no reading on this page depends on telling Schiene from Luftfahrt by
     hue at 5 px. → the note over the legend block in scripts/gen-world-map.py

     TWO KINDS STILL SHARE A COORDINATE. Dubai carries `see` and `luft` 0.2
     degrees apart and Singapore the same, so at the world framing each pair is
     one dot. That was true while both were lime; colour makes it visible
     instead of silent, and the port and the airport really are ~25 km apart.

     SCOPED, AND ALL SIX AT ONE SPECIFICITY. The rules in this file sit at an
     indent but not inside a block -- the two spaces are house style, not
     nesting -- so a bare [data-k="see"] would be a global selector matching
     anything on any page that ever uses that attribute name. :is() carries the
     specificity of its most specific argument, so each of the six weighs the
     same (0,2,0) and their order in this list cannot decide a winner.

     THE FALLBACK IS ON THE var() AND NOT ON A RULE OF ITS OWN, which is the
     cascade trap this went through once: `.map__key[data-k] { --map-k: lime }`
     as a default is (0,2,0) and would have beaten nothing here, but the same
     default written against .map__dot--asset plus .map__key[data-k] outranks a
     bare attribute rule and would have painted all six lime. var()'s second
     argument is not a declaration and cannot win a cascade -- it applies only
     when --map-k was never set, which is exactly the intent. */
  :is(.map, .map__legends) [data-k="see"]      { --map-k: var(--lime-500); }
  :is(.map, .map__legends) [data-k="berg"]     { --map-k: var(--lime-800); }
  :is(.map, .map__legends) [data-k="solar"]    { --map-k: var(--glas-700); }
  :is(.map, .map__legends) [data-k="bahn"]     { --map-k: var(--sky-700); }
  :is(.map, .map__legends) [data-k="batterie"] { --map-k: var(--sky-300); }
  :is(.map, .map__legends) [data-k="luft"]     { --map-k: var(--violett-500); }

  .map__dot--asset  { stroke: var(--map-k, var(--cf-lime)); stroke-width: 5; }

  /* A LABEL BELONGS TO A FRAMING, AND DOWN HERE THERE IS NO FRAMING.

     Each tag carries --x/--y computed for ONE of the camera's three shots —
     Konstanz and Berlin for the Germany zoom, the two country names for the
     DACH zoom — and the whole reason that is legal is stated over the camera:
     "the camera is still for the whole of every reveal and moves only in the
     gaps". Below the pin gate the camera does not merely hold still, it never
     exists: `.map__cam`'s animation is inside the @supports, so the transform
     computes to `none` and the drawing is the WORLD framing at every width.

     The four labels were placed anyway, all four at once — the fill-mode that
     staggers them is on an animation that has no timeline down here — on a map
     they were not placed for. Measured at 375 x 812, label anchor against the
     thing it names: Konstanz 89 px south of its own dot, out past the coast;
     Berlin 37 px east of Berlin, over Ukraine; DEUTSCHLAND 43 px south of
     Germany, in the Mediterranean; UNITED KINGDOM 109 px west of Britain, in
     the Atlantic. The two office dots are 4 px apart there, which is right —
     Germany is four pixels wide on a 327 px world map — and it is also why no
     label could have been correct: there is no room for one. At 768: 204, 68,
     103 and 183 px off.

     So the layer renders in the tier its coordinates mean something in, and
     nowhere else. Nothing is lost by that: all four names are in the beats'
     own copy — "Konstanz ist der Hauptsitz, Berlin der zweite Standort", "in
     der DACH-Region und in Großbritannien" — and those beats DO render down
     here, as the readable column the fallback tier is built around. A reader
     on a phone gets the world, the asset points, and the names in prose,
     instead of four names on the wrong continents.

     Same construction as .lp-flow-data's readings 440 lines up, and for the
     same reason: a layer whose placement was solved at one framing does not
     render at the others. → scripts/check-label-frame.py */
  .map__tags { position: absolute; inset: 0; pointer-events: none; display: none; }
  .map__tag {
    position: absolute;
    left: var(--x);
    top: var(--y);
    translate: 12px -50%;
    white-space: nowrap;
    color: var(--text-secondary);
  }
  /* NO COLOUR AT ALL, WHICH IS THE HONEST WAY TO SAY "the same ink". The <i>
     held --text-muted and measured 1.89:1 on "Hauptsitz" and 2.49:1 on
     "Standort" against the map's own ground — a place name, not chrome. Its
     parent .map__tag is already --text-secondary, so restating that token
     here would be a declaration that changes nothing and one more place for
     the two to drift apart. The separator the ::before draws is what
     distinguishes the detail from the name; the ink never was. */
  .map__tag i { font-style: normal; }
  .map__tag i::before { content: " · "; }
  .map__tag--land { translate: -50% -50%; color: var(--text-primary); }

  /* A KEY WITH ONE COLOUR IN IT KEYS NOTHING, and this was six rows of it.
     Every .map__key-dot was --cf-lime, so the reader was handed six names
     against one mark and no point on the drawing could be assigned to a line
     — while the two points the map does draw differently, the CF-Schwarz
     offices at Konstanz and Berlin, appeared in no row at all. Reported from
     the page on 2026-08-05.

     SIX MARKS WAS ONCE REFUSED HERE, AND THE REFUSAL HAS BEEN OVERTAKEN. The
     argument was that `--accent` is one lit element per object
     (foundations/colors.html#one-per-screen) and six hues would be six; that
     the drawn dot is 8 px across at the world framing, which is the framing the
     fallback tier, the reduced-motion reader and the printer all get; and that
     two of the kinds share a coordinate — Dubai carries `see` and `luft` 0.2°
     apart and Singapore the same — so a per-kind colour would put two of them
     on one dot. The last of those is still true and is now simply visible
     rather than hidden under one lime. The first is answered by taking the six
     steps off the brand's own spectrum instead of inventing hues, which is what
     --map-k does — see the measured note over it, up by .map__dot--asset, for
     what the palette separates and what it does not.

     So the key states BOTH: the two marks the map draws, at the size it
     draws them: the office dot is CF-Schwarz at 9, the asset dot lime at 5
     inside the --border-strong contour at 8, and because both are
     `vector-effect: non-scaling-stroke` those stroke widths ARE CSS pixels at
     every camera scale. The swatches are the same two circles at the same two
     diameters. Every dot on the drawing is on a line of the key.

     And under it, the six kinds, each with the colour its own points carry on
     the drawing — .map__key-dot--kind is .map__key-dot--asset's geometry
     exactly, 5 px inside the same 1.5 px contour, because it stands for the
     same mark. The counts that used to sit on these rows are gone: they were
     the tally of the 27 the drawing draws, and beside a row that now reads
     "über 7.000" they would have read as a breakdown of it. */
  .map__legends { display: grid; gap: var(--space-3); }
  .map__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3) var(--space-6);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .map__key { display: flex; align-items: center; gap: var(--space-2); }
  .map__key-dot {
    border-radius: var(--radius-full);
    flex: none;
  }
  .map__key-dot--office {
    inline-size: 9px;
    block-size: 9px;
    background: var(--cf-schwarz);
  }
  /* THE CONTOUR IS THE DOT'S, not a decoration around a swatch: on the map an
     asset point is two strokes, lime 5 inside black 8, and box-shadow is what
     draws that ring here without giving the swatch a border box 3 px wider
     than the mark it stands for. */
  /* A SWATCH IS READ, A MARK IS FOUND, AND THEY DO NOT WANT THE SAME SIZE.
     These were 5 px — the drawn asset point exactly, contour and all — on the
     argument that the key should state the mark at the size the map draws it.
     That argument was wrong the moment the six kinds got six colours. At 5 px
     inside a 1.5 px contour the ring owns most of the disc and the fill is a
     speck: Solar & Netz, Schiene and Batterie & Speicher are the pale middle
     of the arc, and against black at that size all three read as one hollow
     ring. Reported off the rendered legend, and it is the same fault the note
     at the top of this block describes — a key whose marks cannot be told
     apart keys nothing — arriving through size instead of through colour.

     9 px is the office dot's diameter, so the three swatch kinds now sit on
     one visual weight: a filled black disc, and a filled colour disc inside
     the contour that says "asset point". The drawing is unchanged; what the
     key gives up is a literal size match nobody could have measured by eye,
     and what it buys is the colour actually being visible. */
  /* THE CONTOUR IS DRAWN INWARD, AND THAT IS A CLIPPING FIX, NOT A STYLE ONE.
     An outset box-shadow paints OUTSIDE the element's box and takes no layout
     room for it, so the ring lived 1.5 px beyond the swatch on every side —
     invisible until the swatch sat flush against an edge that clips. It does:
     .map__key-name and .map-beat are set to the page margin, so the leftmost
     key in each list starts exactly on .map-stage's content edge, and the pin
     tier gives that stage `overflow: clip`. Measured on the shipped page at
     1680 x 1050: stage edge x 200.0, "Offshore & Schiff" swatch x 200.0, its
     ring reaching to 198.5 — a crescent sliced off the left of the first mark
     in the row, and only that one. "Standorte" sits at the same x and survived
     because it is a flat background with no shadow to lose, which is exactly
     the asymmetry the report showed.

     THE RENDERING IS UNCHANGED, TO THE PIXEL. 9 px of colour inside a 1.5 px
     ring painted 12 px across; 12 px of box with the same ring drawn `inset`
     paints the same 12 px and leaves the same 9 px of colour, because
     box-sizing is border-box system-wide (base.css). What changes is only that
     the ink now stops at the box, so no ancestor's clip can reach it and the
     key's leftmost ink lines up ON the page margin rather than 1.5 px outside
     it. The office dot keeps its 9 px: it has no ring to lose, and matching it
     to 12 would be a visual change made for a fault it never had. */
  .map__key-dot--asset,
  .map__key-dot--kind {
    inline-size: 12px;
    block-size: 12px;
    background: var(--map-k, var(--cf-lime));
    box-shadow: inset 0 0 0 1.5px var(--border-strong);
  }
  /* THE SUMMARY ROW CANNOT WEAR ONE KIND'S COLOUR. While every asset point was
     lime, a lime swatch on the "Anlagen" row meant "an asset point" and was
     exact. Now that the kinds are separated, lime is what `see` wears — so that
     same swatch would have put Offshore & Schiff's mark against the total, two
     rows apart, with a different meaning each time. It carries the arc the six
     steps are taken from instead: --gradient-spectrum is the brand's own
     Lime → Weiss → Glas → Sky → Violett, so the row reads "all of the below"
     and belongs to no single kind. At 5 px it resolves to a multi-coloured dot
     rather than to seven readable bands, which is the whole of what it has to
     say next to a total. */
  .map__key-dot--asset { background: var(--gradient-spectrum); }
  /* FORCED COLOURS REPAINT THE KEY AND NOT THE DRAWING, which would leave the
     one two blank gaps. An SVG's own fill and stroke are exempt, so under
     Windows High Contrast the map still draws the CF-Schwarz office and the
     lime asset while both swatches here measured rgb(255, 255, 255) — Canvas,
     on Canvas — with the box-shadow ring dropped as well, exactly the way
     .rule loses its background in base.css.

     Redrawn with the two paints forced-colors keeps, and with the distinction
     the drawing makes rather than the colours it makes it in: the office mark
     is filled CanvasText, the asset mark is a CanvasText ring around Canvas.
     Filled against hollow is not lime against black, and it does not have to
     be — it has to tell two marks apart, and it is the same trade base.css
     takes when the three dashed rules collapse to dashed and dotted.

     THE SIX KIND SWATCHES COLLAPSE ONTO THE ASSET MARK, and there is no sixth
     paint to give them. forced-colors keeps a system palette, not a brand one,
     so --map-k's six steps all resolve to the same ink and six identical rings
     would be six marks that key nothing — the exact fault the note at the top
     of this block was written about. One ring, repeated, at least says the true
     thing: each of these rows is an asset kind. The names carry which one, and
     they always did; the swatch has never been the only thing that told them
     apart. The gradient on --asset goes too: a linear-gradient is a background
     image, forced-colors does not repaint it, and it would have been the one
     swatch here still wearing brand colour. */
  @media (forced-colors: active) {
    .map__key-dot--office { background: CanvasText; }
    .map__key-dot--asset,
    .map__key-dot--kind {
      inline-size: 12px;
      block-size: 12px;
      background: Canvas;
      border: var(--stroke-1) solid CanvasText;
      box-shadow: none;
    }
  }
  .map__key-name { color: var(--text-secondary); }
  /* The figure is the legend's payload — two locations, and the fleet those
     locations are watching — and it sat at --text-muted, 2.43:1 measured over
     the legend's own plate at 11 px against a 4.5:1 floor. The mono face and
     --text-xs are
     what set it apart from .map__key-name beside it; both now carry the same
     ink, which is the ink the name has always had. */
  .map__key-n { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-secondary); }

  .map-beat__mark { margin: 0 0 var(--space-3); color: var(--text-secondary); }
  .map-beat__lead {
    margin: 0;
    font-size: var(--text-h3);
    font-weight: var(--weight-bold);
    line-height: var(--leading-snug);
    color: var(--text-primary);
  }
  .map-beat__body {
    margin-block-start: var(--space-3);
    max-inline-size: 62ch;
    font-size: var(--text-lg);
    line-height: var(--leading-relaxed);
    color: var(--text-secondary);
  }
  /* FLOORED, and the one column is declared rather than left implicit. A grid
     with no grid-template-columns has one column of `auto`, and `auto` as a
     MINIMUM is the item's automatic minimum size — the widest unbreakable thing
     inside it. That is the fault check-grid-tracks.py exists for, written in
     base.css twice as prose, arriving here through the door that check does not
     watch: there is no `fr` in the declaration and no track list to inspect,
     because the track was never written down.

     The three beats are prose, and German prose is where this bites. Measured
     at 320 x 812 with a 32 px root — WCAG 1.4.4's 200 % text at the 320 px
     viewport 1.4.10 names — .map-say's implicit column floored at 298.7 px
     inside a .map-stage that is 224 px wide (`min(100% - 2 * --space-6, 76rem)`
     of a 320 px track), so all three beats and every line in them stood 74.7 px
     past the box. .map-track is `position: relative` with overflow visible, so
     that reached the document: html.scrollWidth 347 against a clientWidth of
     320, and the whole page — hero, acts, FAQ, footer — scrolled 27 px
     sideways at every scroll position.

     The legend was not the cause and looked like it: .map__key's own
     min-content is 160.6 and .map__legend's is 174, so both could shrink and
     both were simply given the 298.7 the column had already taken.

     minmax(0, 1fr) is the fix the rest of this system uses and the first of the
     two check-grid-tracks.py sanctions. Measured after, same sweep, at 320 with
     roots 16 and 32 and at 375 / 768 / 1280 / 1440: 0 px of horizontal overflow
     everywhere, and the 16 px render identical.

     WHAT IS LEFT, and it is not this rule's: at a 64 px root — 400 % text,
     beyond what 1.4.4 asks — the document still scrolls, from components.css's
     own chrome rather than from the acts. .cf-nav__list unfolded needs 1880 px
     at 1280, and the consent title and the skip link overflow at 320. */
  .map-say { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }

  @keyframes lp-frame-draw { from { scale: 0; } to { scale: 1; } }
  @keyframes lp-frame-in { from { opacity: 0; } to { opacity: 1; } }
  @keyframes lp-flow-out { to { opacity: 0; } }
  /* The field's three verbs. The stream opens beyond the glow's nearest
     corner (--fx/--fy, generated) and lands on its crossing at rest weight;
     the pulse swells a quarter above rest and back, twice, in place; the
     merge runs every glow to one point — the trunk's head in field units —
     and puts it out. All lengths are user units (fill-box does not change
     that), so the generated numbers land exactly. */
  @keyframes sp-field-arrive {
    from { translate: calc(var(--fx) * 1px) calc(var(--fy) * 1px); opacity: 0; }
    to { translate: 0 0; opacity: var(--iso-rest, 1); }
  }
  @keyframes sp-field-pulse {
    0%, 50%, 100% { opacity: var(--iso-rest, 1); scale: 1; }
    25%, 75% { opacity: var(--sp-peak, 1); scale: 1.12; }
  }
  @keyframes sp-field-merge {
    /* The glow BRIGHTENS through the flight and dies only on arrival: the
       field's light concentrates as it converges, which is the story. A
       linear fade across the whole run had the field invisible while it
       was still mid-screen — the merge un-telling itself. */
    45%, 80% { opacity: var(--sp-flare, 1); }
    to {
      translate: calc((var(--ux, 816) - var(--cx)) * 1px)
                 calc((var(--uy, 272) - var(--cy)) * 1px);
      scale: .25;
      opacity: 0;
    }
  }
  /* ============================================================
     THE STICKY BAR, RESERVED BY THE PAGES THAT HAVE ONE.

     Every act pins a 100vh stage to top: 0. That is right on a page with
     nothing above it — the prototype these were built in — and wrong on one
     with a floating nav: measured on the landing page at 1600 x 900, the
     root's foot sat below the fold and the source was cut in half by the
     bottom edge, because 84 px of the stage was under a bar the drawing did
     not know about.

     :has(), SO THE PAGE ANSWERS FOR ITSELF. The alternative was a page-local
     override, which is the thing this stylesheet exists to stop, or reserving
     the bar unconditionally, which would put 84 px of dead ground at the top
     of the prototype. A stage on a page carrying .cf-nav reserves the nav; a
     stage on a page without one reserves nothing. It is the same question
     .cf-pin__inner answers with a flat padding-block, asked in a way that
     survives being loaded by two pages instead of one.

     A SHORTER STAGE AND NOT PADDING, which is the correction. Padding was the
     first answer here and it moved the wrong things: an absolutely-positioned
     child is laid out against its container's PADDING box, so `inset: 0` spans
     the reserved strip instead of starting below it. Act 1 is three such
     layers — .sp-field, .sp-annots-fig, .sp-say — and all three stayed exactly
     where they were, under the bar: the field's top row of sensors and their
     leader labels were behind the nav, which is the finding this note now
     records rather than the one it used to claim to have fixed.

     THE BOTTOM EDGE DOES NOT MOVE, which was the objection to a shorter stage
     and is not one. The stage sticks at top: --act-bar and stands
     100vh - --act-bar tall, so its foot is at 100vh either way: the tree's
     eight feet still stand on the seam the next section slides in from, and
     only the head of the box comes down. One box, so cqh, every percentage,
     the slice crop and the aim script's matrices all follow it — which is the
     other half of why padding could not work, since none of those follow a
     padding strip either.

     --act-bar IS DECLARED IN THE BASE TIER so both tiers can read it, and it
     is 0 on a page with no .cf-nav — the prototype keeps a 100vh stage at
     top: 0, unchanged, and reserves nothing.
     ============================================================ */
  body { --act-bar: 0px; }
  body:has(.cf-nav) { --act-bar: calc(var(--nav-height) + var(--space-4)); }

  /* ============================================================
     THE HANDOVER LINE OWNS AIR ON BOTH SIDES OF ITSELF, and until this rule
     it owned air on one.

     .cf-section-header ships with `margin-bottom: var(--section-header-gap)`
     and no margin-top, which is right everywhere it is used: a section header
     is the first child of its .container, and the space above it is the
     section's own padding. This one is not. "Was wir machen" is authored at
     the END of act 2's box — the note over it in the markup gives the reason,
     the nineteen drops land on this rule — so the thing directly above it is
     the statement's closing paragraph, in the same box, with nothing between
     them at all.

     Inside the gate that is exactly what is wanted and the gated tier says so:
     the rule IS the drawing's baseline, the feet stand on it, and
     `margin-top: var(--space-3)` is the whole distance the composition can
     spare. Outside the gate there is no drawing over the rule. There is a
     paragraph, and the gated rule is not in scope, so the header inherited
     the component's nothing. Measured, last line of .cf-statement__body to the
     top of .sp-head:

       viewport     above the rule   below it, to card 01
       375 x 812     0 px            144 px
       600 x 900     0               144
       768 x 900     0               144
       834 x 900     0               150
       1023 x 900    0 (via layout) 165
       1024 x 900   12 (gated)       —  pinned

     Zero is not tight, it is touching: at 834 the descenders of "die
     Datenströme in Geschäftsmodelle verwandelt." sit on the cap height of
     WAS WIR MACHEN, and at 375 the header wraps to two lines directly under
     the paragraph's last one and the two read as a single block of text. The
     line that introduces four cards was glued to the sentence before it and
     stood 144 px clear of the cards it names — proximity saying the opposite
     of what the document structure says. The <h2> in here is `id="prozess-titel"`,
     and the section below is labelled by it. The ADDRESS `prozess` is that
     section's own and not this heading's, because this heading stands inside
     .sp-stage and a fragment cannot land in a stuck box — see the note over
     the heading in patterns/landing-page.html, and check-sticky-address.py.

     THE NUMBER IS THE COMPONENT'S OWN, not a new one: --section-header-gap is
     what .cf-section-header already reserves under the rule, so the rule keeps
     the same air on both faces and the section padding below does the rest of
     the separating. No threshold — the gated tier overrides it by cascade, in
     the base-first order this file's preamble requires, and that is why this
     rule is authored HERE and not beside the other .sp-head declarations 1,300
     lines down, which are all after the gate closes.
     ============================================================ */
  .sp-head { margin-top: var(--section-header-gap); }

  @supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
    /* ============================================================
       WHAT BOTH DRAWING TIERS SHARE. Two tiers animate this root — the pinned
       stage below (min-width: 64rem) and (min-height: 45rem), and the flow
       tier under it — and these two declarations belong to neither. They were
       authored inside the pinned block because for as long as there was one
       animated tier, "inside the gate" and "wherever the drawing moves" were
       the same place. They stopped being the same place when the flow tier
       started drawing, and a copy of either of them in the second tier is a
       number that can drift from its twin.
       ============================================================ */
    @media screen and (prefers-reduced-motion: no-preference) {
      /* THE GROW ORIGIN, which is the one thing lp-frame-draw cannot work out
         for itself: `scale: 0 -> 1` grows about transform-origin, and --o is
         where each stroke's own drawing END is. fill-box, so the percentage
         is of the path's own bounding box rather than of the whole viewBox. */
      .lp-flow__seg, .lp-flow__light, .lp-flow__node, .lp-flow__val, .lp-flow__read {
        transform-box: fill-box;
        transform-origin: var(--o, 0 0);
      }
      /* --leaf-peak, AND IT IS NOT --arrive-light-peak. The first pass reused
         that token and the shimmer could not be seen: 0.18 of a ramp that is
         mostly CF-Grau, times a 0.35-1 swing, is 0.06 to 0.18 of a mid grey
         over a light wash — "cant see the shimmer leaves", which is the whole
         test a light like this has to pass.

         The token was the wrong one to borrow. --arrive-light-peak is capped
         at 0.18 because in THAT pattern a light band crosses a dashed ghost
         contour and lightens it, and tokens.css works the contrast out: past
         0.52 the ghost fails. Here nothing is crossed. The leaf's own contour
         is drawn underneath at full weight by .lp-flow__seg and stays there,
         so raising this can only add lime over the wash — it cannot take
         contrast off anything.

         0.5 is chosen against that same arithmetic anyway, one notch inside
         the 0.52 the ceiling row gives, so the number is defensible by a
         measurement this system already has rather than by a fresh sweep. With
         the 0.3-1 swing a leaf sits between 0.15 and 0.5 of the ramp.

         The other half of being visible is not amplitude but WHICH PART OF THE
         RAMP lands on the stroke — see LEAF_STRETCH in gen-flow-root.py. */
      .sp-stage { --leaf-peak: 0.55; }

      /* ============================================================
         ACT 2 WITH NO STAGE UNDER IT — the tree draws on a phone.

         WHAT WAS WRONG. Below the pin gate the root was finished ink. Every
         stroke of it, both passes, every junction and the source, painted at
         rest from the first frame — so the one drawing on this site whose
         whole subject is data ARRIVING was, on the device most readers meet it
         on, a diagram of a tree. The gated tier's own note calls that out
         about the settled fringe ("a still root with no light in it is a
         diagram of a tree, not of a stream"); outside the gate the whole
         drawing was that.

         Nothing was broken and that is why it stood. The flow tier is a
         designed tier, not a fallback — check-fallback-tier.py exists to keep
         it one — and a static drawing is a legitimate thing for it to be. It
         simply was not a decision anyone had taken: the root's timing was
         authored inside the pin gate because the pin gate was where scroll
         time came from, and every viewport under 64rem inherited the absence.

         WHY THE PINNED SCRIPT CANNOT SIMPLY BE POINTED AT A SECOND TIMELINE.
         --sp is .sp-track's own view timeline and the track is 640vh — 4 860 px
         of `contain` at 1440 x 900, bought by a sticky stage. There is no
         sticky stage here: .sp-track is ordinary flow and stands 925 px tall
         at 390 x 844, 81 px taller than the window it is read in, so its whole
         `contain` range is those 81 px and the pinned script's 53 points of
         root would resolve to 43 px of wheel — the tree in a flick. At 430 and
         up the track is SHORTER than the window and the range is zero: the
         same fault check-act-beats.py found in the rail's own arithmetic.

         SO THE DRAWING IS ITS OWN TRACK. The subject is .lp-flow, the <svg>
         itself, and the range is `contain` — which for a subject shorter than
         the window means exactly "while the whole of it is on screen". That is
         the honest budget a page with no pin has, it needs no reserved scroll
         and no second layout, and it is self-fitting: every viewport gives the
         drawing the whole of its own visible pass and nothing is ever drawn
         off-screen. Measured, window minus drawing:

           320 x 568    drawing 145 tall    423 px of scroll
           375 x 812            172         640
           390 x 844            179         665
           430 x 932            198         734
           768 x 1024           353         671
           1023 x 900           272         628
           1440 x 700           387         313

         The floor is structural rather than lucky: .sp-drawing is
         `min(100%, (100vh - 12rem) * 1.9)` wide and the drawing is 620/1200 of
         that, so its height can never exceed 0.98vh - 188px — the subject is
         always shorter than the window, and `contain` always has a run.

         WHAT THE WINDOW NEEDS FROM THE PAGE, and it is one sentence: the
         drawing has to start below the fold. `contain 0%` is the scroll at
         which the subject has just finished entering, and a subject that is
         already whole on screen at scroll 0 puts that moment at a NEGATIVE
         scroll the reader cannot reach — the browser clamps, and the act opens
         part-drawn. On the landing page the hero is above it and the whole
         window is reachable at every viewport measured, 320 to 1440.
         prototypes/statement-to-process.html is the case that shows the edge:
         the drawing is 215 px down a page with no hero, so a phone opens it at
         about two thirds of the act with fifty-six of the sixty-five strokes
         already landed. That is the lab page and it is the page's geometry
         rather than this tier's, but it is what to look at first if a third
         consumer ever opens mid-act.

         THE CONSENT BANNER SHORTENS IT AND NOTHING HAS TO KNOW. base.css
         reserves the banner as `scroll-padding-bottom` on <html>, and a view
         timeline measures the scrollport that padding leaves: measured at
         390 x 844 with the banner up, the run is 331 px against 665 with it
         gone. The pinned tier has to subtract --cf-consent-height by hand for
         its own reservation (see --lp-measure); this one is simply told.

         AND THE SCRIPT IS THE PINNED ONE, RE-RATED. Same four families, same
         verbs, same order, same algebra — `head + l * c` to
         `head + (l + u) * c`, so a stroke still opens on the point its parent
         closes on and one front still travels the whole root. What changes is
         c and the four heads, because 100 points of this timeline are 665 px
         where 100 points of --sp are 4 860, and the offsets that space the
         four families apart are points rather than pixels. --flow-cf is 24
         against --flow-c's 14.96: the longest walk is 3.0, so the run is 72
         points of a 100-point window against 44.88 of the pinned tier's 53.
         The four heads are the pinned ones re-spaced by the same ratio — the
         light leads the contour by 10 points here and by 6 there, which is 0.4
         units of walk in both, so the front runs the same distance ahead of
         its own black in the drawing's own units.

           contain %      2 - 74   the light draws the root, canopy inward
                         12 - 84   the contour lands ten points behind it
                         14 - 91   the junctions fill as the front passes
                         16 - 95   the numerals light on the strokes they name
                         18 - 88   the light fades off each stroke it closed
                         79 - 88   the source fills: the routes are one light
                         88 -100   the finished drawing holds

         WHICH IS 1.4x THE PINNED RATE PER PIXEL OF INK, not 7x per point. The
         two tiers are not drawing the same-sized tree: 179 px tall here
         against 573 in the stage at 1440 x 900, so the honest comparison is
         scroll spent per pixel of DRAWING. The root's run is 72 points of
         665 px over a 179 px tree — 2.7 px of wheel for each px of tree —
         against 44.88 points of 4 860 over 573, which is 3.8. Not quite half
         again as fast, on a device whose scroll gesture is a thumb, which is
         the trade this tier is.

         WHAT IS NOT BROUGHT DOWN HERE. Act 1 — the sensor field, its
         convergence and the annotations — stays withdrawn: .sp-field is
         `display: none` outside the gate by A TIER WITH NO TIME AXIS's own
         ruling, .lp-flow-sources is the static telling that replaces it, and
         act 2's copy stays in flow under the drawing rather than becoming the
         overlay check-fallback-tier.py forbids here. This tier animates the
         ROOT and nothing else on the stage moves.
         ============================================================ */
      @media not all and (min-width: 64rem) and (min-height: 45rem) {
        /* THE SUBJECT IS THE DRAWING AND NOT ITS FIGURE. .sp-root is the same
           box as .sp-drawing and it holds act 2's four paragraphs as well —
           503 px against the svg's 179 at 390 x 844 — so a timeline on it
           would be timing the copy's pass and not the tree's.

           AND THE NUMERALS NAME THEIR OWN COPY OF IT, which is a scope rule
           and not a second decision. A named timeline reaches the element that
           declares it and that element's DESCENDANTS; .lp-flow-data is the
           drawing's SIBLING — the numerals are HTML on purpose, see the note
           over the layer — so `--sp-flow` does not reach them. Measured, the
           head numeral's animation resolved to a null timeline and stood at
           opacity 1 through the whole act: the one label on a phone, lit over
           an empty box, naming a rate no stroke had drawn yet.

           `timeline-scope` on .sp-root is the other way to spell this and it
           is a THIRD feature behind a gate that names two — the exact shape
           check-range-gate.py exists for — so it would need its own @supports
           around one rule. A second view-timeline costs nothing instead,
           because the two subjects are the same box by construction: the
           numeral layer is `inset: 0 0 auto 0` of .sp-root carrying the
           drawing's own aspect-ratio, which is the equality
           check-fallback-tier.py already holds against the viewBox. Measured
           at 390 x 844, both boxes: top 510, height 179. */
        .lp-flow { view-timeline-name: --sp-flow; view-timeline-axis: block; }
        .lp-flow-data { view-timeline-name: --sp-flow-data; view-timeline-axis: block; }

        /* THE FLOW TIER'S --flow-c, and the pair is what check-flow-chain.py
           holds: one rate per tier, declared once, with every window in that
           tier written off it. A typed constant in one of the eight rules
           below is how the two tiers stop being one drawing. */
        .sp-stage { --flow-cf: 24; }

        .lp-flow__light {
          animation: lp-frame-draw linear, lp-flow-out linear;
          animation-timeline: --sp-flow, --sp-flow;
          animation-range: contain calc((2 + var(--l) * var(--flow-cf)) * 1%)
                           contain calc((2 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%),
                           contain calc((6 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%)
                           contain calc((16 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%);
          animation-fill-mode: both, forwards;
        }
        .lp-flow__seg {
          animation: lp-frame-draw linear both;
          animation-timeline: --sp-flow;
          animation-range: contain calc((12 + var(--l) * var(--flow-cf)) * 1%)
                           contain calc((12 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%);
        }

        /* THE LIGHT IS A SHARE OF THE DRAWING AND NOT A NUMBER OF PIXELS, which
           is the one place this tier cannot copy the stage's. The pinned leaf
           is 6 px over a 1 px contour and that is viewport-independent because
           the pinned drawing is: the stage fits it to the window and it is
           never far from 1020 px wide. Here the same drawing is 280 px wide at
           a 320 viewport, 347 on a phone, 684 at 768 and 965 in a short 1440,
           so one authored pixel width is a different object at each end.

           AND IT IS 1 % AND NOT THE PINNED TIER'S OWN 0.59 %, which is the
           number that has to be argued, because carrying the proportion across
           is the obvious thing to do and it does not work. 6 px of 1020 is
           0.588 %, and 0.588 % of a phone's 347 is 2.0 px — under the 1 px
           CONTOUR it is supposed to stand either side of, so it is not a thin
           light, it is no light. The contour does not shrink with the drawing:
           it is `non-scaling-stroke` at 1 px in every tier, by the
           illustration foundation's one-weight rule. So the clearance the
           light has to buy is a fixed 1 px, and buying it out of a smaller
           drawing costs a larger share. 1 % leaves 1.2 px of ramp proud on each
           side at 347 and 2.9 at 684 — the same reading at both, which 0.588 %
           is not.

           .sp-root is the query container (`container-type: inline-size`, for
           the numeral layer's sake) and .lp-flow is 100 % of it, so 1cqi is
           1 % of the drawing to the pixel: measured, the svg box and the
           numeral layer's box are identical at every width in the table the
           numeral layer's own note keeps.

           AND NO GLOW HERE. The pinned leaf carries the composed drop-shadow
           pair so the light pools between the feet. Its reach is 36 px at
           1020 — six times the stroke — and six times this stroke on a phone
           is 21 px into gaps that are 14 px wide, which is not a fringe
           standing in light, it is a lime band with a drawing behind it. The
           feet are close enough at this size to read as a fringe unaided. */
        .lp-flow__leaf { stroke-width: 1cqi; }
        /* WEBKIT WILL NOT PAINT A NON-SCALING STROKE A SCROLL TIMELINE HAS
           SCALED, and the pinned tier's own note further down has half of this
           finding already: "SAFARI NEVER PAINTS THIS LEAF … only dropping
           vector-effect does". It attributes that to the filter-region cache
           for a non-scaling stroke, on the evidence it had — the leaf was the
           one stroke on the page carrying both a filter and `vector-effect`.

           THIS TIER HAS NO FILTER AND THE LEAF STILL GOES DARK, which narrows
           it: the trigger is `vector-effect: non-scaling-stroke` on an element
           lp-frame-draw has driven, and the filter is not part of it. Measured
           in WebKit 26.5 at 390 x 844, contain 70 %, the same frame four ways:
           as authored, the fringe is black; with `vector-effect: none`, lime;
           with the scale keyframe taken out of the leaf's list, lime; and
           Chromium draws it lime in all of them. `.lp-flow__seg` and the
           lights that are not leaves are unaffected in both tiers — a leaf is
           the only stroke still being driven, by the settle, after its own
           draw has closed.

           AND THE WIDTH IS RESTATED IN THE DRAWING'S OWN UNITS, because
           dropping `vector-effect` is what changes what `stroke-width` MEANS:
           an SVG geometry property resolves its length to px and then reads
           that number as USER UNITS, so `1cqi` would be 3.5 units — 1 px on a
           phone, under the contour again. 12 is the same 1 % restated: the
           viewBox is 1200 wide, so 12 units is a hundredth of the drawing at
           every scale and the two branches render the same stroke. That is the
           difference from the pinned tier's own workaround, which loses its
           authored width in WebKit and records the loss. */
        @supports (-webkit-hyphens: none) {
          .lp-flow .lp-flow__leaf { vector-effect: none; stroke-width: 12; }
        }
        .lp-flow__leaf {
          animation: lp-frame-draw linear,
                     sp-leaf-settle linear,
                     sp-leaf-shimmer var(--arrive-period) ease-in-out infinite alternate;
          animation-timeline: --sp-flow, --sp-flow, auto;
          animation-range: contain calc((2 + var(--l) * var(--flow-cf)) * 1%)
                           contain calc((2 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%),
                           contain calc((6 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%)
                           contain calc((16 + (var(--l) + var(--u)) * var(--flow-cf)) * 1%),
                           normal;
          animation-fill-mode: both, forwards, none;
          animation-delay: 0s, 0s, calc(var(--l) * -370ms);
        }
        /* The third one only, for the reason the pinned tier's own copy of
           this line gives: the two scrubbed animations cost nothing while the
           page is still, and the shimmer is the one on the document clock.
           cf-idle.js carries no width gate, so [data-idle] lands on .sp-track
           in this tier exactly as it does in the other. */
        [data-idle] .lp-flow__leaf { animation-play-state: running, running, paused; }

        .lp-flow__node {
          animation: lp-frame-in linear both;
          animation-timeline: --sp-flow;
          animation-range: contain calc((14 + var(--l) * var(--flow-cf)) * 1%)
                           contain calc((24 + var(--l) * var(--flow-cf)) * 1%);
        }
        /* Only .lp-flow__head is on screen in this tier — the layer's own rule
           1,600 lines up hides the other seventeen under a 56rem container and
           the flow tier's drawing never reaches it — so what this window
           actually times is the rate arriving on the trunk once the root has
           become one route. --l 2.89 puts it at 85 to 95, which is the same
           place in the act the pinned tier lights it. */
        .lp-flow-data :is(.lp-flow__val, .lp-flow__read) {
          opacity: 0;
          animation: lp-frame-in linear both;
          animation-timeline: --sp-flow-data;
          animation-range: contain calc((16 + var(--l) * var(--flow-cf)) * 1%)
                           contain calc((26 + var(--l) * var(--flow-cf)) * 1%);
          animation-fill-mode: forwards;
        }
        /* The source arrives last here too: the trunk's own contour closes at
           84, so the orb opens while that last stroke is still landing and is
           full four points after it. */
        .lp-flow__orb {
          animation: sp-orb-fill linear both;
          animation-timeline: --sp-flow;
          animation-range: contain 79% contain 88%;
        }
      }
    }

    @media screen and (prefers-reduced-motion: no-preference) and (min-width: 64rem) and (min-height: 45rem) {
      /* AND THE GROUND IS WHOLE IN HERE, SAID BY THE GATE ITSELF. The band's own
         note 1100 lines up ends "Inside the gate it is off altogether", and
         until this change nothing in this block said so: the only rule turning
         the clip off was `@media (min-width: 64rem)`, which the gate happens to
         imply, so the pinned tier was relying on a declaration written for the
         flow tier's two-column arrangement. The moment that rule became a band
         rather than a `none` the pinned stage inherited it, and act 1's
         full-bleed field — the whole argument for a sliced 1600 x 900 backdrop —
         was cropped to the top of the viewport:

           1024 x 900   field 273 of 800 px of stage    34 %
           1280 x 900         343 of 800                43
           1440 x 900         387 of 800                48
           1920 x 1080        387 of 980                39

         The band belongs to the STACK, and inside the gate there is no stack:
         .sp-say is absolutely placed over the root (THE OVERLAY, below) and the
         stage is the viewport. So the tier that has no copy under the drawing
         states its own `none`, and the flow tier's arithmetic can change without
         reaching in here again. */
      .sp-stage { --sp-ground-clip: none; }

      /* AND ACT 1's INK IS DRAWN IN HERE, WHICH IS THE ONLY TIER THAT CAN
         PUT TIME BETWEEN IT AND THE ROOT. The withdrawal is on the base
         rules — see A TIER WITH NO TIME AXIS, ~1,100 lines up — and this is
         the restoration, stated by the gate rather than by a threshold that
         happens to agree with it, for the same reason the ground's `none`
         directly above is stated here. `block` and not `revert`: .sp-field
         is an <svg>, whose UA display is `inline`, and the rule that lays
         the layer out — `.sp-field { position: absolute; inset: 0; … }`,
         beside THE GROUND LAYER's note near the top of this file — declares
         `display: block` on purpose. */
      .sp-field,
      .sp-annots-fig { display: block; }

      /* AND THE FLOW TIER'S SENSORS LEAVE AS ACT 1's REAL FIELD ARRIVES.
         .lp-flow-sources is the static telling — one bead per canopy entry,
         argued at its own declaration — and it exists for exactly the tier
         this block is not: in here act 1 draws the full field, act 1c
         converges it onto the trunk's head, and a second, permanent row of
         sensors standing on the finished root would be a third telling of
         the same story over act 2's ending. The mirror of the two lines
         above, stated by the same gate, held by the same check: withdrawn
         where the field plays, drawn where it cannot. AFTER the container
         rule that shows the layer, by this file's own base-first order, so
         the gate wins the cascade at every width the pin admits. */
      .lp-flow-sources { display: none; }

      /* THE TYPEWRITER'S RESERVED BOXES, IN THE TIER THAT TYPES. Six
         min-block-sizes hold act 2's, act 4's and act 5's copy runs still
         while cf-stream.js writes them character by character: without them
         the line count grows as characters land and the composition steps
         down the screen under the reader. They were declared on the base
         rules, which is every tier — and cf-stream.js runs in exactly one.
         Its own header says so: "It refuses to run unless the pinned layout
         is actually active — no support for scroll-driven animations,
         reduced motion, or a viewport below the consumer's own gate", and
         the consumer's own gate is the two lines directly above this. In
         every other tier the copy is whole markup at rest, nothing reflows,
         and the reservation is the height of a sentence that is not coming.

         WHAT IT COST, measured as box height minus the same box's natural
         height with the floor lifted, JavaScript on, at rest:

                                          320   375   768  1023   1440
           .sp-say__lead   x2 (2.3em)      36    36    36    36     —
           .sp4-say__lead  "Und das ganze
                            Team" (2.4em)  30    30    30    30     —
           .sp4-say__lead  plate 01         —     —    30    30     —
           .sp4-say__body  x2 (7.4/5.2em)   —     —    63    63     —
           .map-beat__body x2 (4.6em)       —     —    54    54     —
           page total                      66    66   213   213      0

         The 1440 column is the gate met, where every one of those floors is
         load-bearing and stays. The visible fault was act 4's second plate:
         "Und das ganze Team" is one line in a box reserving two, so on a
         phone the heading stood 30 px clear of the paragraph it heads while
         the plate above it — a two-line name — had none. Two plates, one
         section, two different rhythms, and the difference was which
         headline happened to wrap.

         AND THE GATED TIERS DO NOT MOVE, which is the whole test of whether
         this is the right gate. patterns/landing-page.html, document height,
         before -> after:

           320x812     8767 -> 8688      1024x900   22758 -> 22758
           375x812     8429 -> 8347      1440x900   22931 -> 22931
           768x812     8900 -> 8686      1920x1080  27375 -> 27375
           1023x900    8119 -> 7822
           1440x900 reduce  8595 -> 8504
           1440x700 (short) 8395 -> 8304

         Every tier without a timeline is shorter by the air it was holding;
         every tier with one is identical to the byte.

         components.css has the system's only other instance of this device,
         .cf-values__title/__body on ueber-uns, and it has been inside its own
         @supports block since it was written — same reason, stated in its own
         comment. This is that rule, put back where that one already is.
         → cf-stream.js header; scripts/check-stream-reserve.py */
      .sp-say__lead  { min-block-size: 2.3em; }
      .sp-say__body  { min-block-size: 7.5em; }
      .sp4-say__lead { min-block-size: 2.4em; }
      .sp4-say__body { min-block-size: 7.4em; }
      .sp4-plate:last-of-type .sp4-say__body { min-block-size: 5.2em; }
      .map-beat__lead { min-block-size: 1.3em; }
      .map-beat__body { min-block-size: 4.6em; }

      /* THE FLOOR IS CAPPED AT THE VIEWPORT IT IS A FLOOR FOR, and the min()
         is the whole of the fix. 45rem is this gate's own `min-height: 45rem`
         restated as a length so the composition never gets less room than it
         was drawn in — sound, and written in the one unit that means two
         different things in the two places it is written.

         In a MEDIA QUERY, `rem` resolves against the root element's INITIAL
         font size and never against the reader's: `(min-height: 45rem)` is
         720 px at every text size there has ever been. In a DECLARATION it
         resolves against the actual root, so the same 45rem is 900 px at a
         20 px default, 1080 at 24 and 1440 at 32. The gate therefore admits a
         768 px viewport and the floor inside it then asks for 1440 — a floor
         720 px above the ceiling that clips it.

         Measured on patterns/landing-page.html, act 3's twenty copy runs
         (title, subtitle, body, benefit label, benefit, over four cards),
         counting only the ones wholly inside .cf-pin__stage's clip:

           root   1280x900        1920x1080       1024x768
            16    20/20  cut 0    20/20  cut 0    20/20  cut 0
            20    20/20  cut 0    20/20  cut 0    10/20  cut 213   ->  13/20
            24     9/20  cut 278  20/20  cut 0     2/20  cut 421   ->  10/20
            32     0/20  cut 684   5/20  cut 491    0/20  cut 776   ->   8/20

         At a 32 px default — 200 % text, WCAG 1.4.4 at level AA — every word
         of "Was wir machen" was below the clip and the one section that says
         what the company does rendered as the isometric figure, the index, and
         nothing else. Nothing errored, nothing scrolled sideways, and no gate
         caught it, because every check in this repository reads a 16 px root.

         min() IS A NO-OP AT THE DEFAULT SIZE and that is provable rather than
         hoped: the gate does not apply below a 45rem viewport, 45rem is 720 px
         in the gate, so at a 16 px root the floor is never above the height it
         is capped to. Measured row for row above — the 16 px line is identical
         before and after, at all three viewports.

         What is left is a card cropped SYMMETRICALLY against the stage's own
         clip instead of pushed out of the bottom of it, which is what the note
         over .lp-proc-steps's middle row already calls "the right thing to
         lose". Above about 150 % the honest answer is for act 3 to leave the
         pinned mode altogether, and no media query can ask that question —
         see the audit on the pull request that landed this.
         → scripts/check-rem-floor.py */
      /* AND THE FLOOR IS THE GATE'S OWN THRESHOLD LESS THE BANNER, which is the
         one term of this expression that was never measured against the layer
         it is competing with.

         The reservation and the floor are both right and they were written to
         beat each other. `100vh - banner` is the room the reader can see;
         `min(45rem, 100vh)` is the room the composition was drawn in. max()
         takes the larger, so at every height between the gate's floor and
         45rem + the banner the floor wins and the reservation is not paid at
         all — check-consent-clearance.py's link 6 says so in its own words,
         "tapers to nothing at the gate's floor", and 1280 x 720 is where it
         tapers to nothing. It is also the commonest laptop viewport there is.

         Measured on patterns/landing-page.html, first load, the stage stuck,
         banner up, at a 16 px root — px of the bar and of the card's foot
         behind the banner, and what elementFromPoint returns at the bar's
         middle:

           viewport      bar buried        card foot buried     at the bar
           1280 x 720    99.4 ->  0        82.4 -> 42.1         banner -> bar
           1024 x 720    81.4 ->  0        64.4 -> 39.4         banner -> bar
           1366 x 768    51.4 ->  0        34.4 ->  0           banner -> bar
           1024 x 768    33.4 ->  0        16.4 ->  0           banner -> bar
           1280 x 800    19.4 ->  0         2.4 ->  0           banner -> bar
           1280 x 900     0                 0                   bar
           1024 x 900     0                 0                   bar
           1920 x 1080    0                 0                   bar

         Nothing is cropped in either column — the stage's clip cuts 0 px at
         every row before and after — so the crop the floor exists to prevent
         is not the price of paying the reservation. It is the price of paying
         it UNFLOORED, which is a different expression and still wrong: drop
         the floor entirely and a 333 px banner takes the card to 358 px wide
         and 1543 tall and the clip cuts 286.7 of it, and a 20 px root at
         1280 x 720 cuts 241.8. The floor is load-bearing. It was standing in
         the wrong place.

         9rem IS THE BANNER, and it is a floor's constant rather than a
         synchronised one. The banner is 144 px at every width this gate
         admits — 1024, 1280, 1366, 1440, 1512, 1920, measured, because its
         copy stops re-wrapping above 900 — so 45rem - 9rem is the gate's own
         floor restated in the room the banner leaves. It does not have to
         track the real banner and must not: --cf-consent-height still carries
         that, and where the real banner is TALLER than 9rem the floor simply
         stops the reservation early. At 232 px the card holds 736 px and 20
         of its 22 copy runs instead of collapsing to 560 and 15; at 333 px it
         holds the same 736 instead of the 358 that cropped.

         BYTE-IDENTICAL WHEREVER THE BANNER IS NOT UP, unchanged: with the
         property unset the reservation is 100vh, which is never below a floor
         capped at 100vh, so max() returns 100vh exactly as before. Identical
         too at every root font size above 16 px — 36rem caps to 100vh there
         for the same reason 45rem did, so the rem-floor rows above stand.
         → scripts/check-consent-clearance.py link 6, which now requires the
           subtraction and fails on either shape that preceded it */
      main { --lp-measure: calc((max(min(45rem - 9rem, 100vh), 100vh - var(--cf-consent-height, 0px)) - 13rem) * 2); }
      /* ---- ACT 1 + 2: the track and the stage ---- */
      /* 640vh, NOT 400. Nothing about the SCRIPT changed — every window is
         still a contain-percent and the acts still divide the track the same
         way. What changed is how many pixels a point of it is worth: at 400vh
         the root's 49 strokes drew across 36-89 % of four screens, about 1.6
         screens for the whole tree, and the reading of it was the plainest
         kind of evidence — "the tree is generated too fast". At 640vh the same
         53 points are 2.8 screens, so a stroke that had 26 px of wheel now has
         42, and the closing still is two thirds of a screen instead of a third.

         The track's height is the one honest place to spend that. Slowing the
         ranges would have shortened the still or overrun the release; slowing
         the easing would have made the front lurch. Scroll length is the only
         variable that changes the SPEED of everything at once without changing
         the order of anything. */
      .sp-track {
        height: 640vh;
        view-timeline-name: --sp;
        view-timeline-axis: block;
      }
      .sp-stage {
        position: sticky;
        top: var(--act-bar);
        height: calc(100vh - var(--act-bar));
        /* dvh on top of vh, the pair .cf-nav__list already pins in
           components.css and for the same reason: Safari's dynamic toolbar
           makes 100vh the chrome-collapsed viewport, the largest the address
           bar ever leaves, so a stage sized off it alone can stand taller
           than what the reader can see until the bar retracts — and this
           stage is where the tree's nineteen drops land on --sp-head-band's
           rule, so the gap is the rule itself going missing. dvh tracks the
           chrome as it shows and hides; a browser with no dvh keeps the vh
           line above. */
        height: calc(100dvh - var(--act-bar));
        overflow: hidden;
        overflow: clip;
        display: grid;
        /* END, NOT CENTER: the tree's eight feet stand on the stage's bottom
           edge, which at release is exactly where the next section's top
           slides in — so the drops meet the "Was wir machen" rule and the
           lectern's verticals rise out of them. The seam the old landing
           held with check-flow-handover.py, made by layout this time. */
        align-content: end;
        /* HEAD-ROOM, AND NOT A NAV — the distinction --act-bar made necessary.
           This has capped the grid's available height since #233, when the
           stage was a prototype-only box and there was no bar anywhere to
           reserve; it is 5rem of air above a drawing that is bottom-aligned,
           not a strip a floating bar sits in. --act-bar reserves the bar, by
           taking it off the stage's height, and the two compose: on the
           landing page the drawing keeps the same air above it that it has in
           the lab, measured from a stage that starts under the nav. Removing
           this moved the prototype's composition 8 px, which is how it was
           established that it does something. */
        padding-block-start: var(--nav-height);
      }
      /* THE DRAWING TAKES THE WHOLE CONTAINER inside the gate — the frame
         below is container-wide on the same 1200-unit basis, so the three
         pinned arrivals (flow 0, 600, 1200 = frame 0, 500, 1000) only line
         up if the two share a width. The claim stops being a column and
         becomes an overlay on the canopy's empty upper right, and it LEAVES
         with the field it belongs to: "Tausende Sensoren erzeugen Daten,
         aber daraus keine Antworten" is act 1's sentence, and the root is
         the answer drawing itself. Outside the gate the two-column layout
         above still holds and nothing overlaps. */
      /* minmax(0, 1fr) AND NOT 1fr, because the drawing is now wider than the
         column it stands in. `1fr` is `minmax(auto, 1fr)`, and an auto minimum
         reads the item's own specified width — so the track grew to the
         drawing's 1522 px inside a 1280 px container, the grid overflowed to
         ONE side, and `justify-self: center` twelve rules down had nothing left
         to centre: measured at 1682 x 1327, the drawing stood 201 to 1723 in a
         1682 px viewport, its right-hand fringe 45 px off the screen and 121 px
         of clear ground on the left. Flooring the track at 0 keeps it on the
         column, the item overflows both sides evenly, and the source lands on
         the viewport's centre line where the lectern's vertical continues it.
         Same floor the base tier's own minmax(0,3fr) minmax(0,2fr) carries, and
         for the same reason. */
      .sp-stage__inner { grid-template-columns: minmax(0, 1fr); }
      .sp-stage__inner > .cf-statement__figure { grid-column: 1; grid-row: 1; }
      /* INSIDE THE GATE THE GROUND IS THE STAGE'S, WHOLE. The base tier clips
         it to the drawing's band because the stack puts prose under the
         drawing; here the claim is an overlay on the canopy's empty upper right
         and there is no prose below anything — the stage IS the drawing's box,
         which is what the note over .sp-field says was once true everywhere.
         The convergence needs every corner of it too: sp-field-merge carries
         all twenty-three beads to --ux/--uy on the trunk's head, and a bead
         that starts outside a clipped band would appear out of a horizon. The
         annotation layer goes with it, for the reason its own note gives: the
         notes end where the view does, and in here the view is the stage.
         Switched off at 64rem for every tier at once, gate or no gate — see the
         media query beside .sp-field's own note. */
      /* THE HEADER'S HEIGHT COMES OUT OF THE DRAWING'S, so the feet land ON
         the rule instead of behind the words. --sp-head-band is the whole
         bottom band: the title's line box at --text-h2, the component's
         padding-bottom, and one rung of air under the rule. Measured at the
         gate floor and again at 1920 where the clamp is at its top; 5.5rem
         covers both. The drawing gives the band up rather than overlapping it,
         which is the only arrangement in which nineteen drops can be seen to
         arrive at a line. */
      .sp-stage > .container { --sp-head-band: 5.5rem; }
      /* THE STAGE'S HEIGHT, NOT THE VIEWPORT'S: the drawing is fitted to the
         box it stands in, and that box gave the nav back its bar.

         AND THE PAGE'S INNER BOX IS THE OTHER BRANCH, NOT THE COLUMN'S. The
         first branch was `100%` — the content box of .sp-stage > .container,
         which .container caps at --container-max. Both branches were meant to
         be able to bind: the height one on a laptop, the width one when the
         drawing would otherwise run off the sides. Above --container-max the
         width branch stops being a width the viewport has any say in, so past
         the crossover NEITHER branch tracks the stage and the height fit this
         rule exists to perform simply stops. The crossover is where the height
         branch passes 1280: (100vh - 100 - 128 - 88) * 1.9 = 1280 at a 990 px
         viewport. Every pixel of height above that became dead air at the TOP
         of the stage, because `align-content: end` stands the feet on the
         bottom edge and puts the whole surplus in one band over the copy.
         Measured on the shipped page, stage top to the first thing in it:

           1024 x  720    30 px      1682 x 1327   392 px
           1280 x  800    36 px      1920 x 1080   145 px
           1440 x  900    46 px      2560 x 1440   505 px

         The three under the crossover are the air the composition asks for.
         The three over it are the same air plus everything the drawing did not
         grow by, and at 1682 x 1327 — the window this was reported from — that
         is a third of the stage with nothing in it above the copy.

         SO THE WIDTH BRANCH IS THE PAGE'S INNER BOX AND NOT THE COLUMN'S, and
         inside the gate that is the same answer the rest of the stage already
         gives: .sp-field, .sp-stage::before and .sp-annots-fig are all
         `inset: 0` of a full-bleed stage, and --sp-ground-clip is `none` in
         here (this block, "THE GROUND IS THE STAGE'S, WHOLE"). The drawing was
         the one layer still held to the column, in the one tier where nothing
         else is.

         --sp-bleed-stop IS WHERE IT STOPS, AND THE ACT RAIL IS WHAT STOPS IT.
         The first draft of this took the page's inner box at 100vw - 2 gutters
         — this file's own expression for it, the one --sp-ground is built on
         fourteen hundred lines up — and at 1682 x 1327 that put the drawing's
         left edge on 76 and its outermost leaf straight through the rail's
         numerals, which stand from --space-6 to 108 (check-rail-margin.py, row
         "96rem+"). The rail is fixed and the drawing is not, so the drawing is
         what gives way: the stop is the rail's own extent plus --space-4, the
         rung that check holds the rail's own clearance to. 24 + 84 + 16 = 124,
         and the 3 units of ink the fringe carries past the viewBox on each
         side are 3.6 px at this cap, so the leaf's tip clears the last numeral
         by 12 px rather than crossing it.

         max(), SO THIS CAN ONLY EVER WIDEN THE DRAWING. 100vw - 248 is NARROWER
         than the column below about 1530 px of viewport, and a short wide
         window is exactly where the height branch does not bind — at 1440 x
         1200 the height branch is 1679 and the column 1280, so a bare bleed
         stop would have taken 88 px OFF a drawing this rule is here to grow.
         The width branch is therefore "the column, or the page's inner box
         outside the rail's margin, whichever is wider", which is >= 100 % at
         every width by construction.

         THE HEADER STAYS ON THE COLUMN and that is why this moved on the
         drawing rather than on the container. .sp-head is the other child of
         that container — "Was wir machen", its count and the rule the nineteen
         drops land on — and it is the same rule act 3's cards line up under,
         one section down. Widening the container would have taken the rule and
         the words out to the gutters with the drawing and broken that. */
      .sp-drawing {
        --sp-bleed-stop: calc(var(--space-6) + 84px + var(--space-4));
        --sp-measure: min(
          max(100%, calc(100vw - var(--sp-bleed-stop) * 2)),
          calc((100vh - var(--act-bar) - 8rem - var(--sp-head-band)) * 1.9));
      }
      /* AND CENTRED IN IT, WHICH IS THE HALF THE MEASURE CANNOT BUY.
         Fourteen lines up this block says the arrivals "only line up if the
         two share a width" — and then sizes the drawing off the viewport's
         HEIGHT, which is a width the container has no say in. min(100%, …)
         takes the second branch on every laptop, the figure is a grid item
         with a definite width, and a definite width in a grid area is START
         aligned. So the drawing hung on the container's left edge while the
         card below it was centred, and the source — the single point the
         whole root resolves to, and the point the lectern's inner vertical
         is written to continue — stood half the shortfall to the left of
         that vertical. Measured on the shipped page, gate on, consent
         dismissed, source x against inner-vertical x in page pixels:

           viewport      drawing   container   source    vertical   short by
           1024 x  900     911.38    911.38     512.00     512.00      0.00
           1280 x  900    1109.59   1139.22     625.19     640.00     14.81
           1280 x  800     919.59   1139.22     530.19     640.00    109.81
           1280 x  720     767.59   1139.22     454.19     640.00    185.81
           1366 x  768     858.80   1215.75     504.52     683.00    178.48
           1440 x  900    1109.59   1280.02     634.78     720.00     85.22
           1440 x  720     767.59   1280.02     463.78     720.00    256.22
           1600 x  900    1109.59   1280.00     714.80     800.00     85.20
           1920 x 1080    1280.00   1280.00     960.00     960.00      0.00
           2560 x 1440    1280.00   1280.00    1280.00    1280.00      0.00

         Three zeros and they are the three sizes where the height cap does
         not bind — 1920 and 2560 are tall and 1024's container is already
         narrower than the cap. Every other row is a laptop, and 85 px is
         what the trunk ends in mid-air by at the size this drawing was
         tuned at. Same shape as .cf-pin__inner's own note in
         components.css, one element further up the seam: the card narrowed
         and centred while its chrome stayed flush, and nobody had asked
         what the DRAWING did.

         THE WIDTHS DO NOT HAVE TO MATCH AND SHOULD NOT BE MADE TO. The
         drawing is fitted to the stage's height on purpose — that is what
         keeps the whole of it on screen — so the arrival cannot rest on a
         shared width. It rests on a shared CENTRE, which the geometry
         already provides: the source sits at flow x 600 of 1200 and the
         inner vertical at frame x 500 of 1000, both the exact middle of
         their own viewBox, and the card's row is centred in this same
         container. Centre the drawing and the two land on each other at
         every viewport the gate admits, whatever --sp-measure resolves to.
         scripts/check-seam-centre.py holds all four of those facts.

         justify-self AND NOT margin-inline, for the reason
         .cf-pin__inner's note gives about the same fix: a margin here is
         (0,1,0) against base.css's own figure margin and the cascade is
         not the place to settle a join. */
      .sp-drawing { justify-self: center; }
      /* Flush to the drawing: the rule IS the drawing's baseline now, so the
         component's own bottom margin would be a gap between the feet and the
         thing they land on. */
      .sp-head { margin-bottom: 0; margin-top: var(--space-3); }
      .sp-stage__inner > .cf-statement__text {
        grid-column: 1;
        grid-row: 1;
        justify-self: end;
        align-self: start;
        max-inline-size: 28rem;
        position: relative;
        animation: lp-flow-out linear forwards;
        animation-timeline: --sp;
        animation-range: contain 32% contain 40%;
      }

      /* Every animated part inside a sticky stage MUST read the track's
         timeline: a sticky subject's own view() freezes at one value — the
         same fact .cf-pin documents. The field's parts are this page's own
         (sp-field-*), declared in full lists because animation-name is a
         replacement and not an addition; components.css's cf-stmt rules key
         --cf-iso, which does not exist on this svg, and are overridden
         here wholesale rather than left to an inactive timeline.

         THE FLOOR ARRIVES AND THEN STEPS BACK, and it is the same two beats it
         always ran — they have simply moved from the <g> that used to draw the
         lattice onto .cf-ground's own layer. base.css animates that pseudo only
         under .cf-ground--lit, which this stage is not, so there is nothing
         here to override: the material draws, this schedules.

         NO --iso-rest ANY MORE. cf-iso-fade ends `opacity: var(--iso-rest, 1)`
         and the group used to set .26 there, because a #000 dash at full
         strength was 3.8x too dark to stand a numeral on. The floor's ink is
         now 10 % black BEFORE any opacity is applied, which is where
         tokens.css measured the ceiling, so the fallback is the right value
         and the property has nothing left to say.

         --sp-ground-dim CARRIES THE OLD INK ACROSS, and it is .26 where it was
         .1 because the number it multiplies changed. The drawn lattice receded
         to .1 of its own .26, which put 2.6 % black on the pixels a dash
         covered; .26 of the material's 10 % ink lands on the same 2.6 %. The
         floor under the root is a little more present than it was, because the
         line is continuous now where the dash covered one pixel in five — that
         is the same trade the arrival makes and it is the point of the change,
         not a side effect of it. The keyframe reads the property rather than a
         literal — the contract check-authored-opacity.py holds. */
      .sp-stage::before {
        opacity: 0;
        --sp-ground-dim: .26;
        animation-name: cf-iso-fade, sp-ground-recede;
        animation-timeline: --sp, --sp;
        animation-range: contain 4% contain 14%,
                         contain 36% contain 48%;
        animation-fill-mode: both, forwards;
        animation-duration: auto;
        animation-timing-function: var(--ease-out), linear;
      }
      /* ---- THE OVERLAY ----
         The copy stops being a column under the drawing and becomes the two
         blocks either side of the confluence. This is the tier that placement
         was written for and it is now the only tier that has it; the base tier's
         own note above says why.

         ABOVE THE TREE, NOT AT ITS FOOT, and this paragraph has now argued for
         three placements. It read, of the two triangles either side of the
         converging trunk: "the largest clear ground in the figure ... the
         columns are centred on y 74 % and stop at x 420 and 780, which leaves
         the nearest stroke about 70 units and the nearest numeral about 50."
         Every word of that is still true of the geometry and it answered the
         wrong question. Largest clear ground is where copy fits; the FOOT of a
         confluence is where the drawing has finished saying what the copy is
         introducing. "Von oben nach unten gelesen" stood below the bottom of
         the thing it was telling the reader to read downward, beside the trunk,
         with 3 680 /s and 2 000 /s inside 60 px of it.

         So the blocks sit in the band above the leaves, which is empty at every
         viewport this tier admits and empty by construction rather than by
         render: the stage is at least 45rem tall, the drawing is width-driven
         at 1200 / 620, and what is left over is the band. Reserved copy is
         154 px in this tier — the streamed floors make that a constant, not a
         wrap — and measured at 1024x720, 1152x720, 1280x720, 1440x720,
         1600x720, 1920x720, 1920x1080, 2560x900 and 1024x1200 the block clears
         the stage's top edge by 28 to 442 px and the sticky bar by 38 to 452.
         Nothing is clipped at any of them and the narrowest case is the shortest
         viewport, which is where a band argument is supposed to fail.

         THE FOOT OF THE BLOCK IS THE LEAVES' OWN TIP. `100% - 100% * 50 / 620`
         is the drawing's top edge read out of its own viewBox — y 50 of 620,
         where the outermost leaf begins — so the copy is placed against the ink
         rather than against the box, and it stays placed against it if the
         drawing is ever refitted. --space-4 of air on top of that: 16 px, which
         is the gap between a caption and what it captions everywhere else in
         the system.

         THE INNER EDGE IS THE FIXED ONE — left block ragged-left, right block
         ragged-right, each flush toward the middle — because the thing the
         reader is reading around is the axis the drawing resolves onto. And
         --measure is off: 35 % of the stage IS the measure here, and a second
         cap on top of it would leave the inner edge ragged against nothing.

         THE COPY BELONGS TO ACT 2, AND CSS SAYS SO RATHER THAN THE SCRIPT.
         cf-stream.js is optional by contract — "without the script every line
         is simply present" — and a line that is simply present is present from
         the first pixel of the track, over act 1's field, which is not where
         it belongs. So the block fades up on --sp over the window the trunk
         leaves the orb in, and leaves with the section. With the script the
         fade and the typing agree; without it the copy still arrives when the
         root does, whole. */
      .sp-say {
        position: absolute;
        inset: 0;
        pointer-events: none;
        display: block;
        margin-block-start: 0;
        animation: lp-frame-in linear both;
        animation-timeline: --sp;
        animation-range: contain 42% contain 52%;
      }
      .sp-say__col {
        position: absolute;
        bottom: calc(100% - 100% * 50 / 620 + var(--space-4));
        inline-size: 35%;
        max-inline-size: none;
      }
      .sp-say__col--l { right: 65%; text-align: right; }
      .sp-say__col--r { left: 65%; }

      /* ---- THE CALLOUT LAYER, INSIDE THE PIN ----
         The layer only exists where the stage is the viewport, because that
         is the premise --sp-u is built on; see its own note above.

         AND IT READS --sp, NOT view(). components.css drives cf-annot-draw off
         the set's own view() timeline, which is right for a figure scrolling
         past and wrong here for the reason .cf-pin documents in one line: a
         sticky subject's own view() freezes at one value. Every other animated
         part in this stage reads the track's timeline and so does this.

         WHEN, is the order the two things happen in on paper — the drawing is
         finished before it is labelled. The beads land by 26 % and pulse
         through 36 %; the leaders draw over 26-34 %, so a note appears on a
         sensor that has arrived and is holding still. They leave with the
         merge: a label pointing at a light that is already halfway to the
         trunk is a label pointing at nothing. */
      .sp-annots {
        /* THE LAYER FADES IN WITH THE LEADERS, and that is not belt and
           braces. --annot-draw collapses the leader and takes the label's
           opacity with it, but the ANCHOR is a ::before with a border and it
           is drawn at every value — correct in the component's own figure,
           where the drawing is static and the anchor is part of it, and wrong
           here: for the twenty points of track before the notes are due, the
           stage carried twenty-one stray diamonds beside twenty-one spheres,
           with no leader and no word to say what they were. */
        animation-name: lp-frame-in, cf-annot-draw, lp-flow-out;
        animation-timeline: --sp, --sp, --sp;
        animation-range: contain 26% contain 34%,
                         contain 26% contain 34%,
                         contain 36% contain 44%;
        animation-fill-mode: both, both, forwards;
        animation-duration: auto;
        animation-timing-function: var(--ease-out), var(--ease-out), linear;
      }

      /* THE SENSOR'S THREE VERBS, one property ledger. `translate` belongs
         to the stream (corner -> crossing) and then to the merge (crossing
         -> trunk head): the merge is later in the list, so its forwards
         fill takes the property over exactly at its own window and not
         before. `scale`/`opacity` swell in the pulse and die in the merge
         the same way. transform-box: fill-box puts the origin on the glow
         itself; lengths stay user units, the same fact the root's strokes
         ride. The merge target is (--ux,--uy) — published on the svg by
         the aim script below, falling back to the authored focus — minus
         the glow's own crossing, so one keyframe serves all 25. */
      .sp-field .cf-stmt-sensor {
        transform-box: fill-box;
        transform-origin: 50% 50%;
        /* THE SWELL IS A FRACTION OF THE HEADROOM, not a fixed step, and that
           is what lets the field rest bright. `rest + 0.25` was safe while the
           beads rested at 0.2..0.6; at 0.45..0.85 it computes 1.1 for the hot
           ones, the browser clamps it to 1, and the four brightest sensors in
           the drawing stop pulsing at all — an animation deleted by a number
           moving somewhere else, which is exactly the failure mode
           check-authored-opacity.py exists for. Taking 45 % and 65 % of what
           is LEFT can never exceed 1 and always leaves a swell to see. */
        --sp-peak: calc(var(--iso-rest, 1) + (1 - var(--iso-rest, 1)) * 0.45);
        --sp-flare: calc(var(--iso-rest, 1) + (1 - var(--iso-rest, 1)) * 0.65);
        animation-name: sp-field-arrive, sp-field-pulse, sp-field-merge;
        animation-timeline: --sp, --sp, --sp;
        animation-range: contain calc((6 + var(--m) * 12) * 1%)
                         contain calc((14 + var(--m) * 12) * 1%),
                         contain calc((26 + var(--m) * 2) * 1%)
                         contain calc((34 + var(--m) * 2) * 1%),
                         contain calc((36 + var(--m) * 6) * 1%)
                         contain calc((42 + var(--m) * 6) * 1%);
        animation-fill-mode: both, both, forwards;
        animation-duration: auto;
        animation-timing-function: var(--ease-out), ease-in-out, var(--ease-out);
      }

      /* THE SEAM IS TIGHT. The cards' section keeps its ordinary rhythm
         outside the gate; inside it, the tree's feet stand on the stage's
         bottom edge and the section's 126 px of head-padding was 126 px of
         dead ground between the drops and the rule they land on. One rung
         keeps the hairline from touching the dives' tips. */
      .sp-track + .section { padding-block-start: var(--space-4); }

      /* ---- the root: same range algebra the chain check holds ---- */
      /* THE RATE IS DERIVED FROM THE TREE'S OWN DEPTH, and it moves whenever
         the tree does. The act gives the root 42 to 89 points of the track,
         and the drawing's longest walk is whatever its deepest terminal's
         l + u comes to — 3.0805 on the first grown root, 3.74 on the
         symmetric truss, 3.11 on the grown root that ships. So the rate is
         the head divided by the walk: (86.88 - 42) / 3.11 = 14.4309, which
         lands the last contour at 86.88 and the light's fade at 88.88, both
         where they have been since the clearance was swept at them and both
         inside the 89 the still begins at. 14.025 is not a tuned number; it
         is 44.88 / 3.20. check-flow-chain.py recomputes it from the shipped
         strokes and holds both tails. */
      .sp-stage { --flow-c: 14.96; }
      .lp-flow__trunk { display: block; }
      .lp-flow__seg {
        animation: lp-frame-draw linear both;
        animation-timeline: --sp;
        animation-range: contain calc((42 + var(--l) * var(--flow-c)) * 1%)
                         contain calc((42 + (var(--l) + var(--u)) * var(--flow-c)) * 1%);
      }
      .lp-flow__light {
        animation: lp-frame-draw linear, lp-flow-out linear;
        animation-timeline: --sp, --sp;
        animation-range: contain calc((36 + var(--l) * var(--flow-c)) * 1%)
                         contain calc((36 + (var(--l) + var(--u)) * var(--flow-c)) * 1%),
                         contain calc((38 + (var(--l) + var(--u)) * var(--flow-c)) * 1%)
                         contain calc((44 + (var(--l) + var(--u)) * var(--flow-c)) * 1%);
        animation-fill-mode: both, forwards;
      }
      /* ------------------------------------------------------------------
         THE LEAVES GO ON GLIMMERING, and they are the only thing on this page
         that does.

         Everywhere else the light is a FRONT: it draws down a stroke, the
         contour lands behind it, and the light fades out — lp-flow-out, six
         points behind its own draw. That is the right behaviour for a route,
         which is a thing data passed through. It is the wrong behaviour for a
         FOOT, which is the thing data is arriving from and has not stopped
         arriving from: eighteen strokes end on the rail and every one of them
         went dark the moment the front had gone by, so a drawing of thousands
         of sensors producing data ended with nothing on it still producing
         anything.

         So a leaf does not fade to nothing. It settles to --leaf-peak and then
         breathes on --arrive-period, the 2.4 s the system already spends on
         "the one thing on screen that is honestly still happening" — slow on
         purpose, because a fast loop reads as impatience (tokens.css, THE
         WAIT). The amplitude is this page's own; see --leaf-peak below. The lime ramp is
         already on the stroke — this only decides how much of it reaches a
         pixel — so what shimmers is the gradient itself, brightest at the tip
         where the lime stop sits and trailing into Glas at the junction above.

         TWO PROPERTIES, NOT ONE, and that is what lets three animations share
         one element without fighting. The settle owns `opacity` on the scroll
         timeline; the shimmer owns `stroke-opacity` on the document clock.
         They multiply, so the leaf sits between 0.06 and 0.18 of the ramp and
         never brighter — put both on `opacity` and the last one in the list
         simply wins, which would have the leaf breathing all the way through
         its own draw.

         AND THEY DO NOT BREATHE IN UNISON. A negative delay off --l — the
         stroke's own run from the void — starts each leaf at a different
         phase, so the light moves ACROSS the fringe instead of the whole row
         blinking. Eighteen strokes pulsing together is a blink; eighteen
         strokes pulsing out of phase is a root.

         Inside the reduced-motion gate with everything else on this page, so
         a reader who has asked for stillness gets a drawing that is finished
         and still. ------------------------------------------------------ */
      /* AND IT IS WIDER THAN THE STROKE IT LIGHTS, which is the third thing
         that had to change and the one that decides whether this reads as
         lime at all.

         A route's light is 2 px over a 1 px contour, so it very nearly covers
         it — right for a front, where the point is a travelling tip and the
         contour behind it has not been drawn yet. On a settled leaf it is
         wrong twice over. Composited on top of --border-strong the lime comes
         out olive, because half-opacity yellow over black is olive; and if it
         is raised until it is not, it has replaced the contour with a colour
         that has almost no contrast against the wash — #E1FF00 on the page
         wash is about 1.2:1, which is why --arrive-light-peak is capped where
         it is.

         6 px is the way out of that pinch: the black hairline stays crisp
         inside it and the lime sits in the two and a half pixels either side,
         so the leaf is a lit line rather than a line painted lime. Nothing is
         covered, nothing loses contrast, and what shimmers is the ramp beside
         the contour — which is what a light does. */
      /* AND IT GLOWS INTO THE GAPS. "add the shimmer between the bottom
         leaves" — the light was ON the strokes and nowhere else, so a fringe
         of twenty-five feet read as twenty-five lit lines with dead wash
         between them, which is a row of markers rather than a root standing in
         light.

         --glow-light is the answer and it is already in tokens.css: two
         drop-shadows, lime at 45 % of the reach and Glas at all of it, which
         is the same ramp this drawing is drawn in. A filter spreads OUTWARD
         from the ink, so putting it on the leaves puts light in the spaces
         between them without drawing anything there — nothing is added to the
         construction, and there is no geometry in the gaps to explain.

         36 px because the fringe's gaps are 15 to 130 units and one unit is
         0.85 px at the gate floor: 36 px reaches about halfway across a
         median gap from each side, so neighbours meet in the middle and the
         wide gaps stay open. The glow rides stroke-opacity with the rest of
         the leaf, so what pools between the feet breathes with them. */
      .lp-flow__leaf {
        stroke-width: 6;
        --glow-r: 36px;
        /* Composed here, from tokens.css's three ingredients, for the reason
           the orb's note gives above: a filter assembled on :root bakes in the
           :root reach and this asked for 36 px and rendered 12. It is the same
           two shadows the system's own rule composes — the ratio and the
           colours are the published ones, so a palette move still moves this. */
        filter:
          drop-shadow(0 0 calc(var(--glow-r) * var(--glow-core)) var(--glow-lime))
          drop-shadow(0 0 var(--glow-r) var(--glow-glas));
      }
      /* SAFARI NEVER PAINTS THIS LEAF, at any scroll position, once `--sp` has
         driven it — not the gradient stroke, not the glow. Every computed
         value WebKit reports matches Chromium exactly (stroke url, resolved
         gradient stops and coordinates, stroke-opacity, opacity, scale) and
         the same markup painted correctly in an unanimated, static copy of
         this exact SVG. Removing `animation`, pausing it, or forcing a
         repaint on the live element does not bring the colour back — only
         dropping vector-effect does, which points at WebKit's filter-region
         cache for a non-scaling stroke never invalidating once the element
         has been driven by a scroll-linked `scale` keyframe (lp-frame-draw),
         settled or not. `.lp-flow__seg`'s hairline is unaffected because it
         carries no filter.

         Scoped with the standard `-webkit-hyphens: none` feature query —
         true only in WebKit/Safari, so Chromium and Firefox keep the
         viewport-independent 6 px stroke this rule documents above. Losing
         non-scaling-stroke here means the leaf's width now follows the
         drawing's own scale (roughly 0.5-0.9 of the authored 6 px across
         this page's breakpoints) instead of holding it exactly — an accepted
         trade against the alternative, which is no lime at all.

         `.lp-flow .lp-flow__leaf`, not the bare class: line 585's
         `.lp-flow :is(path, circle)` is (0,1,1) and outranks a lone
         `.lp-flow__leaf` at (0,1,0) regardless of source order — the override
         has to out-specify it, not just come after it. */
      @supports (-webkit-hyphens: none) {
        .lp-flow .lp-flow__leaf { vector-effect: none; }
      }
      .lp-flow__leaf {
        animation: lp-frame-draw linear,
                   sp-leaf-settle linear,
                   sp-leaf-shimmer var(--arrive-period) ease-in-out infinite alternate;
        animation-timeline: --sp, --sp, auto;
        animation-range: contain calc((36 + var(--l) * var(--flow-c)) * 1%)
                         contain calc((36 + (var(--l) + var(--u)) * var(--flow-c)) * 1%),
                         contain calc((38 + (var(--l) + var(--u)) * var(--flow-c)) * 1%)
                         contain calc((44 + (var(--l) + var(--u)) * var(--flow-c)) * 1%),
                         /* `normal`, NOT `auto`. animation-range takes `normal`
                            or a range name and a length; `auto` is not one of
                            its keywords, and one invalid item drops the WHOLE
                            declaration — so all three animations fell back to
                            animation-range: normal and ran across the entire
                            track. The leaves were 15 % drawn and glowing at the
                            top of act 1, which is the drawing lit before it
                            exists. animation-timeline DOES take `auto` and that
                            is where the confusion came from: the two lists sit
                            one under the other and only one of them means the
                            document clock by that name. */
                         normal;
        animation-fill-mode: both, forwards, none;
        animation-delay: 0s, 0s, calc(var(--l) * -370ms);
      }

      /* THE THIRD ONE ONLY. The leaf's list is draw, settle, shimmer, and the
         first two are on --sp: a view timeline does not tick unless the reader
         scrolls, so it already costs nothing while the page is still and there
         is nothing to hold. The shimmer is the one on the document clock, and
         it breathes for the life of the tab whether the root has been drawn yet
         or not — for the whole of the page above act 1 the leaves are not even
         on screen, and the fringe is a light on a drawing that does not exist.
         `animation-play-state` is a list like the rest of them, so the pause
         lands on the shimmer and leaves the two scrubbed animations alone.
         → assets/js/cf-idle.js, and the note over .sp-stream */
      [data-idle] .lp-flow__leaf { animation-play-state: running, running, paused; }

      /* THE SOURCE FILLS AS THE FIELD ARRIVES IN IT.
         The orb shipped with no animation at all, so it stood at full size in
         the top left of act 1 — before a single sensor had moved, and next to
         a claim that says the data has nowhere to go yet. A source that is
         already full is not a source; it is a decoration that happens to be
         where one belongs.

         Its window is the field's own convergence, exactly: sp-field-merge
         runs each glow from its crossing to the trunk's head over
         36 + m*6 → 42 + m*6, where --m is the sensor's slot, so the last of
         the twenty-five lands at 48. The orb opens at 36 with the first of
         them and is full at 50, two points after the last — which is the
         difference between growing WITH the merge and growing at the same
         time as it.

         Scale and opacity together, not scale alone: a disc scaling up from
         zero at full opacity reads as a shape being pushed toward the reader,
         and this is a light coming on. --ease-out puts most of the growth in
         the first third, so the orb is present early and then only swells —
         the same curve every arrival in this system uses. */
      /* THE SOURCE ARRIVES LAST, which is the mirror said in the motion lane.
         It filled at 36-50 %, BEFORE the first stroke left it, because it was
         the thing the root grew out of. It is now the thing the root grows
         INTO: the trunk's own window closes at 42 + 3 x --flow-c = 85.3 %, so
         the orb comes on over 84-91 % and the last gesture of the act is
         nineteen routes having become one light. */
      .lp-flow__orb {
        animation: sp-orb-fill linear both;
        animation-timeline: --sp;
        animation-range: contain 84% contain 91%;
      }

      .lp-flow__node {
        animation: lp-frame-in linear both;
        animation-timeline: --sp;
        animation-range: contain calc((43 + var(--l) * var(--flow-c)) * 1%)
                         contain calc((49 + var(--l) * var(--flow-c)) * 1%);
      }
      .lp-flow-data :is(.lp-flow__val, .lp-flow__read) {
        opacity: 0;
        animation: lp-frame-in linear both;
        animation-timeline: --sp;
        animation-range: contain calc((44 + var(--l) * var(--flow-c)) * 1%)
                         contain calc((50 + var(--l) * var(--flow-c)) * 1%);
        animation-fill-mode: forwards;
      }

      /* ---- ACT 5: the map, and the camera pulling back ---- */
      /*
         THE SCRIPT, in contain % of this track. Three stages, one per beat,
         and cf-stream.js divides the track by [data-stream-stage] so the
         typed lines below are fractions of their own third.

             2 - 16   the coastline draws, the two office dots arrive
             4 - 21   beat 01 types (Konstanz, Berlin)
            26 - 34   THE CAMERA PULLS BACK to Europe
            30 - 40   Germany fills with the ramp, Europe with the lattice
            36 - 54   beat 02 types
            58 - 66   THE CAMERA PULLS BACK to the world
            64 - 76   the asset points arrive, kind by kind
            68 - 78   the legend
            70 - 87   beat 03 types
            87 -100   the still

         THE MOVES SIT BETWEEN THE BEATS AND NEVER UNDER ONE. The labels are
         HTML positioned as percentages of the map box for their own stage's
         framing, and a percentage is only exact while the camera is still —
         so the camera is still for the whole of every reveal and moves only
         in the gaps. That is not a rhythm choice, it is what makes one set
         of geometry legal at three scales. */
      .map-track {
        height: 500vh;
        view-timeline-name: --map;
        view-timeline-axis: block;
      }
      .map-stage {
        position: sticky;
        top: var(--act-bar);
        block-size: calc(100vh - var(--act-bar));
        /* vh first, dvh on top — see the note on .sp-stage, above. */
        block-size: calc(100dvh - var(--act-bar));
        overflow: clip;
        padding-block: var(--space-6);
        /* FLOORED, which check-grid-tracks.py requires of every fr track and
           which this row needs on its own account: an <svg> with an intrinsic
           aspect ratio is content with a height, and an unfloored 1fr refuses
           to go under it. Measured before flooring: the map row took its
           891 px intrinsic height, the legend was pushed to y 902 in a 900 px
           stage, and the clip ate it. */
        grid-template-rows: auto minmax(0, 1fr) auto;
      }

      /* THE LEGEND MOVES BESIDE THE MAP, BUT ONLY WHERE THAT PAYS.

         THE DRAWING IS HEIGHT-LIMITED IN HERE AND NOWHERE ELSE. The stage is
         one viewport tall and cannot grow; the copy and the legend take their
         rows off it; what is left is `minmax(0, 1fr)`, and meet then fits a
         1.7943 drawing into a box far wider than that. Measured at 1440 x 900
         before this rule: box 1280 x 486 — 2.63 wide-per-tall — so the drawing
         scaled to the SHORT axis, came out 873 px wide, and left 203 px of dead
         ground down each side. A third of the stage's width, spent on nothing.

         SO THE DEAD GROUND IS WHERE THE LEGEND GOES, and the map turns the row
         it gets back straight into scale: box 1024 x 576, drawing 1024 px wide.
         +17 % on the linear, +38 % on the area, nothing cropped. It also puts
         the key beside the thing it keys — stacked under a letterboxed drawing
         the legend sat flush to the stage's left edge while the map began
         203 px further in, so the two never lined up and the key read as
         stranded at the bottom-left of an empty band.

         AND IT IS A LOSS ON A TALL STAGE, WHICH IS WHY THIS IS GATED. Once the
         drawing is WIDTH-limited the letterbox is gone, there is no dead ground
         to move into, and the column is taken out of the only axis that was
         still binding. Writing d for the drawing's width, S for the stage's and
         H for the height the map row gets when the legend is stacked under it:

             stacked   d = min(S,       1.7943 * H)
             beside    d = min(S - 256, 1.7943 * (H + 74))    256 = 14rem + gap
                                                              74  = legend + gap

         which cross where 1.7943 * H reaches S - 256. Measured both ways:

             1440 x  900    stacked  873    beside 1024     beside, +17 %
             1280 x  800    stacked  723    beside  856     beside, +18 %
             1920 x 1080    stacked 1225    beside 1024     STACKED, +20 %
             1024 x  768    stacked  666    beside  656     STACKED

         The query is that crossing in the two terms a media query can actually
         read. Short stages are where the letterbox forms, and wide ones are
         where the column is affordable; 1024 fails on width and 1080 on height,
         which is exactly the pair the table says to leave stacked.

         The column is DEFINITE and not `auto` on purpose: `auto` resolves
         toward max-content, and .map__legend is a wrap flex whose max-content
         is all six kinds on one line — around 950 px, which would hand the
         legend three quarters of the stage. 14rem clears .map__key's own
         160.6 px min-content, so every row gets a line of its own and the list
         stacks down the column instead of breaking mid-row. */
      @media (min-width: 78rem) and (max-height: 60rem) {
        .map-stage {
          grid-template-columns: minmax(0, 1fr) 14rem;
          grid-template-rows: auto minmax(0, 1fr);
        }
        .map-say { grid-column: 1 / -1; }
        .map-box { grid-column: 1; grid-row: 2; }
        /* Centred against the drawing rather than dropped to the foot of the
           column: meet centres the drawing in its own box too, so this is the
           alignment that reads as one object rather than two stacked ones. */
        .map__legends { grid-column: 2; grid-row: 2; align-self: center; }
      }
      /* THE LABEL LAYER COVERS THE DRAWING, NOT THE BOX, and above the gate
         those stopped being the same rectangle.

         `.map__tag` is HTML at `left: var(--x)` inside `.map__tags`, which is
         `inset: 0` on this box — so --x is read as a percentage of the BOX.
         gen-world-map.place() computes it as a percentage of the VIEWBOX. The
         two agree only while the box has the drawing's shape, and below the
         gate they do: `.map` is `block-size: auto`, the svg takes its
         intrinsic height, and the box is the drawing. Here the row is
         `minmax(0, 1fr)` of whatever the copy and the legend leave, the box
         comes out between 1.85 and 2.50 wide-per-tall against the drawing's
         1.7943, preserveAspectRatio="xMidYMid meet" fits the drawing inside
         that and centres it — and every label sits half a letterbox off the
         point it names. Measured, anchor minus point, at 1440 x 900:
         Konstanz -18.9 px, Berlin +41.3, Deutschland +59.8, United Kingdom
         -58.8. That is L * (1 - 2x), zero at the middle of the drawing and
         worst at its edges, sign flipping across the centre, which is why it
         reads as four unrelated placement mistakes instead of one geometry
         fault. On the render: KONSTANZ printed through the German border
         rather than beside its dot, and UNITED KINGDOM sixty pixels west of
         the country, out in the Irish Sea.

         So the layer is given the box `meet` would give the drawing, and the
         two `min()`s are that algorithm written out: fit by width unless the
         height runs out first, then by height. `margin: auto` against
         `inset: 0` centres what is left over, which is the `xMid`/`YMid` half.
         --map-ar comes from the generator beside the camera, off the same
         VIEW the viewBox is printed from.

         `container-type: size` is what lets the width read the height. It is
         safe HERE and would not be below the gate: it makes the box's own
         size independent of its contents, and down there the contents are
         where the height comes from. Above the gate the height is 100 % of a
         floored grid row and owes the svg nothing. */
      .map-box { block-size: 100%; container-type: size; }
      .map { block-size: 100%; }
      .map__tags {
        /* The camera exists in this block and only in this block, so this is
           the one tier the tags' framings are true in. */
        display: revert;
        inline-size: min(100cqw, 100cqh * var(--map-ar));
        block-size: min(100cqh, 100cqw / var(--map-ar));
        margin: auto;
      }

      /* THE CAMERA. Three transforms, generated from the same projection the
         paths are, applied to the map group about the view box's own origin.
         Each keyframe holds its framing across the whole beat it belongs to
         and moves only in the gap before the next, which is why the steps
         are written as pairs of identical stops rather than as a glide. */
      /* THE THREE FRAMINGS, generated. They are custom properties and therefore
         need a rule to live in — declared loose between rules they parse as
         nothing and the camera silently never moves, which is exactly what the
         first render showed: the world framing at every stage, and the labels
         placed for a zoom that had not happened. */
      .map-track {
        /* gen-world-map: shots */
        --map-ar: 1.794189;
        --cam-de: translate(-7038.85px, -4330.26px) scale(14.25);
        --cam-europa: translate(-2061.42px, -1268.36px) scale(4.89);
        --cam-welt: translate(0px, 0px) scale(1);
        /* gen-world-map: end shots */
      }
      .map__cam {
        transform-box: view-box;
        transform-origin: 0 0;
        animation: map-cam var(--ease-out, ease-out) both;
        animation-timeline: --map;
        animation-range: contain 0% contain 100%;
      }
      @keyframes map-cam {
        0%, 26%   { transform: var(--cam-de); }
        34%, 58%  { transform: var(--cam-europa); }
        66%, 100% { transform: var(--cam-welt); }
      }

      /* The coastline draws the way every other contour on this page does:
         one front, one direction, the light leading. There are 161 of them,
         so they share one window rather than carrying 161 staggers. */
      .map__land {
        animation: map-draw linear both;
        animation-timeline: --map;
        animation-range: contain 2% contain 16%;
      }
      @keyframes map-draw { from { opacity: 0; } to { opacity: 1; } }

      .map__fill, .map__weave {
        animation: lp-frame-in linear both;
        animation-timeline: --map;
        animation-range: contain 30% contain 40%;
      }
      .map__dot--office {
        animation: map-dot var(--ease-out, ease-out) both;
        animation-timeline: --map;
        animation-range: contain 10% contain 18%;
      }
      /* THE ASSETS ARRIVE KIND BY KIND, in the legend's own order, so the
         reader can follow which dots belong to which line rather than being
         handed twenty-seven at once. */
      .map__dot--asset, .map__dot--ring {
        animation: map-dot var(--ease-out, ease-out) both;
        animation-timeline: --map;
        animation-range: contain calc((64 + var(--n, 0) * 2) * 1%)
                         contain calc((72 + var(--n, 0) * 2) * 1%);
      }
      @keyframes map-dot { from { stroke-width: 0; } }

      .map__tag {
        animation: lp-frame-in linear both, lp-flow-out linear;
        animation-timeline: --map, --map;
        animation-range: contain calc((12 + var(--s) * 32 + var(--i) * 3) * 1%)
                         contain calc((18 + var(--s) * 32 + var(--i) * 3) * 1%),
                         contain calc((24 + var(--s) * 32) * 1%)
                         contain calc((30 + var(--s) * 32) * 1%);
        animation-fill-mode: both, forwards;
      }
      .map__key {
        animation: lp-frame-in linear both;
        animation-timeline: --map;
        animation-range: contain calc((68 + var(--i) * 1.6) * 1%)
                         contain calc((74 + var(--i) * 1.6) * 1%);
      }

      /* ONE BEAT ON SCREEN AT A TIME. The three are stacked in one grid cell
         and each is lifted out as the next arrives — the same construction
         act 4's plates use, for the same reason: they are three readings of
         one picture, not three paragraphs. */
      .map-say { grid-template-areas: "beat"; gap: 0; }
      .map-beat {
        grid-area: beat;
        animation: lp-frame-in linear both, lp-flow-out linear;
        animation-timeline: --map, --map;
        animation-range: contain calc((2 + var(--s) * 33) * 1%)
                         contain calc((8 + var(--s) * 33) * 1%),
                         contain calc((28 + var(--s) * 33) * 1%)
                         contain calc((33 + var(--s) * 33) * 1%);
        animation-fill-mode: both, forwards;
      }
      /* THE LAST BEAT ARRIVES AND STAYS, so it takes the in-animation alone
         rather than restating the pair and hoping the second half is ignored.
         The act ends on it; a section that fades its own last frame has
         nothing to hand over. */
      .map-beat:last-child {
        animation-name: lp-frame-in;
        animation-timeline: --map;
        animation-range: contain 68% contain 74%;
        animation-fill-mode: both;
      }

      /* ---- ACT 4: the plates, drawn and swept and read ---- */
      /*
         ONE OFFSET, EVERY WINDOW. --p is the plate's index and --t0 is the
         half of the track it owns, so every window below is written as
         "--t0 plus a number" and the second plate is the first one moved
         fifty points along. That is the construction .lp-flow__seg uses with
         --l and --flow-c: one constant, one arithmetic, no second copy of the
         schedule to keep equal to the first.

         THE SCRIPT, in contain % of this track:

            t0+ 2 - 18   the four edges draw, clockwise from the top-left,
                         each 2 points behind the last — in the family's ramp
            t0+ 6 - 26   the ramp fades off each edge it has closed, leaving
                         the CF-Schwarz contour standing
            t0+14 - 18   the chamfer tick arrives
            t0+12 - 30   the photograph sweeps in under the frame
            t0+ 4 - 45   the words type beside it (cf-stream.js owns this one:
                         it divides the track by [data-stream-stage] and the
                         line windows are fractions of the stage)
            t0+45 - 50   the still. Nothing moves while the plate is read.
            t0+46 - 52   the plate leaves, and the next one has the stage

         THE PLATES ARE STACKED, NOT LAID OUT. Each owns the whole stage, so
         they are in one grid cell and the first is lifted out as the second
         arrives. Only the first needs an exit — :last-of-type keeps the team
         plate standing, because the act ends on it and a section that fades
         its own last frame out has nothing to hand over. */
      .sp4-track {
        height: 400vh;
        view-timeline-name: --sp4;
        view-timeline-axis: block;
      }
      .sp4-stage {
        position: sticky;
        top: var(--act-bar);
        block-size: calc(100vh - var(--act-bar));
        /* vh first, dvh on top — see the note on .sp-stage, above. */
        block-size: calc(100dvh - var(--act-bar));
        align-content: center;
        overflow: clip;
        padding-block: 0;
      }
      /* ONE CELL, BOTH PLATES. And the set is capped so the taller of the two
         ratios fits the stage: the picture is 8 of 12 columns, so a 5:4 plate
         is set-width x 8/12 / 1.25 = set-width / 1.875 tall, and capping the
         set at 1.875 x the height it may spend is what guarantees the
         composition fits a 100vh stage without either plate being measured
         against the viewport on its own. */
      .sp4-set {
        grid-template-areas: "plate";
        inline-size: min(100% - 2 * var(--space-8), 76rem, 66vh * 1.875);
        gap: 0;
      }
      .sp4-plate { grid-area: plate; }
      .sp4-plate:not(:last-of-type) {
        animation: sp4-plate-out var(--ease-out, ease-out) both;
        animation-timeline: --sp4;
        animation-range: contain calc((var(--t0) + 46) * 1%)
                         contain calc((var(--t0) + 52) * 1%);
      }
      @keyframes sp4-plate-out {
        to { opacity: 0; translate: 0 calc(var(--space-8) * -1); }
      }
      .sp4-plate { --t0: calc(var(--p) * 50); }

      /* THE EDGES DRAW. scale on one axis from the corner the pen starts at,
         which is lp-frame-draw's exact verb and the reason each path names
         its own transform-origin. Both passes share the window; only the lit
         one has somewhere to go afterwards. */
      /* EACH PATH NAMES ITS OWN AXIS, and it is a class rather than an
         nth-child for a reason that cost a render. The first pass of this
         selected the horizontals with `.sp4-frame path:nth-child(4n+1)`,
         which is (0,2,1) against `.sp4-lit`'s (0,1,0) — so the nth-child rule
         WON the cascade on animation-name and replaced the light's two-name
         list with one, silently dropping lp-flow-out from the top and bottom
         edges. Measured at 44 % of the track: the frame had settled to black
         on the left and right and was still lime along the other two.
         Matching specificities is the fix; naming the thing you mean is why
         it stays fixed. */
      /* THE LIGHT LEADS AND THE CONTOUR FOLLOWS IT, which is the tree's own
         mechanic and was the thing this frame was missing. The root does not
         draw its two passes together: .lp-flow__light opens at 36 + l x c and
         .lp-flow__seg at 42, so the lit front runs six points of the track
         ahead of the black and you watch a coloured line lay the stroke down
         with the contour arriving behind it. Drawn in the same window — which
         is what this did — the 1 px black sits under the 2 px ramp for the
         whole run and the only colour anywhere is the pixel at the tip. The
         gradients were there; nothing could see them.

         Four points of lead here against the tree's six, because an edge's
         window is sixteen points and the root's longest is forty-five: the
         same fraction of the run, not the same number. */
      .sp4-edge {
        animation: linear both;
        animation-timeline: --sp4;
        animation-range: contain calc((var(--t0) + 6 + var(--e) * 2) * 1%)
                         contain calc((var(--t0) + 16 + var(--e) * 2) * 1%);
      }
      .sp4-edge.sp4-x { animation-name: sp4-draw-x; }
      .sp4-edge.sp4-y { animation-name: sp4-draw-y; }
      @keyframes sp4-draw-x { from { scale: 0 1; } to { scale: 1 1; } }
      @keyframes sp4-draw-y { from { scale: 1 0; } to { scale: 1 1; } }

      /* AND THE COLOUR PASSES. The ramp is the front, not the frame: it draws
         the edge and goes out behind itself, four points after that edge has
         closed, leaving the black hairline that was drawn underneath it. The
         root does this to every stroke it has — .lp-flow__light fades and
         .lp-flow__seg remains — and it is the whole reason a frame can arrive
         in lime on a page whose contour colour is CF-Schwarz. */
      /* THE LIGHT IS OFF AT REST AND ON WHERE A TIMELINE DRIVES IT. Two
         declarations, one per tier, which is what this always meant — the
         third animation that used to carry the same value was there to dodge
         a source-order fight the base block no longer picks.

         lp-flow-out reads the 1 and takes it to 0 behind the front. Its fill
         is `forwards` and not `both`, so before its own range it does not
         apply and the light stands at the value declared here. */
      .sp4-lit {
        opacity: 1;
        animation-timeline: --sp4, --sp4;
        animation-timing-function: linear, linear;
        animation-range: contain calc((var(--t0) + 2 + var(--e) * 2) * 1%)
                         contain calc((var(--t0) + 12 + var(--e) * 2) * 1%),
                         contain calc((var(--t0) + 18 + var(--e) * 2) * 1%)
                         contain calc((var(--t0) + 26 + var(--e) * 2) * 1%);
        animation-fill-mode: both, forwards;
      }
      .sp4-lit.sp4-x { animation-name: sp4-draw-x, lp-flow-out; }
      .sp4-lit.sp4-y { animation-name: sp4-draw-y, lp-flow-out; }

      /* SAFARI DROPS ONE OF THESE FOUR, and it is the same bug #339 found and
         fixed for .lp-flow__leaf, in a second place it does not have the same
         fix available. Measured on the "n" ramp specifically — the plate's
         --e:3 edge, sp4-draw-y, transform-origin 100% 100% — with computed
         styles read from the live element and pixels sampled from the
         rendered frame, WebKit 26.5 against Chromium at identical scrollY:

           computed (both engines)   opacity 1, stroke url(#sp4-ramp-n),
                                      stroke-width 6px, visibility visible
           pixels, that edge         Chromium: (219,252,88) lime,
                                      (197,234,225) Glas — the ramp, present
                                      WebKit:   (207,207,207) throughout —
                                      the page's own grey, the ramp absent

         Repeated across five scroll fractions through the plate's open
         window: Chromium's right edge carries 35-65 colour-diverged pixels
         at each sample, WebKit's 0-6. The left edge (--e:1, sp4-draw-x,
         origin 0 0) is lit in both at every sample — this is not every edge
         losing the ramp, it is the one whose draw keyframe scales from the
         far corner failing to keep it, same as #339's leaf.

         #339's fix does not carry: dropping vector-effect there cost 0.5-0.9
         of the authored 6 px, because .lp-flow's viewBox is close to its own
         rendered size. .sp4-frame's viewBox is a flat 100 x 100 mapped onto
         each plate's own box — roughly 700 x 600 px at this viewport, a 7x
         non-uniform stretch — so the same fix was tried and measured here
         first: the ramp returns, at a stroke ~20 px wide instead of 6, on an
         edge already habituated to reading as a hairline. Worse than the
         defect it repairs, and the ratio is not one number to compensate —
         it moves with each plate's own --ar and the viewport's own card
         width, so no static stroke-width correction is safely additive.

         ALSO TRIED, no effect on any of them: will-change (transform, and
         stroke + opacity) on the path; translateZ(0) on the path and on
         .sp4-frame; isolation: isolate on .sp4-frame; backface-visibility:
         hidden on the path; redriving the same two keyframes through the
         legacy transform: scaleX()/scaleY() instead of the standalone scale
         property #339 named as the trigger. None returned the colour.

         No code change follows from this. → #339, .lp-flow__leaf's own note
         above, for the shared root cause */

      .sp4-tick {
        animation: lp-frame-in linear both;
        animation-timeline: --sp4;
        animation-range: contain calc((var(--t0) + 14) * 1%)
                         contain calc((var(--t0) + 18) * 1%);
      }

      /* THE SWEEP. A clip-path inset opened from the top edge, so the picture
         arrives the way the front travelled in act 2 — downward, into the
         frame that is already waiting for it. Paired with a small scale
         settle on the image itself: the plate does not move, the picture
         inside it comes to rest, which is the difference between a photograph
         arriving and a box growing.

         clip-path AND NOT height, because the window's height is the shot's
         and the shot's is fixed by aspect-ratio. Animating the box would
         re-lay the frame it is inside; animating the clip moves nothing. */
      .sp4-win {
        animation: sp4-sweep linear both;
        animation-timeline: --sp4;
        animation-range: contain calc((var(--t0) + 12) * 1%)
                         contain calc((var(--t0) + 30) * 1%);
      }
      @keyframes sp4-sweep {
        from { clip-path: inset(0 0 100% 0); }
        to   { clip-path: inset(0 0 0 0); }
      }
      .sp4-photo {
        animation: sp4-settle var(--ease-out, ease-out) both;
        animation-timeline: --sp4;
        animation-range: contain calc((var(--t0) + 12) * 1%)
                         contain calc((var(--t0) + 36) * 1%);
      }
      @keyframes sp4-settle { from { scale: 1.06; } to { scale: 1; } }

      /* The mark arrives with the frame it labels; the typed lines below it
         are cf-stream.js's, and they need the column to be there already. */
      .sp4-mark {
        animation: lp-frame-in linear both;
        animation-timeline: --sp4;
        animation-range: contain calc((var(--t0) + 4) * 1%)
                         contain calc((var(--t0) + 10) * 1%);
      }

      /* ---- ACT 3: the cards' pinned stage, carried verbatim ---- */
      /* The same floor and the same cap as --lp-measure's, 830 lines up, where
         the reasoning and the measurements are. `100%` here rather than `100vh`
         because this box has a percentage basis and that basis IS the clip: the
         floor may not exceed the box that crops it. The 9rem the floor subtracts
         is the banner, for the reason given there — and it has to be subtracted
         in BOTH places or the two disagree: this box is what lifts the trio off
         the banner and --lp-measure is what sizes the card that has to fit in
         the lifted box. Floor one and not the other and the card is drawn for a
         screen the box no longer occupies. */
      .lp-proc-stage { --pin-measure: var(--lp-measure);
        block-size: max(min(45rem - 9rem, 100%), calc(100% - var(--cf-consent-height, 0px))); }
      /* AND THE STAGE'S BOX LANDS ON WHOLE PIXELS, because six of the hairlines
         on it are drawn ON that box's edges and a hairline is the one thing a
         fraction of a pixel is visible in.

         The plate gave up its border to .lp-frame — the note 250 lines down says
         so and records the 0.94 px the frame stood off the border box before
         #204 cancelled it. What that fix did not ask is what the SAME contour
         then paints, and the answer is not the same at every edge of the same
         rectangle. `.container` places this box with two fractional terms:
         `--gutter` is `clamp(1.25rem, 5.5vw, 5rem)`, so 5.5vw below the 1454 px
         where it clamps, and `max-width` is `--container-max + 2 * --gutter`,
         so the centring margin above it carries the same fraction. Under
         --container-max the box therefore starts at 5.5vw exactly: 56.31 at
         1024, 70.39 at 1280.

         Read off the render at device scale 1, mid-pinned-run, as the number of
         pixel columns the vertical is spread across and the darkest of them
         (ground is 225):

           1024 x 768   left 56.31  2px 106.8   mid 512.00  3px 102.8   right  967.69  3px 116.8
           1280 x 800   left 70.39  2px 103.5   mid 640.00  3px 116.4   right 1209.61  3px  92.4
           1440 x 900   left 79.98  2px 100.8   mid 719.99  2px 100.8   right 1360.00  2px 100.4

         At 1440 the three verticals of one frame are the same line three times.
         At 1280 the middle rail is spread over three columns at 116.4 while the
         right is spread over three at 92.4 — a quarter of the contour's ink
         between one side of the rectangle and the other, and the base rail is
         123.3 against a top rail of 74.8. That is not a stroke the eye reads as
         one weight, and it is the same class of finding as the corner that does
         not close: the drawing is right and the ink is not.

         SO THE BOX IS ROUNDED, NOT THE STROKE. Both terms .container places it
         with, in the pinned tier only, restated here as the container's own
         expressions inside round() so a change to the container breaks the
         check rather than quietly un-rounding the frame. The step on the
         max-width is 2 px because that width is HALVED into the centring
         margin — round to 1 px and the margin is back on a half. Measured
         after: 56/512/968 at 1024, 70/640/1210 at 1280, all two columns, the
         spread within one frame down from 24.0 levels to 1.0 at 1280 and from
         14.0 to 0.7 at 1024. 1440, 1512 and 1920 are byte-for-byte what they
         were — above --container-max the placement is (100vw - 1280)/2, which
         never had a fraction in it.

         The rounding does not travel to the tiers below the gate: there the
         cards keep their own 1 px borders, which the browser snaps to the pixel
         grid itself, and there is no .lp-frame to be off it.
         → scripts/check-plate-grid.py */
      .lp-proc-stage {
        max-width: round(calc(var(--container-max) + var(--gutter) * 2), 2px);
        padding-inline: round(var(--gutter), 1px);
      }
      /* AND THE SAME FINDING ON THE OTHER AXIS. The note above is about the
         chrome running WIDER than the card; this is the chrome drifting AWAY
         from it as the viewport gets taller, and it is the larger of the two.

         The card has two caps, not one: --pin-measure above, derived from the
         viewport height, and the container's own width, which the card reaches
         at a viewport of about 778 at 1280 wide and 848 at 1440. Above that
         crossover the height cap no longer binds — the card is a fixed size —
         while .cf-pin__inner's middle row is minmax(0, 1fr) and takes every
         further pixel of viewport height. .lp-proc-steps then centres the card
         in a row far taller than it, and the whole surplus lands in the two
         gaps that are the only thing saying the index and the bar belong to
         the card. Measured, index to card / card to bar:

           1280 x 720     21 / 37     the composition as designed
           1280 x 780     22 / 38     the crossover
           1280 x 900     82 / 98
           1512 x 982     88 / 104
           1920 x 1080   137 / 153    an ordinary desktop, 6.5x the design
           1280 x 1200   232 / 248
           1920 x 1440   317 / 333    15x

         At 1280 x 1200 the index stands 232 px above the card it numbers and
         the bar 248 px below it, with the card holding 48 % of the screen and
         the rest empty wash — 01 over nothing, which is the exact sentence the
         width fix was written to end. It is also the answer to whether 719 ->
         720 is the largest discontinuity on this stage: it is not. The gate's
         own step in card width is 115 px (1139 -> 1024 at 1280 wide); the
         continuous drift above it is 331 px over the same page's height range.
         There is no accident at 800 — 800 is simply 22 px into a slide that
         runs everywhere above the crossover.

         SO THE SURPLUS GOES OUTSIDE THE TRIO INSTEAD OF INSIDE IT. The middle
         row is sized to the card rather than to the leftovers, and the three
         rows are centred as one group in the stage's content box, which is
         what the composition already was at 720 where it was drawn. The gaps
         are constant at every size the gate admits — 16 / 16 at a 16 px
         default, 24 / 24 at a 24 px one, because the rung is a rem token and
         the drift was not.

         minmax(0, min-content) AND NOT `auto`, which reads the same and is
         not. Both size the row to the card while there is room. When there is
         not — the card cropped by 16 px with --pin-measure forced off, the
         only way to reach that state — `auto` grows the trio past the padding
         box and the index rides up to 76 px, under a nav band that starts at
         84: the failure .cf-pin__inner's padding exists to prevent. The
         minmax form stays inside the padding box and lets the card crop
         symmetrically against the stage's own clip, which is the right thing
         to lose. Nav clearance is not the part that gives.

         The 13rem chrome is untouched and is now a reservation rather than a
         measurement: with the surplus outside, the trio needs 181 px at 1280 x
         720 against the 208 the cap withholds, so the card is 52 px narrower
         than it could be at the sizes where the height cap binds. Slack, not
         crop — retuning the derivation is a separate change with its own
         measurements, and it is stated here so it is not rediscovered as a
         bug. */
      .lp-proc-stage {
        grid-template-rows: auto minmax(0, min-content) auto;
        align-content: center;
        /* THE RUNG, DECLARED WHERE IT CAN TAKE EFFECT. It used to be
           `.lp-proc-index { padding-block-end: var(--space-4) }` and it never
           once applied: that is (0,1,0) and base.css:150 writes
           `ol[role="list"] { padding: 0 }` at (0,1,1), so the index's computed
           padding-bottom was 0 px at every viewport on this page. It is the
           bug .cf-pin__inner's own note records one property over — `margin-
           inline: auto` losing to the same selector's `margin: 0`, fixed there
           by moving to justify-self, "nobody else's property". The same move:
           the gap between two grid rows is the grid's, and no reset owns it.

           Invisible until now because 21 to 317 px of slack sat in that gap
           anyway. Remove the slack without this and the index would sit flush
           on the card's top edge — which is what the first draft of this fix
           did, and how the dead declaration was found. */
        row-gap: var(--space-4);
      }
      .lp-proc-steps {
        gap: 0;
        align-content: center;
      }
      /* All five in one grid area — the plate and the four cards — so the
         frames coincide exactly and every card stretches to the same
         height, which is what lets the plate's hairline serve them all. */
      .lp-proc-steps > .cf-process { grid-area: 1 / 1; }
      .lp-proc-plate { display: grid; }
      /* The cards give up their chrome to the plate. Not the content —
         the border and the hairline, which would otherwise dim to
         three-quarter strength at every handover as two copies crossfade. */
      .lp-proc-steps > .cf-process:not(.lp-proc-plate) { border-color: transparent; }
      .lp-proc-steps > .cf-process:not(.lp-proc-plate) .cf-process__figure { border-color: transparent; }

      /* The object's crossfade and the panel's hold are .cf-pin__step and
         .cf-pin__copy in the markup — the four cards carry both, the plate
         carries neither, which is what the :not() used to say. The copy
         arrives line by line, top to bottom, all of it inside the reveal —
         the last line is up by 10.2 points of the quarter, well before the
         build finishes at 15.5. */
      .lp-proc-steps .cf-process__panel > * {
        animation: cf-pin-copy linear both;
        animation-timeline: --cf-pin;
        animation-range: contain calc(var(--i) * 25% + 2% + var(--k, 0) * 1.1%)
                         contain calc(var(--i) * 25% + 3.6% + var(--k, 0) * 1.1%);
      }
      .lp-proc-steps .cf-process__panel > :nth-child(2) { --k: 1; }
      .lp-proc-steps .cf-process__panel > :nth-child(3) { --k: 2; }
      .lp-proc-steps .cf-process__panel > :nth-child(4) { --k: 3; }
      .lp-proc-steps .cf-process__panel > :nth-child(5) { --k: 4; }
      .lp-proc-steps .cf-process__panel > :nth-child(6) { --k: 5; }
      .lp-proc-steps .cf-process__panel > :nth-child(7) { --k: 6; }

      /* THE BUILD, RE-TIMED ONTO THE TRACK — the stopped scene, the
         parts' timeline and the trace and node windows are .cf-pin's.
         What is this page's is which keyframes the parts run: same parts,
         same stages, same cf-iso-build — it still reads each part's
         --build-dx/-dy, so every direction authored in the markup
         survives. The order is the build's own: ghosts first (the parts
         arrive out of where the ghosts are), then the forms by stage, the
         traces, the light, the nodes. The reveal ends at 15.5 of 25 —
         62 % of the step, the system's hold rule — and the rest of the
         quarter is a still. → foundations/motion.html#build, #hold */
      /* NAMED THE WAY components.css NAMES IT, and that is the whole rule.
         `.lp-proc-steps .cf-iso__ghost` weighs (0,2,0) and the component's
         `.cf-iso--build .cf-iso__ghost:not(.cf-iso__orbit)` weighs (0,3,0) —
         the :not() is a class — so the page lost every time and cards 02 and
         03 kept the component's `cover 2% cover 14%`. On a
         pinned track that is not a small error: `cover` on --cf-pin is the
         track's whole 7,560 px sweep, so both plans finished fading up at
         contain 2.7 %, while card 01 was still building and 22 points of
         scroll before their own card crossed in. Measured at 1440 x 900: card
         02's ghosts sat at opacity 1 from y = 1822 to the end of the track.
         The plan did not come first on those two cards — it was simply always
         there, and the quarter opened with solids arriving onto nothing that
         had been drawn. Every other part of the build reads `contain` because
         its page rule and its component rule weigh the same and this block is
         later; only the ghost's carries the extra class. So this one is
         written to the same shape, one class heavier, with the same :not() for
         the same reason components.css gives: without it this selector takes
         the orbit's turn away with it. */
      .lp-proc-steps .cf-iso--build .cf-iso__ghost:not(.cf-iso__orbit) {
        animation-name: cf-iso-fade;
        animation-range: contain calc(var(--i) * 25% + 0.5%) contain calc(var(--i) * 25% + 4%);
      }
      /* The orbits keep their two animations — fade with the ghosts, turn
         for longer — excluded from the ghost window above exactly as
         components.css excludes them from its own. */
      .lp-proc-steps .cf-iso__orbit {
        animation-name: cf-iso-fade, cf-iso-orbit;
        animation-timeline: --cf-pin, --cf-pin;
        animation-range: contain calc(var(--i) * 25% + 0.5%) contain calc(var(--i) * 25% + 4%),
                         contain calc(var(--i) * 25% + 0.5%) contain calc(var(--i) * 25% + 12%);
      }
      /* THE BUILD'S HEAD IS ONE POINT BEHIND THE PLAN'S, AND CARD 01 HAS NO
         PLAN. 1.5 is not a free constant: the ghosts open the quarter at 0.5
         and the solids arrive one point later, into geometry that is already
         standing. Every card was given that head, and one of the four has no
         ghosts at all — card 01's object is eleven forms, no plan, no trace —
         so its quarter opened by waiting a point for something that never
         comes.

         WHAT THAT COST, at the one seam this page is built around. The frame's
         relay finishes and the chrome lands together at entry 100 %, which IS
         contain 0; the plate's crossfade reaches opacity 1 at contain 0.41 %
         (cf-pin-step holds from 11 % of a range that opens at -3); and the
         first form used to arrive at contain 1.5 %. Sampled every 4 px at
         1440 x 900 over every animated part on the page — opacity, both scale
         axes, fill-opacity and stroke-dashoffset, 235 parts — the summed
         change was EXACTLY ZERO from y 1626 to y 1682:

           1440 x 900   contain 0.42 -> 1.50 %   62 px
           1280 x 800   contain 0.42 -> 1.50 %   56 px
           1024 x 720   contain 0.42 -> 1.50 %   50 px

         It is the only still anywhere between the flow's first stroke and the
         end of card 01's reveal, and it is at the worst possible place: the
         stage has just taken the scroll, so the page is not moving EITHER —
         and what stands there is a finished empty rectangle with 01 02 03 04
         lit over it. That is the exact picture the note on .lp-proc-index
         calls the tell that a stage was assembled rather than assembling, and
         the paragraph on the frame says in as many words that "the build takes
         over at contain 0". It took over a point and a half later.

         The other three quarters have no such gap: their ghosts start 0.09 of
         a point after their plate lands, which is 5 px and not a still.

         SO THE HEAD IS A VARIABLE AND THE PLAN IS WHAT SETS IT. A card with no
         ghosts takes the plan's own slot — its forms ARE the first thing on the
         stage, so they open the quarter the way a plan would. `:not(:has())` is
         the whole point: it reads the drawing rather than the card's index, so
         the day the craft lane gives card 01 a plan the head goes back to 1.5
         with nothing here to remember. The window TRANSLATES rather than
         stretches — 6.5 points wide either way — so card 01's parts arrive at
         the same rate as every other card's; only the whole build sits one
         point earlier. The stagger, the trace and node windows, the 62 % hold
         and the reveal's end at 15.5 are all untouched.
         scripts/check-quarter-opening.py holds it. */
      .lp-proc-steps { --build-head: 1.5; }
      .lp-proc-steps .cf-pin__step:not(:has(.cf-iso__ghost)) { --build-head: 0.5; }
      .lp-proc-steps .cf-iso__form {
        animation-name: cf-iso-build;
        animation-range: contain calc(var(--i) * 25% + var(--build-head) * 1% + var(--stage, 0) * 2.2%)
                         contain calc(var(--i) * 25% + (var(--build-head) + 6.5) * 1% + var(--stage, 0) * 2.2%);
      }
      /* Two animations, like the build mode it re-times: the arrival on
         the part's own stage, the fill last of all.

         The arrival reads --build-head with the forms — it is a part of the
         same drawing arriving on the same stage, and a light left behind at
         1.5 while its stage-mates moved to 0.5 is a piece finishing after the
         junction it belongs to. The FILL does not move: 11.5 is the number the
         flow's own light pass is written against — the last lit flow stroke is
         a thousand pixels of scroll before card 01's lime, and that clearance
         is what makes one lit element per screen true here
         (→ foundations/colors.html#one-per-screen, and the .lp-flow__light
         note above, which carries the sweep). It is a beat of the reveal
         rather than a beat of the build, and the build is what moved. */
      .lp-proc-steps .cf-iso__light {
        animation-name: cf-iso-light, cf-iso-build;
        animation-timeline: --cf-pin, --cf-pin;
        animation-range: contain calc(var(--i) * 25% + 11.5%) contain calc(var(--i) * 25% + 15%),
                         contain calc(var(--i) * 25% + var(--build-head) * 1% + var(--stage, 0) * 2.2%)
                         contain calc(var(--i) * 25% + (var(--build-head) + 6.5) * 1% + var(--stage, 0) * 2.2%);
      }

      /* THE FRAME ASSEMBLES ON ENTRY, out of the same scroll that takes the
         statement apart above it. The plate gives up its border the way the
         steps gave up theirs — the frame is the .lp-frame hairlines from here
         on, drawn during the section's entry phase and standing for the whole
         pinned run, 1 px under non-scaling-stroke exactly where the border
         stood. Entry is the one viewport of travel before the stage pins, so
         the relay — top across, verticals down, base — finishes as it pins
         and the build takes over at contain 0.

         TWO PASSES PER STROKE, which this paragraph has described twice and
         denied once in between. The original read "the LIGHT pass draws first at
         2 px carrying the near rake; the 1 px contour follows six points behind";
         #170 took the colour off, and the paragraph became "one pass, not two —
         six hairlines, one weight, one colour". It is two again by decision, at
         6 px rather than 2, with the argument and both reversals recorded over
         .lp-frame__lit where the ranges are. What has not changed through any of
         it: the frame that STANDS for the pinned run is a black hairline, and the
         relay's rate, order and origins are untouched — the light is a front on
         the strokes that were already there, not a new drawing.

         So: seven strokes drawn twice, the ramp leading and the contour landing
         behind it, both finished before the stage pins. The whole relay is
         scrubbed: back up, and the frame un-draws — light and all — before the
         statement reassembles. */
      .lp-proc-plate { position: relative; border-color: transparent; }
      .lp-proc-plate .cf-process__figure { border-color: transparent; }
      .lp-frame {
        position: absolute;
        /* ON THE BORDER BOX, NOT THE PADDING BOX, and this is the whole
           difference between a contour that replaces a border and one that
           stands just inside where the border was. `inset: 0` on an absolutely
           positioned child resolves against its containing block's PADDING box,
           and .lp-proc-plate is a .cf-process, which carries
           `border: var(--stroke-1) solid`. The border-color is given up to this
           frame two rules above; the border-WIDTH stays, so the padding box is
           1 px inside the border box on every side, and so was this drawing.

           Measured at device scale 8, reading the ink off the render rather
           than the DOM: at 1920x1080 the plate's border box began at x 320.00
           and the frame's left vertical painted centred on 320.94. The flow
           overhead — same container, same width, so its box IS the plate's
           border box — dropped its leftmost terminal at exactly 320.00. Two
           lines that the markup note below calls the same line, 0.94 px apart,
           at every viewport (L +1.00 R +1.00 T +1.00 B +1.00 at 1440, 1920 and
           1280 alike). Same at the right vertical and both top corners.

           Cancelling the border width puts the frame's box back on the plate's
           border box, which is where the border it replaced was and where the
           flow lands. The three sibling cards keep their own transparent
           borders on that same outer boundary, so the contour now stands on
           theirs too. Written against the token rather than as -1px because it
           is that border being cancelled, not a coincidence about its size.

           AND max-width HAS TO GO WITH IT. base.css:151 resets
           `img, svg, video { display: block; max-width: 100% }`, and for an
           absolutely positioned box that 100 % is the containing block's
           padding box — the very box being escaped. With the insets alone the
           left edge moved to 79.98 and the right stayed at 1357.98: the cap
           held the width at 1278 and the negative left simply slid the old box
           1 px left, which is a worse drawing than the one being fixed, since
           it makes the two verticals disagree with each other as well as with
           the flow.

           AND THE SIZE IS STATED RATHER THAN LEFT TO THE INSETS, because an
           <svg> is a REPLACED element and a replaced element resolves an auto
           size from its intrinsic ratio before it resolves it from opposing
           insets. With `inset` alone and no width or height, this box came out
           1280 x 640: the width followed the two insets, and the height came
           from the viewBox's own 1000 / 500 — 1280 / 2 — so `bottom: -1px` was
           simply dropped as over-constrained and the base rail stayed 1 px
           inside the plate while the other three had moved out. Three sides
           fixed and one not is the state this check exists to make impossible.
           Both percentages are of the containing block's padding box, which is
           the box being escaped, so both add the border back twice. */
        inset: calc(var(--stroke-1) * -1);
        width: calc(100% + var(--stroke-1) * 2);
        height: calc(100% + var(--stroke-1) * 2);
        max-width: none;
        pointer-events: none;
        /* THE BORDER WEIGHED LESS THAN THE LINE DIVIDING IT, and it is the one
           thing about this frame that is not the frame's own doing. Four of the
           five strokes ARE the viewBox edge — M0 0H1000 at y=0, M0 500H1000 at
           y=500, M0 0V500 at x=0, M1000 0V500 at x=1000 — and SVG clips to its
           viewport by default, so the outer half of each of those four was cut
           away. The divider at M500 0V500 is interior and kept all of its.

           Measured as ink per stroke, meaned over 40 samples along each, against
           the interior divider as 1.00x: left 0.63x, right 0.43x, top 0.59x,
           base 0.47x at 1440x900; 0.62 / 0.43 / 0.59 / 0.45 at 1920x1080; and
           0.49 / 0.83 / 0.46 / 0.79 at 1280x900. Not one of the four matched the
           divider, no two of them matched each other, and which edge came out
           lightest changed with the viewport — because a clipped half-stroke
           lands wherever the frame's fractional origin puts it on the device
           grid. With this line: 0.99 / 1.00 / 0.93 / 1.07 at 1440, 0.99 / 1.00 /
           0.94 / 1.05 at 1920.

           components.css:1697 already states this rule and already pays it, on
           .cf-page-header__figure, in these words: "The viewBox is the drawing's
           bounding box, so a 1 px non-scaling contour at the extremes is half
           outside it. SVG clips to the viewport by default; without this the
           outermost shell loses half its stroke." .cf-plot__draw and
           .cf-arrive__object carry it for the same reason. This frame is the one
           drawing in the system whose whole geometry sits on its extremes, and
           it was the one that never declared it. Nothing here overflows but
           those four half-strokes: the plate does not crop, so they land exactly
           where the border they replaced stood — which they do since the inset
           above cancels that border's width, and did not before it. */
        overflow: visible;
      }
      /* Statics only on the shared selector — an `animation` shorthand here
         would outweigh the class rules below ((0,1,1) against (0,1,0)) and
         its implicit `animation-range: normal` would stretch every draw
         across the whole timeline. It did, until this was measured: five
         hairlines creeping in lockstep from one end of the section to the
         other, and no relay at all.

         THE DRAW IS A SCALE, NOT A DASH, and the line above it is why.
         A dashed draw was the first version — stroke-dasharray: 1 over
         pathLength="1", the same maths every .cf-iso__trace uses — and under
         `vector-effect: non-scaling-stroke` it cannot work: the dash is
         measured in SCREEN pixels while pathLength normalises in USER units,
         so the two disagree by exactly the render scale. This frame is a
         1000 x 500 viewBox stretched to the plate — 1278 x 639 measured at
         1440 x 900 — so every stroke stopped at 1000 / 1278 = 78.2 % of
         itself and STAYED there: the top and base rails ended 278 px short of
         the right vertical, the verticals 139 px above the base, and the
         lectern the whole pinned stage rests on was never once a closed
         rectangle, at any scroll position or any viewport. It is the failure
         components.css records twice — on .cf-iso__trace ("the one stroke in
         an illustration that may not have it") and on the Werte grid lines
         ("deliberately NOT under .cf-iso") — reintroduced by the one drawing
         that is under neither. check-iso-motion.py enforces it generally now.

         A straight stroke grown from one end is a scale about that end, and
         `transform-box: fill-box` puts the origin exactly at each stroke's own
         junction: the top and left at (0,0), the divider at (500,0), the right
         vertical at (1000,0), the base at (0,500) — the relay's --a order read
         off the geometry rather than authored twice. Scaling the axis a stroke
         has no extent on is a no-op, so one `scale: 0 -> 1` serves all five and
         the markup says nothing new. Under non-scaling-stroke the weight
         ignores the transform, so 2 px and 1 px stay 2 px and 1 px throughout,
         which the dash version also promised and is the reason it is kept. And
         it is composited rather than paint-only — the cost components.css
         charges .cf-iso__trace for, off the biggest element on the stage. */
      /* STATICS ONLY, AND transform-origin IS NOT ONE — which is the whole of
         the note above read one declaration further. That note catches the
         `animation` shorthand for weighing (0,1,1) against the class rule's
         (0,1,0) and then leaves `transform-origin: 0 0` standing on the same
         selector, where it wins by the same point against the one property
         --o exists to vary. Measured: every one of the six lines computed
         `transform-origin: 0px 0px`, and the one that asks for anything else
         asked in vain. Five of them declare `--o: 0 0` and were right by
         coincidence; `M0 0H500` declares `100% 0` and has never once applied.

         WHAT THAT LOOKED LIKE. The rail's left arm is the stroke whose parent
         junction is the MIDDLE vertical — the point the trunk lands on — so it
         grows leftward out of the split to the corner, which is what the note
         below means by "spreads outward from the middle vertical the way the
         flow spread from its own split". Anchored at 0 0 it grew the other
         way: out of the top-left corner, rightward, closing on the trunk's
         landing point LAST. Its window is entry 72-84 %, y 1346 to 1454 at
         1440 x 900, and through all 108 px of it the rail read as two arms
         running the SAME way — the left one out of the corner the left
         vertical had just left, the right one correctly out of the split —
         with the gap between them, over the trunk, the last thing to shut.
         Measured at half-draw, y 1400: the arm spanned x 80 to 400 when it
         should have spanned 400 to 720. The relay ran backwards through its
         one branching stroke for as long as this drawing has existed.

         So the shared selector keeps the two declarations that are the same
         for all six and gives up the one that is not. `var(--o, 0 0)` on the
         class rule below carries the identical default, so a path that never
         names an end still grows from its own origin.
         scripts/check-grow-origin.py holds it. */
      .lp-frame path {
        fill: none;
        vector-effect: non-scaling-stroke;
        transform-box: fill-box;
      }
      /* The relay is per stroke: --a is where this stroke's parent junction is
         reached, so consecutive strokes chain instead of running abreast, and
         --o is the end it grows FROM. The middle vertical grows down out of the
         flow's arrival; the base rail's two arms spread out of that vertical's
         foot; the two side verticals grow up out of the corners those arms
         reach; the top rail's two arms spread out of the vertical's head the way
         the flow spread from its own split.

         EVERY ONE OF THOSE "OUT OF"S IS A CLAIM ABOUT A MOMENT, and for three of
         the six strokes it used to be false — the base rail and both side
         verticals grew out of corners the relay never reached, which is a line
         beginning in mid-air and the same fault as one ending there.
         scripts/check-relay-chain.py walks the ink forward from the source and
         holds every origin to it; the markup on both pages carries the
         measurements.

         THE WINDOW IS BOUGHT BY THE STROKE'S OWN LENGTH, AND IT USED TO BE A
         FLAT 12. Six strokes, one window width, and five of the six were 500
         viewBox units long — so the flat number was right five times and the
         sixth stroke was the base rail, M0 500H1000, which is 1000. It drew
         twice the line in the same scroll. (The rail is two 500-unit arms now,
         for the chain rather than for the rate, so all seven strokes buy the
         same 12 points — the derivation below is what makes that automatic.) Measured as the tip's travel, which
         is the quantity check-scrub-rate.py is written in:

           1440 x 900    five strokes 5.94 px/px    base 11.85
           1280 x 800                 5.94                11.87
           1024 x 720                 5.52 / 5.27         10.55
           1512 x 982                 5.44                10.86
           1920 x 1080                4.95                 9.88

         Exactly 2.00x at every viewport, because the ratio is a pure number:
         12 points per 500 units against 12 per 1000. A relay whose whole
         premise is that consecutive strokes CHAIN — one line handed on, which
         is what the paragraph above claims and what --o exists to make true —
         cannot have its last leg travel at twice the speed of the five that
         hand over to it. That is not a stagger, it is a different drawing
         speed on one stroke.

         So the rate is declared once, in points of entry per viewBox unit, and
         every window is --u x that. .024 is the value the five correct strokes
         already had (12 / 500); nothing that was right moves, and the base rail
         goes from 12 points to 24.

         AND THE 24 POINTS GO WHERE THE READER IS, which is the second half of
         the same finding. The base rail is the one stroke of the six lying on
         the frame's BOTTOM edge, and during entry that edge is below the fold:
         the plate's foot only crosses the viewport bottom at entry 90.2 % at
         1440 x 900 (90.8 at 1280 x 800, 88.9 at the gate floor, 83.5 at
         1920 x 1080 — it is 812 px under the stage's top, over a viewport).
         At `entry 80 -> 92` that put 84.4 % of the rail down before its own
         line had entered the viewport at all, and 90.0 % at 1280 x 800: the
         reader saw the last tenth of a stroke arrive and the rest was simply
         already there. → foundations/motion.html#arrival, a build happens
         where the eye is.

         Ending at entry 100 instead of 92 is the fix and is also what the
         paragraph on .lp-frame says the relay does — "the relay finishes as it
         pins and the build takes over at contain 0" — which was true of the
         frame's five OTHER strokes and eight points early on this one.
         Measured off the rendered `scale` at the exact scroll where the rail's
         own line crosses the viewport bottom, before -> after:

           1440 x 900    84.4 -> 58.9 %
           1280 x 800    90.0 -> 61.7
           1280 x 848    70.8 -> 52.1
           1024 x 720    73.1 -> 53.2
           1512 x 982    56.5 -> 44.9
           1920 x 1080   28.6 -> 31.0

         The one that rises is the one viewport that was already well under,
         and it rises because the window widened around a fixed moment of first
         sight. 1280 x 800 is 1.7 points over the ~60 ceiling and is left
         there: closing it means narrowing the window, which is the doubled
         rate coming back.

         AND ENTRY 100 IS NOW THE LAST THING IN THE PHASE, which it was not
         when this was measured: the chrome used to fade up over `entry 92% ->
         100%` and #199 moved it past the pin, to an offset in `contain`, so
         that the index is not lit under a drawing still lying across it. That
         left the relay's last eight points carrying nothing but the plate's
         crossfade. They carry the base rail now, and the drawing's last stroke
         lands on the same pixel of scroll the stage takes the page on.

         --a 80 -> 76 follows from the width and not from taste: the base grows
         from x 0, the left vertical's foot, which lands at 60 + 12 = 72, so 76
         keeps the same four points of clearance behind its own junction that
         the stroke had before. It closes on the bottom-right corner at 100,
         twenty points after the right vertical put that corner there.

         THE RATE IS UNIFORM IN THE DRAWING'S UNITS, NOT IN PIXELS, and the
         gap between those is preserveAspectRatio="none": where the plate is
         not exactly 2:1 the two axes scale differently, so a 500-unit vertical
         and a 500-unit horizontal render 4.5 % apart at the gate floor (477 px
         against 456) and identically everywhere the height cap does not bind.
         The pure number is the one a script can hold and the one the authoring
         is done in. scripts/check-relay-rate.py holds it, and holds --u
         against the `d` it claims to measure.

         TWO PASSES AGAIN, AND THIS PARAGRAPH HAS NOW SAID BOTH THINGS. It read
         "one stroke weight, one colour — the light pass that used to lead each
         stroke in the near rake is gone: a border is a contour, and colour on it
         read as a stray tint on the one element that frames everything else."
         That was #170's ruling and it held for as long as the frame was the only
         drawing on this page built out of lines without a front. It is not any
         more: the root lays every stroke down with .lp-flow__light ahead of
         .lp-flow__seg, act 4's plates do the same on four edges each, and this
         frame — six strokes, a relay, drawn in the reader's own scroll — was the
         one that arrived as finished geometry sliding out of a corner.

         So the ruling is reversed by decision, not by drift, and what is kept
         from it is the part that was never about the front: THE SETTLED FRAME IS
         A BLACK CONTOUR. The light exists only while a line is being drawn and
         is out before the stage pins — nothing lime stands around the cards
         while they are read, which is what "a tint on the rectangle that frames
         everything else" was protecting, and it is the same guarantee
         .sp4-lit gives the photographs.
         → foundations/colors.html#one-per-screen: one light per OBJECT is the
         rule the system actually follows, and this frame is one object.

         THE LEAD IS THE ROOT'S OWN SIX POINTS, and that is arithmetic rather
         than a borrow. A lead has to be a fraction of the run it leads: act 4
         took four points because an edge's window is sixteen where the root's
         longest walk is forty-five. This relay runs entry 56 -> 92, so its run
         is thirty-six points of the same phase the root spends forty-five in —
         near enough the same length, so it takes the same six. Flat, like both
         of theirs: the front runs six points ahead of the black through the
         whole relay rather than six points ahead within each stroke, which is
         what makes a chain of six strokes read as one travelling line.

         AND THE FADE IS EXACTLY THE LEAD, which act 4's is not and the root's is
         not. Both of those hold the light a beat and then take it out past their
         own contour — the root's fade ends two points after the black lands.
         This relay cannot afford that: entry 100 is contain 0, the moment the
         stage takes the scroll and the cards start building inside the frame, and
         check-relay-rate.py holds the relay to it. So the light begins to leave
         the instant its own draw closes and is gone exactly as the contour
         lands: `entry (a - 6 + w) -> entry (a + w)`, six points wide, symmetric
         with the lead, and the last stroke's light is out at the same point of
         scroll its hairline arrives on. Nothing is still going out when the pin
         takes over.

         SIX PIXELS AND NOT THE ROOT'S TWO, for the reason .sp4-lit is six: the
         ramp's coloured leg is its first 32 %, a stroke here is the whole width
         or height of the frame, and 2 px of ramp under a 1 px contour is a
         drawing whose gradients cannot be seen. Six under one leaves two and a
         half proud on each side. */
      .lp-frame { --relay-rate: .024; --frame-lead: 6; }
      .lp-frame__line {
        stroke: var(--border-strong);
        transform-origin: var(--o, 0 0);
        animation: lp-frame-draw linear both;
        animation-timeline: --cf-pin;
        animation-range: entry calc(var(--a) * 1%)
                         entry calc((var(--a) + var(--u) * var(--relay-rate)) * 1%);
      }
      /* THE FRONT. Same verb, same origin, same window — one stroke drawn twice,
         which is why the markup gives the lit twin its contour's own --a and --u
         and the offset lives here. The ramp is the presentation attribute on the
         path, not a declaration in this rule, and that is deliberate: a `stroke`
         here would outrank the attribute on every one of them and paint the
         front black, which is exactly how .sp4-lit and .lp-flow__light are
         written and for exactly this reason.

         transform-origin is repeated rather than shared with .lp-frame__line,
         because a shared `.lp-frame path` selector is (0,1,1) and would beat the
         one declaration --o exists to vary — the finding check-grow-origin.py
         was written for, twelve strokes deep now instead of six. */
      .lp-frame__lit {
        stroke-width: 6;
        transform-origin: var(--o, 0 0);
        animation: lp-frame-draw linear, lp-flow-out linear;
        animation-timeline: --cf-pin, --cf-pin;
        animation-range: entry calc((var(--a) - var(--frame-lead)) * 1%)
                         entry calc((var(--a) - var(--frame-lead)
                                     + var(--u) * var(--relay-rate)) * 1%),
                         entry calc((var(--a) - var(--frame-lead)
                                     + var(--u) * var(--relay-rate)) * 1%)
                         entry calc((var(--a) + var(--u) * var(--relay-rate)) * 1%);
        animation-fill-mode: both, forwards;
      }
    }
  }
  /* ------------------------------------------------------------------
     THE HANDOVER LINE — "Was wir machen" / "In vier Schritten".

     PAGE-LOCAL, the line this file's own preamble draws: .cf-section-header
     ships in components.css and this is one section's treatment of it, the
     same way .lp-flow-data is one section's treatment of a label.

     WHY IT IS TITLE TYPE AND NOT A LABEL. The component sets both halves in
     11 px mono because a section header is normally a rule between two blocks
     of copy. This one is not: it is the seam the root hands the reader over
     at, directly under 660 px of drawing, and eleven pixels of mono there is a
     footnote where the page needs a heading. So the two halves take
     --text-h2, keep the component's own tracking and case, and the rule under
     them keeps its weight — the hierarchy moves, the vocabulary does not.

     AND IT ARRIVES THE WAY IT IS READ. Left to right, wiped and not faded: a
     fade is a thing appearing, a wipe is a thing being written, and this line
     is the sentence the drawing above it has just finished making. The rule
     draws with it on the same clock and from the same edge, so the underline
     is always exactly as long as the words standing on it — which is what
     makes the two read as one gesture rather than as a heading and a border
     that happen to animate.

     Its own view timeline, not the track's. --sp is scoped to .sp-track and
     this sits below it in ordinary flow; giving the header its own view() is
     both simpler than a timeline-scope and more honest — it arrives when the
     reader arrives at it, which is exactly when the stage has released.
     ------------------------------------------------------------------ */
  .sp-head__title, .sp-head__count {
    font-size: var(--text-h2);
    line-height: var(--leading-tight);
  }
  /* "4 Schritte" is set at --text-h2 — 32 px, so WCAG's large-text floor of
     3:1 applies rather than 4.5 — and at --text-muted it measured 2.31:1 over
     the wash where the header lands. It is the only place on the page that
     says how many steps there are, and it is the second half of a two-part
     heading whose first half is --text-primary at the same size: the count
     recedes because the ink beside it is black, not because it is grey enough
     to be unreadable. --text-secondary at this size measures 5.88:1 at the
     wash's darkest stop. */
  .sp-head__count { color: var(--text-secondary); }

  /* --wipe HAS TO BE REGISTERED OR IT DOES NOT INTERPOLATE. An unregistered
     custom property animates as a discrete swap at the halfway point, so the
     wipe would be a cut: 100 % for the first half of its range and 0 % for the
     second, which reads as the title switching on. Registering it as a
     <percentage> is what makes it a wipe. */
  @property --wipe {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 0%;
  }

  @supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
    @media screen and (prefers-reduced-motion: no-preference) {
      /* ON THE TRACK'S TIMELINE, NOT ITS OWN. The header is inside a sticky
         stage now, so its own view() would freeze at one value — the fact this
         whole page is built around. It also does not want its own clock: the
         rule is what the root arrives at, so it has to draw WHILE the root is
         arriving, and the only thing that knows when that is, is --sp.

         The windows sit at the end of act 2, after the contour has landed at
         86.88 and inside the still that runs to 100. At 640vh that is about
         two thirds of a screen of scroll for the rule and the two words,
         pinned, with nothing else moving — which is the difference between an
         animation and a thing that has already happened by the time you look
         up. */
      .sp-head {
        border-bottom-color: transparent;
        position: relative;
      }
      .sp-head::after {
        content: "";
        position: absolute;
        inset: auto 0 calc(-1 * var(--stroke-1)) 0;
        block-size: var(--stroke-1);
        background: var(--border-strong);
        transform-origin: left;
        animation: sp-head-rule linear both;
        animation-timeline: --sp;
        animation-range: contain 76% contain 93%;
      }
      /* THE WIPE IS A CLIP, NOT A MASK AND NOT A WIDTH. A width animation
         reflows the flex row and drags the count across the page as the title
         grows; a mask wants an image. inset() from the right edge inward
         composites, touches no layout, and leaves the text selectable and the
         heading whole in the accessibility tree at every frame — the
         characters are never absent, only unpainted. */
      .sp-head__title, .sp-head__count {
        clip-path: inset(0 var(--wipe) 0 0);
        animation: sp-head-wipe linear both;
        animation-timeline: --sp;
      }
      /* The title leads and the count follows it in: the label is the subject
         and the count is what the subject turns out to be, so reading order is
         arrival order. Both inside the rule's own range, so the line is never
         shorter than the words standing on it. */
      .sp-head__title { animation-range: contain 78% contain 88%; }
      .sp-head__count { animation-range: contain 86% contain 96%; }
    }
  }
  @keyframes sp-head-rule { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  @keyframes sp-head-wipe { from { --wipe: 100%; } to { --wipe: 0%; } }
  /* From nothing, through the ease-out the rest of the system arrives on. The
     0.35 waypoint at 40 % of the window is what makes it read as filling
     rather than as inflating: it is a third of the way to full while the first
     handful of sensors are still on their way in. */
  @keyframes sp-orb-fill {
    from     { scale: 0;    opacity: 0; }
    40%      { scale: 0.35; opacity: 1; }
    to       { scale: 1;    opacity: 1; }
  }
  @keyframes sp-leaf-settle { to { opacity: var(--leaf-peak); } }
  @keyframes sp-leaf-shimmer { from { stroke-opacity: .28; } to { stroke-opacity: 1; } }
  @keyframes sp-ground-recede { to { opacity: var(--sp-ground-dim, 1); } }

  /* One value visible at a time, and the switch is INSTANT. The two stops
     either side of a sixth are 16.66 % and 16.67 %, which is a jump, not a
     tween: a readout that crossfades between 34 and 41 shows 37 for half of
     every tick, and 37 is a value the instrument never reported. Written as
     an explicit pair rather than as steps() so the hold is in the keyframe
     where it can be read, instead of in a timing function whose behaviour
     depends on how many intervals the author happened to write. */
  @keyframes sp-stream {
    0%, 16.66%   { opacity: 1; }
    16.67%, 100% { opacity: 0; }
  }

  /* ============================================================
     THE ACT RAIL — where you are in the five acts, and a way into any of them.

     THE ACTS ARE 22 000 PX OF SCROLL and nothing said so. A reader who has
     watched the field converge has no way to see the map without scrolling
     through the root, the cards and the portraits again, and no way to know
     that any of them are ahead. The rail is the document's own table of
     contents for the part of it that is a composition: five marks, one per act,
     on the left margin where the drawings are not.

     COLLAPSED IT IS AN INDEX AND OPEN IT IS A CONTENTS. At rest each entry is
     its number and a 1 rem glyph — small enough to sit beside a full-bleed
     drawing without competing with it. On hover or focus anywhere in the rail
     the glyphs grow to 1.75 rem and the titles slide out beside them, staggered
     by 40 ms so the list opens as a list rather than as a block. One hover
     opens the whole rail, not the entry under the pointer: the reader's
     question at that moment is "what else is there", and answering it one row
     at a time makes them hunt.

     THE GLYPHS COME FROM THE ICON SET, and the first version of this rail did
     not. A miniature of each act would be five more copies of drawings the page
     already carries once — and the map and the photographs do not survive being
     24 px tall — so each act is reduced to the one figure it is about. Which is
     what the icon set already is, and it publishes a law this rail's first five
     marks broke in five ways: a 24 box with a 2 px margin, a 1 px contour held
     at 1 px by vector-effect at EVERY size, only 26.57, 63.43, 45 and 90
     degrees, anything round drawn as a 2:1 ellipse rather than a circle, and
     "exactly one place an icon is drawn". Measured on what shipped: the strokes
     rendered 1.04 px at rest and 1.67 px open, act 5's outline used six angles
     and not one of them was sanctioned, every dot was a true circle, act 2's
     source stood 0.5 units outside the live area, and the five marks existed as
     ten copies across two pages.

     So they are <use> references now:

       01  cf-signal    a reading arriving, a sensor, live data
       02  cf-node      three arms meeting at a junction, dots at the ends —
                        the confluence's own geometry, at 90 and 26.57 deg
       03  cf-layers    the data foundation, a stack
       04  cf-plate     NEW — the frame act 4 draws at full size, chamfer and
                        all, because the set had no flat frame: every other
                        object in it is an isometric solid, and a picture is not
       05  cf-globe     sites across countries, the whole network

     Four of the five were already in the set and already said what the act
     says. That is the argument for a set: the mark for "a sensor, live data"
     is a decision the system made once, and act 1 is not a reason to make it
     again.

     THE TICK IS .cf-pin__bar's. The current act carries a hairline that scales
     in from its left edge — the same move, in the same direction, that the
     process track already uses to mark which of its four quarters is up. Two
     indexes on one page should say "you are here" the same way.

     SCOPED TO screen AND TO THE ACTS' OWN GATE. `position: fixed` is repeated
     on every sheet of paper (check-print-fixed.py), and a fixed left rail at
     900 px of width is furniture in the margin the content needs — 64rem is the
     threshold the acts themselves are pinned behind, so above it there is a
     composition to navigate and below it there is a page to scroll.
     ============================================================ */
  /* OUT OF THE PAGE BELOW THE GATE, AND OUT OF THE TAB ORDER WITH IT. The rail
     is styled only inside the query below, so without this rule a viewport
     under 64rem would get the bare <nav><ol> as flow content the moment
     act-rail.js cleared the `hidden` attribute — an unstyled list of five links
     in the middle of the page. `display: none` and not `visibility` or an
     offscreen shift, because a rail that cannot be seen must not be reachable
     by Tab either, and none of the other three ways of hiding it take the links
     out of the tab order. */
  .act-rail { display: none; }

  /* THE TWO JUMP TARGETS CLEAR THE STICKY BAR, and they are OUTSIDE the gate
     below on purpose. The rail is desktop-only; #anfang and #abschluss are
     addresses at every width — a URL anybody can be sent, and the same rule
     .act-anchor takes above for exactly that reason. check-arrival.py holds
     addressable HEADINGS to this, which neither of these is; the argument it
     enforces ("an id in the document flow is an address, whatever else it is
     for") does not care what tag carries the id.

     ONE IDIOM FOR ALL THREE, including the two where it overshoots. The hero's
     top already sits at --nav-height, so this clearance resolves past the
     document's start and the scroll clamps to 0 — which is precisely what a
     jump labelled "Zum Seitenanfang" should do, and cheaper than a second value
     that says the same thing with arithmetic in it.

     .cf-page-header IS THE OTHER PAGE'S #anfang, and it overshoots the same
     way: patterns/ueber-uns.html has no hero, so the rail's up jump lands on
     the header that opens it — the first box under the nav, at the same
     --nav-height the hero stands at there. One rule for the top of a page,
     whichever of the two elements opens it. */
  .cf-hero,
  .cf-page-header,
  .cf-footer { scroll-margin-top: calc(var(--nav-height) + var(--space-6)); }

  @media screen and (min-width: 64rem) {
    .act-rail {
      /* THREE ROWS AND ONE RHYTHM: the jump to the top, the five acts, the jump
         to the end. A grid rather than the block this was, so the two jumps sit
         on the list's own gap instead of on a margin that would have to be kept
         equal to it by hand. */
      display: grid;
      gap: var(--space-4);
      justify-items: start;
      position: fixed;
      inset-block-start: 50%;
      inset-inline-start: var(--space-6);
      z-index: var(--z-sticky);
      translate: 0 -50%;
      /* THE COLLAPSED SIZE, published as a property so the glyph and its
         hover state are one decision read in two places. Both ends are rungs
         of the icon set's own ladder — components.css sizes .cf-icon at 16,
         24, 40 and 80, on the 4 px unit — and 16 is the floor it names: "below
         it the contour and the counters inside the isometric objects stop
         being readable". A rail at rest is exactly the case that floor was
         written for. */
      --act-glyph: 1rem;
      /* THE RESTING COLUMNS, published for the same reason --act-glyph is:
         the rail has two resting widths now — four columns where there is a
         margin to hold them and one where there is not — and the row that
         draws them must read the decision rather than repeat it. The four are
         the tick, the numeral, the glyph and the collapsed title; the note on
         .act-rail__link below has the arithmetic for each. The narrow tier's
         single column and the argument for it are at the foot of this file.

         TWO NAMES, BECAUSE THE OPEN RAIL IS THE SAME RAIL AT BOTH TIERS. The
         four columns are the geometry of an OPEN rail — which is what the wide
         tier rests at and what both tiers hover to — so they are written once,
         under a name the narrow tier can point back at rather than a literal
         it would have to keep equal by hand. --act-cols is the resting one and
         the only one .act-rail__link reads.
         → scripts/check-rail-margin.py resolves both, per tier */
      --act-cols-open: var(--space-4) var(--space-4) auto auto;
      --act-cols: var(--act-cols-open);
      /* THE MARKS' TIER — the tick, the numeral and the glyph, which are the
         three aria-hidden spans in every row and not the title. Published beside
         the glyph size because it is switched by the same two states, in the
         same rule below.

         WHY THE MARKS AND THE TITLE PARTED COMPANY HERE. They were one
         declaration on .act-rail__link while the plate was opaque. A translucent
         plate makes the row's backdrop the artwork, and the artwork on this page
         reaches near-black — a display heading passes directly behind the rail
         twice — so the two halves of a row stopped having the same answer. The
         title is text and takes the ink the material guarantees (see
         .act-rail__label). These three are marks: the numeral is the same
         ghosted step counter .cf-pin__index is, registered as such in
         check-contrast.py, and the tick and the glyph carry no words at all.
         Recession is what they are for, so recession is what they keep. */
      --act-ink: var(--text-muted);
      opacity: 0;
      transition: opacity var(--duration-slow) var(--ease-standard);
    }
    /* THE PLATE, AND ONLY WHEN OPEN. The rail stands over a full-bleed drawing
       — act 1's field runs to every corner and carries its own labels — so an
       open title lands on a sensor's readout and neither can be read.

       IT IS GLASS, AND THIS PARAGRAPH USED TO SAY IT MUST NOT BE. The rule it
       cited is real — the landing page spends two blurred layers and a third
       renders correctly while costing — but the plate is the one surface on
       this page that answers the material's own definition exactly:
       foundations/materials.html says Milchglas "exists to calm a complex
       graphic beneath it, never to sit on a flat surface, where it is only a
       grey box", and what is beneath this plate is 22 000 px of composition
       with its own labels on it. An opaque --surface-raised over that artwork
       is the grey box, drawn on the one surface with something to calm.

       WHAT PAYS FOR IT is not a budget somebody raised. The three on this page
       are the nav band, the hero CTA and this; the nav is sticky and always one
       of the two, and the CTA and this plate can never be lit at once.
       The plate is only painted on :hover / :focus-within, which needs
       pointer-events, which needs .is-live — and act-rail.js only sets that
       while the acts own the viewport, about 4 000 px below the hero button.
       So the count of SIMULTANEOUSLY blurred layers, which is the number
       tokens.css says the budget is about, is still two. That argument is
       recorded as a page allowance in check-glass-budget.py rather than left
       in prose here, so the census counts three and no page inherits it.

       BEARING GLASS AND THE 46 % FLOOR, not --surface-glass-thin. The thin
       tint's permission is a measurement on ONE named backdrop; this rail
       stands over five different drawings across 22 000 px, and over a backdrop
       that varies the floor for arbitrary artwork is the one that holds. Swept
       at 250 px steps across the whole act range with the rail open, at 1024,
       1440 and 1920, the plate's darkest composite is rgb(164,165,165) — act
       3's display heading, blurred, crossing behind the rail at 1440. That is
       the number the titles are inked against, and the reason they stopped
       receding: see .act-rail__label, which carries the measurement.

       NO RIM AND NO TRAVELLING LIGHT. The other three glass surfaces each end
       in a lit 1 px edge with something crossing it; this one takes
       --glass-border, the material's own contour, and nothing moves on it. A
       band scrubbed by scroll would re-rasterise the blur on every frame of the
       exact scroll range the rail exists to be open during.

       Nothing at rest, because at rest there is nothing to protect: five
       numerals and five 20 px marks in the margin are marginalia, and a plate
       under them would be the loudest thing on the page. Which is also what
       keeps the blur honest — an element at opacity 0 paints nothing, so the
       filter is only ever composited while the reader is on the rail.

       AND THE BLUR IS A MODIFIER, BECAUSE THE THIRD CONSUMER ARRIVED WITH ITS
       BUDGET ALREADY SPENT. patterns/expertise.html adopted this rail for its
       six chapters, and its four-field stage had made the same decision this
       plate did, one page earlier and in as many words: "the material is spent
       once, on the empty card under the copy column... the reason is a hard
       number: a shipping page gets two blurred layers and the navigation band
       is one of them". The lectern is that page's second layer, it is on screen
       for the whole of the 6 660 px chapter the rail is most needed in, and the
       rail's plate opens on top of it — three at once, which is the number
       tokens.css's rule is about and not the proxy around it. Nothing in the
       stylesheet keeps them apart, so no page allowance can be written for it:
       check-glass-budget.py says in as many words that something has to give up
       its blur first, and on that page the plate is what arrived last.

       SO THE PLATE IS THE MATERIAL'S OWN NO-BLUR FORM THERE, and that is a
       state the system already ships rather than a fork of it.
       --surface-glass-solid is what tokens.css swaps bearing glass for in
       @supports not (backdrop-filter) and under prefers-reduced-transparency —
       "with no blur the only way to keep [4.5:1] is an opaque plate" — so a
       reader on a browser without backdrop-filter has been seeing this rail
       exactly this way on both other pages since it shipped. The rim stays: it
       is a contour, not a translucency, which is the same reading those two
       blocks take. What is NOT available is the third state — a translucent
       tint with the blur switched off — which tokens.css names the worst of the
       three, because the artwork behind it then bleeds through sharp.

       WHICH WAY ROUND, AND WHY THIS ONE. The blur could have been the default
       with an opt-out, and then the census in check-glass-budget.py — which
       counts an element whose class matches a selector that DECLARES
       backdrop-filter — would go on counting a blurred layer on the one page
       that does not paint one. A page opts INTO the material by name, the
       count follows the markup, and a fourth consumer inherits the cheap
       plate until it says it can afford the other. */
    .act-rail::before {
      content: "";
      position: absolute;
      inset: calc(var(--space-4) * -1) calc(var(--space-5) * -1);
      background: var(--surface-glass-solid);
      border: var(--glass-border);
      border-radius: var(--radius-sm);
      opacity: 0;
      transition: opacity var(--duration-base) var(--ease-standard);
      z-index: -1;
    }
    .act-rail--glass::before {
      background: var(--surface-glass);
      -webkit-backdrop-filter: var(--glass-blur);
              backdrop-filter: var(--glass-blur);
    }
    .act-rail:hover::before,
    .act-rail:focus-within::before { opacity: 1; }
    /* OFF SCREEN UNTIL THE ACTS ARE, AND HIDDEN BY PAINT RATHER THAN BY THE
       TREE. It used to be both: act-rail.js set the class for the fade AND the
       `hidden` attribute for the tab order, on the argument that a rail nobody
       can see is not a rail anybody should be able to Tab into. The argument is
       right and the mechanism made the rail unreachable by keyboard in BOTH
       directions, because `hidden` is only ever off during the exact scroll
       range in which Tab has already gone past the rail's place in the document.

       Measured on the shipped page at 1440 x 900, consent accepted:

         forward   focus the hero's "Kennenlernen" and press Tab. The rail's
                   five links are the next thing in the document — and the
                   viewport's middle is still in the hero, so `hidden` is on and
                   they are not in the sequence. Focus lands on the first FAQ
                   summary, scrollY 0 -> 16 585: five acts and 16 000 px skipped
                   in one keypress.
         reverse   focus that same FAQ summary and press Shift+Tab. Back to
                   "Kennenlernen" at scrollY 3 923. The rail is live and on
                   screen for that entire jump and is still not in the sequence.
         paging    PageDown x9 to scrollY 7 083, act 3, rail live at opacity 1,
                   focus on <body>. Tab goes to the nav's logo — the document's
                   first stop — because a scroll does not move the sequential
                   focus starting point. Tabbing on from there reaches the hero
                   toggle, which scrolls the viewport back to the top, which
                   turns `hidden` on again before the rail's turn comes.

       Forcing .focus() on a link proved the rail itself was fine: Tab walked
       01 -> 05 in order and left into the FAQ. It was only ever the attribute.

       So the two states are opacity and pointer-events, and FOCUS IS A REASON
       TO BE LIVE. Off screen the rail paints nothing and catches nothing — a
       fixed five-row column in the left margin must not swallow clicks over
       act 1's full-bleed field — and it is still in the tab order, so the one
       reader who cannot scroll to it can Tab to it, at which point it is on
       screen. That is the skip link's own pattern, which this page already
       opens with. --duration-fast rather than --duration-slow for that arrival:
       a fade is the rail's character when the scroll brings it, and 480 ms is
       too long to answer a keypress.

       .act-rail[hidden] stays as the belt: nothing sets it now, and if anything
       ever does again the rail leaves the layout rather than half-leaving it. */
    .act-rail.is-live,
    .act-rail:focus-within { opacity: 1; }
    .act-rail:focus-within { transition-duration: var(--duration-fast); }
    .act-rail:not(.is-live):not(:focus-within) { pointer-events: none; }
    .act-rail[hidden] { display: none; }

    /* ============================================================
       A MARGIN RAIL NEEDS A MARGIN, AND BELOW 96rem THERE IS NOT ONE —
       NOT ONE 84 px WIDE. The rail that fits the one there is, is at the
       foot of this file; this note is why it had to be a different rail.

       The rail is 84 px wide — the four columns of .act-rail__link, which are
       --space-4, --space-4, the glyph at its collapsed --act-glyph, and the
       collapsed title, with three --space-3 gaps between them — and it is
       fixed at --space-6 from the viewport's edge, so it ends at 108 px. The
       page's content starts at --column-inset, which tokens.css derives as
       max(--gutter, (100% - --container-max) / 2) and which is a function of
       the viewport all the way up to 1536:

           width   --column-inset   rail ends   the rail is inside the column by
            1024        56.31          108        51.7
            1280        70.39          108        37.6
            1440        80             108        28
            1536       128             108       -20   the first width that clears

       So from the moment the rail's own gate opened at 64rem until 96rem, the
       five marks were painted ON the page's first 28 to 52 px of text, and the
       reader saw it because the acts put text there. Swept at 35 scroll stops
       per width with getClientRects() over every text run in the viewport, the
       runs the rail crossed, and by how much:

                                             1024   1280   1440   1536
           act 1  .cf-annot__label "S08"        4     20      -      -
           act 1  the field's kPa readings     54     19      -      -
           act 2  .lp-flow__read "12.4 bar"    38      7      -      -
           act 2  .cf-stream__text, the copy   25      7     10      -
           act 3  "Was wir machen", the head   52     38     28      -
           act 4  .sp4-mark "01 / Geschäfts…"  76     25      -      -
           act 4  the founders' name and copy  76     25      -      -
           act 5  .map-beat__mark, all three    -      -     28      -

       Act 4 is the one that cannot be argued about: at 1024 the rail stands in
       76 of the 84 px it occupies over the copy column, so "01 / Geschäfts-
       führung", the founders' names and the paragraph under them are all read
       through five numerals and five glyphs.

       AND IT CANNOT BE FIXED BY MOVING THE RAIL, which is worth writing down
       so the next hand does not try. Anchored flush at the viewport's edge —
       inset 0, which is already wrong for a margin mark — the rail still ends
       at 84 and the column still starts at 56.31, 70.39 and 80: it overlaps at
       every width below 1424 no matter where it is put. Collapsing the glyph
       column at rest does not save it either; the grid's three gaps survive
       the collapse, so the row is still 68 px against a 56 px margin. The rail
       does not fit, and 96rem is where it starts to: --column-inset is 128
       there, which leaves the 20 px between it and the rail's 108.

       SO THE TIER MOVED AND THE RAIL DID NOT, WHICH WAS THE CORRECT HALF OF
       THE ANSWER. Below 96rem the scroll no longer lifted it — `is-live`
       kept setting the class, and the class no longer painted — and
       :focus-within still did, at which point the rail already brings the glass
       plate and the five titles with it and is a deliberate overlay rather than
       ink on ink. Nobody read act 4 through it, which was the fault this
       answered, and it is still true of everything below.

       WHAT IT COST, WHICH THAT NOTE STATED AND LEFT OPEN. Between 64rem and
       96rem the passive position indicator was gone for a pointer — the acts
       still pin there, so those readers had 18 000 to 23 000 px of scroll and
       no contents unless they Tab. Measured on the shipped page, scrolled into
       act 4, before the rail at the foot of this file existed:

           width   document   .act-rail   act-rail.js  what a pointer had
            1024    22 758     opacity 0   is-live      nothing
            1280    22 864     opacity 0   is-live      nothing
            1440    22 931     opacity 0   is-live      nothing
            1536    22 971     opacity 1   is-live      the whole rail

       Three of the four most common desktop widths on the ladder, on the page
       this system's flagship is, running the script that marks the current act
       correctly and painting none of it. "A rail that fits a 56 px margin is a
       different rail, not a smaller one" was the right sentence and it was
       written as a hand-off. THE DIFFERENT RAIL IS AT THE FOOT OF THIS FILE.
       → scripts/check-rail-margin.py holds the arithmetic, both tiers
       ============================================================ */
    .act-rail:hover,
    .act-rail:focus-within {
      --act-glyph: 1.5rem;
      /* One tier up when the plate is behind them: the marks are over glass now
         rather than over the page margin, and a mark that recedes on the wash
         recedes further on a pane of it. */
      --act-ink: var(--text-secondary);
    }

    .act-rail__list {
      display: grid;
      gap: var(--space-4);
      margin: 0;
      padding: 0;
      list-style: none;
    }

    /* THE TARGET STOPS AT ITS OWN CONTENT. Without this the link stretches to
       the widest row in the list — act 5's title is twice act 2's — and the
       empty space beside a short entry becomes a hot zone that marks the wrong
       act when it is clicked. */
    .act-rail__item { display: grid; justify-items: start; }

    /* TICK, NUMBER, GLYPH, TITLE. The first two columns are fixed rungs and the
       second holds a tabular numeral, so the glyphs stay on one axis as they
       grow: the third column is `auto` and follows the glyph's own transition
       rather than being animated itself, which keeps one size in one place.

       THE NUMERAL'S COLUMN IS A DECLARED RUNG AND USED TO BE `auto`. It measured
       13.42 px — two tabular digits of the mono face at --text-xs plus its
       tracking — which is a number no other rule in this file could name. The
       two jumps below have to put their chevrons on the SAME axis as the five
       glyphs and have no numeral of their own to derive it from, so either they
       repeat 13.42 as a literal or the column stops being content-sized. It is
       --space-4, the rung above the measurement and the one the tick already
       takes, which moves the glyph axis right by 2.58 px once and makes the
       alignment a shared declaration instead of two numbers that have to agree. */
    .act-rail__link,
    .act-rail__jump {
      display: grid;
      grid-template-columns: var(--act-cols);
      align-items: center;
      gap: var(--space-3);
      text-decoration: none;
      color: var(--act-ink);
      transition: color var(--duration-base) var(--ease-standard);
    }
    /* THE KEYBOARD GETS WHAT THE POINTER GETS. Without :focus-visible here the
       ring lands on an entry still drawn at the muted tier — the one entry the
       reader has singled out, and the faintest thing in the rail. */
    /* THE FIVE ACT ROWS ARE 16 PX TALL AND THAT IS NOT THE DEFECT IT LOOKS
       LIKE. .act-rail__jump takes `min-block-size: var(--space-6)` with WCAG
       2.5.8 named over it, and the five links next to it do not, which reads
       like an oversight — the rail's primary targets left at 84 x 16 while the
       two secondary ones are 84 x 24. It was measured before it was changed.
       2.5.8's spacing exception clears a target under 24 px when no other
       target's 24 px circle reaches it, and the rail's rows are 16 px on a
       --space-4 gap: a 32 px pitch, so the circles sit 8 px apart and every
       row passes on offset. Giving the links the floor anyway costs the rail
       40 px of height (224 -> 264 here, 144 -> 184 on the lab page, which has
       five rows and no jumps), and on the lab page those 40 px reach act 1's
       field: swept at 31 scroll stops at 1536, the taller rail crossed
       .cf-annot__label and the field's own readings 13 times where the 144 px
       rail crossed nothing. A conformant row grown into a drawing is a worse
       page than a conformant row that was already conformant. Left at 16. */
    .act-rail__link:hover,
    .act-rail__link:focus-visible,
    .act-rail__link[aria-current] { color: var(--text-primary); }

    /* ============================================================
       THE TWO JUMPS — out of the composition at either end.

       The rail answers "which act am I in" and "take me to another one". It did
       not answer the two questions a reader inside 22 000 px of scroll asks
       most: take me back to the top, and take me to the end. Scrolling out of
       the acts by hand is the whole distance in, again, in the direction you
       have already read.

       PLAIN ANCHORS, AND THAT IS THE POINT. act-rail.js exists because two of
       the five acts begin at a scroll position rather than at an element — it
       selects, marks and intercepts `[data-act-track]` and nothing else. These
       two have elements: the hero and the footer are boxes with addresses on
       them, so the platform does the work. No branch in the script, no beat to
       measure, and with scripting off they are the two entries in the rail that
       still land exactly where they say. The scroll takes html's own
       scroll-behavior, smooth by default and `auto` under
       prefers-reduced-motion, which is the same journey every other in-page
       link on the site makes.

       NEITHER IS EVER A NO-OP, and that is why they need no disabled state. The
       rail is only live while the viewport's middle is inside the acts — never
       within half a screen of the hero, never inside the footer — so at every
       moment a reader can reach these, both jumps have somewhere to go.

       THE CHEVRON DOUBLES, WHICH THE ICON SET ALREADY MEANT FOR THIS.
       cf-icons.js gives cf-chevron-double-up the use "Collapse all, back to the
       top" and cf-chevron-double-down "Expand all, keep reading" — the two
       marks for travelling past what is next rather than to it, which is the
       distinction against the singles and the reason the acts' own singles are
       not borrowed here. Same 63.43° keyline as everything else in the set, and
       drawn by <use> like the five act glyphs.

       THE TARGET IS 24 PX TALL AND THE GLYPH IS 16. A chevron at rest is one
       rung of the icon ladder and WCAG 2.5.8's floor is 24, so the row keeps a
       --space-6 minimum and the glyph centres in it. The other four columns of
       the shared grid carry the row past 24 px wide on their own — WHERE THERE
       ARE FOUR. Below 96rem the resting rail is one column and this row is
       16 x 24, which stands on 2.5.8's spacing exception at a 40 px pitch,
       exactly as the five act rows two paragraphs above it do at 32. See "THE
       NARROW TIER'S RAIL" at the foot of this file.

       They take --act-ink with everything else in the rail: at rest the marks in
       the margin, one tier up when the plate is behind them, --text-primary on
       hover and on :focus-visible — which is the pointer feedback the act rows
       get from the same pair of rules, and the only ink either jump ever needs,
       because a jump is never "current".

       ONLY ON THE SHIPPED PAGE, unlike every other rule in this file. The lab
       page shares this stylesheet and has neither a hero nor a footer to jump
       to, so the two anchors would be addresses that resolve to nothing —
       patterns/landing-page.html carries the markup and the ids.
       → foundations/iconography.html, "the four chevrons"
       ============================================================ */
    .act-rail__jump {
      min-block-size: var(--space-6);
    }
    .act-rail__jump:hover,
    .act-rail__jump:focus-visible { color: var(--text-primary); }
    /* THIRD COLUMN, EXPLICITLY: the chevron is the row's only cell, and auto
       flow would put it in the first one — under the ticks, half a rung off the
       axis it exists to stand on. */
    .act-rail__jump .act-rail__glyph { grid-column: 3; }

    .act-rail__tick {
      block-size: var(--stroke-1);
      background: currentColor;
      /* FROM THE LEFT, which is the direction the reader arrives from and the
         direction .cf-pin__bar already grows in. */
      transform: scaleX(0);
      transform-origin: left center;
      transition: transform var(--duration-base) var(--ease-standard);
    }
    .act-rail__link[aria-current] .act-rail__tick { transform: scaleX(1); }

    .act-rail__num {
      font-family: var(--font-mono);
      font-size: var(--text-xs);
      letter-spacing: var(--tracking-label);
      font-variant-numeric: tabular-nums;
    }

    /* THE SIZE, AND NOTHING ELSE. .cf-icon owns the stroke, the caps, the
       joins and the colour; this rule owns how big the box is and how it gets
       there. Overriding anything else here would fork the icon set inside one
       rail, which is the whole reason these are <use> references and not five
       drawings typed into two pages. */
    /* TURNED, BECAUSE THE DIRECTION IS THE ACT. cf-node is drawn as a stem
       rising from two arms — one thing feeding upward out of a junction — and
       act 2 is the reverse of that by design: the review's own note was that
       the tree had to be flipped so the sensors collapse DOWNWARD into a
       single source below them. Half a turn puts the two arms at the top with
       their dots on them and the stem's dot underneath, which is the
       confluence exactly. Rotation is the set's own idiom for direction — its
       four chevrons are one glyph turned 90 degrees four times — so this
       borrows the mark rather than forking it into a sixth drawing. */
    .act-rail__glyph--down { rotate: 180deg; }

    .act-rail__glyph {
      display: block;
      inline-size: var(--act-glyph);
      block-size: var(--act-glyph);
      transition: inline-size var(--duration-base) var(--ease-standard),
                  block-size var(--duration-base) var(--ease-standard);
    }

    /* THE TITLE IS ALWAYS IN THE DOM and never display: none — a reader on
       assistive technology gets the five titles whether or not a pointer has
       been over the rail, and what the hover changes is only how much room
       they are given. `clip`, not `hidden`, so a collapsed title is a crop and
       not a scroll container (check-overflow-clip.py). */
    .act-rail__label {
      /* THE INK THE MATERIAL GUARANTEES, AND IT IS MEASURED RATHER THAN CHOSEN.
         tokens.css derives the 46 % tint as the floor at which BLACK type clears
         4.5:1 over a backdrop nobody controls, and this rail crosses five
         drawings over 22 000 px, so that is the guarantee in force here. Swept
         at 250 px steps across the whole act range with the rail open, at 1024,
         1440 and 1920, the darkest pixel the plate ever composites to is
         rgb(164,165,165) — act 3's display heading, blurred, behind the rail at
         1440; 1024 finds rgb(167,169,156) over the map's ground at 20 000, and
         1920 never goes below rgb(218,221,223) because nothing dark passes the
         rail at that width. On the worst of them:

           --text-primary    8.50:1  the ink below
           --text-secondary  3.70:1  fails AA. 6.70:1 at 1920, which is the
                                     width where the case never arises
           --text-muted      1.28:1

         (46 % white over ANYTHING composites at 117 or above, which is how the
         sweep knows it sampled the plate and not a rect that had slipped off
         it — every reading here is above that floor.)

         So the titles do not recede on the plate, and this rule is why the two
         tiers this rail used to run on became --act-ink for the marks and black
         for the words. Secondary would have held over a backdrop somebody could
         name — it is what .cf-info-card__count takes, measured at 6.73:1 on the
         one stage that card ships on — and this rail cannot name one. Which act
         you are on is still said twice, by the tick and by the row's marks. */
      color: var(--text-primary);
      font-size: var(--text-sm);
      line-height: var(--leading-snug);
      white-space: nowrap;
      overflow: clip;
      max-inline-size: 0;
      opacity: 0;
      transition: max-inline-size var(--duration-base) var(--ease-standard),
                  opacity var(--duration-base) var(--ease-standard);
      transition-delay: calc(var(--i) * 40ms);
    }
    .act-rail:hover .act-rail__label,
    .act-rail:focus-within .act-rail__label {
      max-inline-size: 16rem;
      opacity: 1;
    }

    /* ============================================================
       THE NARROW TIER'S RAIL — 64rem to 96rem, where the margin is 56 to 80 px
       and the four-column rail is 84.

       LAST IN THE FILE BECAUSE IT OVERRIDES THE MARKS, and the marks are
       declared above it at the same specificity. A block placed where the
       argument for it is — up at "A MARGIN RAIL NEEDS A MARGIN" — would lose
       the tick's own `transform` and the glyph's own `display` to source order,
       silently and only in this tier. The argument stays there; the rules are
       here.

       IT IS THE WIDE RAIL'S FIRST COLUMN AND NOTHING ELSE. Not a smaller rail
       — a rail whose resting state is the one mark that answers the one
       question a reader inside 22 000 px of scroll asks passively: which of
       these five am I in. The numeral, the act glyph and the title all answer
       "which five ARE there", and that is a question with a control attached —
       so they arrive with the control, on :hover and on :focus-within, where
       the plate and the overlay the note above argues for arrive too. Nothing
       about the open rail changes in this tier; only what is standing there
       before it is opened.

       THE ARITHMETIC, WHICH IS THE WHOLE REASON THE COLUMN IS ONE:

           tier      inset       resting extent          ends   inset at   clears
           64–96rem  --space-4   --space-4 (the tick)      32     56.32      24.32
           96rem+    --space-6   84 (four columns)        108    128         20

       --column-inset is max(--gutter, (100% - --container-max) / 2) and binds
       at 1024, where --gutter's 5.5vw resolves to 56.32 — the narrowest margin
       either tier ever stands in. 24.32 px of clearance there, against the
       --space-4 floor scripts/check-rail-margin.py holds both rows to. That is
       the check's own `-v` table, printed from these declarations.

       THE INSET DROPS A RUNG WITH THE MARGIN. --space-6 here would end the
       rail at 40 and clear 56.32 by 16.32 — 0.32 px over the floor, which is a
       number that passes rather than a number that was chosen, and one
       --gutter revision from being neither. The rail's inset is a share of the
       margin it stands in; the margin is 56 to 80 px here against 128 above,
       so the share is the rung below.

       THE IDLE TICKS ARE A QUARTER, AND THE QUARTER IS --space-1 OF THE
       COLUMN'S --space-4. Above this tier the four idle ticks are scaleX(0)
       and only the current one is drawn, because the numerals beside them
       already say there are five. Alone in the margin one 16 px rule is a mark
       with no scale — it cannot say "the fourth of five", only "here". A 4 px
       stub at each of the other four gives the current tick something to be
       long against, on the transform the component already animates and at the
       ink it already recedes at: --act-ink for the four, --text-primary for the
       one, which is 1.28:1 and 8.50:1 over the worst backdrop the sweep in
       .act-rail__label found. Length is the signal and length survives a
       backdrop nobody controls.

       THE TWO JUMPS COME DOWN ONTO THE TICK'S AXIS. Their chevron is their
       whole visible content — a hidden label and a <use> — so it cannot be the
       thing that is dropped, and column 3 does not exist here. `grid-column: 1`
       puts it where the numerals' column went, which is the axis every other
       mark in this rail is already on.

       THEIR TARGET IS 16 x 24 AND THAT IS THE SAME EXCEPTION THE FIVE ROWS
       TAKE. The note on .act-rail__jump above says the other four columns
       "carry the row past 24 px wide on their own", which is true where there
       are four. Here there is one, so the jumps stand on WCAG 2.5.8's spacing
       exception exactly as the 16 px act rows three paragraphs above it do: a
       24 px row on a --space-4 gap is a 40 px pitch, and 40 px of offset clears
       the 24 px circle with 16 to spare. Undersized on one axis, cleared on
       offset, and the arithmetic is the same arithmetic.

       POINTER-EVENTS COME BACK, which they could not while the rail was 84 px
       of invisible fixed column over act 4's copy. At 32 px the rail's box ends
       24.32 px short of the first character on the page, so there is nothing
       for it to swallow — and it must be hittable or :hover cannot open it,
       which is where the other three quarters of the rail live in this tier.
       .is-live still gates it: off screen the rail catches nothing, and that
       rule is above and unchanged.
       ============================================================ */
    @media screen and (max-width: 95.999rem) {
      .act-rail {
        --act-cols: var(--space-4);
        inset-inline-start: var(--space-4);
      }
      .act-rail:hover,
      .act-rail:focus-within {
        --act-cols: var(--act-cols-open);
      }

      /* The two marks that answer "which five are there" — dropped from the
         resting column, restored with the title they belong to. `display`
         rather than a width, because a cell in a grid with one column is a new
         implicit column and the rail would still be 84 px wide with nothing
         in it. The title needs no rule: it is `max-inline-size: 0` at rest at
         every width and stays in the DOM for a reader who is not looking. */
      .act-rail__num,
      .act-rail__link .act-rail__glyph { display: none; }
      .act-rail:hover .act-rail__num,
      .act-rail:focus-within .act-rail__num { display: block; }
      .act-rail:hover .act-rail__link .act-rail__glyph,
      .act-rail:focus-within .act-rail__link .act-rail__glyph { display: block; }

      .act-rail__jump .act-rail__glyph { grid-column: 1; }
      .act-rail:hover .act-rail__jump .act-rail__glyph,
      .act-rail:focus-within .act-rail__jump .act-rail__glyph { grid-column: 3; }

      /* A quarter of --space-4 is --space-1: the scale's own smallest rung and
         the unit the whole scale is built on. [aria-current] takes it back to
         scaleX(1) from the rule above — higher specificity, later in the file
         than this one is not required and is true anyway. */
      .act-rail__tick { transform: scaleX(.25); }
      .act-rail:hover .act-rail__tick,
      .act-rail:focus-within .act-rail__tick { transform: scaleX(0); }
      .act-rail:hover .act-rail__link[aria-current] .act-rail__tick,
      .act-rail:focus-within .act-rail__link[aria-current] .act-rail__tick {
        transform: scaleX(1);
      }
    }
  }
