/* ==========================================================================
   Control-F — Design Tokens
   Single source of truth. Derived from "Brand Design Review 2" and the
   Figma redesign of the Landing Page / Über uns page.

   Nothing in this file styles anything. It only declares values.
   ========================================================================== */

:root {
  /* ------------------------------------------------------------------
     1. CORE PALETTE — "Hauptfarben"
     The seven brand colours, exactly as defined in the manual. Lime is the
     light layer and is used sparingly; CF-Grau is the atmosphere the whole
     site sits in.
     ------------------------------------------------------------------ */
  --cf-lime:       #E1FF00;
  --cf-glas:       #C5EBE2;
  --cf-sky:        #72B0E2;
  --cf-violett:    #7E7FE1;
  --cf-weiss:      #FFFFFF;
  --cf-grau:       #CFCFCF;
  --cf-schwarz:    #000000;

  /* Schwarz is the dark surface of the system — the footer, the logo plate,
     the consent banner, the nav plates. The mockups paint those a near-black
     anthracite (#1B2022); the palette already contains a black, and a second
     dark that differs from it by 1.2:1 is a colour nobody can name and every
     component has to remember. The eighth token is gone: --surface-inverse
     resolves to --cf-schwarz. */

  /* ------------------------------------------------------------------
     2. RAMPS — "Schattierungen"
     Each accent is mixed with white (100–300) and with black (700–900).
     Step 500 is always the core brand colour.
     ------------------------------------------------------------------ */
  --lime-100: #FAFFD9;  --lime-200: #F6FFB3;  --lime-300: #EFFF80;
  --lime-500: #E1FF00;
  --lime-700: #A8BF02;  --lime-800: #4E5901;  --lime-900: #222600;

  --glas-100: #F6FCFB;  --glas-200: #EEF9F6;  --glas-300: #E2F5F1;
  --glas-500: #C5EBE2;
  --glas-700: #94B0A9;  --glas-800: #45524F;  --glas-900: #1E2322;

  --sky-100:  #EAF3FB;  --sky-200:  #D5E7F6;  --sky-300:  #B8D7F1;
  --sky-500:  #72B0E2;
  --sky-700:  #5684A9;  --sky-800:  #283E4F;  --sky-900:  #111A22;

  --violett-100: #ECECFB; --violett-200: #D8D9F6; --violett-300: #BEBFF0;
  --violett-500: #7E7FE1;
  --violett-700: #5F5FA9; --violett-800: #2C2C4F; --violett-900: #131322;

  /* Neutral ramp — the workhorse. CF-Grau (300) is the anchor, not white.
     075 is the manual's 70 %-white step. The plate paints it #F1F1F1 but
     labels it #D9D9D9; the painted value is the arithmetically correct one
     (0.7·255 + 0.3·207 = 240.6), so the ramp follows the paint.
     D9D9D9 is kept as 200 because the mockups use it as a UI grey — it is
     just not a step on this ramp. See foundations/colors.html. */
  --grey-000: #FFFFFF;
  --grey-050: #F8F8F8;
  --grey-075: #F1F1F1;
  --grey-100: #E7E7E7;
  --grey-200: #D9D9D9;
  --grey-300: #CFCFCF;  /* = CF-Grau */
  --grey-400: #919191;  /* UI-only: mono labels, meta text */
  --grey-500: #9B9B9B;
  --grey-700: #484848;
  --grey-800: #1F1F1F;
  --grey-900: #000000;

  /* ------------------------------------------------------------------
     3. SEMANTIC COLOUR
     Use these in components — never a raw ramp value.
     ------------------------------------------------------------------ */
  --surface-page:     var(--grey-300);
  --surface-base:     var(--grey-300);

  /* A STEP AWAY FROM THE PAGE IS A RATIO, NOT A GREY, and this token was the
     grey until it was measured.

     --surface-sunken was grey-100 (#E7E7E7). The number is only meaningful
     against --surface-base, CF-Grau — and no page in the system is painted
     CF-Grau. Every page carries .page-wash, which is --surface-page-wash:
     CF-Grau to white, `background-attachment: fixed`, `background-size:
     cover`. Fixed and cover together mean the wash spans the VIEWPORT, not
     the document, so it does not scroll: the page under any element is a
     function of where that element currently sits on screen, and it runs the
     full 207 → 255 every time. The steps were calibrated against a colour
     that only exists in the top row of pixels.

     Measured on patterns/landing-page.html at 1280x900, walking .cf-process
     __note down the viewport in 6 % steps and sampling the wash beside it in
     the same screenshot row:

       note at 14 % of viewport height   wash 210   #E7E7E7 reads  +21
       note at 44 %                      wash 222   #E7E7E7 reads   +9
       note at 65 %                      wash 231   #E7E7E7 reads    0   <-
       note at 86 %                      wash 241   #E7E7E7 reads  -10

     So an absolute grey inside the wash's own range is raised at the top of
     the screen, INVISIBLE at 65 % of it, and sunken below that — one block
     that changes which way it steps as the reader scrolls past it. Nothing
     renders wrong; the material simply is not one.

     A veil is the same declaration made relative. 6 % black removes 6 % of
     whatever light is there, which is a constant 0.858 ratio in linear light
     at both ends of the wash, so the step is the same step everywhere:

       over the wash's CF-Grau end   197   -12   secondary 5.19:1  primary 11.91:1
       over its middle               217   -14   secondary 6.48:1  primary 14.88:1
       over its white end            240   -15   secondary 8.03:1  primary 18.43:1

     THE DIRECTION HAD TO FLIP AND THAT IS THE ONE REAL CHANGE HERE. The old
     light-theme steps both climbed toward white (see the [data-theme=
     "inverse"] note below, which says so). Up is not available on this wash:
     its far end IS white, so a lighter step has nowhere to go and dies at the
     bottom of every screen. Down works at both ends. Sunken now means sunken.

     Worst case is the CF-Grau end at 5.19:1, which clears AA for body text
     with room; --text-muted still must not sit on it, for the same reason it
     may not sit on CF-Grau itself.

     --surface-card is RETIRED. It was grey-050, the same value as
     --surface-raised, and its only two users were .cf-process and .ex-step on
     patterns/expertise.html — both of which now draw as contour on the wash,
     which is what foundations/materials.html already said they did and what
     mockups/landing-page.jpg measures: the card's interior and the page
     margin beside it read within 0.2 of each other at every row of the plate.
     Do not reintroduce it; a panel that needs a surface takes the veil, and a
     panel that needs a boundary takes a contour. */
  --surface-sunken:   rgba(0, 0, 0, 0.06);

  /* --surface-lifted is RETIRED, and it lasted exactly one consumer.

     It was rgba(255, 255, 255, 0.26): the step toward the light, for a panel
     with another column beside it to be a plate against. The process card's
     copy half was that panel and was the only one in the system.
     mockups/landing-page.jpg draws that half as an opaque light plate, +22 to
     +29 over the page margin beside it, and this token was the honest form of
     it — a veil rather than the flat #F8F8F8, because a flat grey inside the
     wash's own range inverts as the reader scrolls and a veil cannot. What the
     source drew and what the card wanted turned out to be two questions: on the
     rendered card the plate divides the two halves a second time and more
     loudly than the hairline that is there to divide them. Both halves are the
     wash now, and this has nobody left. See .cf-process__panel in
     components.css and foundations/materials.html#copy-panel-gap.

     THE DERIVATION IS KEPT, because it is the arithmetic and not the decision,
     and whoever wants a light veil next should not have to redo it: 6 % black
     at the wash's opening stop #CFCFD2 removes 0.06 x 207 = 12.42; a white veil
     has 255 - 207 = 48 to work with there; 12.42 / 48 = 0.2588, and 0.26 takes
     the same size step in the other direction, within 0.06 of one 8-bit level
     of the sink it mirrors. Sink and lift were one move with a sign. The
     measurement that unblocked it stands too — .cf-process sits in a sticky
     stage and holds viewport rows 54 to 785 identically across the whole pin
     range, so the objection that a light step "dies at the bottom of every
     screen" was silent about this one panel. Both were true. Neither was the
     question the card was actually asking.

     So the paragraph above --surface-sunken is unqualified again: up is not
     available on this wash, its far end IS white, and a lighter step has
     nowhere to go. Do not reintroduce this; a panel that needs a surface takes
     the sunken veil, and a panel that needs a boundary takes a contour. */

  --surface-raised:   var(--grey-050);
  --surface-inverse:  var(--cf-schwarz);

  /* Milchglas has three jobs, so it has three tints. See section 9. */
  --surface-glass:       rgba(255, 255, 255, 0.46);
  --surface-glass-thin:  rgba(255, 255, 255, 0.30);
  --surface-glass-veil:  rgba(255, 255, 255, 0.04);
  --surface-glass-solid: var(--grey-100);

  /* THE PAGE ITSELF IS A SLOW VERTICAL WASH FROM CF-GRAU TO WHITE, and it is
     the largest gradient in the system by two orders of magnitude — roughly
     5,800 px on the original. It used to be the only one with no hue in it at
     all: two neutral stops, zero chroma, a member of no family.

     It is the foil, reversed, at the amplitude of atmosphere. The page
     descends from shadow into light — CF-Grau at the top, Weiss at the
     bottom — so it runs the foil's own ramp backwards: Violett, Sky, Glas,
     Weiss. STOP POSITIONS ARE --foil-stops' OWN, SUBTRACTED FROM 100 %, which
     is what "reversed" means arithmetically and why nothing here is picked by
     hand. Weiss lands at 100 % by construction, so the bottom of the page is
     the same #FFFFFF it has always been.

     THE LIGHTNESS PATH IS UNCHANGED, exactly and by construction. Each
     chromatic stop is the neutral wash's own OKLab L at that position —
     0.8545 + t(1 − 0.8545) — so the interpolated lightness of the four-stop
     ramp is identical to the two-stop one it replaces. Only the hue moves.
     That is the brand's own definition of iridescence taken to its limit:
     hue travel inside a narrow luminance band, with the band held at zero
     width relative to the ramp that was already there.

       stop          position    L        C       h          renders
       Violett         0.00 %    0.8545   0.005   283.86°    #CFCFD2
       Sky            43.90 %    0.9184   0.005   243.65°    #E1E4E7
       Glas           82.93 %    0.9752   0.005   179.38°    #F3F8F7
       Weiss         100.00 %    1.0000   0       —          #FFFFFF

     The three hues are --foil-stops' three, to the digit. Hue travel across
     them is 104.5°, which is the lit foil's own figure (see 3c); the band
     they span is 0.120, against the foil's 0.090 and the ink foil's 0.115.
     By the family's own defining metric this is a family member.

     CHROMA 0.005 IS THE WHOLE BUDGET, and the ceiling is set by something
     already on this surface: no channel may move further from the neutral
     than the grain does. Worst channel deviation over the whole ramp is 4 of
     255, inside the ~5 levels of grain the wash already carries. Black text
     measures 13.51:1 at the top against 13.48:1 before, --text-secondary
     5.88:1 against 5.87:1 — both marginally better, because a stop nudged off
     the achromatic axis at fixed L loses no luminance.

     IT IS ALSO WHAT FIXES THE BANDING, and that is the part worth keeping.
     A NEUTRAL RAMP IS QUANTIZATION-BOUND: #CFCFCF to #FFFFFF is 49 greys and
     there is no fiftieth, because R, G and B are locked together and cross
     every 8-bit boundary at the same place. Put chroma on it and they cross
     at different places, so the ramp has more rungs to stand on.

     Measured off rendered pixels in Chromium — 4,000 px tall, oklab
     interpolation, Skia's own gradient dither left on, which is what actually
     paints rather than what the arithmetic predicts:

                                        neutral        iridescent
       distinct colours down the ramp      51              130
       widest flat run                     40 px            36 px
       transitions moving all 3 channels   99.8 %           23.6 %

     Two and a half times the levels, and three quarters of the steps are now
     one or two channels moving instead of three — a partial contour rather
     than a full one. The neutral ramp's 51 is not a shortcoming of the
     renderer; it is the ceiling, 49 greys plus dither noise. --grain still
     rides on top: this is the layer under it.

     THE THREE HEXES ARE RE-DERIVABLE, NOT HAND-TUNED — recompute the ramp and
     you get them back — and they carry the same trap the #DBFC60 waypoint
     does: they are literals, so they do not follow --cf-grau. Move CF-Grau and
     these must be recomputed or the wash quietly stops starting where the page
     starts. At this chroma the 8-bit grid is coarser than the target: each hex
     lands 2.5–4.2° off its hue and within 0.001 of the chroma. That is the
     grid, not the arithmetic.

     The two calc()s below reference --spectrum-*-n and --rake-near-n, which
     are declared in 3b and 3c, further down this file. That resolves fine —
     custom properties are substituted at computed-value time, not in source
     order — and it is deliberate: the wash borrows the foil's geometry rather
     than restating it, so it has to point forwards at where the foil lives. */
  --wash-stops: #CFCFD2 0%,
                #E1E4E7 calc(100% - (var(--spectrum-cool-n) - var(--spectrum-hot-n)) / var(--spectrum-span-n) * 100%),
                #F3F8F7 calc(100% - (var(--rake-near-n)     - var(--spectrum-hot-n)) / var(--spectrum-span-n) * 100%),
                var(--grey-000) 100%;

  --surface-page-wash: linear-gradient(180deg, var(--wash-stops));

  --text-primary:     var(--cf-schwarz);
  --text-secondary:   var(--grey-700);
  --text-muted:       var(--grey-400);
  --text-inverse:     var(--grey-000);
  --text-inverse-dim: rgba(255, 255, 255, 0.62);

  --border-strong:    var(--cf-schwarz);
  --border-default:   rgba(0, 0, 0, 0.24);
  --border-subtle:    rgba(0, 0, 0, 0.12);
  --border-inverse:   rgba(255, 255, 255, 0.18);

  --accent:           var(--cf-lime);
  --accent-ink:       var(--cf-schwarz);   /* text on lime */
  --focus-ring:       var(--cf-schwarz);

  /* The one colour outside the brand palette, and the only one. A form error
     needs a signal nobody can misread and lime does not do that job — see
     components/forms.html. Declared once here rather than hard-coded per
     component, the same way --surface-inverse is.

     #A72019, not the #B3261E it was: the field error is 12 px, so it is body
     text and owes 4.5:1. #B3261E measures 4.20:1 on CF-Grau — the top of the
     page wash, where a form on a short page sits — and passed only against
     the white end. #A72019 is the same hue two steps darker: 4.71:1 on
     CF-Grau, 7.33:1 on white. */
  --feedback-error:   #A72019;

  /* ------------------------------------------------------------------
     3b. THE LIGHT FAMILY — "Licht-Ebene"
     Every gradient in the system is the same ramp: lime → Glas → CF-Grau.
     Lime is the light source, CF-Grau is the surface it falls away to,
     and Glas is the one free parameter — where the two meet.

     That position is the RAKE: how steeply the light crosses the surface.
     Near = a bright, short falloff. Far = a long grazing wash. Three rakes
     are sanctioned, and all three are measured off the designer's material,
     not invented. Read every stop below as a percentage of the band from
     the lime end.

       near  32 %  Figma dump, 132.36° linear · 01-discovery.svg (0.32)
       mid   51 %  Figma dump, 239.25° linear · 03-weniger-ausfaelle.svg (0.51)
       far   64 %  Figma dump, 45.72%/95% radial · 04-mehr-leistung.svg

     The Figma percentages look different from these because Figma states
     them across the whole fill box, with flat colour either side of the
     ramp. Renormalise to the band and they land exactly here:
       (40.45 − 24.97) / (73.05 − 24.97) = 32.2 %
       (43.85 −  4.82) / (81.19 −  4.82) = 51.1 %
     One ramp, three rakes.

     ONE RAMP, FOUR SOURCES, and this file has under-counted them twice. It
     said one — "Nothing else is a gradient in this brand" — while
     .cf-plot__col--fell was already drawing Violett → #8A94E3 → Glas →
     CF-Grau on the evidence plot's two falling columns, on the Landing Page
     and not only on the component page. It then said two, and named a third
     as unsanctioned, while .cf-block was shipping three: a categorical figure
     needs a face per series, so Glas, Sky and Violett each run down to
     CF-Grau beside the one lit block's lime. That is on components/block.html
     and argued in components.css under ONE RAMP, FOUR SOURCES. Every number
     in every one of them is this ramp's: the near rake's own Glas at 0.32,
     CF-Grau at the rim, and — where the leg needs one — the waypoint at 19 %
     measured from the source, computed on the oklab path by exactly the
     method that yields #DBFC60 from lime.

     NONE OF THEM IS A SECOND LIGHT, and the distinction is the one the lime
     rule turns on. Lime is the source in this brand and stays the one thing a
     screen's single lit moment spends; a cool falloff is the same rake with
     the warm source swapped for a cool one, on a face that is not lit. What
     they are is further SOURCES of the same falloff — so the rule that
     governs the lime leg governs them.

     AND THE COUNT IS NO LONGER A LIST. Saying "a third source is not
     sanctioned" was an enumeration of what had been drawn when the sentence
     was written, which is what it went stale as: the fourth arrived on a
     component page and the sentence stayed. scripts/check-gradient-family.py
     no longer holds a set of source colours. It measures the leg — one end
     chromatic enough to be a source, and an sRGB chord that leaves the oklab
     path by more than the 0.00148 this file already publishes as the
     divergence it tolerates — and derives the source from that. A fifth
     source needs no edit anywhere; a leg that owes a waypoint is caught the
     first time it is drawn, which the list could not do.
     → foundations/colors.html#sources

     The argument for the swap is a chart's and lives with the chart:
     components.css, THE HUE MOVES TO THE FACE, and components/plot.html.
     → foundations/colors.html#rakes-and-blooms

     Each rake is declared twice: once as a bare number and once as the
     percentage built from it. calc() cannot divide a percentage by a
     percentage, and the foil below renormalises one stop against another,
     so that arithmetic needs the unitless form. Change the number and both
     the rake and everything derived from it follow.
     ------------------------------------------------------------------ */
  --rake-near-n: 32;
  --rake-mid-n:  51;
  --rake-far-n:  64;
  --rake-near: calc(var(--rake-near-n) * 1%);
  --rake-mid:  calc(var(--rake-mid-n)  * 1%);
  --rake-far:  calc(var(--rake-far-n)  * 1%);

  /* The signature. Angle and stops are the designer's measured values from
     the Figma dump and are kept verbatim, inset and all. Note that 132.36°
     is 2.6° off the sanctioned 135° (90° + 45°) — the material wins, but
     see foundations/geometry.html before deriving new angles from it. */
  --gradient-light: linear-gradient(132.36deg,
                      var(--cf-lime)  24.97%,
                      var(--cf-glas)  40.45%,
                      var(--cf-grau)  73.05%);

  /* The same ramp mirrored onto a horizontal run — buttons, edges, rules.
     calc() keeps it provably the mirror of --rake-near rather than a
     second hand-picked number. Sampling the primary button out of the
     landing-page mockup puts Glas at 63 %; the mirror puts it at 68 %. */
  --gradient-light-90: linear-gradient(90deg,
                      var(--cf-grau)  0%,
                      var(--cf-glas)  calc(100% - var(--rake-near)),
                      var(--cf-lime)  100%);

  /* THE OTHER THREE MEMBERS OF THIS FAMILY ARE NOT TOKENS, and their absence
     here is deliberate. The grazing linear (the dump's second, 239.25°) and
     the three radial blooms all used to sit at this point in the file, and
     nothing in the system ever consumed one of them. Each was a further
     literal statement of the ramp already stated above, restated a second time
     in the oklab block at the foot of this file — which is the exact condition
     .material-rake was created to end.

     They are now .material-rake--grazing and .material-bloom in base.css,
     composed on a class so the ramp is written once and the rake, the angle
     and the shape are the parameters they always were. See base.css, and
     foundations/colors.html for the table that governs the family.

     A token cannot do this job. calc() substitutes a custom property at
     computed-value time on the element that DECLARES it, so a bloom written
     at :root would resolve its rake against :root's and inherit that literal
     into every modifier — the same reason --field-stops and --foil-image are
     composed on their classes rather than here. */

  /* ------------------------------------------------------------------
     3b-ii. WHERE ARTWORK MEETS THE PAGE
     Two gradients that carry no colour of their own. Both are the light
     family doing the two jobs a wash has to do when a full-bleed image is
     dropped into a page made of CF-Grau: end, and be read over.

     THE DISSOLVE. The landing-page hero was cut off with a knife. Measured
     on the rendered page at 1280 x 1400, the last row of artwork is
     rgb(42,41,55) at the left and rgb(198,237,167) at the middle, and the
     next row down is the page wash at rgb(233,237,238) — a step of about
     190 levels, in three channels at once, across one pixel.

     The designer's mockup does not do that. It has no edge at all: the
     artwork decays continuously into CF-Grau and the page simply carries
     on. Measured off assets/source/mockups/landing-page.jpg as the mean
     absolute channel deviation from #CFCFCF over each full row — which is
     what alpha is, when the thing composites onto CF-Grau:

         y   604    647    701    755    804
         a  1.000  0.712  0.386  0.132  0.000

     The hero ends at y 804 and the decay is already running at 642, so
     extrapolating the top of it back to a = 1 puts the band at 598 → 804:
     the last 25.6 % of an 804 px hero. Fitting a = (1 − t)^k over the band
     gives k = 1.31, 1.43, 1.46, 1.46 at the four quartiles — call it 1.45,
     which is a plain ease-out and not a curve anybody would have to
     justify. The four stops below are that power law sampled at the
     quartiles of the band; between them the mask interpolates linearly,
     which departs from the curve by at most 0.018 of alpha.

     It is a MASK and not an overlay, and that is the whole reason it can be
     one token. An overlay has to be painted the colour of whatever is
     behind it, so it would have to know that the hero sits on the page wash
     and at what point of the ramp. A mask hands the question back to the
     page: the artwork stops being there, and whatever the page is at that
     height is what you see. Move the hero, restyle the wash, put this on a
     dark page — the dissolve still lands.

     THE READING SCRIM. The hero sets black display type, a black hairline
     and a 12 px mono line over a loop nobody controls the frames of.
     Sampled off the rendered page in the text column, the artwork behind
     the headline reaches rgb(48,55,59) — 1.74:1 against black — and behind
     the kicker rgb(70,70,81), 2.26:1. The floors are 3:1 for the display
     line and the rule and 4.5:1 for the kicker, so the hero was failing
     both, on the page as shipped.

     The still frame the designer composed does not fail, because in the
     mockup the left column is pale grey-blue the whole way down and the
     colour is all on the right. The loop drifts a dark violet plane under
     the type instead. So the scrim is not a correction applied over the
     designer's composition — it restores it, and the amount of it is set by
     what the mockup already looks like rather than by the contrast target.

     CF-Grau and nothing else. The page's own surface, bleeding up into the
     artwork exactly where the type sits: the light layer of the material
     stack, doing the one thing it is for. White would frost it and black
     would bruise it; both would read as a plate laid on top, which is the
     failure mode this has to avoid.

     63.43° — --angle-a, the steep isometric — so it rises from the
     bottom-left corner where the text block is and is gone by the top
     right, where the artwork is at its best and the glass button needs
     something complicated behind it to be worth being glass. A horizontal
     ramp would have taken the button's backdrop with it; a vertical one
     would have greyed the top of the picture for no reader.

     0.42 at the corner is a measured floor, not a chosen weight. Sampled on
     the rendered page with the type hidden, the worst backdrop pixel anywhere
     inside each element's box — ACROSS ALL 360 FRAMES OF THE LOOP, which is
     the part that had never been done:

                    375     768    1280    1440    1920    2560   floor
       headline    3.28    3.15    3.51    3.41    3.47    3.55    3:1
       kicker      6.08    9.64   14.65   15.12   16.41   14.10    4.5:1

     THE ROW ABOVE USED TO READ "1.74:1 before, 4.08–6.26:1 after" AND BOTH
     HALVES OF IT WERE ONE FRAME. It was frame 61 of 360 — the frame that
     ships as hero-poster.jpg, which is the only one a browser will hand you
     with the loop paused, and the reason the sample was taken there. This
     paragraph said so itself, in the sentence that used to follow: "Every
     number above is one frame of a loop, and the next frame is not obliged
     to be as kind", and closed by pricing that risk at "about 36 % over its
     floor". Decoded and measured, all 360 of them, the loop is not as kind
     and the margin was not 36 %:

       headline, worst frame of the loop, before the reach was fixed below
         375  3.28   768  3.15   1280  3.51   1440  3.41   1920  2.91
         2560  1.65

     — two failures against a 3:1 floor, and the published figure for 1920
     was 6.26 against a true 2.91, wrong by a factor of 2.15. The un-scrimmed
     artwork is understated by the same single-frame error: the note above
     records it reaching rgb(48,55,59) behind the headline, and across the
     loop it reaches rgb(8,8,16) at 768 — 1.05:1, not 1.74:1.

     Both failures are one geometric fault and neither is about the weight,
     which is why 0.42 is unchanged and every figure at and below the 1440
     frame is byte-identical to what it was. See --scrim-reach.

     AND THE TABLE IS STILL A REGISTER OF WIDTHS, which is the next thing it
     was short by. The reach is a horizontal displacement, so widths are all it
     can depend on; the ARTWORK under the type is not, because .cf-hero__media
     covers with a square and the box's aspect decides which part of that
     square lands under the type. Re-sampled at frames instead of widths, the
     kicker's 6.08 at 375 is a 375 x 896; at 375 x 812 it is 4.03 and at
     768 x 800 it is 4.37, both under the 4.5:1 floor. That is the weight and
     not the reach, and it is what --scrim-depth below answers.

     The headline is still the binding one and 768 is now the tight width at
     3.15, on 5 % rather than 36 %. That is a fact about this artwork at that
     crop, not about the reach — 768 takes the 39rem plateau whole — so the
     only lever there is the weight, and much past 0.42 it starts taking the
     picture instead of the risk. Held by scripts/check-hero-scrim.py, which
     re-derives the reach from --angle-a and the layout tokens rather than
     trusting this table: a table measured with a decoder nothing in CI has
     is exactly the kind that rots quietly.

     THE "after" COLUMN WAS RE-MEASURED when .cf-hero__body stopped clamping
     itself to 44ch. That clamp resolved in the wrapper's own 14 px font, so
     the column had been 342.59 px wide and four display lines tall; it is now
     as wide as the longest line the display face sets and three lines tall,
     which is what the markup and the plate both say. Wider and shorter moves
     the far corner outward along the axis and downward off it at the same
     time, and on this artwork the second wins: every figure above is equal to
     or better than the one it replaces. The rule no longer has a row of its
     own because it is no longer an element — it is the kicker's border-top,
     so it is measured inside the kicker's box.

     THE STOPS ARE LENGTHS AND NOT PERCENTAGES, and that is the one thing
     here that had to be got right rather than merely chosen. A percentage
     stop is a percentage of the gradient LINE, and the line's length is
     0.894·W + 0.447·H — so the same 38 % is 587 px in a 1280 x 896 hero and
     402 px in a 768 x 828 one. The scrim has to cover a text column that does
     not scale with the line: the column is anchored to the left gutter and
     capped by the type, so it stays about the same size while the line grows
     by 0.894 of every pixel of viewport. A fixed reach, then.

     Projecting the column's corners onto the 63.43° axis puts its far corner
     — the top right of the headline, the point furthest from the light — at
     485 px at 375, 693 px at 768, 720 px at 1280 and 943 px at 1920. The
     plateau does NOT cover all four, and is not asked to: 39rem = 624 px is
     where the scrim is still at full strength, and past it the falloff is
     shallow enough that the corner still sits at 0.37 of full alpha at 768
     and 0.21 at 1920. It is the contrast column that is binding here, not the
     projection; the projection only says which corner to go and sample.

     AND THE PROJECTION IS THE PART THAT GOES WRONG, above the crossover.
     943 px at 1920 is not the corner moving because the viewport is wider —
     it is the corner moving because the COLUMN is, by half of every pixel
     past --container-max. Carry the same arithmetic one width further and
     the corner projects past 1248 px, which is 2 × 624: the far end of the
     falloff, where the scrim is not weak but ZERO. Measured at 2560 with the
     old fixed reach, the headline read 1.65:1 — the artwork with nothing on
     it at all. That is what the second term of --scrim-reach answers.
     Measured with percentage stops in place instead, the headline read
     4.46:1 at 1280 but only 3.58:1 at 768 — the same column and the same
     artwork, failed by a shorter gradient line.

     min(39rem, 62 %) rather than 39rem flat, because on a phone the line is
     623 px end to end and a 624 px plateau would lay a flat 42 % sheet over
     the entire picture. Below about 1000 px of gradient line the percentage
     wins and the artwork keeps its top corner. The falloff is the plateau
     again, doubled — the scrim is out at twice the distance it is full.
     ------------------------------------------------------------------ */
  --dissolve-start: 74.4%;
  --hero-dissolve: linear-gradient(180deg,
                      #000                    0 var(--dissolve-start),
                      rgba(0, 0, 0, 0.659)    80.8%,
                      rgba(0, 0, 0, 0.366)    87.2%,
                      rgba(0, 0, 0, 0.134)    93.6%,
                      transparent            100%);

  /* THE PLATEAU FOLLOWS THE COLUMN, because above --container-max the column
     stops being anchored to the gutter and the scrim was still assuming it
     was. See the note above: "the column is anchored to the left gutter …
     so it stays about the same size while the line grows". That premise is
     true up to a viewport of 1438.2 and false above it — at exactly the
     crossover --column-inset's own note names, three tokens down: "once it
     has hit --container-max the container stops and centres, so the column
     moves inward while --gutter stays put … 80 px at the 1440 frame, 240 at
     1920, 560 at 2560". The hero's CTA and its pause switch already take
     --column-inset for that reason. The scrim is the third full-bleed layer
     that has to line up with the column and the one that did not take it.

     The second term is that displacement projected onto the axis: the column
     slides right by (100vw − --container-max) / 2 − --gutter, and sin(63.43°)
     = 0.8944 of every pixel of that lands on the gradient line. 100vw and not
     100 %, because a percentage inside a gradient stop is a percentage of the
     LINE — the trap the note above is about. It is zero at and below 1438.2,
     so nothing at or under the 1440 frame moves. */
  --scrim-reach: calc(min(39rem, 62%)
                      + 0.8944 * max(0px, (100vw - var(--container-max)) / 2
                                          - var(--gutter)));
  /* How deep the scrim is across the plateau. A token rather than a literal
     typed twice, so the narrow value below cannot lift one end of the plateau
     and leave the other — which would turn the flat run into a ramp with
     nothing in the diff that says so. See the block after --hero-scrim for
     why it is viewport-conditional, and for why both values went up four
     points when the artwork was replaced on 2026-08-03. */
  --scrim-depth: 0.46;
  /* THE TINT IS THE PAGE'S OWN GREY, AND FOR A WHILE IT WAS THE PAGE'S OLD
     ONE. The rule above is "CF-Grau and nothing else. The page's own surface,
     bleeding up into the artwork exactly where the type sits" — and it is the
     second sentence that decides the number, not the first. This layer is not
     a grey chosen to sit over artwork; it is THE SURFACE THE PAGE ALREADY IS,
     continued upward under the type. So it has to be whatever the wash is at
     the height the hero occupies, and it stopped being that.

     --wash-stops used to open on #CFCFCF. It opens on #CFCFD2 — the
     Violett-tinged stop the wash acquired when it was given the foil's own
     ramp reversed, at chroma 0.005, for the reasons set out in full up at the
     wash. The scrim was not part of that change and kept painting
     rgb(207, 207, 207): the anchor colour the page had BEFORE the wash moved.
     Every pixel of the plateau was therefore a flat sheet of a grey the page
     no longer has anywhere, laid over the artwork at 46 %, at the top of a
     page whose first painted row is three levels of blue away from it.

     Nothing caught it, and the two things that could have both had a reason
     not to. check-gradient-family.py exempts a stop carrying alpha, correctly,
     because "what such a stop renders as depends on what is behind it".
     check-hero-scrim.py asks that the scrim be ONE TINT — which it was, and
     which was the wrong question by itself: one tint is what stops this
     reading as a plate, and it says nothing about WHICH. That check now asks
     both, and derives the answer from --wash-stops rather than from a literal
     of its own, so the next time the page's grey moves this cannot silently
     fail to follow it again.

     WHAT MOVED, MEASURED. #CFCFCF to #CFCFD2 is +3 levels of blue and nothing
     else: OKLab L 0.854514 to 0.855402, a rise of 0.00089, and chroma 0 to
     0.0041 — inside the 0.005 ceiling --wash-stops sets and gives the reason
     for ("no channel may move further from the neutral than the grain does"),
     and inside the ~5 levels of grain the page already carries. The alpha path
     is untouched to the digit: both plateau stops still carry --scrim-depth,
     the third is still zero at twice the reach, so check-hero-scrim.py's
     geometry is the geometry it was. Re-run after this, all 16 element/frame
     pairs in check-hero-contrast.py's register hold, and they hold with more
     room than before rather than less — a hair more of the scrim's own
     luminance is a hair more separation for the black type on top of it.
     Those figures cost a decoder and 360 frames per viewport; this does not
     spend them.

     MEASURED ON THE RENDERED PAGE, 1280 x 900, this change against the commit
     before it, so the claim is about pixels and not only about the token:

       band                     max delta   mean signed R / G / B
       hero, y 0–300                4        0.00 / 0.00 / +1.25
       hero, y 300–620              2        0.00 / 0.00 / +1.22
       below the hero, y 700+       1        0.00 / 0.00 / +1.00  (2.3 % of px)

     R AND G ARE ZERO TO TWO DECIMALS ACROSS THE WHOLE PAGE. Only blue moves —
     the token's own +3, laid on at --scrim-depth — and it decays to nothing
     past twice the reach exactly as the alpha ramp says it should. A mean of
     1.2 levels sits under the ~5 levels of grain the page already carries,
     which is the ceiling --wash-stops sets and the one this had to meet.

     AND NO FOIL CAN ENTER THIS BOX — worth stating here, because the hero is
     the obvious place to want one and the answer is measured rather than
     cautious. check-hero-contrast.py holds the headline to 3:1 and the kicker
     AND THE RULE THAT IS ITS BORDER-TOP to 4.5:1, against a 12-second loop
     whose worst backdrops sample at relative luminance 0.108–0.154. Black
     clears those at 3.15–4.08:1 with little to spare. Every stop of
     --gradient-foil-ink measured against the same three backdrops:

       Glas 800     1.22 – 1.59:1
       Sky 800      1.66 – 2.16:1
       Violett 800  1.99 – 2.58:1

     The LIGHTEST stop of the shadow foil is the worst of them, and the best
     case of the best stop is still under the 3:1 floor for the headline, let
     alone the 4.5:1 the kicker and its rule owe. The lit half is further out
     again — it is light-on-dark and this backdrop is neither. So the hero
     takes its light from the scrim underneath the type, not from the type,
     and that is a property of the artwork rather than a gap to be closed. */
  --hero-scrim: linear-gradient(var(--angle-a),
                      rgba(207, 207, 210, var(--scrim-depth)) 0,
                      rgba(207, 207, 210, var(--scrim-depth)) var(--scrim-reach),
                      rgba(207, 207, 210, 0)    calc(var(--scrim-reach) * 2));

  /* ------------------------------------------------------------------
     3c. THE FOIL
     The same light, carried far enough to split. Lime → Weiss → Glas →
     Sky → Violett is the brand's own spectrum: it is the order of the
     column on Farben > Dosierung, and it is what the hero artwork does
     across a single isometric plane — lime at the lit edge, a white hot
     spot, then cyan, blue and violet in the falloff.

     Iridescence is hue travel inside a narrow luminance band, not more
     saturation. Sky and Violett are taken at their 300 tints so the four
     chromatic stops sit between OKLab L 0.820 and 0.945 — 166° of hue
     (118° → 284°) across 0.125 of lightness. At their 500 values that
     band opens to 0.31 and the ramp reads as a fade, not a foil.

     Weiss is the exception and is meant to be one: at L 1.000 it sits
     deliberately *above* the band, which is what makes it read as a
     specular hot spot rather than as one more step in the ramp. Counting
     it, the full span is 0.180. Both numbers matter — the band is what
     keeps the ramp iridescent, the hot spot is what makes it catch.
     ------------------------------------------------------------------ */
  /* The spectrum's own stop positions, named so the foil below can be
     derived from them instead of being hand-picked a second time. Same
     number-plus-percentage pairing as the rakes, and for the same reason. */
  /* THESE TWO ARE THE ONLY POSITIONS IN THE FAMILY WITH NO DERIVATION, and the
     plate this section cites has now been read rather than merely named.
     scripts/check-spectrum-plate.py samples the ramp column on
     assets/source/manual/colour-dosage.jpg — the spectrum drawn as one
     continuous material, highlight to shadow — and every stop on it is a
     palette value:

       --cf-weiss     0.00 %   the head, and it is BEFORE lime
       --cf-lime     21.29 %
       --cf-glas     44.41 %
       --cf-sky      64.47 %   CORE Sky, not sky-300
       --cf-violett  81.16 %   CORE Violett, not violett-300
       --violett-900 98.93 %   the tail. Schwarz is 0.1518 away and never lands

     TWO DISAGREEMENTS WITH THE PLATE, both deliberate and neither closed here.
     The plate opens on Weiss and lime is its first COLOURED stop; the hot spot
     below sits after lime instead, on the authority of the hero artwork rather
     than of this plate. And renormalised onto the family's own axis
     (lime = 0, Violett = 100) the plate puts Glas at 38.6 % and Sky at 72.1 %,
     where the two stops below take --rake-near and --rake-far, measured off the
     process-card Figma dump. Two of the designer's sources disagree, and moving
     the ramp onto the plate's figures would move --gradient-spectrum, both
     foils and the three literals in --wash-stops in one step. That is a brand
     decision with a page-wide blast radius, not a correction to make in
     passing. → foundations/colors.html#plate

     AND 64 IS TWO NUMBERS AT ONCE, which is why it is worth naming rather than
     quietly binding: it is --rake-far-n exactly, and it is also Sky's position
     read from the TOP of the plate's column (64.47 %). The family's axis starts
     at lime, so those are different readings that happen to agree. The Glas
     stop below is written as var(--rake-near) and this one as its digits;
     coupling them would assert that the spectrum's cool stop IS the far rake,
     and the plate is the reason that cannot be assumed. */
  --spectrum-hot-n:  18;  /* the white hot spot */
  --spectrum-cool-n: 64;  /* where Sky takes over from Glas */
  --spectrum-hot:  calc(var(--spectrum-hot-n)  * 1%);
  --spectrum-cool: calc(var(--spectrum-cool-n) * 1%);

  /* The band the foil renormalises onto: everything above the hot spot. */
  --spectrum-span-n: calc(100 - var(--spectrum-hot-n));

  /* THE ARC WAYPOINTS. #B9E3EB and #B8CCF3 are not new colours and not picked
     by eye: each is the OKLCH midpoint of the leg it sits in the middle of,
     and it is there because a straight line between two chromatic stops is
     not the path this brand's own definition of iridescence asks for. The
     full argument, the measurements and the reason the family is not simply
     switched to `in oklch` are at THE ARC below --foil-stops. Recompute them
     if either endpoint moves; they are literals, like the #DBFC60 waypoint,
     and they do not follow the palette on their own. */
  --spectrum-stops: var(--cf-lime)   0%,
                    var(--cf-weiss)  var(--spectrum-hot),
                    var(--cf-glas)   var(--rake-near),
                    /* arc waypoint, Glas -> Sky */
                    #B9E3EB          calc((var(--rake-near-n) + var(--spectrum-cool-n)) / 2 * 1%),
                    var(--sky-300)   var(--spectrum-cool),
                    /* arc waypoint, Sky -> Violett */
                    #B8CCF3          calc((var(--spectrum-cool-n) + 100) / 2 * 1%),
                    var(--violett-300) 100%;

  --gradient-spectrum: linear-gradient(132.36deg, var(--spectrum-stops));

  /* The cool half — the spectrum with lime removed and the remaining band
     renormalised onto 0–100 %, so it can be used on a screen that has
     already spent its one lime moment. Nothing here is hand-picked: every
     stop is the spectrum's own, re-based off the white hot spot.

       Weiss    ( 18 − 18) / 82 =   0 %
       Glas     ( 32 − 18) / 82 =  17 %
       Sky      ( 64 − 18) / 82 =  56 %
       Violett  (100 − 18) / 82 = 100 %

     That arithmetic is written as calc() rather than pasted in as literals,
     so moving --rake-near or --spectrum-cool moves this gradient with it.
     Weiss and Violett are 0 % and 100 % by construction — they are the ends
     of the band being renormalised, so they cannot be anything else.

     Keeping the hot spot is the point. The earlier three-stop version
     started at Glas and read as a flat mint tint; starting at Weiss gives
     the letterform an edge for the light to land on, and lands much closer
     to the near-white the designer actually painted this headline (see
     README, "Decisions that differ from the source material").

     116.57° = 90° + 26.57°, the shallow isometric rake, not the 132.36° of
     the spectrum. A headline is a wide, short box: at 132.36° the ramp
     completes inside the first two or three letters. The shallow angle
     spreads it across the whole line.

     This is the type foil: on black its worst sample — Violett 300 — is
     11.9:1, well past AA at any size.

     STOPS AND RAKE ARE DECLARED APART, and that split is what lets the foil
     move. The stop list is the material — which colours, in which order, at
     which positions — and the rake is the angle light happens to be crossing
     it at. Written together, the material had to be restated in full every
     time the angle changed, which is how it came to exist twice already (once
     here and once in the oklab block at the foot of this file). Written apart,
     the material exists once and anything that wants a different angle asks
     for one: see base.css, THE RAKE SWINGS. */
  --foil-rake: 116.57deg;

  /* The two renormalised positions as bare numbers, so the midpoint between
     them can be arithmetic rather than a third hand-picked figure. Same
     number-plus-percentage pairing as the rakes and the spectrum's stops, and
     for the same reason: calc() cannot average two percentages. */
  --foil-glas-n: calc((var(--rake-near-n)     - var(--spectrum-hot-n)) / var(--spectrum-span-n) * 100);
  --foil-sky-n:  calc((var(--spectrum-cool-n) - var(--spectrum-hot-n)) / var(--spectrum-span-n) * 100);

  --foil-stops: var(--cf-weiss)    0%,
                var(--cf-glas)     calc(var(--foil-glas-n) * 1%),
                /* arc waypoint, Glas -> Sky. Recompute if either moves. */
                #B9E3EB            calc((var(--foil-glas-n) + var(--foil-sky-n)) / 2 * 1%),
                var(--sky-300)     calc(var(--foil-sky-n) * 1%),
                /* arc waypoint, Sky -> Violett. Recompute if either moves. */
                #B8CCF3            calc((var(--foil-sky-n) + 100) / 2 * 1%),
                var(--violett-300) 100%;

  --gradient-foil: linear-gradient(var(--foil-rake), var(--foil-stops));

  /* ------------------------------------------------------------------
     THE ARC
     The foil's whole claim is HUE TRAVEL INSIDE A NARROW LUMINANCE BAND.
     Every stop above is a palette value and every stop is right. What was
     wrong was the path between them.

     Interpolating in oklab draws a STRAIGHT LINE across the a/b plane. Two
     stops of similar chroma and different hue sit on roughly the same circle
     about the neutral axis, so the straight line between them is a CHORD and
     the middle of it falls INSIDE that circle. Chroma sags where no stop
     asked it to:

       leg                     chord    arc      midpoint
       Glas    -> Sky          0.0383   0.0452   +17.9 %
       Sky     -> Violett      0.0553   0.0588   + 6.3 %
       Glas800 -> Sky800       0.0254   0.0289   +13.6 %
       Sky800  -> Violett800   0.0484   0.0513   + 6.0 %

     A ramp that loses a sixth of its chroma halfway through its longest leg
     is greying out exactly where it is supposed to be turning, which reads as
     a fade with a colour cast at each end rather than as one material seen at
     an angle. THIS IS NOT AN ARGUMENT FOR MORE SATURATION — the brand's own
     rule forbids that and this does not do it. Peak chroma on both ramps is
     unchanged (0.0686 lit, 0.0620 ink); both peaks are still the endpoints'
     own, and no point on either ramp is more chromatic than the palette value
     it is travelling between. The sag is removed, nothing is added.

     THE CORRECT PATH IS POLAR — oklch, shorter hue, which interpolates L, C
     and h each linearly and therefore holds chroma across the turn. `in oklch`
     IS NOT USED, and the reason is measured rather than cautious:

     CHROMIUM DISCARDS THE HUE OF A LOW-CHROMA STOP. Rendered and sampled at
     1x, a two-stop `linear-gradient(90deg in oklch shorter hue, #45524F,
     #283E4F)` — Glas 800 to Sky 800, the ink foil's first leg — comes out with
     hue pinned flat at Sky's 242.5 for its whole length. Glas 800's own 179.95
     never appears, at t = 0 or anywhere else. Sweeping the start stop's chroma
     in steps of 0.004 at fixed hue puts the threshold between 0.016 and 0.020:
     at or above ~0.018 the declared hue renders, below it the neighbour's hue
     is carried forward. Glas 800 is C 0.0171. So the one ramp with the most to
     gain from a polar path is the one a polar path silently flattens — it
     would ship 39.7 of its 102.2 degrees of hue travel. Writing the stops as
     oklch() literals renders correctly and was rejected for a different
     reason: it takes the three ramps off the palette tokens.

     SO THE ARC IS PUT INTO THE RAMP INSTEAD, as one waypoint per chromatic
     leg at that leg's midpoint. This is the system's own idiom one level up:
     an inline SVG cannot interpolate in oklab, so each lime -> Glas leg there
     carries #DBFC60 to put the sRGB path back on the oklab one. Same move,
     same reason, one space further out — a waypoint puts the oklab path back
     on the polar one. It is exact where it sits and it is engine-independent,
     which `in oklch` is not.

     Max deviation from the true polar arc, measured over 2,001 samples:

                          before    after
       lit, chromatic legs 0.00722   0.00212
       ink                 0.00741   0.00257

     EVERY MEASURED FIGURE IN THIS FILE SURVIVES, and by construction rather
     than by luck. Polar interpolation moves C and h and leaves L alone, so
     each waypoint's lightness is the mean of its leg's endpoints and sits on
     the linear L path already there — within 9.2e-4, which is the 8-bit grid
     rather than the arithmetic. The band depths (0.090 lit, 0.115 ink, 0.180
     counting the specular), the hue travel (104.5 / 102.2 degrees) and the
     endpoints are all untouched. Re-measured over the whole ramp anyway:
     11.900:1 on black, 5.239:1 on CF-Grau, 8.162:1 on white — the same three
     numbers to three decimals.

     A LEG TURNS WHEN ITS TWO ENDS SIT AT SIMILAR CHROMA, and that is the
     boundary this stops at. It was written here as two rules — no waypoint on
     a leg that ends in grey, and no waypoint on a leg carrying lime — and
     they are one rule counted twice. The premise above is that two stops of
     similar chroma sit on roughly the same CIRCLE about the neutral axis, so
     a leg whose ends sit at very different RADII is moving radially, and a
     radial move has no corner to cut. --gradient-light, --gradient-light-90,
     the rakes, the blooms and the page wash all run into CF-Grau or out of
     Weiss, whose chroma is zero: oklab's straight line from the neutral axis
     to a chromatic stop already is the constant-hue radial, and it is the
     correct path. The lit foil's own Weiss -> Glas leg is the same case.

       leg                        ratio of the two ends' chroma
       Glas    -> Sky 300          1.19  turns
       Sky 300 -> Violett 300      1.40  turns
       Sky 800 -> Violett 800      1.53  turns
       Glas800 -> Sky 800          2.37  turns
       ------------------------- 2.88 --------------------------
       Violett -> Glas             3.50  falls off, bows to #74C1E6
       lime    -> Glas             5.32  falls off, bows to #A8FFB6
       lime    -> Weiss             inf  falls off
       Glas    -> CF-Grau           inf  falls off

     2.88 is the geometric mean of that gap — a ratio's midpoint is geometric
     — and scripts/check-gradient-family.py measures it rather than reading a
     list of endpoint names. Naming lime was a proxy that held only while lime
     was the only source drawn: it would have demanded #74C1E6 the first time
     a Violett falloff reached CSS, which is a vivid sky blue at twice the
     chroma of the stop that leg travels to and is in no palette in this
     brand. Only a leg that turns can cut a corner, and this is what turning
     means.
     ------------------------------------------------------------------ */

  /* THE SHADOW HALF OF THE FOIL.
     The foil above is light-on-dark and cannot be anything else: every stop
     in it is above OKLab L 0.82, so on the page's own CF-Grau it lands
     between 1.1:1 and 1.5:1. That left the two designed pages — which are a
     CF-Grau-to-white wash almost end to end — with no gradient type at all,
     and the only foil moment on either of them down in the black footer.
     This is the same material seen from the other side: not a
     second gradient, the other half of the one that already exists.

     It is built by the same rule the foil is, because iridescence is hue
     travel inside a NARROW LUMINANCE BAND — so the band moves and the hue
     travel does not. The ramps are white-mixes and black-mixes of one
     palette, so the dark mirror falls straight out of them:

                 light foil            ink foil          Δ
       stop 1    Glas 500   L 0.910    Glas 800   0.426  h 179.4 / 180.0
       stop 2    Sky  300   L 0.865    Sky  800   0.353  h 243.6 / 242.5
       stop 3    Violett300 L 0.820    Violett800 0.311  h 283.9 / 282.2
       band                   0.090                0.115
       hue travel            104.5°               102.2°

     Two decimal places of hue apart across three stops, and a band 0.025
     wider. It reads as the same material because it is.

     NO HOT SPOT, and that is the one place the mirror is not symmetric.
     The foil's Weiss sits deliberately above its band as a specular. The
     ink mirror of that would be a stop deliberately BELOW L 0.311, and
     there is no room for its opposite either: the lightest ink that still
     clears 4.5:1 on CF-Grau is about L 0.44, which is where Glas 800
     already sits. So the ink foil is the falloff without the highlight —
     which is what the shadow side of a foil actually looks like. Do not
     "complete" it by adding Schwarz at 0 %: that puts a 0.426 span on a
     ramp whose whole claim is a 0.115 one, and it reads as a fade to black.

     DO NOT use the 700 tints instead. They are the obvious choice for a
     lighter, prettier ink and they fail: Glas 700 is 1.5:1 on CF-Grau.
     The 800s are the lightest rung of the ramp that clears AA on the
     page's own grey, which is why the band sits exactly where it does.

     Stops are the spectrum's own again, renormalised onto the band above
     the near rake — the foil drops Weiss and rebases off the hot spot, so
     the ink drops Weiss and the hot spot both and rebases off the rake:

       Glas     ( 32 − 32) / (100 − 32) =   0 %
       Sky      ( 64 − 32) / (100 − 32) =  47 %
       Violett  (100 − 32) / (100 − 32) = 100 %

     Same 116.57° shallow rake, for the same reason. Worst sample on
     CF-Grau — Glas 800, the light end — is 5.24:1; on white it is 8.16:1.
     AA at any size across the whole ramp, and the worst case is the top of
     the page wash, not the bottom, because this ink darkens as it travels. */
  --foil-ink-sky-n: calc((var(--spectrum-cool-n) - var(--rake-near-n)) / (100 - var(--rake-near-n)) * 100);

  --foil-ink-stops: var(--glas-800)    0%,
                    /* arc waypoint, Glas 800 -> Sky 800. See THE ARC above. */
                    #33494E            calc(var(--foil-ink-sky-n) / 2 * 1%),
                    var(--sky-800)     calc(var(--foil-ink-sky-n) * 1%),
                    /* arc waypoint, Sky 800 -> Violett 800. See THE ARC above. */
                    #273650            calc((var(--foil-ink-sky-n) + 100) / 2 * 1%),
                    var(--violett-800) 100%;

  --gradient-foil-ink: linear-gradient(var(--foil-rake), var(--foil-ink-stops));

  /* Dither. 8-bit sRGB cannot resolve a 5,000 px ramp between two colours
     this close, so the page wash bands into visible steps. One tile of
     static, greyscale fractal noise breaks the step edges up. Pure SVG, no
     request, no build step, `stitchTiles` so the 140 px tile has no seam.

     IT IS NO LONGER THE ONLY THING WORKING ON THAT PROBLEM. --wash-stops
     decorrelates the three channels' step positions, taking the wash from 51
     rendered levels to 130 and dropping full-strength band edges from 99.8 %
     of transitions to 23.6 % — see the note on the wash above. That is the
     ramp itself being given more rungs; this is grain laid over what is left,
     and the two are not substitutes. Nor is the first trick free to apply
     anywhere: it costs a hue budget, and the wash could afford one only
     because it had 104.5° of the foil's own travel going spare.

     8 % is measured, not guessed: it puts about five 8-bit levels of grain
     on the wash — a band edge is one level, so that is plenty — while
     shifting the mean by 0.9 of a level, i.e. CF-Grau still rounds to
     #CFCFCF. Louder than this and the brand's anchor colour moves. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)' opacity='0.08'/%3E%3C/svg%3E");

  /* ------------------------------------------------------------------
     4. TYPEFACES
     Publica Sans → the face of the company (logo, display headlines)
     Geist        → the communicator (body copy, section headings)
     Geist Mono   → the analytic one (labels, numbers, data, nav)
     ------------------------------------------------------------------ */
  --font-display: "Publica Sans", "Geist", "Helvetica Neue", Arial, sans-serif;
  --font-sans:    "Geist", "Helvetica Neue", Arial, sans-serif;
  --font-mono:    "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --weight-light:     300;
  --weight-regular:   400;
  --weight-medium:    500;
  --weight-semibold:  600;
  --weight-bold:      700;

  /* ------------------------------------------------------------------
     5. TYPE SCALE
     Sizes are the rendered values from the 1440 px Figma frame.
     Display sizes are fluid so they hold their proportion on small screens.
     ------------------------------------------------------------------ */
  --text-display-1: clamp(2.5rem, 1.4rem + 4.9vw, 4rem);      /*  40 → 64 */
  --text-display-2: clamp(2rem,   1.3rem + 3.1vw, 2.5rem);    /*  32 → 40 */
  --text-h1:        clamp(1.75rem, 1.3rem + 2vw, 2.5rem);     /*  28 → 40 */
  --text-h2:        clamp(1.375rem, 1.1rem + 1.2vw, 2rem);    /*  22 → 32 */
  --text-h3:        1.5rem;    /* 24 */
  --text-h4:        1.25rem;   /* 20 */
  --text-lg:        1rem;      /* 16 */
  --text-md:        0.875rem;  /* 14 — smallest size allowed for prose */
  --text-sm:        0.75rem;   /* 12 */
  --text-xs:        0.6875rem; /* 11 — mono labels only, never prose */

  --leading-tight:  1.02;   /* display headlines, set solid */
  --leading-snug:   1.15;
  --leading-normal: 1.3;    /* brand default — headings and mono */
  --leading-relaxed:1.45;   /* body copy */

  /* Line length. Prose wants roughly 55–65 characters; a display line wants
     far fewer, so it breaks where the eye expects rather than where the
     column happens to end. */
  --measure:       56ch;
  --measure-tight: 26ch;

  /* Running text is a different problem from a paragraph inside a layout.
     56ch is right for a block of copy that sits beside something else; an
     article is read for minutes at a time, and the readable band there is
     45–90 characters (USWDS measure tokens, and the reading research behind
     them).

     THE UNIT IS NOT THE TARGET. `ch` is the advance width of the shipped
     font's "0", and Geist's zero is 0.6875em — 11 px at 16 px, measured as
     the resolved .cf-prose grid track (66ch computed to 726 px, 726 / 66 =
     11 exactly). The average glyph of German running text on these pages is
     ~0.47em, so every "Nch" here holds about 1.45 × N characters. 66ch was
     chosen with the fallback font's arithmetic — Helvetica's zero is
     0.556em, 66 × 8.9 = 587 px, the figure the prose-track comment in
     components.css glosses — and Geist drew 726 px from the first day: 90
     measured characters per line at a 768 viewport, 100.5 on datenschutz at
     1280, both at or past the band's top.

     53ch × 11 px = 583 px: the column the design documents, now actually
     drawn, measured at 77–80 characters of running German — inside the band
     and level with .cf-value-row__body's capped 56ch. If the body font ever
     changes, re-measure the zero before trusting any ch figure here;
     scripts/check-measure-gloss.py holds the copies of this arithmetic
     together. Only .cf-prose and the excerpt rows use it. */
  --measure-prose: 53ch;

  /* THE TWO MONO TRACKINGS ARE THE TWO RUNGS OF THE LABEL RAMP, and neither
     is free to pick. --text-xs (11) takes --tracking-label; --text-sm (12)
     takes --tracking-wide. Two sizes one step apart are told apart by their
     tracking before they are told apart by their size, so a rule that takes
     one rung's size and the other's tracking reads as neither — which is what
     three rules in the tree were doing when this comment was written.
     scripts/check-label-ramp.py holds the pairing now, the same way it
     already held the sizes.

     "e.g. buttons, footer meta" is what this line used to say about
     --tracking-wide, and .cf-footer__meta has never used it: it is 11 px, so
     it is on --tracking-label with the other forty-seven. The users are
     .t-label-lg and the two rules that restate it, .cf-btn and .cf-prose h4.
     → foundations/typography.html#scale */
  --tracking-display: -0.02em;
  --tracking-normal:   0;
  --tracking-label:    0.01em;  /* Mono/Label as specced in Figma. --text-xs. */
  --tracking-wide:     0.08em;  /* The 12 px label: .t-label-lg, .cf-btn,
                                   .cf-prose h4. --text-sm. */

  /* ------------------------------------------------------------------
     6. SPACE — 4 px base unit
     Every multiple of 4 up to 24, then the doubling run. The step number
     is the multiple, so --space-6 is always 6 × 4 px. Nothing in between.
     ------------------------------------------------------------------ */
  --space-1:  0.25rem;  /*   4 */
  --space-2:  0.5rem;   /*   8 */
  --space-3:  0.75rem;  /*  12 */
  --space-4:  1rem;     /*  16 */
  --space-5:  1.25rem;  /*  20 */
  --space-6:  1.5rem;   /*  24 */
  --space-8:  2rem;     /*  32 */
  --space-12: 3rem;     /*  48 */
  --space-16: 4rem;     /*  64 */
  --space-20: 5rem;     /*  80 */
  --space-24: 6rem;     /*  96 */
  --space-30: 7.5rem;   /* 120 — --section-gap's ceiling, reached at the 1440 frame */
  --space-40: 10rem;    /* 160 — the vacancy register's picture square */

  /* Vertical rhythm. Both are one fraction of the frame, clamped to a scale
     step at each end, so the endpoints land on real device widths rather than
     somewhere off the edge of the range:

       --section-gap     100vw / 12   64 at 768 · 120 at 1440 (capped)
       --section-gap-sm  100vw / 18   32 at 576 ·  80 at 1440 (capped)

     BOTH CEILINGS ARE REACHED AT 1440, AND THE LARGE ONE USED TO RUN ON TO
     160 AT 1920. That was the one fluid measure in the system still growing
     after every other one had stopped: --container-max caps at 1438.2,
     --section-gap-sm and --section-header-gap at 1440.0, --gutter at 1454.5,
     and --section-gap arrived 466 px later. From 1454 to 1920 the page was a
     fixed 1280 column with 80 px gutters whose vertical air kept loosening by
     a further 40 px — measured as a share of content width, 9.4 % flat from
     768 to 1440 and then climbing to 12.5 % at 1920, against a column that no
     longer grew. Nothing in the designer's material reaches past the 1440
     frame, so the 160 was never a measured value; it was where 100vw / 12
     happened to land on the next common monitor width.

     The ceiling is now the frame's own value. --space-30 is 120 by
     construction — 1440 / 12 — so the rhythm freezes on the same width the
     column freezes on, and the step that used to be a landmark with no
     consumer is the ceiling that stops it. Nothing at or below 1440 moves by
     a pixel. Above it, sections stand 240 apart instead of up to 320.

     One twelfth of the FRAME is what 100vw / 12 is, and that is why the
     reference frame resolves it to exactly --space-30 (120). It is NOT one
     column of the 12-column grid, which this comment used to claim: measured
     at 1440, a real column is 84.67 (content 1280, less 11 gaps of 24, over
     12) and a column pitch — column plus gap — is 108.67. Neither is 120.
     The unit is the frame divided by the column count, which is a coarser
     thing than a column because it still carries both 80 px gutters. That is
     a sound unit and the designer's own frame is the thing it divides; it
     just is not the grid. See foundations/layout.html, where the claim was
     load-bearing for a note about what changes above 1438.

     The 2:3 ratio between them (1/18 : 1/12) holds exactly where BOTH are
     fluid, and from 768 up it holds everywhere. Measured: 2.00 below 576 where
     both sit on their floors, sliding 2.00 → 1.50 across 576–768 as -sm
     leaves its floor first — 1.80 at 640, 1.64 at 704 — then a flat 1.50 from
     768 to 1440 and, now that both ceilings are 1440's, a flat 1.50 beyond it:
     120 / 80. It used to open back out to 2.00 by 1920 as -sm capped and
     --section-gap climbed on alone; that was the ceiling drift described
     above, and it is gone with it. This used to say the slide STARTS at 1.80,
     which put the ratio below 2.00 at 576 where -sm is still exactly on its
     floor and the ratio is still exactly 2.00. foundations/layout.html had it
     right; this comment was the copy that drifted.

     These are padding on `.section`, applied to both edges — so the air BETWEEN
     two adjacent sections is twice the token. See foundations/layout.html. */
  --section-gap:    clamp(var(--space-16), calc(100vw / 12), var(--space-30));
  --section-gap-sm: clamp(var(--space-8),  calc(100vw / 18), var(--space-20));

  /* The air between a section header's hairline and the content it opens.
     Measured off both mockups: 88 px at the 1440 frame, so 80 px on the scale.
     A section whose content is a ruled box is the exception — there the box's
     own top border IS the header rule. See .cf-section-header--flush.

     THIS IS THE THIRD VERTICAL RHYTHM TOKEN AND IT WAS THE ONLY ONE THAT WAS
     A CONSTANT. The chapter next door opens "two fluid tokens carry all
     vertical air between sections", and the table under it — four tokens and
     the width each stops growing at — had no row for this one, because it
     never started. 80 px at 320 and 80 px at 2560, while everything it is
     measured against moves.

     What that costs is a RATIO, not a number, and the number was only ever
     right at the frame it was measured on. The air ABOVE a section header is
     the section's own padding-block-start; the air below it is this. Measured
     as below ÷ above, every pattern page, before:

                         320   375   768   900  1024  1280  1440  1920
       .section         1.25  1.25  1.25  1.07  0.94  0.75  0.67  0.50
       .section--tight  2.50  2.50  1.87  1.60  1.41  1.13  1.00  1.00

     A 5x swing across the range on one relationship, and at the left-hand end
     it is INVERTED: at 375 a header on a --tight section stands 32 px under
     the thing before it and 80 px above the thing it introduces, so proximity
     says the header belongs to the section above. Four pattern pages open that
     way — karriere, news, kontakt, and the confirmation pages behind them.
     Nothing about a screenshot at 1440 says so; at 1440 the numbers are the
     designer's and the ratio is 0.67.

     THE FIX IS THE FRACTION THE SYSTEM ALREADY OWNS. 80 at the 1440 frame is
     100vw / 18 — the same fraction --section-gap-sm is built on, and exactly
     2/3 of --section-gap's 100vw / 12 wherever both run fluid. So the designed
     value is not re-derived or traded away: the ceiling IS 80 and the fluid
     term reaches it at exactly 1440, the frame the mockups were drawn on. The
     token stops growing where the column does, which is the coordination the
     chapter's own note asks for and the reason no ceiling above --space-20 was
     considered.

     THE FLOOR IS THE PROXIMITY CONSTRAINT, WRITTEN AS A NUMBER. It has to sit
     under --section-gap's own 64 px floor or the inversion survives at every
     width where both are floored. 48 is the scale step that does, it is what
     100vw / 18 resolves to at 864 — between the two device widths either side
     of it — and it lands the ratio at 0.75 below 768 and a flat 2/3 from 864
     up. After:

                         320   375   768   900  1024  1280  1440  1920
       .section         0.75  0.75  0.75  0.67  0.67  0.67  0.67  0.67
       .section--tight  1.50  1.50  1.12  1.00  1.00  1.00  1.00  1.00

     The 1920 column read 0.50 while --section-gap's ceiling was --space-40:
     the air above kept growing to 160 after the air below had capped at 80.
     Both cap at 1440 now, so the 2/3 holds at every width from 864 up.

     --tight is still over 1.00 at the phone and that is deliberate: it takes
     the small rhythm precisely BECAUSE a page header above it has already
     spent the air, so its 32 is not the whole distance a reader sees and 1.50
     there is not the inversion 2.50 was. The plain section — the one both
     mockups show — is the case that had to stop being upside down, and it is
     0.75 or tighter at every width now.

     Nothing above 1440 moves by a pixel, and neither does the reference frame.
     → foundations/layout.html#rhythm */
  --section-header-gap: clamp(var(--space-12), calc(100vw / 18), var(--space-20));

  /* ------------------------------------------------------------------
     7. LAYOUT
     1440 frame, 80 px gutters, 1280 content. 12 columns, 24 px gap.

     The gutter is proportional on purpose: (1440 − 1280) / 2 / 1440 = 5.5556 %,
     rounded down to the 5.5vw below. Above that the container caps and the
     outer margin grows instead. Below 364 px the 20 px floor takes over.

     What lands content on exactly 1280 is --container-max, NOT the gutter.
     At 1440 the gutter is 79.2, so the gutter alone leaves 1281.6; the cap
     binds because .container's max-width is calc(1280 + 2 * gutter), and the
     padding subtracts back to 1280 precisely. The two decouple at the top,
     which is the tell: 5.5vw does not reach 80 until a viewport of 1454.5,
     while content reaches 1280 at 1438.2. See foundations/layout.html.

     THE FLOOR IS CAPPED, AND THAT IS THE ONE THING IN THIS TOKEN THAT IS NOT
     ABOUT THE 1440 FRAME. A clamp() floor written in rem grows with the
     reader's text size while the viewport it is spent on does not, and the
     gutter is the one fluid token in the system that is subtracted from the
     measure rather than added to it. At a 32 px browser default — 200 % text,
     which is WCAG 1.4.4 at level AA — 1.25rem is 40 px, so the two gutters
     took 80 px of a 320 px viewport and left a 240 px content column for type
     that had just doubled. Measured across all eighteen pattern pages at a
     32 px default, 320–480 px: 27 (page, width) pairs scrolled sideways
     against 0 at a 16 px default.

     min(1.25rem, 6.25vw) is that floor with a ceiling of its own: 20 px, or a
     sixteenth of the viewport, whichever is smaller — so the two gutters can
     never take more than an eighth of the screen. 6.25vw is not a chosen
     number. It is 20 / 320: the rem floor and the cap meet exactly at 320 px,
     the reflow width WCAG 1.4.10 measures at, which makes the cap PROVABLY
     INERT at a 16 px default for every viewport the record documents. Every
     figure on foundations/mobile.html is unchanged to the last hundredth, and
     check-fluid-record.py re-derives the crossover and holds the cap to that
     proof rather than to this paragraph.

     It does not fix everything it was measured against, and it was not
     supposed to: the residue at 320 px is the nav bar's own row (325 px of
     content in a 286 px bar, which no gutter reaches) and a handful of
     component-level strings. Those are named in the PR that landed this and
     belong to the lanes that own them. What this token owed was the 40 px it
     was taking off the column, and that is now the 40 px the column keeps.
     ------------------------------------------------------------------ */
  --container-max: 80rem;   /* 1280 */
  --gutter:        clamp(min(1.25rem, 6.25vw), 5.5vw, 5rem);  /* 20 → 80 */

  /* THE DISTANCE FROM THE VIEWPORT EDGE TO THE CONTENT COLUMN'S EDGE.
     --gutter is not that distance, and the difference is the whole point of
     this token. While the container is still growing the two are equal; once
     it has hit --container-max the container stops and centres, so the column
     moves inward while --gutter stays put. Past that crossover an element
     positioned at --gutter is no longer on the column — it is on the window,
     and the gap between them opens without limit: 80 px at the 1440 frame,
     240 at 1920, 560 at 2560.

     Anything full-bleed that has to line up with the column takes this
     instead: the hero's CTA and its pause switch, the values section's
     counter and progress bar. All four were on --gutter and all four are
     drawn on the column in the mockups.

     The Über uns page header takes it for the other reason a full-bleed box
     needs the column: not to line up with it, but to MEASURE it. The header
     reserves room below the rule for the object hanging there, and how much
     room is a fraction of the column's width — which the header cannot read
     off its own 100 %, because its own 100 % is the window. The object
     itself needs no inset at all: it stops two columns short of the column
     edge, so it lives inside .container. → foundations/layout.html#column-inset

     The crossover is exact rather than approximate, because .container's own
     max-width is calc(--container-max + 2 * --gutter): at that width the
     centring margin is exactly --gutter and the two arms of the max() meet.

     The percentage resolves against the USER's containing block, so this only
     means what it says on an element whose containing block spans the page.
     That is the same condition under which --gutter was ever correct here. */
  --column-inset:  max(var(--gutter), (100% - var(--container-max)) / 2);

  /* The floating nav's own height: 2 × --space-5 plus the 44 px logo plate.
     The hero pulls itself up by exactly this so the bar overlays the artwork
     instead of sitting on a band above it. */
  --nav-height:    5.25rem; /* 84 */
  --grid-columns:  12;
  --grid-gap:      var(--space-6);

  /* THE 12-COLUMN TRACK SET ITSELF, written once and read by everything that
     lays out on it. --grid-columns is the COUNT and --grid-tracks is the track
     list; the two are the same pair .subdivide already keeps as
     --subdivide-count and --subdivide-cols, and for the same reason it gives:
     a track set declared in more than one place is a track set two rules can
     disagree about.

     It had already been written three times. base.css's .grid is the primitive;
     components.css declares the identical repeat() twice more for
     .cf-values__frame and .cf-values__item, which are the pinned values
     section's own 12-column frame and the five items stacked in it. Those two
     are not a component reinventing the grid — they are the one place in the
     system that genuinely needs the page's column set inside a component, and
     they could not reach for .grid because they are set inside an @supports
     block on markup that carries no utility class. So the fix is not to move
     the markup; it is to give all three one declaration to read.

     minmax(0, …) rather than a bare 1fr, and that is the load-bearing half of
     this token. 1fr is minmax(auto, 1fr), and auto floors a track at its
     content's MIN-CONTENT width — so one long German compound in any cell
     widens its own track past its share and pushes the page sideways.
     overflow-wrap: break-word does not save it: the reset sets it on
     everything, and it does not enter intrinsic track sizing at all. Measured
     on .cf-progress, the one grid in the system that carried a bare 1fr: a
     61-character compound in its label took the document from 320 to 469 px
     wide at a 320 px viewport, and from 375 to 469 at 375.
     → foundations/layout.html#intrinsic-minimum */
  --grid-tracks:   repeat(var(--grid-columns), minmax(0, 1fr));

  /* ------------------------------------------------------------------
     THE BREAKPOINT REGISTER
     A threshold cannot be a custom property: var() is not allowed in the
     prelude of @media or @container, and @custom-media needs a build step
     this system does not have. So every threshold is a literal, and the
     only way to keep them from multiplying is to write them all down in
     one place and add to the list deliberately.

     The rule: a component asks its CONTAINER. Only page furniture that is
     genuinely pinned to the viewport — the nav bar, the hero, the footer,
     the consent banner — may ask the viewport.

     EVERY THRESHOLD IS IN rem, AND THAT IS THE SECOND RULE. It used to be
     true of the container thresholds and false of the viewport ones, which
     were px — so half the register answered to the reader's default font
     size and half did not. That is not a style preference; it is where the
     page broke.

     A media query's rem resolves against the BROWSER's default font size,
     not against anything an author writes on :root — so a rem threshold is
     the only kind that tracks the reader's own preference. Every query in
     this register is a "does this still fit" decision, and what fits
     depends on how big the type is. A px query answers that question at a
     text size the reader may not be using.

     Measured, before the conversion, on the two designed pages with the
     browser default set to 24 px (150 %, a common low-vision setting and
     well inside what WCAG 1.4.4 asks for):

       browser default   viewport widths that scrolled sideways
       16 px             0
       24 px             12   — 320–380, and a band at 792–820

     The 792–820 band was the nav: it collapses to a toggle below 780 px,
     but at a 24 px default the expanded links need 823 px, so between the
     threshold and the fit there were 43 px of horizontal scroll on every
     page in the system. 48.75rem is 780 px at a 16 px default — the same
     number, to the pixel — and 1170 px at a 24 px one, which is where the
     nav actually needs to fold. The band is gone at every default font
     size, and nothing moves at 16 px. The 320–380 band was a different
     fault entirely; see the note on overflow-wrap in base.css.

     The conversion is arithmetic, not a re-measurement: 780 / 16 = 48.75,
     900 / 16 = 56.25, 560 / 16 = 35. Each renders identically at the
     default. If you ever need to change one of these numbers, change it as
     a rem figure — reverting one to px silently opts it out of the reader's
     preference, which is the exact bug this paragraph exists to prevent.

     CONTAINER thresholds — the width of the component's own container:

       21rem / 336   .cf-pie's labels stop riding the ring and become the
                     ruled list under it — the base tier the component
                     already ships for a browser without CSS trig, which is
                     the same figure a box without room needs. The number is
                     the component's own arithmetic and not a taste: 6u of
                     ring plus half a cell and a --pie-label-w cap on each
                     side is 7u + 10rem, which at the --pie-u floor and a
                     16 px root is 328 px. Eight px of slack over that, on the
                     container it is measured on — cf-pie, the figure itself.
                     SAME SHAPE AS 28rem BELOW AND DELIBERATELY NOT THE SAME
                     NUMBER: reusing it would drop the working 446 px figure
                     the landing page draws at 1024 to the legend to save a
                     row here. → components/pie.html
       28rem / 448   .cf-annot stops being a callout and becomes a legend:
                     the notes flow under the figure as a row of marks and
                     words, and the leaders go with the label rules. The
                     threshold is on the FIGURE — cf-annot-fig, its own
                     named container — because what decides whether a
                     callout fits is the width of the drawing, never the
                     viewport. → components/annotation.html
       30rem / 480   .cf-breadcrumb keeps only the last two crumbs; whatever
                     is now first drops its divider. Same shape as 34rem
                     below — a row of parts that has stopped being one row —
                     and 3 rem away from it. It is a SEPARATE number because
                     a trail truncates from the head while a route folds in
                     place, and they are measured on different containers;
                     it is registered rather than merged so the adjacency is
                     visible rather than hidden.
                     Also .lp-flow-sources, the flow tier's canopy beads,
                     querying sp-root: below this rung the tightest pair of
                     entries (15 viewBox units) cannot hold a bead and a
                     pixel of air — check-canopy-sources.py re-derives the
                     arithmetic. A second consumer, same argument — and the
                     third gate this layer wore; the two rungs above it
                     each stranded a real reader, see acts.css.
       34rem / 544   .cf-error__route folds — the 12 rem label drops above
                     its description and the arrow keeps the right edge.
                     .cf-pagination hides the page numerals and keeps the
                     prev/next pair and the status line. Two consumers, one
                     shape: a row of parts that has stopped being one row.
                     The number is measured — 34rem is the width the routes'
                     two-column form needs, and it is why .cf-error carries
                     no measure cap (see the note there). The same figure is
                     .cf-error__figure's max-width — one number the whole
                     component is built around, so it is a threshold with a
                     reason and not a fifth arbitrary one.
                     (34.625rem / 554 stood here until 2026-09-01: the
                     card width where .cf-process__figure's height cap stops
                     binding, behind which the trace kept a second constant.
                     It was the register's only fractional entry and its own
                     text called it "a question rather than an answer". The
                     answer was that a derived geometric crossover is a min()
                     and not a query: the trace weight is now a ratio of the
                     three lengths the figure already declares, with 100cqw
                     standing for the card, and the crossover falls out of it
                     at every root font size. → components.css, PROCESS CARD)
       42rem / 672   .cf-team-grid's cell takes its second column, and with
                     63rem below it is the ONE PAIR IN THIS REGISTER THAT
                     DECIDES NOTHING — the grid already changes column count
                     here on its own, by auto-fill, and has since it was
                     written. What these two rungs carry is the count as a
                     NUMBER, because the cell's 15 % inset needs one and cqi
                     cannot supply it: gap is 0 in this grid, so auto-fill
                     lays floor(container / --tile) tracks, and 2 x and 3 x
                     the 21rem tile are 42 and 63rem exactly. They are
                     therefore not a second opinion about where the field
                     folds; they are the same arithmetic, and if --tile moves
                     they both move with it. Measured on cf-team, the grid's
                     own content box, and not on the enclosing `layout` — one
                     element out is one border wide, which is a 1 px window at
                     each rung where the two would disagree.
                     Why the inset needs a length at all rather than the
                     percentage it was: Gecko measures this card with its
                     inline size indefinite, every percentage inside it
                     resolves against nothing there, and the two that land in
                     the block axis reached the grid as 364.03 px of dead band
                     per row under the last person. → components.css, over
                     .cf-team-grid__item
       44rem / 704   .subdivide folds; the blog axis goes with it, and now
                     goes with it structurally — the axis is a .subdivide__row
                     of the same grid, so both queries measure one element.
                     Reached at a viewport of 792 inside a .container;
                     binary-searched, folded at 791 and open at 792.
                     Also .grid--early, the row that takes its spans here
                     instead of at 56rem — see base.css. A second consumer
                     for an existing number, not a second number.

                     AND IT IS THE NUMBER FOR THREE COLUMNS. A fold width is a
                     property of the column count, not of the primitive:
                     blog-artikel.html runs the grid at three and three columns
                     of 44rem are 234 px each, which reads. Five columns of the
                     same container are 141 px. A five-column subdivision takes
                     .subdivide--late and folds at 56rem below.

                     .cf-logo-wall WAS THE THIRD CONSUMER HERE AND HAS MOVED TO
                     64rem BELOW. It sat at 44rem behind a one-line width of
                     617.0 px, and that width was seven MONO WORDS — the wall
                     was still carrying stand-in text. The wordmark files landed
                     and the marks are wider: 988.52 px, measured, which no
                     longer fits above this fold. The row moved with the number
                     rather than the number with the row, because 44rem is
                     already three columns of .subdivide and .grid--early's
                     opt-down and neither of those has anything to do with a
                     logo.

                     WHAT THAT ROW USED TO SAY ABOUT ITSELF IS WORTH KEEPING
                     HERE, because it was right and it is now spent: it called
                     itself "THE REGISTER'S ONE THRESHOLD WHOSE TRIP POINT IS A
                     STRING rather than a box ... no script can hold it, because
                     the width is a rendered word in a font this repository does
                     not ship", and it warned that "minting a threshold two
                     pixels above a string is how a mark gets stranded the day
                     the wordmark files land". The day came. What it bought was
                     that nothing was stranded: the marks arrived, the width
                     went up by 371 px, and the only edit needed was moving the
                     rule to a number the register already had. The successor
                     entry is a box and can be counted. → 72rem, the last row
                     of this list
       48rem / 768   .cf-value-row splits into icon + copy.
       56rem / 896   the two-column threshold: .col-*, .cf-statement,
                     .cf-process. Inside a .container this is reached at a
                     viewport of about 1007, because a size query measures
                     the CONTENT box and the gutter is 5.5vw. It was measured
                     for the two illustration components; .col-* inherited
                     it, and a row that does not hold an illustration can
                     opt down to 44rem with .grid--early.

                     ALSO .subdivide--late, and the blog axis with it — the
                     fold a FIVE-column subdivision needs, opting UP the same
                     way .grid--early opts down. Measured on the landing page
                     at the pixel after the 44rem fold, a container of 705:
                     .cf-blog-card--compact is one line with an ellipsis, so
                     its cell must hold the headline's first word plus the
                     ellipsis or it labels nothing, and the cell gives
                     C/5 - 1 - 2 x --space-3 of text — 116 px there against
                     the 130 px "Smart-Home-Systeme" needs. Two of the twelve
                     tail headlines rendered as a fragment of one word and an
                     ellipsis; six showed under two whole words; one pixel
                     earlier, folded, all eighteen were complete. The floor
                     solves to a 775 px container, binary-searched to 773.
                     48rem misses it by 5 px — the wrong side of a window too
                     narrow to build on, since the next real headline can have
                     a longer first word — and this clears it by 121, on the
                     width where .cf-statement and .cf-process already change
                     shape, so the page folds once rather than twice. A second
                     consumer for an existing number, not a second number.
       60rem / 960   .cf-article splits into rail + body and the rail goes
                     sticky. Reached at a viewport of about 1079. It is the
                     only container threshold that is not derived from
                     anything — see the note in foundations/layout.html
                     before adding another.

                     THIS ENTRY HAS BEEN RENUMBERED TWICE AND NOW CARRIES NO
                     NUMBER AT ALL. It said FOURTH while 34rem was live with
                     two consumers, then FIFTH while 28rem, 30rem and
                     34.625rem were live with one each. An ordinal in a
                     register kept by hand is a second thing to remember
                     that says nothing the list above does not, and it went
                     stale both times the list did. The count is whatever
                     scripts/check-breakpoints.py prints.
       63rem / 1008  .cf-team-grid's cell takes its third column — the upper
                     half of the pair described at 42rem above, and 3 x the
                     21rem tile. The grid stops here: the content column is
                     capped at 1280 and four tiles are 1344, which is the same
                     fact the sweep in components.css records as "4 and 5 do
                     not occur at any width". If that cap ever moves, this
                     ladder needs an 84rem rung on the same day.
       72rem / 1152  .cf-logo-wall justifies its marks to the measure at or
                     above this width and centres them below it. A NEW NUMBER,
                     which this register does not hand out, so here is the
                     arithmetic that earns it: the seven partner marks at the
                     wall's 2.5rem are 132.70, 86.14, 174.61, 60.72, 150.89,
                     99.41 and 94.27 px, six --space-12 gaps are 288, and the
                     wall therefore holds one line at 1086.73 px — 67.92rem. A
                     size query measures the CONTENT box and .container's
                     content box IS the wall's width, so 1086.73 is the number
                     the query has to clear. 60rem above is 960 and would
                     strand a mark on a justified last line.

                     68rem WOULD ALSO CLEAR IT, BY 1.27 px, AND THAT IS THE
                     REASON THIS IS 72. The old 44rem row called minting a
                     threshold two pixels above a measurement the way a mark
                     gets stranded, and the objection is weaker here than it
                     was there — the trip point is a sum of DECLARED BOXES now,
                     not a rendered string, so every browser gives the same
                     figure. Weaker is not gone: those widths are fractional,
                     sub-pixel rounding differs between engines and zoom
                     levels, and 1.27 px is inside that. 72rem clears by
                     65.27 px and costs the 1086.73-to-1152 band — a viewport
                     of about 1223 to 1294 — centred where it could have been
                     justified, at most 32.6 px an end. Cheap half of an
                     asymmetric error. Binary-searched on the shipped
                     page: one line from a viewport of 1222, and the
                     query engages at 1295 where the container's content
                     box reaches 1152.56.

                     IT WAS 64rem FOR ONE RELEASE and the 98.21 px that moved
                     it are two marks the first pass got wrong: Azure shipped as
                     the glyph ALONE, a bare A 15.19 px wide beside six marks
                     that each say their own name, and Spark shipped at the
                     normalisation rule's own answer, which is the one place
                     that rule under-reads (it assumes upright stems; Spark's
                     wordmark is a script). Naming the brand and matching its
                     neighbours cost 84.22 px and 14.00 px. This is exactly the
                     event the row says to re-measure for — the SET of marks
                     changed — and it is the second time in two releases, which
                     is what a wall of third-party artwork is like.

                     IT SHARES NO NUMERAL WITH ANYTHING IN THE VIEWPORT LIST
                     BELOW, which the 64rem it replaces did. That was a
                     coincidence and it was flagged as one; it is gone now, and
                     nobody has to be told twice that two lists asking two
                     questions may agree on a figure.

                     MOVED HERE FROM 44rem ORIGINALLY, AND THE OLD ROW EXPLAINS
                     WHY. The wall sat there behind a one-line width of
                     617.0 px, and that width was seven mono WORDS — the marks
                     were stand-in text, and the row said of itself that it was
                     the register's one threshold whose trip point was a string
                     rather than a box, that no script could hold it, and that
                     it would move the day the wordmark files landed. It did.
                     What is different now is that this row can be counted: the
                     marks are SVGs with declared boxes, normalised to a shared
                     optical height before they were committed
                     (assets/img/partner/README.md), so the sum above is the
                     same in every browser and does not move when a font lands
                     or a type scale is edited. Re-measure it when the SET of
                     marks changes — that is the only event left that moves
                     it. → components.css

     VIEWPORT thresholds — page furniture only:

       48.75rem / 780   nav collapses to a toggle; the accordion loses its
                        indent; the footer CTA folds
       56.25rem / 900   the page-header figure is dropped; the consent
                        banner goes single column; the hero action drops into
                        the flow, and the hero's reading scrim goes to
                        --scrim-depth 0.54 — the same reflow that moves the
                        action is what puts the artwork's dark band under the
                        kicker.

                        THE HERO ACTION MOVED HERE FROM 48.75rem ABOVE, and
                        the move is the reason to read this row twice. It was
                        folded with the nav because it was written with the
                        nav, and 48.75rem measures the NAV's links — nothing
                        about the hero's own bottom row. The action floats at
                        the column inset over a kicker that is
                        `width: fit-content` on the same band, and between the
                        two numbers the kicker's last words sat under the
                        glass: 70 px of them at a 16 px default, 109 px at a
                        24 px one, across a band that widens with the reader's
                        type because the kicker is rem and the gutter is vw.
                        A second consumer for an existing number, not a second
                        number — the way 44rem carries .grid--early and 48rem
                        carries .cf-logo-wall. → components.css .cf-hero__action
       51.25rem / 820   the Über uns values section takes its pinned,
                        scroll-scrubbed form. Below it the values stack and
                        read straight through. THIS ENTRY IS THE FOURTH TIME
                        THIS REGISTER HAS BEEN CAUGHT SHORT, and the first
                        time the missing threshold was hiding behind a
                        correct statement about a different file: the SCOPE
                        list below has always named 820 as
                        prototypes/werte-scroll.html's, which is true, out of
                        scope, and a different query — so the figure looked
                        accounted for while a live one in components.css was
                        not. Reading "820 is listed" as "820 is registered"
                        is the mistake; the register is a list of QUERIES,
                        not of numbers.
       64rem / 1024     the pinned track — .cf-pin, the stage Expertise's
                        fields and the landing page's process both scrub
                        their builds on. Adopted from the landing page,
                        which derived it: the stage is only habitable once
                        the step has its two columns, and that fold is the
                        container 56rem above, reached at a viewport of
                        about 1007. A px gate cannot track a rem fold — the
                        820px this replaced pinned a stacked step into
                        100vh of clip and cropped it worst for the reader
                        with the largest type.
       95.999rem / 1536 the act rail is paint at rest only above this. The
                        rail's row needs 108 px and the page margin holds it
                        only once --column-inset reaches its 128 px arm,
                        which is 96rem — below that the rail hides at rest
                        (`opacity: 0` off focus) and :focus-within still
                        lifts it, plate and titles and all. Written as
                        95.999 so the off tier ends where the fit begins
                        instead of both matching at exactly 96rem. Lives in
                        acts.css, the landing page's own sheet — the first
                        registered threshold outside the three shared
                        stylesheets, and the reason the register's scope
                        names the sheet rather than assuming the three.
                        → scripts/check-rail-margin.py holds the arithmetic
       45rem / 720      (height) a pinned track's other gate: a stage
                        that must also clear the nav band cannot hold a
                        card worth reading under 45rem of viewport. The
                        card is width/2 high plus its copy — measured on
                        the landing page, adopted by Expertise unchanged.
                        THIS ENTRY WAS "one mechanism, one gate" UNTIL A
                        THIRD CONSUMER SHOWED THE CLAIM WAS OPTIMISTIC: the
                        Über uns values section (51.25rem, above) shares the
                        same pinned/scrubbed construction and had never
                        asked this question, so a phone turned sideways —
                        932 x 430, 915 x 412, both real device sizes, both
                        engines — cleared the width gate and ran the scroll
                        hijack the height gate exists to stop. Same term,
                        same reasoning, third component. → components.css
                        .cf-values__track
       78rem / 1248     with 60rem below, the pair that decides whether act
                        5's map key stands beside the drawing or under it.
                        They are one decision and are registered as two
                        entries only because a media query cannot state a
                        ratio. Inside the pin gate the map row is what the
                        copy and the key leave of one viewport, and
                        preserveAspectRatio="meet" fits a 1.7943 drawing
                        into it: on a WIDE, SHORT stage that box is far
                        wider than tall, the drawing scales to the short
                        axis and leaves a letterbox down both sides — 203 px
                        each at 1440 x 900 — which is dead ground the key can
                        occupy for free, and the row it gives back becomes
                        scale. Above 60rem of height the drawing is already
                        width-limited, there is no letterbox to move into,
                        and a 14rem column would be taken out of the only
                        axis still binding: measured 1225 stacked against
                        1024 beside at 1920 x 1080. 78rem is where the stage
                        can afford the column at all — 1024 x 768 clears the
                        height term and fails this one, and the table in
                        acts.css says to leave it stacked. → acts.css,
                        .map-stage
       60rem / 960      (height) the other half of 78rem above, and the
                        term that actually names the letterbox: above this
                        much viewport height the map row is deep enough that
                        the drawing is limited by the stage's width instead
                        of by its own, so the dead ground the key was going
                        to stand in does not exist. Written as a MAX and not
                        a min for that reason — it is the only height gate
                        in this register that turns a layout OFF as the
                        screen grows, because what it guards is a shape and
                        not a fit. → acts.css, .map-stage
       35rem / 560      (height) the consent banner takes its short-screen
                        form: the ghost rejoins the accept/reject row and the
                        block padding drops a rung. It exists because a tall
                        banner on a short screen hides the page it is asking
                        about. Both remaining declarations are landscape's
                        answer to landscape — a screen with width to spend and
                        no height — and the banner's max-height backstop is
                        no longer one of them; it stands on .cf-consent
                        itself, ungated. → components.css

                        AND THE UNIT DOES NOT DO WHAT THIS ROW SAID IT DID.
                        This entry read "in rem for the same reason as the
                        widths, and it is the case where the unit matters
                        most: enlarged type makes the banner taller while a
                        px threshold holds the screen it is measured against
                        still." The first half is the finding and the second
                        half does not follow from it. rem in a MEDIA QUERY
                        resolves against the root's INITIAL font size and
                        never against the reader's — see check-rem-floor.py,
                        which is written about that exact distinction — so
                        35rem here is 560 px at a 16 px default, at a 24 px
                        default and at every setting in between. The unit
                        buys consistency with the widths and nothing else,
                        and the one thing this row claimed it bought was the
                        one thing it cannot. Measured at 320 x 812 with a
                        24 px default, this query not matching: 644 px of
                        banner, 41.6 px of clear band, a 72 px call to action
                        that has no scroll offset at which it can be pressed.

                        THE ROW ABOUT THIS ROW. For as long as this entry
                        existed the block held one declaration — a 70vh cap
                        with an inner scroll — and a cap only does something
                        where the banner is already taller than it. The
                        banner is 253 px at every width from 375 to 900, so
                        across the whole band this threshold governs the cap
                        never engaged: 68 % of a 375 px screen, 32 px left
                        between the nav and the banner, both of them fixed.
                        A threshold that is registered, live and inert is
                        the one kind this register cannot catch, because
                        every copy of it agreed. → components.css

     The px figure after each slash is what the threshold resolves to at a
     16 px default and nothing more — the same convention the container
     thresholds above use. It is not a second number to keep in sync.

     Two viewport tiers rather than one is a live question — they govern
     different objects and neither was measured off the mockups. Flagged
     for a designer rather than reconciled here. 51.25rem sits between
     them and 64rem above them, which does not settle the question but
     does mean any answer now has four widths to reconcile rather than
     two.

     There is no fifth viewport width. There was a candidate: .cf-error--page
     split its two columns at @media (min-width: 62rem), a figure that
     appeared once in the whole repository and in neither copy of this
     register. It was not converted to a container query, it was folded
     onto 56rem — 62rem of viewport is 55.18rem of container, so the two
     were the same threshold wearing different units. See components.css.

     This register is only worth having if it is complete, and kept by hand it
     was never complete for long. It was caught four times. First: 60rem and
     the height threshold were live in components.css while this list claimed
     three container thresholds and two viewport ones. Second: 34rem was live
     with two consumers, and the 62rem above with one, while the corrected
     list claimed four and three. Third: 51.25rem, hiding behind a true
     statement about a different file. Fourth: 28rem, 30rem and 34.625rem —
     three live container queries, one of them added the same day the sweep
     that found them ran.

     Every one of those fixes ended with the same instruction to the next
     person: grep the preludes, and do not trust the previous fix to have been
     exhaustive. Four times is enough evidence about that instruction. NOTHING
     RAN — which is the same sentence the space scale, the light family, the
     isometric assembly, the glass budget and the grid tracks each turned out
     to be one script short of.

     scripts/check-breakpoints.py is that script. It reads every prelude in the
     three shipping stylesheets and holds the three copies of this rule to each
     other in both directions: a query with no entry is a finding, an entry
     with no query is a finding, and this comment disagreeing with the table on
     foundations/layout.html is a finding. It keeps the rem rule above, and it
     re-derives every px gloss rather than comparing it against itself.

     So the instruction is shorter now. Add a threshold to a shipping
     stylesheet and add it here in the same commit; CI says so if you do not.

     SCOPE: the stylesheets that ship — this file, base.css and
     components.css, and acts.css, the landing page's own sheet. A page
     stylesheet is page-local by ownership but its thresholds are folds a
     reader meets on control-f.de, so they register here with everything
     else that ships; acts.css spent its first weeks outside every literal
     gate precisely because this list used to say "three" and mean it.
     Everything else is out, and "everything else" is three distinct
     things, so none is safe to leave implicit:

       docs.css            the documentation chrome. Does not ship to
                           control-f.de. Carries one query,
                           @media (max-width: 56.25rem), which collapses
                           the sidebar. Same figure as the 56.25rem above
                           and unrelated to it — do not reconcile them.
                           IT IS STILL OUT OF THIS REGISTER AND IT IS NO
                           LONGER OUT OF THE REM RULE. Those are two
                           rules: this list decides which thresholds are
                           written down, and the rem rule decides whose
                           font size a threshold answers to. The second
                           question is about a reader rather than about
                           who owns a file, and this query was the last px
                           answer to it in front of one. It read 900px on
                           the argument that converting it would make this
                           register look like it governs the chrome; the
                           sidebar track beside it is 17rem, so the two
                           drifted apart as the reader's default rose —
                           492 px of horizontal scroll at 200 % text,
                           measured, against none at 16 px, where the two
                           spellings are identical. 900 / 16 = 56.25 is
                           this register's own arithmetic.
                           → foundations/layout.html#outside-register
       inline <style>      per-page demo styles. foundations/
                           iconography.html asks a VIEWPORT 48rem where
                           48rem above is a CONTAINER threshold.
       prototypes/         declared not-yet-system by the README, with
                           their own unreconciled styling. werte-scroll
                           .html asks a viewport 820 — which collides
                           with nothing, but sits between the 780 and
                           900 above, the pair already flagged as an
                           open question. Do not reconcile it; do know
                           it is there.

     Same figures, unrelated queries. Do not read one as evidence of the
     other, and do not reconcile them.

     Complete about QUERIES, not about behaviour. A min() or clamp()
     against a viewport unit also changes layout at a specific width,
     when its arms swap, and none of those are listed because none is a
     query. So the height rows above are true of queries and false of
     behaviour. They stay out because a crossover is a continuous
     function swapping arms rather than a rule switching on — but a
     register arguing completeness has to say which kind it means.

     THIS PARAGRAPH USED TO NAME THREE AND SAY "three exist in shipping
     CSS". There are thirty, in sixteen declarations, and the three were
     the three somebody had noticed; the eleven token-level ones were
     excused in a clause and the two that matter most — .cf-plot's
     --plot-u and .cf-pie's --pie-u, both reaching their ceiling at
     941 px — were named nowhere. A crossover census kept in prose is
     the same hand-kept count this register was gated for, one panel
     down. It is derived now: check-fluid-crossovers.py resolves every
     min(), max() and clamp() in these four sheets and writes the table.
     Do not restate a crossover here; there is nowhere for it to be
     wrong. See foundations/layout.html#crossover-census.

     This register accounts for THRESHOLDS only — the numbers. Which
     elements are query containers, and what those containers are NAMED,
     is a separate axis and is documented where the container is
     declared, not here. Worth saying because the two interact: a named
     container changes which element a threshold resolves against
     without changing the threshold, so a complete register can still
     leave you guessing if you came looking for the wrong axis.
     ------------------------------------------------------------------ */

  /* ------------------------------------------------------------------
     8. GEOMETRY — the isometric system
     2:1 isometry: a square rotated 45° and squashed to 50 % height.
     ------------------------------------------------------------------ */
  --angle-a:        63.43deg;
  --angle-b:        26.57deg;
  --angle-neutral:  45deg;
  --angle-square:   90deg;

  /* The horizontal run of an --angle-a edge across one viewport height.
     tan(90° − 63.43°) = 0.5, so a steep isometric edge drawn from the top of
     the viewport to the bottom arrives half a viewport height to the side.
     This is the one place the isometry is resolved against the viewport
     rather than against an object — a page-wide edge has no bounding box of
     its own to be built on. It is a token rather than a literal so that
     moving --angle-a moves the edge with it.
     Used by the page transition. → foundations/transitions.html */
  --angle-a-run:    50vh;

  /* THE ORDER IS THE WHOLE TRANSFORM, and it used to be the other way round.
     A CSS transform list composes right to left in the element's own axes:
     `rotate(45deg) scaleY(0.5)` squashes FIRST, along the square's own
     vertical, turning it into a 2:1 rectangle — and then tilts that rectangle
     45°. What came out was a slab with two long edges and two short ones,
     sitting at 45°, whose bounding box was still 1:1. An isometric projection
     squashes along the SCREEN's vertical, which means the rotation has to
     happen first.

     Measured on a 200 px square, both orders:

                                 edges                     angles      box
       rotate() scaleY()   200 / 100 / 200 / 100      ±45°             1:1
       scaleY() rotate()   158.11 × 4, all equal      ±26.57° / 63.43° 2:1

     The second line is the tile this brand is built on: four equal edges, the
     2:1 in the width-to-height rather than in the edges, and every edge landing
     on --angle-b. The first produces no 26.57° anywhere on the shape, which is
     the tell — the section header two lines up says the 2:1 ratio is where the
     brand angles come from, and the old value could not produce them.
     → foundations/geometry.html */
  --iso-transform:  scaleY(0.5) rotate(45deg);
  --iso-ratio:      2 / 1;

  /* Stroke weights — 1 pt / 2 pt / 4 pt / 8 pt. Screen work uses 1–2 px. */
  --stroke-1: 1px;
  --stroke-2: 2px;
  --stroke-4: 4px;
  --stroke-8: 8px;

  /* Four sanctioned line types. Dash ratios from Formsprache > Linien. */
  --dash-solid: 0;
  --dash-2-1:   4px 2px;
  --dash-1-2:   2px 4px;
  --dash-1-4:   1px 4px;

  /* ------------------------------------------------------------------
     8a. THE PRESENCE LADDER — the four line types read as one axis

     The Linien plate is drawn twice. Its top half is the reference strip
     every system copies: four types, four weights, side by side, equals.
     Its BOTTOM half is the part nothing here had implemented — the same
     object, a rhombus with an ellipse in it, drawn four times across the
     plate on one dotted axis, solid and small at the left and 1-4 and huge
     at the right. Four line types, but one figure. Read left to right it
     is not a specimen sheet at all: it is a thing coming into being, or
     going out of it.

     So the four types carry TWO registers, and they do not conflict:

       use    where a line appears — a divider, an axis, a construction
              line. This is what foundations/geometry.html catalogues, and
              it is a property of the line.
       rank   how present the thing it draws is. Only ever legible when
              the SAME object is drawn more than once, which is why no
              static page has needed it and every state does.

     The ladder is the second register, named. It has one hard rule: an
     object may only move ALONG it, never across it — a thing that is not
     here yet is the same drawing as the thing that is, one rung down. The
     moment a pending state is drawn as a different shape (the grey slab
     every other system reaches for) the reader is being shown an object
     that will never arrive, and the fill has beaten the contour.

     --presence-absent is the ghost line: construction geometry, a footprint,
     a cell that says where a thing stands rather than what it measures. It
     was already doing exactly this job on .cf-plot__cell before it had a
     name. ------------------------------------------------------------ */
  --presence-absent:  var(--dash-1-4);
  --presence-faint:   var(--dash-1-2);
  --presence-near:    var(--dash-2-1);
  --presence-present: var(--dash-solid);

  /* Two corners, and 2 px is the one you reach for. Counted off the Figma
     export rather than eyeballed off the JPGs: 170 <rect> elements across
     landing-page-full.svg and ueber-uns-full.svg, of which exactly ten carry
     a radius, and every one of those ten is rx="2".

       logo lockup      152.09 x 35.25   rx 2      (a pill would be 17.6)
       nav bar          417 x 41         rx 2      (a pill would be 20.5)
       hero CTA         189 x 48         rx 2
       footer CTA       396 x 46         rx 2
       ueber-uns CTA    228 x 46         rx 2
       team photo       200 x 300        rx 2      (x5)
       blog container   1277 x 524       rx 2

     The other 160 are square. The split is not arbitrary: what carries the
     2 px is an OBJECT — something with a body of its own, a control, a plate,
     a photo. What stays square is STRUCTURE — the hairline frames, the
     section rules, the grid cells that are drawn by their neighbours' borders
     rather than by a box of their own.

     Nothing in the source is a pill. --radius-pill used to exist here at
     999px and was applied to the logo lockup and the nav bar, which was a
     reading of the compressed mockup JPGs, not of the vectors. It is gone;
     do not reintroduce it. If a rounded control is needed, it is 2 px.

     2 px is small enough that it does not read as "rounded" at all — it reads
     as a corner that has been cut rather than left raw, which is the whole
     intent. Anything above --radius-sm on a control is a mistake. */
  --radius-none: 0;
  --radius-sm:   2px;
  --radius-full: 50%;

  /* ------------------------------------------------------------------
     8b. THE FIELD — the lattice as a surface

     The manual's Isometrie-Raster plate is not a diagram of the isometry.
     It is a GROUND: a rhombic lattice running edge to edge, with the cube,
     the cylinder and the dotted wireframe standing on it. Every spatial
     object in this brand is drawn on that lattice, and the site has said
     since its first line that it stands on "a data space, not a sheet of
     paper" — while drawing no space at all. The field is the space.

     ONE RHOMBUS, TILED. The cell is --field-unit wide and half that tall,
     which is what 2:1 means and is the same cell foundations/illustration
     .html builds objects out of. 96 x 48 at rest: both are rungs of the
     space scale (--space-24 and --space-12), so the lattice is
     commensurable with the layout rather than merely near it.

     DRAWN BY TWO GRADIENTS, ONE STOP LIST. A repeating-linear-gradient
     paints bands PERPENDICULAR to its own gradient line, so the two
     families of lattice edges — screen slope +1/2 and -1/2, the 26.57 deg
     rhombus edge read off each axis — are drawn by gradient lines at
     26.57 deg and at its mirror, 180 - 26.57 = 153.43 deg. Both are
     sanctioned; the mirror is written as the subtraction rather than as
     153.43deg so it cannot drift away from --angle-b.

     Same split as the foil: --field-stops is the material and the two
     rakes are the angles it is crossed at, so the stop list exists once.

     THE STEP IS DERIVED, NOT CHOSEN. Perpendicular distance between two
     adjacent parallel lattice lines is the cell width over sqrt(5) —
     96 / 2.2360679775 = 42.93 px at rest. Getting this wrong by a little
     is not a slightly wrong grid, it is two families of lines that no
     longer meet at a lattice point, so it is computed rather than typed.

     1 PX AT EVERY UNIT, which is why this is a gradient and not a tiled
     SVG. The lattice tiles perfectly as an X in a --field-unit by
     half-unit box, and a background-size on that tile would be shorter to
     write — but scaling the tile scales its stroke, and "1 px contour at
     every size" is a device pixel everywhere else in this system
     (vector-effect: non-scaling-stroke on every drawn thing). A gradient
     stop measured in px stays 1 px however large the cell is set.

     --field-edge IS THE ANTIALIAS. A hard stop on a diagonal is a hard
     diagonal edge: gradients are evaluated per pixel and a zero-width
     transition has no partial coverage to give, so the hairline stair-
     steps. Half a pixel of ramp either side hands each pixel a fraction
     and the line resolves. It is centred in its period — --field-half —
     so both edges are soft and the wrap from one period to the next
     happens in transparency, where there is nothing to alias.

     10 % BLACK IS A CEILING, NOT A TASTE. The field is decoration that
     text is allowed to cross, so its ink is capped by the worst thing
     that has to survive crossing it: --text-secondary at 14 px, which
     owes 4.5:1 and sits on CF-Grau at the top of the page wash.

       field ink   surface under the line   --text-secondary   verdict
       0.10        #BABABA                  4.71:1             holds
       0.12        #B6B6B6                  4.51:1             no margin
       0.16        #AEAEAE                  4.15:1             fails

     Black body copy on the same line is 10.8:1, and the white end of the
     wash is 7.33:1, so CF-Grau under the darkest line is the whole story.
     Inverse: white 0.10 on Schwarz puts --text-secondary at 9.46:1.

     Everything the field does after this only ever REMOVES ink — the lit
     variant masks it, nothing composites over it — so the floor above
     bounds every state of it by construction and none of them needs
     measuring again. Same argument the swinging foil makes.
     ------------------------------------------------------------------ */
  --field-unit: var(--space-24);   /* 96 — the rhombus is 96 x 48 */
  --field-line: var(--stroke-1);
  --field-edge: 0.5px;
  --field-step: calc(var(--field-unit) / 2.2360679775);
  --field-half: calc(var(--field-step) / 2);
  --field-ink:  rgba(0, 0, 0, 0.10);

  --field-rake:        var(--angle-b);
  --field-rake-mirror: calc(180deg - var(--angle-b));

  /* THE STOP LIST AND THE FINISHED IMAGE ARE COMPOSED IN base.css, ON
     .cf-ground::before, AND NOT HERE. Not a filing preference — the file
     already carries the reason twice. A custom property is substituted at
     computed-value time on the element that DECLARES it, so a --field-stops
     written at :root would resolve --field-ink against :root's black and
     inherit that literal into every inverse section, where a black lattice
     on Schwarz is a lattice nobody can see. Measured exactly that way
     before it was moved.

     Composing on the class is the same shape .text-foil uses for
     --foil-image, for the same reason, and --foil-stops can live up here
     only because none of its stops is theme-dependent. This one's is. */

  /* THE POOL OF LIGHT. The lattice is a floor and a floor is only visible
     where something lights it. The lit variant masks the field with one
     2:1 ellipse — the isometric circle, the same shape .material-bloom--iso
     is — and slides it down the surface as the reader passes.

     A LENGTH, NOT A PERCENTAGE, for the reason --scrim-reach is one: the
     pool is a fixed object in the world and the section it lies on is not.
     72 x 36 rem is 1152 x 576, one 1440-frame content column wider than the
     1280 container, and closest-side on a box that shape gives radii of
     36 and 18 rem — 2:1 by construction rather than by a second number.

     Below about 1150 px of viewport the pool is wider than the screen, so
     the reader sees a band of light crossing the floor rather than a pool
     on it. That is what a light source further away than the room is wide
     actually does, and it is the reason the pool is not clamped: clamping
     it would keep the ellipse on screen and take the light source into the
     room with it.

     0.45 at 46 % is the plateau: full ink in the middle third, then out.
     The mask multiplies, so the lit field is everywhere at or below the
     0.10 the floor above was measured at. */
  --field-pool-w: 72rem;
  --field-pool-h: 36rem;
  --field-bloom: radial-gradient(closest-side,
                   #000                 0%,
                   rgba(0, 0, 0, 0.45) 46%,
                   transparent        100%);

  /* ------------------------------------------------------------------
     8c. THE FOUND STATE — the presence ladder applied to attention

     THE BRAND IS NAMED AFTER A KEYBOARD SHORTCUT and the system had no
     drawing for a match. Control-F is find-in-page; the site's whole
     argument is that data can be found in a space rather than read off a
     sheet. Every register the platform gives an author for "this is the
     thing you were looking for" was either unstyled here or styled by the
     browser: <mark> was UA yellow, ::target-text was UA yellow, and
     ::highlight() had no names registered at all. One line of the system —
     ::selection — was doing the whole job.

     WHAT THE DRAWING MAY BE MADE OF IS DECIDED FOR US, and that is the
     useful part. Highlight pseudo-elements take a closed property list, so
     that the browser can repaint a moving highlight without relayout: per
     css-pseudo-4 § 3 it is color, background-color, text-decoration and its
     longhands, text-shadow, and the text stroke/fill properties. No border.
     No outline. No padding. NO BACKGROUND-IMAGE — which means no --gradient-
     foil, no --gradient-light, no member of the light family can enter a
     highlight at all. The one place in this system where a surface may not
     carry a gradient.

     So the found state is drawn from the two things that are left, and they
     happen to be the two the brand puts first anyway:

       contour   text-decoration-line: underline, --stroke-2, in ink. THE
                 GROUND LINE. In an isometry an object does not have a
                 bounding box; what says where it stands is the lattice edge
                 under it, which is how every object on foundations/
                 illustration.html is placed. A match is a word standing on
                 one.
       light     background-color: --cf-lime, flat, behind the glyphs.
                 Material layer 5 sits under layer 6, so a lime fill behind
                 text is the light layer in its correct place. Lime as a
                 SURFACE is what the manual bans — a plate, a card, a band
                 of chrome — and a word's worth of it is not that.

     UNDERLINE + OVERLINE WAS TRIED FIRST AND IS THE OBVIOUS ANSWER: two
     rules, the top and bottom edge of the cell, the only box a highlight is
     allowed. It was dropped after looking at it. There is no
     text-overline-offset — the underline can be pushed off the descenders
     and the overline cannot be pulled down at all, so it lands on the font's
     ascent, well clear of cap height, and reads as a detached rule floating
     over the word rather than as its opposite edge. An asymmetric band is
     worse than no band. One line, at twice the weight, is the version that
     draws what it means.

     2 px, NOT THE 1 px HAIRLINE, and the reason is a collision rather than a
     preference: a:not([class]) is already a 1 px underline at 0.22em's
     neighbour, 0.2em. A hairline mark and a link would be the same drawing.
     Doubling the weight is the brand's own way out — 1 and 2 px are both
     sanctioned screen strokes — and it is what lets the contour hold the
     boundary on its own (see below).

     TWO RUNGS, WHICH IS WHERE THE LADDER COMES IN. Find-in-page has always
     drawn two states and no system here had named the difference: every
     match, and the one you are standing on.

       every match      contour only. Present, not current. --cf-mark,
                        ::highlight(cf-found).
       the current one  contour + light. --cf-mark--current,
                        ::highlight(cf-found-current), ::target-text.

     Same drawing, one rung apart, which is the ladder's own hard rule (8a):
     an object moves ALONG it and never across it. It also settles the lime
     budget without an exception. One lime moment per screen is a rule about
     composition, and a page with twelve matches on it has ONE lit match and
     eleven contoured ones — so the budget is satisfied by the drawing rather
     than waived for it. The reader's own torch, not a fixture.

     CONTRAST IS CARRIED BY THE INK, NOT BY THE FILL, and this is the reason
     the contour is not optional. The floor a custom highlight owes is two
     figures: 4.5:1 text against the highlight, and 3:1 highlight against the
     surrounding surface. Black on lime is 18.51:1, comfortably clear. Lime
     on CF-Grau is 1.37:1 and on the white end of the wash 1.13:1 — the fill
     CANNOT hold that boundary, at either end of the page. The ground line
     holds it instead: --cf-schwarz measures 13.48:1 on CF-Grau and 21:1 on
     Weiss. That is also the "never colour alone" requirement discharged by
     construction rather than by a second decision, because the rule is what
     makes the mark and the fill is what ranks it.

     THE ELEMENT COULD CARRY A REAL BOX AND DOES NOT. <mark> is an element,
     so .cf-mark could have a 1 px border, 2 px of padding and a --radius-sm
     corner like every other object in the system. ::target-text cannot, ever.
     Two drawings for one meaning is worse than one constrained drawing, so
     the element is held to the pseudo-element's means. A match looks the same
     wherever the match came from — the server rendering search results, the
     Custom Highlight API scanning the DOM, or the URL arriving with a text
     fragment on it.

     --found-offset pushes the rule clear of the descenders, with
     text-decoration-skip-ink: none holding it unbroken underneath them, so it
     reads as a line the word stands on rather than as a line drawn through
     the bottom of it. 0.22em against a:not([class])'s 0.2em: the extra notch
     is what a 2 px rule needs to sit at the same optical distance a 1 px one
     does at 0.2em.
     ------------------------------------------------------------------ */
  --found-rule:        var(--border-strong);
  --found-rule-weight: var(--stroke-2);
  --found-offset:      0.22em;
  --found-light:       var(--cf-lime);
  --found-ink:         var(--cf-schwarz);

  /* ------------------------------------------------------------------
     9. MATERIALS — "Formsprache > Materialien"
     ------------------------------------------------------------------ */
  /* Milchglas — frosted glass. It exists to calm a complex graphic beneath
     it, never to sit on a flat surface, where it is only a grey box.

     Two tints, because glass does two different jobs:

     --surface-glass       BEARING glass. Text sits directly on it, over a
                           backdrop we do not control — the hero video. 46 %
                           white is the floor at which black type still clears
                           4.5:1 composited over a *pure black* backdrop, so
                           the guarantee holds for every frame of any video
                           rather than for the frames someone happened to
                           check. Measured against the hero poster's darkest
                           blurred patch it lands at 6.5:1.
                           This value has almost no room to move. 46 % white
                           over pure black is 4.58:1 unquantized, but what
                           actually renders is rgb(117,117,117), and that is
                           4.56:1 — clearing 4.5 by 1.3 %, not 1.7 %. Drop the
                           tint to 0.45 and it renders rgb(115,115,115) and
                           lands at 4.43:1, which fails. (Both figures
                           quantized, which is the whole point: 0.45
                           unquantized computes 4.41:1, and quoting that here
                           would repeat the error this paragraph exists to
                           correct.)
                           Lowering it for a lighter, glassier look breaks AA
                           on the worst backdrop while every backdrop anyone
                           thinks to check still looks fine. Raise it or leave
                           it. The blur cannot rescue or damage this either
                           way: a blur of black is black and saturate(1.08) of
                           black is black, so the worst-case frame is the
                           whole story.
     --surface-glass-thin  BEARING GLASS OVER A BACKDROP THE PAGE OWNS. The 46 %
                           above is the floor for a backdrop we do NOT control:
                           it is the tint at which black type still clears
                           4.5:1 over a pure black video frame. That premise is
                           not universal. Where the thing behind the glass is
                           the page's own field -- a lattice on the wash, whose
                           darkest pixel is a hairline -- the floor is whatever
                           the measurement on THAT surface says, and 46 % is
                           then a plate rather than a pane.
                           30 % measured on the Expertise stage: the plate
                           composites to rgb(228,229,230) at its darkest pixel,
                           where --text-secondary reads 6.73:1, the ink foil's
                           worst stop 6.01:1 and the ruled question 15.45:1.
                           THE MEASUREMENT IS THE PERMISSION, not this number.
                           Reach for this tint only over a backdrop you can
                           name, and re-measure the type on it; over anything
                           arbitrary the 46 % floor is the one that holds.
     --surface-glass-veil  VEIL glass. Carries nothing but opaque objects, so
                           it stays almost clear and lets the blur do the work.
                           Measured off the navigation band in both mockups:
                           CF-Grau reads 207 above the band and 208 inside it.
     --surface-glass-solid The opaque stand-in when the browser cannot blur, or
                           when the reader has asked for less transparency.

     Blur radius is 16 px. Larger reads no better and costs more.

     THE BUDGET IS PER PAGE, AND IT IS TWO. Never a third composited beside
     them — blurred area is the whole cost of this material, and the count of
     simultaneously blurred layers is the first thing to cut when scrolling
     stutters. Simultaneously is the word that matters and it was already the
     word here; the surface that finally tested it is the last of the four
     below.

     FOUR SURFACES CARRY THE BLUR, which is not the same number and the
     distinction is the point. This comment used to read "two blurred layers
     exist and no more: the navigation band and the hero button", written when
     that was a complete list. It stopped being one when .cf-info-card--glass
     shipped, and a stale enumeration is worse than none, because the next run
     in this lane reads it as permission to add the surface it thinks is
     missing. The four, and where each is allowed:

       .cf-nav::before          every page. Always one of the two.
       .cf-btn--glass           the hero CTA. One per page.
       .cf-info-card--glass     one card per page, and only a card standing on
                                something the reader can still see through it.
                                Expertise is the exception and says why: outside
                                its pinned gate there is no plate, so the four
                                copy cards carry the material themselves and two
                                of them are on screen together at every seam.
       .act-rail::before        the landing page's chapter rail, open. Only ever
                                composited beside the nav — see below.

     So a shipping page spends the nav plus at most one of the other three, and
     no shipping page spends more AT ONCE. The landing page holds three of them
     and that is not the budget moving: the rail's plate paints only on hover or
     focus, which needs the .is-live the rail only has while the acts own the
     viewport, 4 000 px below the hero button it therefore cannot appear beside.
     The argument is a named page allowance in the checker (PAGE_BUDGET), and it
     expires by failing the moment the page drops back under two.

     AND THE SENTENCE ABOVE WAS AN ARGUMENT THE STYLESHEET WAS NOT MAKING. For
     three releases "paints only on hover or focus" described the OPACITY and
     nothing else: .act-rail--glass::before declared backdrop-filter
     unconditionally, so the material was live at every scroll position the rail
     was live at, and what hid it was a layer at alpha zero. Whether a compositor
     throws that pass away is not in any specification, and this file, the
     checker's PAGE_BUDGET entry and the verdict column on materials.html were
     all arguing from a premise none of them had asked the CSS for. The blur is
     declared in the open state now and held through the fade out by a delayed
     zero-duration transition, so the sentence is true of the material as well as
     of the plate. check-glass-budget.py's ninth claim holds the general form —
     no counted glass layer is invisible at rest — and it is written against this
     surface because this surface is where it was found.
     → foundations/materials.html, "Cost", and acts.css .act-rail--glass::before

     THERE ARE TWO SUCH ALLOWANCES NOW AND THE SECOND ONE IS NOT FREE, which is
     why it is spelt out here rather than left in the script. patterns/
     expertise.html spends the nav and the plate on its pinned tier — two, this
     rule unchanged — and below that gate the plate is gone and its four copy
     cards carry the material one each, standing on the same full-bleed lattice
     the plate was there to calm. They are one column, so a seam puts two of
     them and the bar on screen together: measured at 375 x 812, 463 px of every
     1 050 px step. A third composited layer for about 44 % of that section's
     scroll, on a phone. It is a design ruling and not a measurement — the
     no-blur stand-in on those cards costs nothing and was what it was weighed
     against — so it is recorded in both places rather than only in the one that
     fails.

     DO NOT TAKE ANY OF THAT ON TRUST AND DO NOT COUNT IT BY HAND:
     scripts/check-glass-budget.py already enforces it, and it derives what
     counts as glass by reading every backdrop-filter rule out of the shipping
     stylesheets rather than from a list. A fifth frosted surface therefore
     enters the budget by existing. The list above is prose for a reader; the
     script is the thing that fails.

       python3 scripts/check-glass-budget.py

     It also holds the two rules underneath this one: every backdrop-filter
     reads --glass-blur rather than writing its own radius, and nothing
     scroll-driven moves ON a blurred layer. That second one is why every
     surface with a travelling light puts it on an unblurred 1 px rim beside the
     sheet instead of on the sheet — see .cf-nav::after, .cf-btn--glass::before
     and .cf-info-card--glass::before, which are the same construction three
     times. .act-rail--glass::before is the fourth surface and has no such
     light, for the same rule read the other way: it is open exactly while the
     reader is scrolling through the acts, so a band crossing it would
     re-rasterise the blur through the whole range it is visible. It takes
     --glass-border and stays still. It is also the one surface a page opts
     into by name — the chapter rail on patterns/expertise.html spends its
     second layer on the lectern in the four-field stage instead, and takes the
     same plate with --surface-glass-solid under it.

     THE NAV AND THE HERO BUTTON DO OVERLAP, unavoidably — the CTA is on the
     first screen and the bar is sticky, so the button passes under it on the
     way out. Measured at 1280x900 on the landing page: the sheet occupies
     0..108 of the viewport (84 px bar plus the 24 px lookahead) and the button
     intersects it from scrollY 628 to 776, sitting entirely inside it from
     672 to 732. About 150 px of scroll with two stacked backdrop-filters.

     This is stated because an earlier version of this comment claimed they
     never overlap, and a later run would have planned against a guarantee
     the system does not have. It costs nothing measurable — sweeps through
     the overlap window and through clear page produce the same median
     (16.7 ms) and overlapping p95 ranges. */
  --glass-blur:       blur(16px) saturate(1.08);
  --glass-border:     1px solid rgba(255, 255, 255, 0.55);

  /* THE REFRACTION LINE IS GONE, and this is the record of it rather than the
     token. A 1 px dark ring — black 55 %, drawn one pixel inside the lit rim —
     was added to .cf-btn--glass and .material-glass to give a glass perimeter
     a boundary the rim alone cannot hold. It came off the material first and
     the button second, both by design ruling: THIS BRAND DOES NOT OUTLINE
     GLASS. A black line round a lime-gradient plate is the first thing anybody
     sees, and no contrast argument survives being the ugliest mark on the
     page.

     What the line was answering is real and is still open. .cf-btn--glass is
     the primary call to action, it stands on the hero video, and its plate
     measured 1.07-1.31:1 against that artwork on every edge at every desktop
     width, against a 3:1 requirement for a control boundary. The label was
     never in question — 16.8:1 — so what is lost is the plate's claim to a
     measured boundary, not the button's legibility.

     KEEP THE PART THAT GENERALISES. Nothing painted against a video can be
     guaranteed: black holds only where the backdrop is light, white only
     where it is dark, and a frame can be either. So a control on artwork has
     exactly two honest boundaries, and neither is a line —

       an opaque plate, which holds 16:1 against every frame and is what the
       mobile navigation menu already does for the same reason; or

       no claim at all, which is where this button now sits, identified by
       its label, its arrow, its lime end and its specular rather than by
       its perimeter.

     Whoever picks one of those is making a design decision, not restoring a
     token. Do not reintroduce the ring. See foundations/materials.html,
     "A control's edge", which carries the measurements. */

  /* How far a glass sheet reaches past its own edge so that content
     arriving from outside is already inside the blur. backdrop-filter only
     samples what is directly behind the element, and the sample that
     matters is one blur kernel deep — reaching further blurs more pixels
     every frame for no visible gain. 16 px radius, 24 px of reach. */
  --glass-lookahead: 24px;

  /* The lit edge of a glass sheet — white at the rim, drifting through Glas
     into Sky as it runs out. This is the foil, at one pixel: hue travel
     inside a narrow luminance band, not a shine.

     ON BOTH GLASS SURFACES, which it was not until now. The navigation sheet
     ends at it full width (.cf-nav::after); the hero button ends at it too,
     windowed to the left 62 % of the plate so the Sky tail expires before
     the face gradient's lime begins. The window is the whole of the
     adaptation — same stops, same order, same alphas — because a plate with
     lime at one end cannot carry the material's chroma at that end as well.
     See .cf-btn--glass for the measurement the 62 % comes from.

     Anything windowing this token has to size the LAYER and leave the
     gradient alone. Rewriting the stop positions to fit a narrower box would
     fork the family into two edges that merely resemble each other.

     It resembles the foil in hue travel only, NOT in interpolation error,
     and the difference is worth a number because "it is the foil" invites a
     later run to move it into the oklab @supports block with the rest of the
     family. Max ΔEok between the sRGB and oklab paths, computed per leg:

       lime  -> Glas     0.04430     the legs the family corrects for
       Glas  -> Sky      0.00148     --glass-edge
       Weiss -> Glas     0.00045     --sheen-panel
       Glas  -> CF-Grau  0.00073     already left on sRGB deliberately

     --sheen-panel diverges LESS than a leg the system has already decided
     is not worth correcting, and --glass-edge is thirty times below the lime
     legs. Neither carries lime, which is where the two paths actually part.
     They belong in sRGB; leaving them here is a decision, not an oversight.
     Composited at their real alphas the numbers are a third of that again —
     --glass-edge peaks at 0.00049 over CF-Grau, --sheen-panel at 0.00019.

     THE ARC IS THE OTHER CORRECTION AND IT DOES APPLY HERE, which the
     paragraph above does not say and was read for a year as though it did.
     The path question is "sRGB or oklab", and the answer for this leg is the
     one just given: they agree to 0.00148 and either is fine. The ARC
     question is "straight line or polar", and BOTH straight lines cut the
     same corner — see THE ARC under --foil-stops. Glas -> Sky turns 64.6
     degrees of hue at nearly constant chroma, so the chord between them falls
     inside the circle they sit on and the middle of this edge greys out
     exactly where it is supposed to be turning.

     THE ALPHA IS NOT AN EXCUSE, and that is the part that took a measurement.
     The reason this leg went uncorrected is that it carries alpha and
     check-gradient-family.py exempted a translucent stop, on the argument
     that "what such a stop renders as depends on what is behind it". True of
     the colour and false of the PATH. A gradient interpolates premultiplied,
     so over any fixed backdrop the pixel is

         R(t) = lerp(C1a1, C2a2) + bg * (1 - lerp(a1, a2))

     which is affine in t: a straight line between the two composited ends, on
     every surface this is ever drawn over. The chord is therefore a fact
     about the stop pair, and the sag is there on all of them —

       chroma at the midpoint, against the polar arc through the same two ends
         over CF-Grau  -16.9 %    over Weiss  -20.1 %    over the wash  -15.2 %
         inverse, over Schwarz  -22.4 %    over anthracite  -11.5 %

     against the 16.0 % the foil's own Glas -> Sky leg was corrected for. The
     exemption was hiding a defect the size of the one the arc exists to fix,
     on the one lit edge in the system drawn the full width of the viewport.

     SO THIS LEG CARRIES ITS WAYPOINT, at 67 % — the midpoint of 54 % and
     80 % — and its value is the leg's own polar midpoint, derived the way the
     other four are and not picked. Alpha changes only the arithmetic, and CSS
     Color 4 states it: in a polar space the rectangular components are
     premultiplied and hue is not. So L and C are weighted 0.6 / 0.4 by the
     two alphas and the hue sits at a plain half, which is exactly what
     color-mix(in oklch shorter hue, <this leg's two stops>) returns —
     oklch(0.8402 0.0635 211.67 / 0.349) — and the alpha is the mean, because
     alpha interpolates linearly and a waypoint carrying anything else would
     put a step in the ramp's transparency at the one position it was added to
     smooth.

     Max dEok from the true polar arc over the leg, before and after:

       over CF-Grau  0.00513 -> 0.00234      over Weiss  0.00493 -> 0.00253
       over the wash 0.00483 -> 0.00230      inverse, Schwarz 0.00539 -> 0.00291

     The same order the foil's own waypoints bought (0.00722 -> 0.00212), and
     nothing else moves: both ends, both alphas and the 16 % and 100 % stops
     are untouched, so every composited figure in this block — the 0.916 /
     0.878 / 0.819 band, the 152 degrees across it, the 62 % window on
     .cf-btn--glass — holds unre-measured. The waypoint sits inside the band it
     splits at L 0.840 and adds no hue the leg was not already travelling
     through.

     It is a literal, like the four in the two foils, and it carries their
     trap: recompute it if Glas or Sky moves. check-gradient-family.py checks
     the arc on translucent legs now and re-derives this from --cf-glas,
     --cf-sky and the two alphas rather than trusting the hex.

     DO NOT "correct" the Sky stop below to sky-300 for consistency with the
     foil. It looks like drift — the foil is built on sky-300 at OKLab L
     0.865 to hold its 0.820-0.945 band, and this uses core Sky at L 0.735 —
     but the comparison has to be made composited, because these stops carry
     alpha and the foil's do not. At their declared alphas over CF-Grau these
     three land at Weiss 0.916, Glas 0.878, Sky 0.819: a span of 0.098 inside
     the foil's own 0.125, with 152 degrees of hue across it. A family member.
     Swapping in sky-300 takes the composited Sky to 0.857 and collapses the
     span to 0.059, which is where iridescence becomes a flat tint — the same
     failure the foil's note describes for the 500s, running the other way. */
  --glass-edge: linear-gradient(90deg,
                  rgba(255, 255, 255, 0)    0%,
                  rgba(255, 255, 255, 0.42) 16%,
                  rgba(197, 235, 226, 0.42) 54%,
                  /* arc waypoint, Glas -> Sky. See THE ARC IS THE OTHER
                     CORRECTION above; recompute if either end moves. */
                  rgba(154, 214, 227, 0.35) 67%,
                  rgba(114, 176, 226, 0.28) 80%,
                  rgba(255, 255, 255, 0)   100%);

  /* The specular that crosses the rim as the page scrolls. It is the only
     motion the navigation sheet has, and — since the hero button and the
     info-card plate grew rim layers of their own — the motion every lit glass
     surface in the system shares.

     ON ALL THREE, on the same band, at the same two endpoints, and this
     sentence named two of them for two releases after the third shipped.
     .cf-nav::after carries it across the full width of the bar, scrubbed by
     the document, because a bar is a plane the whole page moves under.
     .cf-btn--glass::before carries it across the rim's own 62 % window,
     scrubbed by the button's passage through the viewport, because a button
     is one object the reader carries up the screen once.
     .cf-info-card--glass::before carries it across the plate, scrubbed the
     same way — or by the pin's own timeline where the plate is pinned and has
     no passage of its own. Same band, same parking positions; only the
     container and the clock differ. The button's copy exists because every
     other response that plate makes is gated on a pointer, and a touch reader
     was getting none of them.

     ACHROMATIC ON PURPOSE, and the reason is already settled elsewhere in
     the system rather than decided here: a specular is the source reflected,
     not the surface coloured. --glass-edge above carries this material's hue
     travel and the specular carries none — one lit edge with hue, one
     specular without. That is exactly the division .cf-btn--glass makes
     between --glass-edge and --cf-specular, and this is the same division
     one pixel tall. A Glas tail would put the sheet's chroma in two places
     at once. Do not "fix" it.

     Transparent at both ends so the band arrives and leaves without a seam,
     and 40 % of the bar wide — narrow enough to read as light passing rather
     than as the rim brightening. White 0.45 is the hero button's specular
     value, near enough; over --glass-edge's own 0.42 white it peaks around
     0.68 composited, on one pixel.

     IT IS PARKED OFF THE LEFT EDGE AT REST, and that is what makes the
     un-animated state byte-for-byte the drawing that is there today: no
     scroll timeline, no reduced-motion preference honoured, no support at
     all, and the only thing on the rim is --glass-edge unchanged. The light
     is an enhancement on top of the designed state, never a replacement for
     it. Both consumers declare that parked position themselves rather than
     leaning on the keyframe's `from`, so the guarantee survives the
     animation being switched off by any of its four doors.

     IT TOUCHES ONE ROW, measured on the button rather than argued. The
     rendered plate with the band running, differenced against the same plate
     with it parked, landing page at 1280 px: of 256 x 48 pixels, 54 differ by
     more than one level and all 54 are on row 0, max delta 13. Rows 1 to 47 —
     the whole label and every pixel of bearing tint under it — come back
     byte-identical. A 1 px strip on the border box cannot reach a floor set
     by background-color, and this is the measurement that says so. */
  --glass-rim-light: linear-gradient(90deg,
                       rgba(255, 255, 255, 0)    0%,
                       rgba(255, 255, 255, 0.45) 50%,
                       rgba(255, 255, 255, 0)   100%);

  /* WHERE THAT BAND STANDS AND HOW FAR IT TRAVELS — three numbers that were
     written out three times, plus twice more inside three pairs of keyframes,
     and had to stay equal to each other by hand.

     The gradient above is the material. These are its geometry, and geometry
     is exactly what the comment two blocks up says the three surfaces share:
     "same band, same parking positions; only the container and the clock
     differ". A fact three rules restate is a fact three rules can disagree
     about — the same argument --glass-blur already won for the radius, one
     property over.

     THE TWO ENDPOINTS ARE ARITHMETIC, NOT TASTE, and the arithmetic is what
     binds them to the width. A background layer positioned at P is offset by
     P x (container - image), so a band b wide sits with its left edge at
     P(1-b)W and its right edge at P(1-b)W + bW. Fully off the left needs
     P <= -b/(1-b); fully off the right needs P >= 1/(1-b). At b = 40 % that
     is -66.7 % and +166.7 %, and the two values below clear both with a
     little air. Move the band and both endpoints move with it — which is now
     a check rather than a warning: check-glass-budget.py re-derives both
     bounds from the band and fails a pair that no longer clears the box, and
     fails a rim rule that writes any of the three as a literal.

     They are percentages without a unit pair on purpose: each consumer writes
     `var(--glass-rim-park) 0` and supplies its own vertical, because the rim
     layer is one pixel tall on every surface and 0 is the only value it can
     have. Keeping the y out of the token is what lets the button — whose rim
     is a single layer, where the bar's and the plate's are two — read exactly
     the same names. */
  --glass-rim-band:  40%;
  --glass-rim-park:  -70%;
  --glass-rim-cross: 170%;

  /* Light crossing a flat panel — the hover state of any surface that is
     *not* glass. Parked in its own transparent half and slid across on
     hover, so the panel reads as lit rather than tinted. Never lime: the
     screen only gets one of those. */
  --sheen-panel: linear-gradient(90deg,
                   rgba(255, 255, 255, 0)    0%,
                   rgba(255, 255, 255, 0)   50%,
                   rgba(255, 255, 255, 0.5) 78%,
                   rgba(197, 235, 226, 0.34) 100%);

  /* AND THE LIT PANEL WAS THREE COPIES OF THREE NUMBERS. The same fact the
     three tokens above rescued the rim from, one material over. The gradient
     is the material; this is its geometry, and it was written out by hand on
     .cf-accordion__summary, .cf-blog-card and the register — a `200% 100%`
     size, a `0 0` park and a `100% 0` answer, three times each, on three
     surfaces that are never on screen together. The rim's own comment two
     blocks up says why that is a defect rather than a repetition: a fact
     three rules restate is a fact three rules can disagree about, and no
     screenshot of any one of them would show it.

     THE PARK IS ARITHMETIC, AND ITS MARGIN IS ZERO. The sheen slides rather
     than crosses — it arrives and stays for as long as the pointer does — so
     its two endpoints are the image's own two ends, 0 % and 100 %, and none of
     claim 8's off-canvas arithmetic applies. What is not free is the rest
     state. The image is b times the box, so at the park the box shows the
     image's first 1/b, and the panel is clean there ONLY IF the gradient's
     transparent head reaches at least that far. At b = 200 % the window is
     50 % and --sheen-panel's head ends at exactly 50 %: the guarantee holds,
     with nothing to spare, in both themes.

     That is a tighter tolerance than the rim's, which clears its bounds "with
     a little air", and it fails in the direction nobody reviews. Narrow the
     band, or move the 50 % stop one point right, and every lit panel in the
     system ships a permanent white wash across its text at rest — on the page
     wash, under prose, with no hover involved and no frame of animation to
     catch it. The park is also what the reduced-motion reader sees, because
     the durations collapse to 1 ms and the transition stops being one.
     check-glass-budget.py's tenth claim re-derives the window from the band
     and reads the head out of --sheen-panel itself, in every theme that
     declares one, rather than comparing either to a table.

     Unitless, for the reason --glass-rim-park is: each consumer writes
     `var(--sheen-park) 0` and supplies its own vertical, which is 0 on every
     surface because the light crosses horizontally on all of them. */
  --sheen-band:  200%;
  --sheen-park:    0%;
  --sheen-cross: 100%;

  /* THE GLOW IS THE RAMP, NOT A LIME CLOUD.
     A glow is light spilling past the thing that is lit, so it is subject to
     the same rule as every other lime area: it is never flat. See
     foundations/colors.html, "Lime is never flat".

     A drop-shadow takes one colour, so the falloff is built from two of them —
     lime tight to the shape, Glas spreading past it. The composite shifts hue
     as it spreads, which is what light does and what the three rakes already
     say in a different form. A single lime shadow does not: its alpha falls
     off while its hue does not, and at any usable radius it reads as a yellow
     cloud around the mark rather than as light coming off it.

     --glow-r is the reach, set per element. Lime takes --glow-core of it, so
     the two stops keep their ratio at every size. The old --glow-lime was a
     single flat shadow at 32 px, had no consumer in any stylesheet, and is
     gone.

     THREE INGREDIENTS AND NO COMPOSED TOKEN, which is this block withdrawing
     a claim it could not keep. --glow-light sat here as the finished filter,
     with var(--glow-r) inside it and "the reach, set per element" written
     alongside — and that sentence was never true of it. A custom property's var()s are
     substituted at computed-value time ON THE ELEMENT THAT DECLARES IT, so
     --glow-light computed on :root with 12px already baked in, and every
     descendant inherited that string. Four consumers set --glow-r and none of
     them ever moved: .cf-icon--light asked for 6, .cf-relay__head for 8,
     .cf-plot__cap for 12 and the prototype's .lp-flow__leaf for 36, and all
     four rendered 12. Measured in Chromium 141.

     So what is published is the parts, and the FILTER is composed where it is
     applied — in a real property, where var(--glow-r) resolves against the
     element using it and the reach finally works. One selector list in
     components.css carries the composition for the system's three; a page
     that draws its own light composes it from these same three names. */
  --glow-r: 12px;                      /* the default reach */
  --glow-core: 0.45;                   /* lime's share of it */
  --glow-lime: rgba(225, 255, 0, 0.5);
  --glow-glas: rgba(197, 235, 226, 0.6);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.10);
  --shadow-lg: 0 24px 64px rgba(0, 0, 0, 0.14);

  /* ------------------------------------------------------------------
     10. MOTION
     Isometric objects rotate and settle; UI moves short and fast.
     ------------------------------------------------------------------ */
  --duration-fast:  120ms;
  --duration-base:  240ms;
  --duration-slow:  480ms;
  --duration-scene: 1200ms;
  --ease-standard:  cubic-bezier(0.2, 0, 0, 1);
  --ease-out:       cubic-bezier(0, 0, 0.2, 1);
  --ease-in-out:    cubic-bezier(0.4, 0, 0.2, 1);

  /* How far an isometric object travels as it arrives. Unitless, in viewBox
     units, because the drawing it moves is in viewBox units: the object travels
     a fixed fraction of its own size rather than a fixed number of screen pixels,
     so the same illustration reads the same at 352 px and at 268 px.

     The number is the RISE. The run is always twice it — that is what makes the
     26.57° brand angle, and writing it as one value keeps the 2:1 provable
     instead of leaving two hand-written numbers that merely happen to agree.
     16 on a 640 viewBox is 2.5 % of the drawing down, 5 % across. */
  --iso-travel:     16;

  /* HOW FAR AN ORBIT TURNS BEFORE IT SETTLES.
     Unlike --iso-travel this one is in SCREEN pixels, not viewBox units, and
     that is not an inconsistency — it is forced. An orbit turns by moving its
     dash pattern along its own path, and under the non-scaling-stroke that
     .cf-iso puts on every contour, BOTH stroke-dasharray and stroke-dashoffset
     are measured in screen pixels. Measured on the shipped card 04 at a 352 px
     render: dashoffset 0 and 5 are pixel-identical, as are 2.5 and 12.5. The
     period is exactly 1 + 4 = 5 px, the --dash-1-4 the orbits are drawn in.

     SO THE TRAVEL MUST BE A WHOLE MULTIPLE OF 5, and that is the whole reason
     this is a token rather than a number in a keyframe. A dashed ring is
     periodic: it settles on the phase the designer drew only if it stops an
     exact number of dashes away from where it started. 60 px is 12 dashes.
     Any multiple of 5 is safe; 63 would leave every orbit three fifths of a
     dash off the source vector, which nothing would report and a diff against
     assets/source/illustrations/ could never show, because the drift is in the
     rendered phase and not in the markup.

     One value for all three rings, so they share a linear speed and therefore
     NOT an angular one — the inner ring turns through more of itself than the
     outer one in the same scroll. That is what stops three concentric rings
     reading as one rigid body being spun.

     AND ONE SPEED NEEDS A CEILING, WHICH THIS TOKEN DID NOT HAVE. The 60 was
     measured on card 04, whose three rings are the largest in the system —
     1 520.53 units round the great circle and 1 171.37 round each 2:1 orbit.
     A share of a ring is what the reader sees, and at 60 px that is one part
     in 25.34 on the outer ring and one part in 19.52 on the tighter two: a
     visible differential, both of them plainly a turn that settles.

     The system has nine orbits, not three. The other six are on Expertise and
     its prototype, and they are an order of magnitude smaller — 103.04 and
     139.83 and 323.90 units. The same 60 px is 0.58, 0.43 and 0.19 of those
     rings. Two of them therefore went round MORE THAN ONCE while the object
     assembled and landed back on the phase they started from, which is not a
     turn that settles; it is a spin, and a spin whose only visible evidence
     is the blur on the way past. Measured across 375 to 1280 px the nine
     orbits spanned 7.2 % to 171.4 % of a turn on one token.

     SO THE TOKEN IS A DEFAULT WITH A BOUND, and the bound is card 04's own
     tightest share rather than a new number: no orbit may travel more than
     one part in 19.52 of its own ring. An orbit that would exceed it declares
     its own travel — the largest whole number of dashes that stays inside —
     the way a part of a built object declares its own --build-dx. The four on
     Expertise declare 5, 15, 5 and 5 px. scripts/check-orbit-turn.py holds
     both halves, and re-derives the 19.52 from the rings that still run this
     default rather than remembering it. */
  --iso-orbit-travel: 60px;

  /* THE WAIT.
     A pending object is drawn at --presence-absent and one light crosses it.
     The period is deliberately far slower than any UI duration above: this is
     not a transition between two states, it is the one thing on screen that is
     honestly still happening, and a fast loop reads as impatience. 2.4 s is
     twice --duration-scene, the slowest thing the system otherwise animates.

     THE LIGHT IS CAPPED AT 0.18 FOR RESTRAINT, and it is worth being exact
     about what that cap is and is not doing, because the obvious worry here
     turns out not to bind. The light layer sits ABOVE the contour layer —
     the manual's own stacking order — so the band passes over the ghost
     contour and lightens it, and a lightened contour is the one thing here
     that could lose contrast in frames too short to catch in a screenshot.
     Worst case is the whitest stop of the band over the blackest contour on
     the page's own CF-Grau, composited the way CSS actually composites it,
     in sRGB and not in linear light:

       alpha   contour        CF-Grau        contrast
       0.18    46 -> 0.0272   216 -> 0.684   9.51:1
       0.28    71 -> 0.0637   241 -> 0.874   6.76:1
       0.50    128 -> 0.2140  231 -> 0.799   3.22:1
       0.52                                  3.03:1   <- the 3:1 floor

     Measured off rendered pixels over eight frames of one period in Chromium,
     the worst inked pixel on the shipped ghost comes out at 9.57:1, which is
     the 0.18 row plus the page wash. So contrast does not choose this number:
     anything under 0.52 clears the floor for a non-text contour, and 0.18 is
     chosen because lime is not the only light in this system that has to stay
     a moment rather than become a surface. What the arithmetic gives is the
     CEILING — raise the token past 0.52 and the ghost fails, quietly. */
  --arrive-period:     2400ms;
  --arrive-light-peak: 0.18;

  /* 11. Z-INDEX */
  --z-base: 0;
  --z-sticky: 100;
  --z-nav: 200;
  --z-overlay: 300;
  --z-modal: 400;
}

