/* ==========================================================================
   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-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. Nothing else is a gradient in this brand.

     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;
  --hero-scrim: linear-gradient(var(--angle-a),
                      rgba(207, 207, 207, var(--scrim-depth)) 0,
                      rgba(207, 207, 207, var(--scrim-depth)) var(--scrim-reach),
                      rgba(207, 207, 207, 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. */
  --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.

     NO WAYPOINT ON A LEG THAT ENDS IN GREY, and that is the boundary this
     stops at. --gradient-light, --gradient-light-90, the rakes, the blooms and
     the page wash all run into CF-Grau or out of Weiss. An achromatic stop has
     NO HUE, so there is no arc for the chord to be short of: 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 and gets no waypoint for the same reason. Only a leg
     chromatic at BOTH ends turns, and only a leg that turns can cut a corner.
     ------------------------------------------------------------------ */

  /* 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;

  --tracking-display: -0.02em;
  --tracking-normal:   0;
  --tracking-label:    0.01em;  /* Mono/Label as specced in Figma */
  --tracking-wide:     0.08em;  /* spaced-out mono, e.g. buttons, footer meta */

  /* ------------------------------------------------------------------
     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 at the 1440 reference frame */
  --space-40: 10rem;    /* 160 — --section-gap at 1920, its ceiling */

  /* 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 · 160 at 1920
       --section-gap-sm  100vw / 18   32 at 576 ·  80 at 1440 (capped)

     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 — 768 to 1440 — and nowhere else. 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 — a flat 1.50 from 768
     to 1440, then opening back out to 2.00 by 1920 as -sm caps and
     --section-gap climbs on alone. 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.

     Above ~1438 the "one grid column" rationale stops being literally true:
     the container caps, so a real column stops growing while 100vw/12 does
     not. Section rhythm therefore keeps opening on a very wide window even
     though the content column is frozen at 1280. That is a deliberate-looking
     choice nobody has actually ruled on — see foundations/layout.html.

     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-40));
  --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. */
  --section-header-gap: 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.
     ------------------------------------------------------------------ */
  --container-max: 80rem;   /* 1280 */
  --gutter:        clamp(1.25rem, 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:

       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, the system's
                     one 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  .cf-process__figure's isometric drawing keeps the
                     --trace-weight constant it carried before that property
                     existed. 554 is not a chosen width: it is where the
                     figure's height cap stops being the tighter constraint,
                     so below it the drawing is letterboxed into a box that
                     grows with the card and no fixed multiplier lands the
                     trace on 1 px across the ramp. THE ONLY FRACTIONAL
                     THRESHOLD IN THE SYSTEM, and it sits 10 px from 34rem
                     above — the near-duplicate this register warns about,
                     recorded rather than reconciled. The two never measure
                     the same container, so nothing collides today; whether
                     a derived geometric crossover should be a threshold at
                     all is the illustration lane's question, not this
                     register's. → components/process-card.html
       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.
       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.5 — 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, the
                        block padding drops a rung, and what is left caps
                        against the nav band and one control rather than
                        against a share of the screen. It exists because a
                        tall banner on a short screen hides the page it is
                        asking about. 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 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: 900px), which collapses
                           the sidebar. It renders at the same width as
                           the 56.25rem above at a 16 px default and is
                           unrelated to it — and it is still px, which is
                           now a visible difference rather than a hidden
                           one. Left as it is deliberately: docs.css is out
                           of scope by the rule at the head of this list,
                           and converting it would make this register look
                           like it governs the documentation chrome.
       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. Three exist in shipping CSS: the consent dialog's width
     swaps at 640 wide, its max-height at 920 tall, and min(92vh, 56rem)
     at 974 tall. 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.
     See foundations/layout.html.

     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.

     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.

     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%,
                  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 grew a rim
     layer of its own — the motion BOTH frosted surfaces share.

     ON BOTH, on the same band, at the same two endpoints. .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. Same 40 % width, same -70 % and
     170 % 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%);

  /* 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%);

  /* 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. */
  --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
     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.

   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);
  --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);

  --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(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. */
@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);
  }
  [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);
  }
}

/* The reader has asked for less transparency. Give them none of it: opaque
   plates, no blur, and the edge light stays because it is a contour, not a
   translucency. 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. */
@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);
  }
  [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);
  }
}

/* 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)

   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. background-color is NOT among the properties
   forced colours overrides in Chromium, so --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. */
@media (forced-colors: active) {
  :root {
    --glass-blur:          none;
    --surface-glass:       Canvas;
    --surface-glass-thin:  Canvas;
    --surface-glass-veil:  Canvas;
    --surface-glass-solid: Canvas;
  }
  [data-theme="inverse"] {
    --glass-blur:          none;
    --surface-glass:       Canvas;
    --surface-glass-thin:  Canvas;
    --surface-glass-veil:  Canvas;
    --surface-glass-solid: Canvas;
  }
}