/* ==========================================================================
   INTERPOLATION
   Everything above is stated in sRGB, because sRGB is what every browser
   understands and what the designer's files are measured in. sRGB is not,
   however, where light mixes. Interpolating lime → Glas in sRGB dips through
   #D0F38C at the quarter point; the same stops in OKLab give #CFF4AF, which
   holds 0.01 more lightness and visibly more chroma through the crossover.
   The dip is small on any one gradient and cumulative across a page of them.

   So the family is re-declared once, in OKLab, behind a support test. The
   stops do not move — only the path between them.

   OKLab and not OKLCh: OKLCh interpolates hue along an arc, and CF-Grau and
   Weiss are achromatic, so their hue is arbitrary and the arc through them
   is unpredictable. The family is built on both. OKLab is rectangular and
   has no hue to swing. Where a hue path is wanted it is written as an
   explicit stop instead — see --gradient-spectrum.

   Browsers without `in oklab` (roughly 7 %) keep the sRGB declarations
   above unchanged. Nothing degrades; the ramp is only slightly duller.

   SVG CANNOT DO THIS, AND EVERY SVG IN THE SYSTEM STILL HAS TO. There is no
   `in oklab` on a <linearGradient>, so a drawing that carries the family's
   ramp carries the oklab path by hand instead: ONE EXTRA STOP AT 19 % OF THE
   LIME LEG, measured from lime, coloured at the oklab path's value there.
   That is the convention, and it is the same 19 % everywhere so the drawings
   cannot drift apart from each other:

     lime -> Glas    #DBFC60 at 19 %   process cards, 404, illustration docs,
                                       and the signet — which is the one place
                                       this ramp is not in a file at all: see
                                       EMITTERS in check-gradient-family.py
     lime -> Weiss   #E6FF66 at 19 %   cf-wallpaper-feld-dark.svg

   THIS PARAGRAPH IS NOW ENFORCED RATHER THAN MERELY WRITTEN. It held across
   twenty gradients and broke on the twenty-first — patterns/expertise.html drew
   four objects on the near rake and gave all four the mid rake's waypoint
   offset, so 19 % of the leg was painted at 30.3 % of it and the ramp measured
   dE 0.0265 instead of 0.0116. A convention applied by hand has a half-life.
   scripts/check-gradient-family.py walks every gradient the site ships and
   recomputes both numbers — the offset from the leg's own ends, the colour from
   the oklab path — so the two hexes above are re-derived rather than trusted,
   and CI runs it on every pull request. If you add a gradient, add it in this
   family and the script will say so.

   IT WALKS FILES, AND ONE MEMBER OF THIS FAMILY IS NOT IN ONE. The signet's
   ramp is a stop table in javascript, printed into a <linearGradient> at
   DOMContentLoaded, and the same four stops again in python, rasterised into
   every Open Graph plate. Neither is markup on disk, so the brand's own mark
   carried the one gradient here nothing had ever read — while its two copies
   were held to EACH OTHER by check-signet-parity.py, which is the single
   arrangement in which moving a palette value goes stale in both at once and
   still passes. Both tables are now read directly. → EMITTERS in that file.

   19 % is a convention rather than an optimum, and the difference is small
   enough to be worth spending: on the lime-to-Weiss leg the best single
   waypoint sits at 21 % and buys 0.0013 of dE over the family's 19 %.

   ONE STOP AND NOT TWO. A single waypoint takes the lime legs from dE 0.045
   and 0.047 down to 0.012 — about three quarters of the error. A second
   waypoint reaches 0.009 and no further, because what is left is the short
   first segment either side of the first stop, which no stop placed after it
   can straighten. Not worth a stop.

   AN INLINE STYLE ATTRIBUTE IS THE SAME PROBLEM WEARING CSS. @supports cannot
   reach one, so a gradient written into a style="" is on the sRGB path
   whatever this block says. Compose it on a class instead — .material-rake is
   the worked example, and it exists because two documentation pages had done
   exactly this.

   Legs the family deliberately leaves in sRGB are listed with their measured
   divergence at --glass-edge above. Weiss -> Glas is one of them, at 0.00045;
   LIME -> WEISS IS NOT, at 0.04651, and one file spent a while confusing the
   two. See the comment in cf-wallpaper-feld-dark.svg.
   ========================================================================== */
@supports (background: linear-gradient(in oklab, red, blue)) {
  :root {
    /* One line, for the reason the two foils are one line each: the stops are
       a token, so only the path between them changes here. The wash's stops
       were computed on the oklab path in the first place, which is why this
       branch is the one they are exactly right on — see the table above. */
    --surface-page-wash: linear-gradient(180deg in oklab, var(--wash-stops));

    --gradient-light: linear-gradient(132.36deg in oklab,
                        var(--cf-lime)  24.97%,
                        var(--cf-glas)  40.45%,
                        var(--cf-grau)  73.05%);
    --gradient-light-90: linear-gradient(90deg in oklab,
                        var(--cf-grau)  0%,
                        var(--cf-glas)  calc(100% - var(--rake-near)),
                        var(--cf-lime)  100%);
    /* The grazing linear and the three blooms used to be restated here too.
       They are classes now and carry their own @supports branch in base.css,
       next to the stop lists they belong to — see the note where they used to
       be declared, in section 3b. */
    /* One line for the same reason the two foils are one line each. The
       spectrum's stops became a token when the arc waypoints went in, because
       a waypoint written twice is a waypoint that can be recomputed once. */
    --gradient-spectrum: linear-gradient(132.36deg in oklab, var(--spectrum-stops));
    /* Two lines rather than nine, because the stops are a token now and only
       the interpolation path changes here. Nothing about the ramp is restated,
       so the two declarations cannot drift apart. */
    --gradient-foil:     linear-gradient(var(--foil-rake) in oklab, var(--foil-stops));
    --gradient-foil-ink: linear-gradient(var(--foil-rake) in oklab, var(--foil-ink-stops));
  }
}

/* Dark context — used for the footer and any inverted section. Set
   `data-theme="inverse"` on a section to flip the semantic layer. */
[data-theme="inverse"] {
  --surface-page:   var(--cf-schwarz);
  --surface-base:   var(--cf-schwarz);

  /* Black is the floor of the neutral scale — there is no rung below it — so
     here the step has to move upward, and that is now the difference between
     the two themes rather than something they share. The light theme's steps
     used to climb toward white as well; they no longer can, because the wash
     they sit on ends AT white (see --surface-sunken above). Black does not
     have that problem from the other side: nothing composites below it, so a
     white veil over it is the same constant ratio the light theme gets from a
     black one.

     6 % white over Schwarz renders #0F0F0F, within one step of the #121212
     this used to be, and it keeps its order under --surface-raised. Written
     as a veil rather than as that literal so the material is ONE idea in both
     themes: a step away from the page is a proportion of the light already
     there. Raised stays an absolute value — it is the ramp's own darkest
     non-black step and there is no wash under it to track. */
  --surface-sunken: rgba(255, 255, 255, 0.06);

  /* There is no --surface-lifted here, and there was nothing to lose when it
     was retired: on this ground the two steps had already collapsed into one.
     Black is the floor, nothing composites below it, so the only step
     available in either direction is up and --surface-sunken above has taken
     it — the light theme's pair was a pair and this theme's was a single move,
     a property of the two grounds rather than an omission. The light theme has
     the single move now too — see the retirement note under --surface-sunken
     in the :root block. */

  --surface-raised: var(--grey-800);

  /* Inverted glass darkens instead of lightening, and the floor flips with
     it: 56 % black is where white type still clears 4.5:1 composited over a
     pure white backdrop. */
  --surface-glass:       rgba(0, 0, 0, 0.56);
  --surface-glass-thin:  rgba(0, 0, 0, 0.38);
  --surface-glass-veil:  rgba(0, 0, 0, 0.06);
  --surface-glass-solid: var(--surface-raised);
  --glass-border:        1px solid rgba(255, 255, 255, 0.18);

  /* The same edge at the inverse theme's own alphas, and it carries the same
     arc waypoint for the same reason — the leg turns wherever it is drawn.
     Its value is not a copy of the light theme's: the polar midpoint weights L
     and C by the two alphas, and 0.26 / 0.18 weights them 0.591 / 0.409 where
     0.42 / 0.28 weights them 0.6 / 0.4. Measured over this theme's own
     grounds, chroma at the midpoint against the arc: -22.4 % to +2.4 % on
     Schwarz, -11.5 % to -2.0 % on anthracite. */
  --glass-edge: linear-gradient(90deg,
                  rgba(255, 255, 255, 0)    0%,
                  rgba(255, 255, 255, 0.26) 16%,
                  rgba(197, 235, 226, 0.26) 54%,
                  rgba(153, 214, 226, 0.22) 67%,
                  rgba(114, 176, 226, 0.18) 80%,
                  rgba(255, 255, 255, 0)   100%);
  --sheen-panel: linear-gradient(90deg,
                   rgba(255, 255, 255, 0)    0%,
                   rgba(255, 255, 255, 0)   50%,
                   rgba(255, 255, 255, 0.12) 78%,
                   rgba(197, 235, 226, 0.10) 100%);

  --text-primary:   var(--grey-000);
  --text-secondary: rgba(255, 255, 255, 0.72);
  --text-muted:     rgba(255, 255, 255, 0.48);

  --border-strong:  rgba(255, 255, 255, 0.72);
  --border-default: rgba(255, 255, 255, 0.18);
  --border-subtle:  rgba(255, 255, 255, 0.10);
  --focus-ring:     var(--cf-lime);

  /* The lattice inverts with everything else. Same 0.10, drawn in light
     instead of in ink: white 0.10 on Schwarz puts --text-secondary at
     9.46:1 across the line, so the dark side is the comfortable one. */
  --field-ink: rgba(255, 255, 255, 0.10);

  /* The ground line under a match follows the ink and has to be RESTATED here,
     not just aliased in :root. --found-rule: var(--border-strong) is resolved
     where the DECLARATION lives — on :root, where --border-strong is Schwarz —
     and the substituted value is what inherits down, so an element inside an
     inverse section would draw a black rule on a black surface. Measured
     before this line existed: rgb(0, 0, 0) on --surface-inverse. Composited,
     the value here is #B8B8B8, 10.6:1 on Schwarz.
     Only the rule inverts. --found-light stays lime, because the light IS the
     light and there is nothing for it to become on black — and on a dark
     surface it is the one place the fill holds the 3:1 boundary on its own
     (lime against Schwarz is 18.51:1, against CF-Grau 1.37:1). */
  --found-rule: var(--border-strong);

  /* The dark red is unreadable on black (2.9:1). Same hue, lightened
     until it clears the floor: 12.3:1 on --surface-base here. */
  --feedback-error: #F2B8B5;
}

/* THE SCRIM GOES DEEPER ON A NARROW HERO, and the reason is the cover crop.
   The reach fixed the width axis — the plateau follows the column now — but
   the reach is a horizontal displacement and its own register is widths only,
   in as many words: "heights are irrelevant to the reach". They are not
   irrelevant to the ARTWORK. .cf-hero__media covers with a 1080 px square, so
   which part of that square lands under the type is set by the box's aspect,
   and the box's aspect is set by the viewport's height as much as its width.
   At 1280 x 800 the box is 1280 x 656, cover fits the WIDTH, and only the lit
   middle band of the square is in the hero. At 375 x 812 the box is 375 x 501,
   cover fits the HEIGHT, and the square's dark top and dark bottom are both in
   it. Measured raw with the scrim off, the worst pixel under the kicker is
   rgb(188,193,198) at 1280 x 800 and rgb(37,38,43) at 375 x 812: five times
   darker under the same 11 px of type.

   So the kicker, alone among the hero's type, is a 4.5:1 element sitting where
   the artwork is darkest, and 0.42 does not carry it there. Sampled the same
   way as the table above but at FRAMES rather than widths — the height varied
   as well as the width — the worst composited pixel under it was:

       375x740   4.03     800x800   4.25     1024x768  13.25
       375x812   4.03     860x800   4.14     1024x800  13.35
       375x844   4.01     900x800   4.15     1152x800  13.58
       375x896   4.44     768x800   4.37     1280x800  13.67
                          768x1024  8.13     1440x900  14.73

   Nine frames under the 4.5:1 floor, and the poster is among the failing ones
   — so it fails before a frame of the loop has decoded, and for the
   prefers-reduced-motion reader who is shown nothing else. It is not a reach
   problem: the kicker's worst point at 375 x 812 projects to 231 px along the
   axis where the plateau runs to 347, so that pixel sits under the scrim at
   its full 0.42 and still reads 4.03:1.

   AND THE CLIFF IS EXACTLY 56.25rem, which is why that is the threshold and
   not a number picked to cover the failures. 900 x 800 reads 4.15 and
   1024 x 768 reads 13.25 — a factor of three across 124 px of viewport, with
   nothing gradual in between. 56.25rem is where .cf-hero__action leaves the
   flow and floats at the column inset, and the hero's media box goes from
   567 px tall to 624: below it the kicker sits low in a short hero, on the
   square's dark bottom band, and above it the hero is tall enough that the
   kicker is on the lit middle. The breakpoint the hero's own bottom row moved
   to is the breakpoint its reading floor was already keeping. A second
   consumer for an existing number, which is what layout.html says that
   threshold is for.

   Re-derived from the same measured floor the weight always was:
   rgb(37,38,43) under 0.50 of CF-Grau renders rgb(122,122,129) and clears at
   4.93:1. It also answers the tight width the note above had to leave — "768
   is now the tight width at 3.15, on 5 % rather than 36 % ... the only lever
   there is the weight" — because 768 is below this breakpoint.

   And "much past 0.42 and it starts taking the picture" stays true, which is
   why the deeper value is not global: at and above 1024 nothing moves, and
   those are the frames with a picture to take. At 375 the hero is 501 px tall
   behind five lines of display type, a kicker and the CTA in flow. Only
   :root: nothing in [data-theme="inverse"] declares --scrim-depth, so there
   is no per-element declaration for this to fail to reach.

   BOTH VALUES WENT UP FOUR POINTS WHEN THE ARTWORK WAS REPLACED, 2026-08-03,
   and every figure in the two tables above belongs to the artwork it replaced.
   The loop is a different render of the same motion, not a better encode of
   the same frames: no frame of it matches the poster the old one was cut from,
   its mean luminance is 20 levels lighter — and, the part that binds here, the
   thin shadow lines along its plane edges go DARKER than anything in the old
   render, to rgb(9) against rgb(20) at the floor. Brighter overall, worse
   exactly where a 4.5:1 element sits. Re-measured against the new artwork at
   the depths that used to hold, three rows had gone under their floor and a
   fourth was resting on it:

       375 x 812  kicker  4.48    900 x 800   kicker  4.45
       768 x 800  kicker  4.38    1024 x 768  title   3.00

   0.46 and 0.54 put the worst row back where the old artwork had it — 4.77
   against the 4.5 floor below the cliff, 3.34 against 3.0 above it, versus
   4.42 and 3.29 before the swap. The weight is the lever the note above
   already named for exactly this ("the only lever there is the weight"), and
   it is the one direction that cannot invalidate a measurement: every figure
   in check-hero-contrast.py's register is monotone in the depth, which is why
   check-hero-scrim.py objects to this number going down and not up.
   → scripts/check-hero-contrast.py */
@media (max-width: 56.25rem) {
  :root { --scrim-depth: 0.54; }
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 1ms;
    --duration-base: 1ms;
    --duration-slow: 1ms;
    --duration-scene: 1ms;
  }
}

/* Both fallbacks below have to name [data-theme="inverse"] as well as :root,
   and repeat every token the inverse block declares — not just the ones that
   differ. A custom property is resolved per element: an element inside an
   inverse section has its own declaration from the block above, so a rule
   targeting :root never reaches it, however specific the surrounding @media
   or @supports is. Redeclaring --glass-blur there is redundant and kept
   anyway, so that adding a token to the inverse block cannot silently punch
   a hole in a fallback again. */

/* No backdrop-filter, no glass. Redefining the tokens here means no component
   needs its own fallback.

   The two tints do NOT land in the same place, and this comment used to say
   they did — "everything that would have been frosted becomes an opaque plate
   instead". Bearing glass does: it holds text, its 4.5:1 floor was the blur's
   to keep, and with no blur the only way to keep it is an opaque plate. Veil
   glass does not, and stays at white 72 %. It carries nothing but the two
   opaque plates — the logo lockup and the link bar — so the page showing
   through it costs no contrast, and a fully opaque band would put a solid
   grey bar across the top of every page over the one thing the bar exists
   to sit lightly on.

   That is deliberately NOT what the reduced-transparency block below does
   with the same token, and the difference is the reason, not the mechanism.
   Here the browser cannot blur; keeping some translucency is ours to choose.
   There the reader has asked for less transparency, and a request is answered
   in full or not at all. Same token, two floors, because the two blocks are
   answering different questions.

   AND THE RIM HAS TO TURN OVER WITH THE TINT, which neither block did. This
   is --surface-sunken's argument, sixty lines from the top of this file,
   arriving at the one token that never got it. That comment retired
   #E7E7E7 as a panel surface because "an absolute grey inside the wash's own
   range is raised at the top of the screen, INVISIBLE at 65 % of it, and
   sunken below that", and it published the row that says so — wash 231 at
   65 % of the viewport, #E7E7E7 reading 0 against it. --surface-glass-solid
   IS #E7E7E7. In the light theme these two blocks therefore paint an
   absolute grey panel on the wash and bound it with --glass-border, a white
   55 % rim: a light line on a light plate that the wash catches up with and
   passes.

   Measured on patterns/expertise.html, the act rail open — the one plate in
   the system that ships this tint by DEFAULT rather than as a fallback,
   278 px tall in the left margin — sampling the plate's own border column
   and the wash six pixels outside it, at the plate's top and bottom rows:

                          wash   plate:wash   white rim   --border-default
     1440x900   top row    220      1.090       1.226         1.627
                bottom     233      1.049       1.072         1.861
     1920x1080  top row    221      1.072       1.205         1.655
                bottom     231      1.030       1.092         1.827

   The plate is lighter than the page at its top row and darker at its
   bottom one — it inverts inside its own height — and the white rim goes
   with it rather than surviving it, down to 1.07:1 where the panel is
   already at 1.05:1. At the bottom of its own 278 px the panel had no
   boundary at all.

   SO THE EDGE BECOMES A CONTOUR, because that is what the material chapter
   already prescribes for this exact case: "a panel that needs a surface
   takes the veil, and a panel that needs a boundary takes a contour."
   --border-default removes 24 % of whatever light is behind it, which is a
   constant ratio at both ends of the wash — the same property that made
   --surface-sunken a veil instead of a grey — so the boundary cannot invert
   however the plate and the page drift past each other. It is the column
   the table's last row holds at 1.63-1.86:1 while the rim it replaces runs
   1.07-1.23.

   THE FILL STAYS OPAQUE AND STAYS THIS GREY. It is not free to become a
   veil: the comment above names a translucent tint with the blur switched
   off as the worst of the three states, because the artwork behind it then
   bleeds through sharp, and the bearing tint's 4.5:1 floor over a backdrop
   nobody controls — the hero video — is the whole reason an opaque plate
   exists here. Nothing opaque and light escapes the wash's range anyway:
   the wash runs 207 to 255 and covers it. It is the boundary that has to be
   relative, not the surface.

   ONLY THE LIGHT THEME'S. [data-theme="inverse"] paints #1F1F1F here and a
   white 18 % rim on a dark plate is the same argument the right way up, so
   both inverse blocks restate the inverse theme's own value rather than
   inheriting anything — the redundancy this file's note above asks for, so
   that a token added to the inverse block cannot punch a hole in a fallback
   again. */

/* AND THE LIGHT HAS TO GO WITH THE MATERIAL, which is this same argument one
   token further out and the reason it took a third pass to find. The block
   above turned --glass-border over. It left --glass-edge and --glass-rim-light
   declared, and those two are not a boundary — they are the material's own
   light, and with the blur off there is no material for light to be on.

   WHAT THAT COSTS, ARITHMETIC RATHER THAN ARGUED. The lit edge is painted on
   the same row as the contour the block above installs, and it is white, so it
   takes the contour back off. On the light theme's opaque plate, #E7E7E7:

                                        paints    against the plate
     --glass-edge      white 42 %      (241)          1.095:1
     + --glass-rim-light white 45 %    (247)          1.154:1
     --border-default  black 24 %      (176)          1.754:1

   And against the page it stands on, which is the number that matters, because
   the wash runs the whole 207-to-255 range on every screen at every scroll
   position:

                    wash 207   wash 231   wash 243   wash 255
     lit edge         1.376      1.095      1.053      1.129
     contour          1.395      1.754      2.022      2.169

   The lit edge INVERTS inside the wash's own range — lighter than the page at
   the top of the viewport, darker at the bottom, 1.05:1 where the two cross —
   which is the exact failure --surface-sunken was retired for and the act
   rail's white rim was measured out of. The contour holds one direction the
   whole way down. Same sentence, same wash, one token over.

   IT IS ONE SURFACE'S WHOLE BOUNDARY AND THREE OTHERS' TOP EDGE. .cf-nav::after
   and .material-glass-edge are sheets that END at a single edge rather than
   closing a perimeter, so the lit hairline is all they have; .cf-btn--glass,
   .cf-info-card--glass::before and the button's own rim paint it ON the border
   they already close with, so there it is a white line washing out a contour
   that was correct underneath. Turning the token off fixes the second case by
   itself — the border is already right — and the two single-edge sheets take
   the contour explicitly, beside the forced-colours rules that already redraw
   them for the same reason one door over (components.css .cf-nav::after,
   base.css .material-glass-edge).

   `none` and not a colour, because both tokens are consumed as background-image
   LAYERS in a list — `var(--glass-rim-light), var(--glass-edge)` on two of the
   three rims, one layer of three on the button's plate. `none` is a valid
   background-image, the sizes and positions beside it are simply not consulted,
   and @keyframes cf-glass-rim goes on animating a background-position that now
   moves nothing. No consumer needs a fallback of its own, which is what these
   blocks are for.

   BOTH SCOPES, THOUGH ONLY ONE OF THEM MEASURES AS A FAILURE. In the inverse
   theme the edge is white 26 % on #1F1F1F and reads 2.35:1, brighter than the
   18 % rim beside it rather than weaker — no plate is lost. It still goes,
   because the rule is about the material and not about the number: no blur, no
   glass, no light on glass. A theme-dependent answer here would be two
   materials wearing one name.

   scripts/check-glass-solid-edge.py's third claim holds this. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root {
    --glass-blur:          none;
    --surface-glass:       var(--surface-glass-solid);
    --surface-glass-thin:  var(--surface-glass-solid);
    --surface-glass-veil:  rgba(255, 255, 255, 0.72);
    --glass-border:        var(--stroke-1) solid var(--border-default);
    --glass-edge:          none;
    --glass-rim-light:     none;
  }
  [data-theme="inverse"] {
    --glass-blur:          none;
    --surface-glass:       var(--surface-glass-solid);
    --surface-glass-thin:  var(--surface-glass-solid);
    --surface-glass-veil:  rgba(0, 0, 0, 0.72);
    --glass-border:        var(--stroke-1) solid rgba(255, 255, 255, 0.18);
    --glass-edge:          none;
    --glass-rim-light:     none;
  }
}

/* The reader has asked for less transparency. Give them none of it: opaque
   plates, no blur, and the edge STAYS AN EDGE — but as the contour the block
   above derives, not as the lit rim, for the reason that block measures. This
   sentence used to read "the edge light stays because it is a contour, not a
   translucency", which conflated the two lines this material has and the
   material chapter separates: --glass-border is the contour a sheet closes
   with, --glass-edge is the lit hairline it ends at. Only the first of them is
   a boundary, and on an opaque plate inside the wash's own range a white one
   is not a boundary either. Leaving a tint translucent while switching the
   blur off is the worst of the three states — content behind it bleeds through
   sharp — so the inverse case is spelled out rather than inherited.

   THE TWO LIGHT TOKENS GO HERE TOO, and here the request settles it before the
   measurement does. The block above turns --glass-edge and --glass-rim-light
   off because a plate with no blur has no material for light to sit on; a
   reader who has asked for less transparency has asked for the plate, and a
   specular travelling across it on every scroll is the material asserting
   itself anyway. It is also the one door where the tint goes fully opaque on
   all three surfaces, so the nav band — the only sheet whose whole boundary is
   that hairline — is at its most exposed: an absolute grey bar across the top
   of every page, bounded by a white line the wash catches up with and passes.
   The arithmetic is in the block above and is the same on this side of it. */
@media (prefers-reduced-transparency: reduce) {
  :root {
    --glass-blur:          none;
    --surface-glass:       var(--surface-glass-solid);
    --surface-glass-thin:  var(--surface-glass-solid);
    --surface-glass-veil:  var(--surface-glass-solid);
    --glass-border:        var(--stroke-1) solid var(--border-default);
    --glass-edge:          none;
    --glass-rim-light:     none;
  }
  [data-theme="inverse"] {
    --glass-blur:          none;
    --surface-glass:       var(--surface-glass-solid);
    --surface-glass-thin:  var(--surface-glass-solid);
    --surface-glass-veil:  var(--surface-glass-solid);
    --glass-border:        var(--stroke-1) solid rgba(255, 255, 255, 0.18);
    --glass-edge:          none;
    --glass-rim-light:     none;
  }
}

/* THE THIRD FALLBACK, and it was missing. foundations/materials.html said in
   so many words that forced colours is "the one context where none of the
   arithmetic above applies" — that the platform decides the edge, the border
   survives, and this system has nothing to do. That is true of the BUTTON,
   which has a real border and gets it back in LinkText. It is not true of the
   material, and the sentence is why nobody looked.

   Measured in Chromium with forced colours active, landing page at 1280:

     .cf-nav::after   background-image  ->  `none, none`
     .cf-btn--glass   background-image  ->  `none, none, none`
     .cf-nav::before  backdrop-filter   ->  blur(16px) saturate(1.08)   (live)
     .cf-btn--glass   background-color  ->  rgba(255,255,255,0.46)      (kept)

   THE LAST ROW IS RIGHT AND THE READING TAKEN FROM IT WAS NOT, which is worth
   more than the row. It used to be read here as "background-color is not among
   the properties forced colours overrides in Chromium" — and that sentence,
   written from a WHITE tint, cannot be told apart from the truth by a white
   tint. Chromium does force this property; what it PRESERVES is the alpha.
   (css-color-adjust-1 issue 4175, shipped in Chromium and Edge: the
   user-preferred background-color on every channel except alpha, because a
   translucent overlay forced opaque would black out the page under it.)

   The case that separates the two readings is the one this file had never
   painted: an author surface that is not white. Measured on
   components/article.html at 1280 with the mode active, .cf-prose__note-tag's
   rule — --border-strong, OPAQUE BLACK, painted as a background because a flex
   ::after is how a hairline runs out of a label —

     light palette   background-color  #000  ->  rgb(255,255,255)
     dark  palette   background-color  #000  ->  rgb(0,0,0)

   An opaque black becoming an opaque white is not a declaration surviving.

   AND HALF OF THIS WAS ALREADY WRITTEN ONE FILE OVER, which is the part that
   should sting. components.css says it twice — above .lp-ev-card__rule, "forced
   colours discards background-color and maps border colours into its own
   palette, so a mark made of border survives and a mark made of fill does not",
   and again above .cf-page-header__rule with the measurement, "background-color
   forced to rgb(255,255,255) — Canvas, on Canvas". Two files of this system have
   held opposite accounts of the same property for as long as both have existed,
   and the one that was right was the one about LINES, so nobody carried it back
   to the tokens. What is added here is the other half: the alpha is kept, and
   that is what turns a veil from a wrong colour into no colour.

   It is the colour being replaced and the alpha kept, and the correction matters
   because everything below turns on it: a tint that survived would be the
   wrong COLOUR, while a tint repainted in the reader's own surface at the
   author's alpha is a veil of the backdrop over the backdrop — no colour at
   all, 1.000:1, in every palette, by construction. See the end of this comment.

   Three things follow, and all three are this file's to fix because all three
   are the tokens rather than the components.

   THE BLUR IS DEAD WEIGHT. Forced colours drops every gradient on both frosted
   surfaces, so what the blur composites under is a flat system colour. The
   Don't list on foundations/materials.html already rules this out in general —
   no backdrop-filter under an opaque background, the blur cannot be seen and
   still costs a GPU pass — and this is that case arriving by way of the user's
   own settings rather than by an author's mistake.

   THE TINT IS THE REAL FAILURE. --surface-glass survives at white 46 % and the
   hero CTA stays translucent on a page where every other surface has gone flat:
   the video plays through the primary call to action. Chromium
   backplates the LABEL, so the text is legible either way — what is wrong is
   the plate, which asserts a material the mode does not have. Canvas is the
   answer rather than --surface-glass-solid, and that distinction is the whole
   reason this is a separate block instead of another line in the one above:
   grey-100 is an author grey, and in a dark high-contrast theme an author grey
   is the one thing the reader switched the mode on to stop seeing. The other
   two fallbacks answer "the browser cannot" and "the reader wants less"; this
   one answers "the reader has chosen the palette", so it answers in the
   palette.

   Both selectors are named for the reason the comment above the first fallback
   gives — a custom property resolves per element, so :root never reaches
   inside an inverse scope — and here the two blocks are identical rather than
   merely parallel: Canvas and CanvasText already ARE the theme, whichever
   theme the reader picked.

   The two lit EDGES cannot be recovered here, because they are painted rather
   than tinted: an edge drawn as a background-image is gone whatever this file
   says. They are redrawn as borders — the only paint forced colours keeps —
   next to the rules that draw them. See .cf-nav::after in components.css and
   .material-glass-edge in base.css.

   AND IT MISSED A TINT. --surface-glass-thin was left out of this block for as
   long as the block has existed, while the two above it both cover all three.
   The tint is not unused: patterns/expertise.html sets --info-card-tint to it,
   so the pinned stage's lectern is the one glass surface in the system that
   stayed translucent under forced colours. Measured in Chromium with the mode
   active at 1280, .cf-info-card--glass computed background-color
   rgba(255, 255, 255, 0.30) with backdrop-filter already none — so it was worse
   than the failure the paragraph above describes, not milder: thinner than the
   button's 46 %, and with the blur switched off the section behind it came
   through SHARP rather than calmed. What the reader saw was an author grey over
   Canvas with the page legible through it, which is precisely the thing this
   block's own argument says the mode exists to stop.

   Why it survived three releases is worth writing down, because it is the same
   shape as the hole in the inverse blocks: the thin tint is the one tint
   foundations/materials.html could describe and not DRAW. There was no
   .material-glass--thin, so no page rendered it, so no screenshot of any
   fallback ever contained it. There is one now, and check-glass-budget.py's
   fifth claim closes the general case — every tint neutralised in every block
   that neutralises the material, for every selector that block names. */
/* --surface-lifted USED TO BE NEUTRALISED HERE AND ITS LINE LEFT WITH IT.
   The token is retired — see the note under --surface-sunken in the :root
   block — and a neutralisation for a token nothing declares is a rule about
   nothing. The argument it stood on is unchanged and is the paragraph above:
   background-color is not among the properties forced colours overrides in
   Chromium, so an author veil declared as one survives the mode intact, and
   any veil this file grows next needs its line in these two blocks on the day
   it is written.

   --surface-sunken IS IN THE BLOCK, AND THE THREE COMPONENTS WENT FIRST.
   This paragraph used to record the token as a defect deliberately left
   standing: the veil carried through forced colours on .cf-process__note and
   two blocks in components.css, and Canvas "would not neutralise them, it would
   dissolve them", because the veil was the only thing making each of them read
   as a block. The reasoning was sound and the premise was the misreading
   corrected at the top of this comment. Nothing was holding those blocks
   together to begin with:

     .cf-process__note   authored rgba(0,0,0,0.06)  ->  used white 6 % over a
                         white Canvas, black 6 % over a black one. 1.000:1.
     .cf-prose__note     the same, and its opening rule gone with it — see the
                         two-palette measurement above.

   So there was nothing to dissolve, and the line here was never the risk. The
   contours went in first anyway, because that is the order the argument runs
   in: .cf-process__note and .cf-prose__note take a boundary under
   `forced-colors: active`, the aside's rule and the process card's divider are
   redrawn as borders beside them, and only then does this block answer for the
   surface. What the token line changes is not a pixel — Canvas at 6 % over
   Canvas already WAS Canvas — but the claim: an author surface asserting a
   material in the one mode where the reader has chosen the palette. The two
   blocks above cover all three glass tints; this one now covers every author
   surface this file declares.

   .cf-prose pre is the control case and it needed nothing: identical veil,
   already bounded by --border-default, and it comes through the mode with its
   box intact. .cf-prose code needed nothing either, and for the other reason —
   an inline span carries Geist Mono, which the mode does not touch, so it
   keeps a mark without being given a box it never had.

   WHAT IS STILL OPEN, and it is a line rather than a surface: .cf-prose hr and
   every other rule this system draws as a repeating-linear-gradient computes to
   `background-image: none` in this mode and is gone outright, not repainted.
   Two of those were found and fixed above because they are inside panels; the
   general sweep belongs to whoever owns the line chapter, and the brand's four
   line types are the reason it is not mechanical — `border-style: dashed` has
   no period of its own to hold 2-1 or 1-4 to.

   THE TWO LIGHT TOKENS ARE STATED HERE AND CHANGE NOTHING, which is the same
   kind of line as the surfaces above. The mode already computes every gradient
   background-image to `none` — that is measured two chapters up, and it is why
   .cf-nav::after and .material-glass-edge carry border rules in this mode at
   all. So this declares what the mode does rather than doing it, and the
   material's own rule — no blur, no material light — reads the same in all
   three doors instead of holding in two of them by declaration and in the third
   by the platform. check-glass-solid-edge.py's third claim reads every tier the
   same way and would otherwise need this one exempted by name. */
@media (forced-colors: active) {
  :root {
    --glass-blur:          none;
    --surface-glass:       Canvas;
    --surface-glass-thin:  Canvas;
    --surface-glass-veil:  Canvas;
    --surface-glass-solid: Canvas;
    --surface-sunken:      Canvas;
    --glass-edge:          none;
    --glass-rim-light:     none;
  }
  [data-theme="inverse"] {
    --glass-blur:          none;
    --surface-glass:       Canvas;
    --surface-glass-thin:  Canvas;
    --surface-glass-veil:  Canvas;
    --surface-glass-solid: Canvas;
    --surface-sunken:      Canvas;
    --glass-edge:          none;
    --glass-rim-light:     none;
  }
}
