/* ==========================================================================
   Control-F — Components
   Every component in the redesign. Depends on tokens.css + base.css.
   Naming: .cf-<block>__<element> --<modifier>
   ========================================================================== */

/* Registered so it can be interpolated. A plain custom property jumps; a
   typed one animates, which is the difference between a highlight that
   moves and a highlight that switches. */
@property --cf-specular {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 150%;
}

/* --------------------------------------------------------------------------
   ICON — the arrow
   The CF arrow is the brand's only decorative glyph. It is the logo symbol
   reduced to a single chevron stack and always points at the action.
   -------------------------------------------------------------------------- */
.cf-arrow {
  flex: none;
  width: 1.25em;
  height: 1.25em;
  fill: currentColor;
  transition: transform var(--duration-base) var(--ease-standard);
}
.cf-arrow--sm { width: 1em; height: 1em; }

/* --------------------------------------------------------------------------
   ICON — the drawn set
   The arrow above is a filled glyph and stands apart. Everything else in the
   system is drawn: a 24 x 24 box, a 20 x 20 live area, a 1 px contour that
   stays 1 px at any size. Shapes live in assets/js/cf-icons.js.
   -------------------------------------------------------------------------- */
.cf-icon {
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  fill: none;
  stroke: currentColor;
  stroke-width: var(--stroke-1);
  stroke-linecap: square;
  stroke-linejoin: miter;
}

/* Sizes are on the 4 px unit. 16 px is the floor — below it the contour and
   the counters inside the isometric objects stop being readable. */
.cf-icon--sm { width: 1rem;    height: 1rem;    }   /* 16 */
.cf-icon--md { width: 1.5rem;  height: 1.5rem;  }   /* 24 — default */
.cf-icon--lg { width: 2.5rem;  height: 2.5rem;  }   /* 40 */
.cf-icon--xl { width: 5rem;    height: 5rem;    }   /* 80 */

/* 2 px for large formats or a low-contrast surface. Never above 2 px on screen. */
.cf-icon--bold { stroke-width: var(--stroke-2); }

/* The light layer. The contour stops being a line and starts being a source.
   Decorative only, dark surfaces only, one per screen: lime is 18.5:1 on
   black and 1.4:1 on CF-Grau. See foundations/iconography.html. */
.cf-icon--light {
  stroke: var(--cf-lime);
  --glow-r: 6px;
}

/* AN INK NAMED ON THE <svg> IS NOT IN THE READER'S PALETTE, and this note is
   the icon set's, not this modifier's — it is written here because this is the
   first rule in the sheet to name one.

   Chromium's UA stylesheet gives every <svg> `forced-color-adjust:
   preserve-parent-color`. That is the value that lets an icon inside a link
   come out in LinkText instead of CanvasText: it hands the element the colour
   it inherits and forces nothing else on it. So the moment a rule names an ink
   ON the svg itself, that ink is preserved too — and `stroke` and `fill` were
   never on the forced list to begin with. Either way the icon paints the
   author's colour in a palette the reader chose. Four selectors in this sheet
   name an ink on an <svg>, and only .cf-scroll-cue__glyph had ever handed it
   over, by naming CanvasText on its stroke in a block of its own.

   Measured in Chromium 151 over http, forced-colors active, prefers-color-
   scheme dark — Canvas #000000, CanvasText #FFFFFF, LinkText #FFFF00 — by
   sampling every <svg> on every page under design-system/ and comparing the
   computed paint against the same page in the light forced palette. A forced
   colour flips between the two; an author colour is byte-identical, which is
   the whole test. Three were identical:

     .cf-icon--light             stroke rgb(225, 255, 0), 1 instance
     .cf-error__route .cf-arrow  rgb(72, 72, 72) at the end of a LinkText row,
                                 2.29:1 on Canvas, 14 instances on 4 pages
     .cf-notice__glyph           rgb(0, 0, 0) on Canvas #000000 — 1:1, gone
                                 outright — and rgb(167, 32, 25) at 2.86:1 for
                                 the Fehler tone. 5 instances on 2 pages

   `forced-color-adjust: auto` is not the answer and was measured before it was
   ruled out: on the error arrow it maps `color` to the inherited LinkText and
   leaves `fill: currentColor` computing rgb(72, 72, 72), so the arrow is still
   grey and only the computed style looks repaired. What works is naming the
   value — `color: inherit` where the icon should take its context's ink,
   `stroke: currentColor` where it had been opted out of one.

   HERE, THE LIGHT IS WHAT GOES. The spill is already dropped in this mode (see
   the note beside the composed --glow-r filter, 15,800 lines down), and a lime
   contour with no light behind it is not a source — it is one icon in a colour
   the reader did not ask for, 1.13:1 on a light Canvas, standing next to three
   neighbours on foundations/iconography.html drawn as CanvasText hairlines. It
   degrades to a plain .cf-icon, which is what this modifier means once there is
   no light in the palette to be made of. */
@media (forced-colors: active) {
  .cf-icon--light { stroke: currentColor; }
}

/* An icon inside a button or link tracks the label's motion, nothing more.

   HOVER AND FOCUS ARE ONE STATE HERE, and everywhere else in this file. The
   system had settled that twice already — .cf-subdiv puts :focus-visible in
   the same :is() as :hover so tabbing opens a cell exactly as pointing at it
   does, and --cf-specular travels on both — and then declared the affordance
   hover-only in nine other places. A reader arriving by keyboard got the ring
   and none of the movement the same control gives a pointer, so the two ways
   of reaching a control answered differently. The ring is not the answer to
   this: it says *where you are*, and these declarations say *what this does*.
   → foundations/motion.html#principles */
.cf-icon--motion { transition: transform var(--duration-base) var(--ease-standard); }
a:hover > .cf-icon--motion,
a:focus-visible > .cf-icon--motion,
button:hover > .cf-icon--motion,
button:focus-visible > .cf-icon--motion { transform: translateX(4px); }

/* --------------------------------------------------------------------------
   BUTTON
   Rectangular, 2 px corners, mono uppercase label, arrow on the right.
   Primary uses the lime gradient — the "Licht-Ebene" material.

   The corner is --radius-sm and not --radius-none. All three CTAs in the
   Figma export carry rx="2" — hero 189x48, footer 396x46, ueber-uns 228x46 —
   so a square button was never what was drawn. See the note on the radius
   tokens for the full count.
   -------------------------------------------------------------------------- */
.cf-btn {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  min-height: 3rem;
  padding: var(--space-3) var(--space-4);
  border: var(--stroke-1) solid transparent;
  /* border-box, or a gradient variant grows a hairline of the wrong colour
     down each side. background-origin defaults to padding-box while
     background-clip defaults to border-box, so the image is sized to the
     padding box but painted across the border too — and the default
     background-repeat tiles it to cover the difference. On --primary that put
     the ramp's lime end in the 1 px strip at the LEFT edge and its CF-Grau
     start in the strip at the RIGHT, one full period out of place. Sizing the
     image to the border box removes the strip to be filled. */
  background-origin: border-box;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
  transition: background var(--duration-base) var(--ease-standard),
              color   var(--duration-base) var(--ease-standard),
              border-color var(--duration-base) var(--ease-standard);
}
/* The specular already answered :focus-visible and the arrow did not, so a
   keyboard reader got the light and not the gesture — half a state. */
.cf-btn:hover .cf-arrow,
.cf-btn:focus-visible .cf-arrow { transform: translateX(4px); }

/* background-image, not the background shorthand: the shorthand resets
   background-origin to padding-box and would undo the rule above. */
/* Hover and press are LIGHT MOVING, not a change of colour. The plate keeps
   --gradient-light-90 in every state — CF-Grau falling to Glas at the near
   rake with lime at the lit edge — and what responds is a specular travelling
   in from off the right edge, the same gesture .cf-btn--glass makes and the
   same registered property driving it. Refraction hinted at, not simulated.

   It used to swap the whole plate for flat --cf-lime on hover. That reads as
   the button changing material rather than catching the light, it spends the
   screen's one lime moment on a hover state, and it throws away the ramp the
   brand spent its gradient family establishing.

   Press is the one place the contour comes up. .cf-btn already reserves a
   1 px transparent border, so inking it costs no layout and says "pressed" in
   the brand's own primitive rather than in another colour. The specular eases
   back at the same time: the surface tilts away from the source under the
   finger, and returns to the hover position on release.

   --cf-specular is declared here for the reason it is declared on --glass:
   without it, a browser with no @property support has an unresolvable var()
   in this declaration, which is guaranteed invalid and takes the whole
   background-image with it. Keep it equal to the @property initial-value. */
.cf-btn--primary {
  --cf-specular: 150%;
  background-image:
    radial-gradient(55% 300% at var(--cf-specular) 50%,
                    rgba(255, 255, 255, 0.5) 0%,
                    rgba(255, 255, 255, 0) 60%),
    var(--gradient-light-90);
  color: var(--accent-ink);
  border-color: transparent;
  /* The two speeds the motion doctrine asks for, on one element: the control
     acknowledges at --duration-base, the light crossing it takes
     --duration-slow, because light does not snap.

     This is an OVERRIDE, not a subtraction, exactly as on --glass. `background`
     is absent because nothing about the background moves any more — both hover
     and press move only --cf-specular, and a transition whose only moving part
     is a gradient animates nothing. Give this button a state that moves
     background-color and `background` goes back in this list on the same day. */
  transition: color var(--duration-base) var(--ease-standard),
              border-color var(--duration-base) var(--ease-standard),
              --cf-specular var(--duration-slow) var(--ease-standard);
}
.cf-btn--primary:hover,
.cf-btn--primary:focus-visible { --cf-specular: 64%; }
.cf-btn--primary:active {
  --cf-specular: 92%;
  border-color: var(--accent-ink);
}

/* On top of imagery or the hero. The one place glass and the light layer
   meet: a frosted plate with the light falling across it, lime at the lit
   edge. It used to declare backdrop-filter underneath a fully opaque
   gradient, which is a GPU cost with nothing to show for it — the blur was
   never visible. The plate is genuinely translucent now.

   Every layer BEHIND THE LABEL only adds light, so the darkest point the
   label ever sits on is --surface-glass itself. That is the bearing tint,
   which holds black type at 4.5:1 over an arbitrarily dark backdrop.

   "Behind the label" and not "on the button" — the qualifier is new and it
   is there because the lit rim below is the one layer that is not strictly
   additive. Measured on the rendered plate at 1280 px: over a WHITE backdrop
   the rim's Sky stop reads rgb(232,243,249) against a plate of
   rgb(235,248,245), which is relative luminance 0.881 against 0.914 — the
   rim is 3.6 % darker. Over a BLACK backdrop the same point reads
   rgb(202,213,219) against rgb(144,157,154) and is far lighter. That is a
   real edge taking the colour of the light rather than always brightening,
   and it is confined to the top pixel, above the label rather than behind
   it, so the sentence above holds exactly where it is load-bearing. */
.cf-btn--glass {
  position: relative;
  color: var(--accent-ink);
  /* DELETE THIS LINE AND BROWSERS WITHOUT @property LOSE THE LIGHT ON THE
     PLATE. The long version follows; that sentence is the whole of it.

     The rest position is declared here rather than left to the initial-value
     in the @property block, because leaving it there makes --cf-specular
     unset wherever @property is unsupported — and an unresolvable var() is
     *guaranteed invalid*, which takes the whole declaration it sits in with
     it at computed-value time. Both gradient layers go, and the button
     computes to background-image: none.

     What that costs is now smaller than it was, and only because of the
     split below. While this was one `background` shorthand it also took
     --surface-glass down with it, leaving a blur over a transparent
     background-color — a pure-black label (--accent-ink) directly over the
     hero video with nothing holding the contrast floor. --surface-glass is
     its own background-color declaration now, so it survives an invalid
     background-image and the bearing tint still holds 4.5:1. The plate
     degrades to flat glass rather than to nothing. Keep the declaration
     anyway: losing the light layer is still a real loss, and the guarantee
     costs one line.

     A var(--cf-specular, 150%) fallback would resolve the value too, and is
     not wrong — a registered property with an initial-value is never
     guaranteed-invalid, so its fallback is never consulted and the two forms
     are equivalent. Use the plain declaration because it says plainly what
     the fallback only implies, and because the guarantee above is then
     visible at the declaration rather than buried in a var() argument.

     (A Chromium 141 repaint quirk was also cited here and has been demoted:
     it reproduces only under pointer hover, not under focus, so a later run
     checking it the way this system recommends would find nothing and delete
     the surrounding argument with it. The condition is recorded once in
     foundations/materials.html. Nothing in this rule depends on it.)

     Keep this value equal to the initial-value in the @property block. */
  --cf-specular: 150%;
  /* background-image, not the `background` shorthand: the shorthand resets
     background-origin to padding-box, and the 1 px transparent border then
     gets filled by tiling the image — the far end of the ramp down the near
     edge. See .cf-btn. */
  /* THREE LAYERS: the specular on top, the lit rim on the top pixel, the
     plate's own face underneath. The middle one is new and it is the token,
     not a copy of it — --glass-edge, the same one pixel of Weiss-Glas-Sky
     the navigation sheet ends at.

     Until now this component's own comments claimed that edge in three
     places — here, in tokens.css on --glass-rim-light, and in
     foundations/materials.html ("on the sheet exactly as on the button") —
     and it was on the sheet only. What the button actually had was a flat
     white 55 % border and a face gradient carrying Glas into lime, which is
     a lit FACE, not a lit edge. The claim was the right design; the code
     had not caught up. Now the two glass surfaces in the system end at the
     same edge, drawn from the same token, and the division the family asks
     for — one lit edge with hue, one specular without — is literally true
     on both rather than true on one and asserted on the other.

     WHY IT IS WINDOWED TO 62 %. --glass-edge is authored for a full-bleed
     sheet with no lime anywhere on it. This plate has lime at its right
     end, and running the edge's Sky tail into that end would put the
     material's chroma in two places at once — the same failure the specular
     comment above rules out for a Glas tail. So the layer is sized to 62 %
     of the plate and the gradient's whole 0-100 % is rendered inside that
     window: Weiss peaks at 9.9 % of the plate, Glas at 33.5 %, Sky at
     49.6 %, and the ramp is back to fully transparent at 62 %.

     62 % is not a taste figure. The specular comment below measures where
     the face's lime actually becomes visible — about 63 % across, well
     before its declared 87 % stop — and the rim expires one point before
     that. Same relationship the band already has: the edge dies exactly
     where the lime arrives, so the one-lime-moment rule is strengthened
     rather than merely not broken. Move the face's stops and this number
     moves with them.

     It is painted on the border box (background-origin is border-box, from
     .cf-btn) so it lands ON the 1 px rim rather than inside it, under the
     semi-transparent border colour — two layers on one pixel, exactly the
     idiom .cf-nav::after uses. It is decorative and sits above the label
     rather than behind it, so it touches no text contrast; and the plate's
     floor is background-color, which is beneath all three image layers and
     covers the whole box either way.

     background-repeat is now load-bearing. It was absent while every layer
     sized to `auto` and filled the box; a 1 px layer with the initial
     `repeat` would tile the rim down all 48 px of the plate.

     The var() above is a THIRD reference in a declaration that the note on
     --cf-specular says can be killed by one unresolvable one. It is safe for
     a different reason than that note gives: --glass-edge is an unconditional
     :root declaration in tokens.css with no @property behind it, so it always
     resolves. Should it ever move behind a feature query, this layer must get
     a fallback or the whole plate goes with it. Verified the degraded shape
     rather than assumed it — forced to `none`, the layer empties, the other
     two hold their positions in the three-item size list, and the label row
     renders byte-identical to the plate before this rim existed. */
  /* THE FACE IS A RAMP AND AN ANGLE, DECLARED APART, for the reason
     --foil-stops and --foil-rake are: written together, the material has to
     be restated in full every time the path changes, and the path does change
     one rule below.

     It had to. This is the only lime leg in the system's CSS that was never
     put on the oklab path — every other one is (--gradient-light,
     --gradient-light-90, --gradient-spectrum, .material-rake, .material-bloom)
     and lime -> Glas is precisely the leg where the two paths part: dEok
     0.04430 declared, 0.03866 composited over CF-Grau at these stops' own
     alphas. --glass-edge, one layer up, is left in sRGB deliberately at
     0.00049 composited, and the table at that token says why. This was
     seventy-nine times that, in the same declaration, by omission.

     THE STOPS DO NOT MOVE. Only the path between them, which is the whole
     claim of the INTERPOLATION block in tokens.css and the reason the rim's
     62 % window — measured off where this face's lime becomes visible, about
     63 % across — is unaffected. A ramp and its oklab twin sharing one stop
     list is also what makes the pair legible to the family check.

     AND GECKO DOES NOT WALK IT, WHICH IS OPEN. The rule below is `in oklab`
     and Firefox answers @supports (background: linear-gradient(in oklab,
     red, blue)) true, so this ramp takes the gated declaration there and
     then does not honour it. The condition is exact and it is this ramp's:
     Gecko interpolates a gradient in the declared space only while every
     stop carries the SAME alpha. Where the alphas differ across a leg — this
     ramp's whole 62 % -> 87 % run, Glas at 0.55 into lime at 0.88 — Gecko
     interpolates the PREMULTIPLIED colour in sRGB and the declared space is
     dropped. Measured, Firefox 153 against Chromium 141, these four stops
     rendered at 400 px over CF-Grau, and against the arithmetic:

         across the leg   Firefox        Chromium       premult. oklab
           25 %           (172,194,164)  (171,193,171)  (171,193,171)
           50 %           (191,215,100)  (190,215,131)  (190,215,132)
           75 %           (210,236, 37)  (209,235, 59)  (209,235, 59)

     Firefox's `in oklab` row is its own sRGB row to within 4 levels at every
     sample; Chromium is on the premultiplied-oklab path to within one. On the
     shipped plate, face layer alone over CF-Grau, label and rim excluded:
     Firefox stands 41 levels of blue off Chromium at worst and 2.35 on
     average. Isolated to two stops: equal alpha agrees between the engines
     exactly at every alpha from 0.5 to 1; differing alpha diverges by up to
     42 levels, peaking four fifths along this leg. So the divergence this
     block was written to remove — lime -> Glas, the one leg where the two
     paths part — is still what a Gecko reader gets, and the whole of the
     oklab branch below is spent on Blink and WebKit.

     WHAT WAS TRIED, AND WHY IT IS NOT HERE. The system already owns the move:
     an inline SVG cannot interpolate in oklab either, so every lime leg in it
     carries a stop at the oklab path's own value to pull the sRGB path back
     (tokens.css, SVG CANNOT DO THIS). Two such stops on this leg — 79 % and
     85.5 %, searched on a 0.5 % grid against the exact path — take Firefox
     from 41 levels to 7 at worst and 2.35 to 0.44 on average, and cost
     Chromium one level on 803 of 10 500 plate pixels, which is the 8-bit
     rounding of the two rgba() and nothing else: oklab interpolation is a
     straight line, so a stop ON the line adds no geometry.

     check-gradient-family.py refuses it, and it is right to. THE ARC allows
     ONE stop inside a leg and only at that leg's POLAR midpoint; a stop
     anywhere else is read as an endpoint, which splits the leg, and the two
     halves — both near lime, chroma ratio inside ARC_RATIO_CEILING — are
     turns that then demand polar waypoints of their own. Glas -> lime is a
     FALLOFF, so the arc gives it nothing, and the one stop the rule would
     accept is the polar midpoint of Glas and lime, which is #A8FFB6: a green
     in no palette in this brand, and a real change to what Blink draws. The
     rule and the correction want opposite stops in the same place.

     So this is recorded rather than fixed. Closing it means a person deciding
     whether a falloff leg may carry oklab-path stops for an engine's sake —
     a new category in THE ARC, not a number in it — and check-gradient-family
     .py is where that decision would have to be spelt out. Until then the
     divergence is stated here rather than absent, which is the difference
     between a known cost and a surprise.

     --cf-face is an unconditional declaration on this rule with no @property
     behind it, so it always resolves. That is the same safety --glass-edge
     has and NOT the safety --cf-specular needs; should it ever move behind a
     feature query, it needs a fallback or the whole plate goes with it. */
  --cf-face-ramp: rgba(197, 235, 226, 0)    30%,
                  rgba(197, 235, 226, 0.55) 62%,
                  rgba(225, 255, 0, 0.88)   87%,
                  var(--cf-lime)           100%;
  --cf-face: linear-gradient(var(--angle-square), var(--cf-face-ramp));
  background-image:
    radial-gradient(55% 300% at var(--cf-specular) 50%,
                    rgba(255, 255, 255, 0.42) 0%,
                    rgba(255, 255, 255, 0) 60%),
    var(--glass-edge),
    var(--cf-face);
  background-repeat: no-repeat;
  background-size: auto, 62% var(--stroke-1), auto;
  background-position: 0 0;
  background-color: var(--surface-glass);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  /* The token, not the literal it used to repeat. --glass-border held this
     exact value and was read by one rule in the whole system — the
     .material-glass documentation utility — while the only shipping glass
     surface with a border wrote it out by hand. The two could not be
     changed together, and the inverse theme's redeclaration of the token
     reached the documentation swatch and never the button. */
  border: var(--glass-border);
  /* NO DARK RING. A 1 px black inset ran round this plate for one release, to
     carry a boundary the lit rim cannot hold against a video — measured at
     1.07-1.31:1 on every edge at every desktop width. It is gone by design
     ruling: glass in this brand is not outlined, and a black line round a
     lime-gradient plate is the first thing anybody sees.

     So the plate's own perimeter no longer claims 3:1 against arbitrary
     artwork, and what identifies this control instead is everything that is
     not its outline: the mono label, the arrow, the lime end, and the
     specular that answers the pointer. See foundations/materials.html,
     "A control's edge", for the measurement and for the two ways to get the
     boundary back without ink — neither of which is a line. */
  /* `background` is not in this list, and never needed to be: a gradient
     background-image does not interpolate. Given two gradients differing in
     one number, Chromium jumps straight to the new value on the first frame
     rather than transitioning between them. Naming `background` here
     animates nothing and only tells the next reader that it does.

     Two things follow that are easy to get wrong in opposite directions.

     This is an OVERRIDE, not a subtraction. `transition` is declared whole
     here, so it replaces the base list rather than editing it: computed
     transition-property is `color, border-color, --cf-specular` on this
     button against `background, color, border-color` on the others. The
     omission is free only while the gradient is the button's sole background
     change, which is true today. Give it a state that moves
     background-color and that state will jump — demonstrated, not assumed:
     a background-color set on this button reads its final value 60 ms in,
     with no intermediate. Add `background` back to this list on the same day
     you add such a state.

     And read the gradient claim narrowly. It is true of THIS button, whose
     only moving part is a gradient. It is NOT a reason to strip `background`
     from the base .cf-btn rule above, where it is load-bearing —
     background-color interpolates perfectly well and three modifiers depend
     on it. Measured mid-transition, slowed to 2 s:

       --outline   rgba(0,0,0,0) -> rgba(0,0,0,0.88) -> rgb(0,0,0)
       --solid     rgb(27,32,34) -> rgb(3,4,4)       -> rgb(0,0,0)
       --primary   gradient      -> image already none, colour at 0.88 -> lime

     --primary is the mixed case: its image leg snaps on frame one while the
     colour leg fades in underneath over --duration-base. The rule is "a
     transition whose only moving part is a gradient animates nothing", not
     "`background` in a transition list animates nothing".

     --cf-specular, registered above, is the one thing that actually moves.
     Which means a browser that does not support @property gets a switch
     rather than a travel: an unregistered custom property does not
     interpolate, and there is no background-image fade underneath it to
     fall back on. Measured both ways, not assumed. The state change is still
     communicated, which is the part that matters — but only because the rest
     value is declared above. Without that declaration the same browser gets
     no plate at all rather than a switch; see the note on it. */
  /* Two speeds on one element, deliberately. The control responds at
     --duration-base, which is the "UI moves short and fast" half of the
     motion doctrine. The light crossing it takes --duration-slow, which
     looks like a violation and is not: what travels is the light, not the
     button, and light does not snap. At --duration-base a band crossing
     most of the plate reads as a twitch rather than as a source moving past.

     TWO KINDS OF THING TAKE 480 ms, AND THE COUNT IS NOT KEPT HERE ANY MORE.
     This comment said "three consumers", then "four", and both were short —
     the second missed .cf-btn--primary's specular, which is this same
     construction on the other button, and the register row sliding
     --sheen-panel, which is the third of that token's three drawings. A
     roster in a comment ages the moment somebody adds the surface it does
     not know about, which is the same lesson foundations/materials.html
     learnt twice about this exact token.

     What survives, because it is a rule rather than a count: light crossing
     a surface takes 480 ms — the two buttons' speculars, and the accordion
     row, the blog cell and the register row sliding --sheen-panel by
     background-position — and so does a panel opening, which is a reveal
     rather than light. Both belong at 480 ms and the token's stated purpose
     covers both; what does NOT belong there is the control's own
     acknowledgement of a click, which is --duration-base.

     The sheen half of that list is derived and no longer worth writing down:
     scripts/check-glass-budget.py's seventh claim reads every shipping rule
     that paints --sheen-panel and holds the panel table on
     foundations/materials.html to the set. See foundations/motion.html,
     whose table this comment used to contradict. */
  transition: color var(--duration-base) var(--ease-standard),
              border-color var(--duration-base) var(--ease-standard),
              --cf-specular var(--duration-slow) var(--ease-standard);
}
/* The face on the oklab path, the same way every other lime leg in the system
   takes it; browsers without it keep the sRGB ramp above. One line, because
   the stops are a custom property and only the path changes here — the same
   shape as the two foils in tokens.css and .cf-arrive__ghost below.

   NOT --glass-edge, and that is the point of leaving it out. The layer above
   this one runs Weiss -> Glas -> Sky and diverges by 0.00148, which the table
   at that token measures and deliberately declines to correct. This leg
   carries lime and diverges by thirty times as much. */
@supports (background: linear-gradient(in oklab, red, blue)) {
  .cf-btn--glass {
    --cf-face: linear-gradient(var(--angle-square) in oklab, var(--cf-face-ramp));
  }
}
/* The highlight travels in from off the right edge rather than sweeping
   across. Refraction hinted at, not simulated.

   It is a band, not a blob: the vertical radius is 300 % of the button, so
   the falloff across a 48 px plate is almost nothing and what crosses it is
   a soft column of light — which is what a flat sheet of glass does with a
   grazing source. The old figure put the centre 40 % *above* the plate with
   a 210 % radius, so its falloff expired about 5 px short of the bottom
   edge — the 62 % stop lands at y 90.2 % on the centre column and 88.5 % at
   the right edge, leaving the bottom tenth of a 48 px plate unlit. (An
   earlier version of this comment said the bottom of the button was outside
   the gradient entirely. It was not: the ellipse reaches y +170 %. The
   band is real but it is a tenth, not a third.)

   The win is shape and rest state, not amplitude. Measured on the same
   surface — the hero poster, which is what the CTA actually sits on — the
   old figure peaked at 15.6 levels of grey and the new one at 20.2, which
   is a modest gain. What changed is the ends: the old profile is 2.0 levels
   at the far left and -2.1 at the far right, meaning the highlight covered
   the whole plate and merely slid its weight around, reading as a tint. The
   new one returns to 0.0 at both ends and peaks at 38 %, so it is a band
   with unlit plate either side of it, and the rest state is genuinely
   unlit rather than washed. Tinted to lit.

   (An earlier figure of "5 levels" for the old geometry was measured on the
   documentation page's light backdrop, not on the poster, and understated
   it. Same-surface numbers only.)

   It clears the lime in both states — but only just, and the margin is
   smaller than the stop positions suggest. 87 % is where the lime STOP is
   declared, not where lime becomes visible: the leg ramps Glas 0.55 -> lime
   0.88 from 62 % to 87 %, so the plate is already reading lime long before
   the stop. Measured on the rendered button, label and arrow masked,
   limeness = (R+G)/2 - B:

     across      59 %   63 %   66 %   69 %   72 %   75 %
     limeness    -4.3   13.9   41.3   68.6   95.8  123.0
     band delta  +5.5   +3.6   +1.9   +0.6   -0.0    0.0

   So lime is visible from about 63 %, and the band does not stop short of
   it — the two overlap from roughly 63 % to 72 %. What saves it is that the
   band is dying exactly where lime is starting: the most it ever adds to a
   lime pixel is 3.6 levels, at the very foot of the ramp, and by the time
   lime has any strength the band contributes nothing at all. Read the
   margin as "the band expires as the lime arrives", not as the 14-point gap
   the declared stops imply. Anyone nudging the hover endpoint rightward has
   very little room.

   The old figure — centre 64 %, x-radius 120 % — spanned roughly -10 % to
   138 % and washed white straight over the lime, which shows in the profile
   as a NEGATIVE delta at the right end on hover: light that was sitting on
   the lime at rest and moving off it. So the new geometry still strengthens
   the one-lime-moment rule rather than merely not breaking it.

   This band only ever adds, so the darkest point behind the label stays
   --surface-glass and the contrast floor below is untouched. See the note
   at the top of the rule for the one layer on this plate that is not
   strictly additive — the rim, which is above the label, not behind it.

   The band is pure white end to end, which makes it the one achromatic
   gradient in the light family — --glass-edge travels Weiss to Glas to Sky,
   the foil goes further, and the brand's north star is hue travel inside a
   narrow luminance band. This is a deliberate exception, not an oversight: a
   specular is physically achromatic, because it is the source reflected
   rather than the surface coloured. It is settled, not pending: the gradients
   lane has ruled on it. A Glas tail would put the plate's chroma in two
   places at once, since --glass-edge already carries the hue travel on this
   same component — one lit edge with hue, one specular without, is the
   division the family asks for. Do not "fix" it.

   The transparent stop is rgba(255,255,255,0) and not `transparent`, which
   is transparent BLACK and would drag a grey cast through the falloff. Same
   reason the other two gradients in this block spell their colour out. */
.cf-btn--glass:hover,
.cf-btn--glass:focus-visible { --cf-specular: 40%; }
/* PRESS. The plate tilts away from the source under the finger and returns to
   the hover position on release — the same gesture .cf-btn--primary makes,
   and this button did not have it. The two light-bearing buttons in the
   system had a hover each, a focus each, and a press between them, which left
   them disagreeing about what a press is on a lit surface.

   76 % is that button's own proportion, not a second guess at the same
   number. --primary rests at 150 %, hovers to 64 % and gives 28 of that 86
   back on press: 33 %. This plate has a longer throw — 150 % to 40 %, so 110
   — and 33 % of it is 36, which lands at 76 %.

   THE CONTOUR DOES NOT COME UP HERE, and that is the one place the two
   buttons diverge rather than an omission. --primary inks its reserved 1 px
   border on press because that border is transparent at rest and the ink is
   free. This plate's border is already a lit rim at --glass-border,
   which is half of the glass edge itself; inking it would trade the
   material's own edge light for a black line drawn over the hero video. On
   glass the specular moving IS the signal, which is the whole reason the
   material has one.

   That reasoning outlived the refraction line, which for one release sat
   inside this rim and has since been removed: the objection here was to
   spending the lit rim to get ink at the edge, and it holds whether or not a
   second line exists. The press still has only the specular to say anything
   with, and now so does the rest of the plate. */
.cf-btn--glass:active { --cf-specular: 76%; }

/* THE RIM PASS — the second half of what the navigation sheet already does,
   on the other frosted surface in the system.

   WHY IT EXISTS. Until now every response this plate made was gated on a
   pointer: --cf-specular travels on :hover, :focus-visible and :active and on
   nothing else. On a touch device none of those three arrive, so the primary
   call to action of the whole site was a static plate — the one surface where
   the brand's north star, a foil that shifts as you move past it, had the best
   chance of being true and was not. The sheet had already answered the same
   question the right way: a bar is not a thing you point at, it is a thing you
   scroll past, so its light is scrubbed by scroll. A hero button is BOTH, and
   it only had the half a mouse can reach.

   Two lights on one control do not collide, because they are on the two layers
   the family already keeps apart. --cf-specular is on the FACE — a radial band
   crossing the plate, achromatic, the source reflected. This is on the EDGE —
   --glass-rim-light over --glass-edge, exactly the pair .cf-nav::after paints.
   One lit edge with hue, one specular without: the same division, now literally
   true on both frosted surfaces rather than on one.

   ON A PSEUDO-ELEMENT, AND THAT IS THE WHOLE ENGINEERING OF IT. The plate
   itself carries backdrop-filter, and moving a gradient across a blurred layer
   re-rasterises the blur every frame — the rule foundations/materials.html
   states as "never move anything on a blurred layer", and the reason both of
   the navigation's animations live on its one-pixel rim rather than on its
   sheet. The rim there is .cf-nav::after, a separate element from the blur in
   .cf-nav::before. This button has no such split: it paints its blur, its face
   and its rim on one element. So the travelling band gets its own unblurred
   layer and the plate's own background-image list is left exactly as it was —
   which is also what keeps every contrast figure measured on it still valid,
   since not one of its three layers moved.

   THE WINDOW IS THE RIM'S OWN, and it is 62 % for the reason the rim is: this
   plate has lime at its right end and the material's chroma cannot be in two
   places at once. Sizing this layer to the rim's window rather than to the
   plate means the band parks off the left of the WINDOW and leaves off its
   right, so the light dies at exactly the x where --glass-edge has already
   faded to nothing — and never reaches the lime, which the face's own band was
   measured to only just avoid. Window the layer, leave the gradient alone.

   The width looks like numerology and is not. The rim layer is sized against
   the BORDER box (background-origin is border-box, from .cf-btn), while an
   absolutely positioned element resolves its percentages against the padding
   box, which is 2 x --stroke-1 narrower. 0.62 of that difference puts the two
   windows on the same pixel. Drop the correction and the band expires about a
   pixel short of the rim; keep it and the two edges end together by
   construction rather than by looking right at one width.

   -70 % AND 170 % ARE THE SHEET'S OWN ENDPOINTS, not a second guess at them.
   A band 40 % of its container wide is fully off-canvas at both — a layer
   positioned at P is offset by P x (container - image), so at -70 % the band's
   right edge sits at -0.02 of the window and at 170 % its left edge is past
   the far end. Same band, same width, same travel, on both glass surfaces.

   IT TOUCHES ONE ROW, AND THAT IS MEASURED RATHER THAN REASONED. Differencing
   the rendered plate with the band running against the plate with it parked,
   landing page, 1280 px: of 256 x 48 pixels, 54 differ by more than one level
   and every one of them is on ROW 0. Max delta 13. Rows 1 to 47 — which is the
   whole label, the arrow and every pixel of the bearing tint the type sits on
   — are byte-identical at every scroll position sampled. The plate's contrast
   floor is its background-color, and this layer is a 1 px strip on the border
   box above the type, so there was never a mechanism by which it could reach
   the floor; the diff is here because "it cannot" is worth one measurement.

   That is also why this needs no contrast table of its own. The plate makes no
   boundary claim against the artwork — glass is not outlined in this brand, so
   the rim is the material's edge rather than a guaranteed 3:1 line, and adding
   white to a decorative rim cannot cost anything that was being counted.

   ONE PASS PER TRAVERSE OF THE SCREEN, on view() rather than scroll(). The
   sheet is a plane the whole document moves under, so its band is scrubbed by
   the document; this is one object the reader carries up the screen once, so
   its band is scrubbed by its own passage through the viewport. Proportional
   to the trip rather than timed, like every other scrubbed thing here, and it
   runs backwards when the reader scrolls back up.

   PARKED AT REST, so the un-enhanced state is the designed one: no view
   timeline, reduced motion, print, or forced colours all leave the band off
   the left edge and the plate rendering byte-for-byte what it renders today.
   The light is an enhancement on top of the drawing, never a replacement. */
.cf-btn--glass::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--stroke-1));
  left: calc(-1 * var(--stroke-1));
  width: calc(62% + 0.62 * 2 * var(--stroke-1));
  height: var(--stroke-1);
  pointer-events: none;
  /* The plate's corner, on the one corner this strip can overhang. */
  border-top-left-radius: var(--radius-sm);
  background-image: var(--glass-rim-light);
  background-repeat: no-repeat;
  /* The material's own band and its parked endpoint, not a third copy of the
     numbers. This rim is one layer where the bar's and the plate's are two;
     the token is a bare percentage with no vertical in it for exactly that
     reason, so all three read the same two names. → tokens.css */
  background-size: var(--glass-rim-band) 100%;
  background-position: var(--glass-rim-park) 0;   /* off-canvas — see above */
}
@supports (animation-timeline: view()) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-btn--glass::before {
      animation: cf-glass-rim linear both;
      animation-timeline: view();
    }
  }
}

/* The focus ring is the one part of this button the plate's contrast floor
   does NOT protect: at outline-offset it lands 2-4 px OUTSIDE the plate, on
   the hero artwork, which nobody controls and which is a video — so the
   backdrop under the ring changes frame to frame. The global black ring is
   fine on the documentation page's flat grey and is not guaranteed here.

   Neither single colour can be: against a 3:1 requirement, black holds only
   where the backdrop is lighter than L 0.100, and lime only where it is
   darker than L 0.258. Sampling frames cannot fix that, the same way it
   could not fix the bearing tint.

   The two are complementary, though — those ranges OVERLAP, so between them
   every possible backdrop is covered, and they contrast 18.5:1 with each
   other. So the ring is two-tone: black inside, lime outside. Black sits
   against the plate, whose floor IS guaranteed, giving a 13:1 inner edge
   that no frame can take away; lime sits against the artwork and carries the
   dark frames.

   Be precise about which edges the artwork cannot touch, because it is two
   and not three: the black/plate boundary and the black/lime boundary. Both
   are internal to the ring and the plate, so no frame reaches them. The
   third edge, lime against the artwork, is the one that varies — at the
   crossover luminance where the two ranges meet (artwork L 0.246) lime
   measures 2.96:1 there and the indicator is carried by its internal edges
   alone. That is the design working, not a gap, but "two edges survive"
   means these two specifically.

   This is the one focus indicator in the system that does not read
   --focus-ring, and it cannot: a single token cannot be two-tone. Anyone
   retheming that token will not reach this button, so change it here too.

   Order matters. Lime inside would put it against the light plate at 1.4:1
   and waste the one guaranteed edge. box-shadow is the inner ring because
   outline cannot be doubled; it is dropped in forced-colors mode, where the
   outline survives and the system takes over anyway. */
.cf-btn--glass:focus-visible {
  outline: var(--stroke-2) solid var(--cf-lime);
  outline-offset: var(--stroke-2);
  /* One ring, not two. This used to carry a second, inset one — the plate's
     refraction line, restated here because box-shadow is a single property
     and naming only the outer ring would have dropped it for the duration of
     the focus. The plate no longer draws that line at rest, so there is
     nothing to restate: what is left is the focus indicator itself, black
     against the plate and lime against the artwork, and it is a STATE. The
     ruling this file records is about the button's resting edge; an indicator
     that exists only while the control is focused is a different object and
     keeps its ink. */
  box-shadow: 0 0 0 var(--stroke-2) var(--cf-schwarz);
}

.cf-btn--outline {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-strong);
}
.cf-btn--outline:hover,
.cf-btn--outline:focus-visible { background: var(--cf-schwarz); color: var(--grey-000); }

/* The black button. Its label is not white — it is the foil, clipped into the
   letterforms, and the arrow is the colour the ramp has arrived at by the time
   it reaches the right edge.

   This is the brand's own move, not an invention. The manual's app icon
   (Farben > Anwendung) is a black plate with the mark filled by the light
   ramp; the mark is what carries the colour and the plate stays black. A black
   button is the same object at UI scale, so the label carries the colour.

   The foil and not --gradient-light, and that is the load-bearing choice: the
   light ramp starts at lime, and lime is a moment — one element per screen.
   A black button is furniture and there can be several of them on a page, so
   a lime-tipped label would spend the screen's one lime moment on a secondary
   download link. The foil is the cool half of the same spectrum with the lime
   cut off, which is exactly why it exists. See base.css, FOIL TYPE.

   The plate is black at rest, so hover has to LIFT rather than deepen —
   there is nothing under black to go to. It goes to grey-800, the ramp's
   darkest non-black step, which is also what --surface-raised resolves to
   on an inverse surface: the button rises the same distance a raised panel
   does. It reads as a state change without the plate stopping being black.

   Contrast, measured, worst sample of the ramp against the plate:

     Weiss       #FFFFFF   21.00:1 on black   16.48:1 on grey-800 (hover)
     Glas        #C5EBE2   16.36:1            12.84:1
     Sky 300     #B8D7F1   14.03:1            11.01:1
     Violett 300 #BEBFF0   11.90:1             9.34:1   <- floor

   So the floor is 11.9:1 at rest and 9.3:1 on hover, against a 4.5:1
   requirement. There is no size at which this label is the weak part of the
   page, which is why the treatment can be the default here rather than an
   opt-in for large type only. */
.cf-btn--solid {
  background: var(--surface-inverse);
  color: var(--text-inverse);
}
.cf-btn--solid:hover,
.cf-btn--solid:focus-visible { background: var(--grey-800); }

/* The gradient box is the WHOLE button, not the label, and that is what makes
   the effect read. At 116.57° across a 352 x 48 slab the ramp is 336 px long
   and runs left to right, so the label — which sits in the left half — gets
   the Weiss -> Glas -> Sky leg and the arrow at the far right gets Violett.
   Light enters at the first letter and arrives, coloured, at the arrow. Scope
   the gradient to a label span instead and every button ramps the full
   spectrum inside ten characters, which reads as a rainbow rather than as
   light crossing a plate.

   It also means no markup changes: a label span would have to be added to
   every .cf-btn--solid on the site, and the ones that were missed would
   silently keep white text.

   TWO BACKGROUND LAYERS WITH TWO CLIPS, and the second one is not optional.
   background-clip: text clips the whole background — background-color
   included — so a single-layer version leaves the button with no plate and a
   label that reads as pale type directly on the page. The layer list splits
   the two jobs: layer 1 is the foil, clipped to the glyphs; layer 2 is empty
   and exists only to carry `border-box`, because the background-color is
   painted with the clip of the FINAL layer. The plate is therefore still a
   plain background-color and still transitions on hover, which a pseudo-
   element plate would not have done.

   The obvious alternative — plate on a ::before at z-index: -1 — is wrong,
   and wrong in a way that looks like a browser bug. Within a stacking
   context, negative-z-index children paint AFTER the element's own
   background, so the plate covers the clipped-to-text gradient and the label
   disappears entirely. Measured, not reasoned about: it renders as a black
   slab with a violet arrow and no text.

   Everything is inside @supports, and the condition names two things, not
   one. background-clip: text is not the whole trick this rule depends on —
   the plate needs LAYER 1 clipped to text and LAYER 2 clipped to border-box
   in the SAME declaration, which is a browser committing to per-layer
   background-clip, not merely to background-clip: text on its own. Firefox
   153 parses `background-clip: text, border-box` as valid — CSS.supports
   reports true, so the query above alone lets it into this block — and then
   does not honour the split at paint time: every layer clips the same way,
   the foil paints as an ordinary box fill instead of glyph shapes, and the
   label, `color: transparent` throughout this block, has nothing underneath
   it to read. Measured on the shipped 404 page's "Zur Startseite" and on
   every other .cf-btn--solid on the site, in Firefox: a plain gradient
   rectangle, no visible character, over the fallback's own black plate —
   worse than the unenhanced rule above, which this block exists to improve
   on. `and (not (-moz-appearance: none))` is a second gate for a fact
   @supports has no direct way to ask: it does not test per-layer clip
   support, because there is no such feature query — the syntax above is
   valid everywhere and the failure is in what a layer clip DOES, not in
   whether it parses. -moz-appearance is a Mozilla-only ident no non-Gecko
   engine has ever recognised, so `not (-moz-appearance: none)` reads "not
   Firefox" without asserting anything about what Firefox — or any other
   engine — supports of the feature actually in question. The unenhanced
   state is the rule above it, untouched — opaque plate, white label.
   Nothing to fall back to, because nothing was taken away. */
@supports ((-webkit-background-clip: text) or (background-clip: text))
      and (not (-moz-appearance: none)) {
  .cf-btn--solid {
    background-image: var(--gradient-foil), none;
    -webkit-background-clip: text, border-box;
    background-clip: text, border-box;
    background-color: var(--surface-inverse);
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
  /* The hover state has to RESTATE the layer list, not just recolour the
     plate. `.cf-btn--solid:hover { background: … }` above is a shorthand, so
     it sets every background longhand for the hovered state — including
     background-image: none and background-clip: border-box. Overriding only
     background-color here leaves those two standing, and the button loses its
     foil the moment a pointer touches it: clip gone, image gone, and the label
     is still -webkit-text-fill-color: transparent, so it disappears entirely.
     Caught in review, before it shipped.

     Restating is the fix rather than removing the shorthand above, because
     that shorthand is the fallback path's hover and has to stay intact for
     browsers that never enter this block. */
  /* :focus-visible is here because it is on the shorthand above. The two
     selector lists have to stay identical: the shorthand sets
     background-image: none and background-clip: border-box for every state it
     names, so a state named there and not here loses the foil and the label —
     still -webkit-text-fill-color: transparent — vanishes. That is the bug the
     comment above records, and adding a state to only one of the two rules is
     how it comes back. */
  .cf-btn--solid:hover,
  .cf-btn--solid:focus-visible {
    background-image: var(--gradient-foil), none;
    -webkit-background-clip: text, border-box;
    background-clip: text, border-box;
    background-color: var(--grey-800);
  }

  /* fill: currentColor would resolve to transparent and the arrow would
     disappear. --violett-300 is not a picked colour: it is the foil's own
     100 % stop, which is where the ramp stands at the arrow's x. */
  .cf-btn--solid .cf-arrow { fill: var(--violett-300); }

  /* NO ::selection RULE HERE. It was the second copy of the same three
     declarations — this clipping context and .text-foil each carried one,
     because the global ::selection stated `color` and not the fill. It states
     both now, so both copies are gone. See base.css, ::selection. */
}

/* All three of these hand the label back a real colour. Ink on paper has no
   plate to sit on, forced-colors has already decided what a button looks
   like, and a reader who asked for more contrast asked for the top of the
   range rather than a ramp whose floor happens to be high.

   EACH DOOR NAMES ALL THREE STATES, for the reason the @supports block above
   names them together: the shorthand's hover restates the whole layer list
   at a higher specificity than a bare .cf-btn--solid, so a door written for
   the resting button alone is beaten on :hover and :focus-visible and the
   foil clip comes back under it. The two older doors used to be written that
   way. It never showed, because the fill each door hands back is declared
   later in source than the transparent one and the hover rule never touches
   the fill — a label that stays legible by source order rather than by
   statement, which is exactly the dependency the comment above says this
   bug comes back through. scripts/check-foil-doors.py holds every door to
   every state the context clips in. */
@media print {
  .cf-btn--solid,
  .cf-btn--solid:hover,
  .cf-btn--solid:focus-visible {
    background: none;
    color: var(--cf-schwarz);
    -webkit-text-fill-color: var(--cf-schwarz);
  }
  .cf-btn--solid .cf-arrow { fill: var(--cf-schwarz); }

  /* AND --primary, WHICH HAD NEVER BEEN HANDED ANYTHING BACK. It is the one
     button on the site that is a plate of light rather than a plate of ink, and
     its only paint is a background-image: the specular and --gradient-light-90,
     over `border-color: transparent`. A print dialog ships background graphics
     off, so what actually reached paper was --accent-ink — black — floating on
     white with a transparent hairline around it. The single call to action on
     every page in the site printed as a phrase rather than as an object.

     What the plate was carrying is "this is the one thing to do here", and the
     rule for that is the brand's founding one: what a colour carried, a line
     carries. The contour is the object; the label was already black. Contour
     before fill, on the one surface that cannot hold a fill. */
  .cf-btn--primary {
    background-image: none;
    border-color: var(--cf-schwarz);
    color: var(--cf-schwarz);
  }
}
@media (forced-colors: active) {
  .cf-btn--solid,
  .cf-btn--solid:hover,
  .cf-btn--solid:focus-visible {
    background: none;
    color: ButtonText;
    -webkit-text-fill-color: ButtonText;
  }
  .cf-btn--solid .cf-arrow { fill: ButtonText; }
}
/* The plate stays and only the image goes — background-image: none rather
   than background: none — because this door, unlike paper and forced
   colours, still has a black plate to keep: Weiss on Schwarz is 21:1 at rest
   and 16.5:1 on the grey-800 hover, against the foil's own 11.9 and 9.3. The
   arrow takes the same ink because --violett-300 was the ramp's own last
   stop, and there is no ramp. See base.css, THE FOURTH DOOR, for why the
   answer is solid ink and not a darker foil. */
@media (prefers-contrast: more) {
  .cf-btn--solid,
  .cf-btn--solid:hover,
  .cf-btn--solid:focus-visible {
    background-image: none;
    -webkit-background-clip: border-box;
    background-clip: border-box;
    color: var(--text-inverse);
    -webkit-text-fill-color: var(--text-inverse);
  }
  .cf-btn--solid .cf-arrow { fill: var(--text-inverse); }
}

/* The one button that goes back to square, because it has no plate: it is a
   label over a 1 px rule, and a rule is structure. Inheriting --radius-sm from
   .cf-btn would curl the last 2 px at each end of that hairline upward, which
   at 1 px reads as a rendering fault rather than as a corner. */
.cf-btn--ghost {
  min-height: auto;
  padding: var(--space-2) 0;
  gap: var(--space-3);
  background: none;
  color: var(--text-primary);
  border: 0;
  border-bottom: var(--stroke-1) solid var(--border-strong);
  border-radius: var(--radius-none);
  /* An OVERRIDE of .cf-btn's list, not an addition to it, so the properties
     that still move are restated: colour, because the consent banner's copy of
     this button lifts its label to Weiss, and the line's weight, which is this
     button's whole state. background and border-color never move here. */
  transition: color var(--duration-base) var(--ease-standard),
              border-bottom-width var(--duration-fast) var(--ease-standard);
}
/* THE ONE BUTTON WITH NO STATE, and it was the one that could not borrow the
   others'. --primary and --glass move a specular, --outline and --solid move a
   plate; this button has neither, so the three rules above it left it with the
   arrow's 4 px nudge and nothing else. On the consent banner that never showed,
   because the dark surface supplies its own hover further down this file — so
   the gap was only ever visible on the two pages that use the button as page
   furniture: the 404's "Defekten Link melden" and the article's way back to
   News.

   Its ink cannot answer, either: --border-strong on :root IS Schwarz and
   --text-primary is already the strongest ink on the wash, so there is nothing
   darker for either the line or the label to go to. What is left is the line's
   WEIGHT, which is the indicator .cf-field__input already uses for the same
   drawing — a label over a 1 px bottom rule — and the padding gives the pixel
   back so nothing under the button moves. One idiom, two components.

   :focus-visible is named alongside :hover the way every other variant names
   it, and the global ring stays: unlike the field, this control has somewhere
   for a ring to go, and the thickening is a second signal rather than the
   only one. */
.cf-btn--ghost:hover,
.cf-btn--ghost:focus-visible {
  border-bottom-width: var(--stroke-2);
  padding-bottom: calc(var(--space-2) - var(--stroke-1));
}

.cf-btn--block { display: flex; width: 100%; }

/* Width steps. A CF button is a wide slab with the label left and the arrow
   pinned right, so it needs a set width to keep that proportion — two steps
   cover every use on the site: --wide for an inline CTA, --xwide for the
   footer's.

   Width plus max-width, not min-width. A minimum cannot be overridden by a
   maximum, so a floor wider than its column is an overflow with no way out.
   `min-width: min(16rem, 100%)` reads like it solves that but does not: the
   hero's CTA sits in an absolutely positioned shrink-to-fit wrapper, so the
   percentage is circular and collapses the button to its label — measured at
   171 px against a designed 256 px. A definite width keeps that wrapper
   sized, and max-width gives way when the column is genuinely narrower. */
.cf-btn--wide,
.cf-btn--xwide {
  width: var(--btn-w);
  min-width: 0;
  max-width: 100%;
}
.cf-btn--wide  { --btn-w: 16rem; }
.cf-btn--xwide { --btn-w: 22rem; }

/* --------------------------------------------------------------------------
   THE TWO RUNGS THE BUTTON NEVER HAD — unavailable, and busy

   The presence ladder (tokens.css 8a, drawn in components/arrival.html) has
   four rungs and one hard rule: an object may only move ALONG it, never
   across it — a thing that is not here is the SAME DRAWING as the thing that
   is, one rung down. The field took rung 2 for `disabled` years ago. The
   button, the most-used control in the system, stood at rung 3 and had
   nowhere else to stand: nothing in this file matched `[disabled]` on a
   .cf-btn, so `<button class="cf-btn cf-btn--primary" disabled>` rendered
   the full lime plate, full ink, pointer cursor — a dead control drawn
   exactly like a live one, which is the failure shape every checker in
   scripts/ exists for.

   AND THE DOCUMENTATION POINTED THE OTHER WAY. components/buttons.html's Do
   list read "Disabled states via the `disabled` attribute plus a
   --grey-200 surface", which is two faults in one line: --grey-200 was never
   written on a button anywhere in this file, so the instruction produced the
   live-looking control above; and a grey plate is precisely the GREY SLAB the
   ladder names as its one prohibition — "it is not a low-presence drawing of
   anything". The button's own page instructed the reader to break the
   system's own rule, in a treatment the stylesheet did not implement.

   SO THE TWO STATES ARE THE TWO RUNGS THE LADDER ALREADY DESCRIBES:

     unavailable  rung 2, --presence-near (2-1). The ladder reserves that rung
                  for "a claim about the object, not about the wait", which is
                  what unavailable is. Same box, same size, same place, same
                  corner — the fill goes and the contour comes up, which is
                  contour-before-fill read as a state.
     busy         rung 0, --presence-absent (1-4), with the arrival band
                  crossing it. Busy is not a weaker claim about the object; it
                  is the object not being here YET, which is the ghost's own
                  sentence. It reuses --arrive-period, --arrive-light-peak and
                  @keyframes cf-arrive-sweep rather than inventing a second
                  wait, because two waits at two speeds on one page is the
                  thing the ladder exists to prevent.

   FOUR GRADIENTS, ONE PER EDGE, for the reason this file has now written
   three times: CSS gives no control over the dash-to-gap ratio of
   border-style: dashed and the four line types ARE ratios. 4 on / 2 off is
   2-1 and 1 on / 4 off is 1-4 — the same numbers as .cf-field__input
   [disabled] and .cf-arrive__plate. Change one, change all three. They land
   on the BORDER box because .cf-btn already sets background-origin:
   border-box and reserves a 1 px transparent border, so the dashes sit
   exactly where the resting contour sits and nothing moves by a pixel.

   --glass IS EXCLUDED, and the exclusion is in the selector rather than left
   to chance. Its edge is a lit rim and not a contour — foundations/
   materials.html rules that glass is never outlined in black, so a black
   dashed edge on it would be a claim the material forbids — and the ladder
   moves a contour. It also has nothing to move for: the one .cf-btn--glass
   on the site is an <a>, the hero's "Kontakt aufnehmen", and neither
   `disabled` nor a busy state applies to a link. A glass control that must
   go unavailable is an --outline control.

   WHICH ATTRIBUTE IS A RULING, not a preference, and it differs by state:

     unavailable  `disabled`. The control is not going to become available on
                  this page, so there is nothing for a reader to discover by
                  reaching it, and dropping it out of the tab order is the
                  honest report. This is the case current guidance reserves
                  the native attribute for.
     busy         `aria-disabled="true"` plus `aria-busy="true"`, never
                  `disabled`. A focused element that is given `disabled`
                  drops focus to <body> — so a keyboard reader who pressed
                  the button loses their place at the exact moment the page
                  starts working. aria-disabled keeps the element focusable
                  and says the same thing to assistive technology. It blocks
                  NOTHING on its own: the activation has to be blocked in the
                  handler, which is why the busy state is documented as a
                  pair of attributes and a guard clause rather than as a
                  class.

   Both are matched here so that either attribute draws, whichever a page
   reaches for. [aria-busy] draws on its own too — it does not depend on
   aria-disabled being present as well — because a state that renders only
   when two attributes agree is a state that silently does not render.
   -------------------------------------------------------------------------- */
.cf-btn:not(.cf-btn--glass)[disabled],
.cf-btn:not(.cf-btn--glass)[aria-disabled="true"],
.cf-btn:not(.cf-btn--glass)[aria-busy="true"] {
  /* Rung 2. The busy rule below moves these two and nothing else, which is
     what "one figure, four line types" means in a stylesheet. */
  --btn-rung-x: repeating-linear-gradient(90deg, var(--border-strong) 0 4px, transparent 4px 6px);
  --btn-rung-y: repeating-linear-gradient(0deg,  var(--border-strong) 0 4px, transparent 4px 6px);
  /* Parked, not transitioning: the specular is the plate catching light and
     an unavailable plate catches none. Equal to the @property initial-value
     and to the rest value --primary declares, so a browser without
     @property lands in the same place. */
  --cf-specular: 150%;
  background-color: transparent;
  background-image: var(--btn-rung-x), var(--btn-rung-x), var(--btn-rung-y), var(--btn-rung-y);
  background-size:
    100% var(--stroke-1), 100% var(--stroke-1),
    var(--stroke-1) 100%, var(--stroke-1) 100%;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: no-repeat;
  /* RESTATED, and this is the trap --solid sets. Inside the @supports block
     above, --solid clips layer 1 to `text` and paints the label with
     -webkit-text-fill-color: transparent. Take its foil away without handing
     the fill back and the label does not go grey — it disappears entirely,
     which is the bug that block's own comment records having caught twice. */
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: var(--text-secondary);
  -webkit-text-fill-color: var(--text-secondary);
  border-color: transparent;
  box-shadow: none;
  cursor: not-allowed;
}
/* The arrow says the action leads somewhere. It does not, so it neither
   carries the plate's ink nor keeps the 4 px nudge .cf-btn:hover gives it —
   an unavailable control that still leans when a pointer crosses it is
   answering, which is the one thing it must not do. */
.cf-btn:not(.cf-btn--glass)[disabled] .cf-arrow,
.cf-btn:not(.cf-btn--glass)[aria-disabled="true"] .cf-arrow,
.cf-btn:not(.cf-btn--glass)[aria-busy="true"] .cf-arrow {
  fill: var(--text-secondary);
  transform: none;
}
/* The one variant with no plate: a label over a 1 px rule, so one rung down
   is one edge changing type, not four appearing. Exactly the drawing
   .cf-field__input[disabled] makes — one idiom, two components, and now
   three. The bottom border goes transparent so the gradient is the only
   line, and the hover thickening below is left with nothing to thicken. */
.cf-btn.cf-btn--ghost[disabled],
.cf-btn.cf-btn--ghost[aria-disabled="true"],
.cf-btn.cf-btn--ghost[aria-busy="true"] {
  background-image: var(--btn-rung-x);
  background-size: 100% var(--stroke-1);
  background-position: 0 100%;
  border-bottom-color: transparent;
}
.cf-btn.cf-btn--ghost[disabled]:hover,
.cf-btn.cf-btn--ghost[aria-disabled="true"]:hover,
.cf-btn.cf-btn--ghost[aria-busy="true"]:hover {
  border-bottom-width: var(--stroke-1);
  padding-bottom: var(--space-2);
}

/* BUSY — rung 0, and the light that says it is still happening.

   Two declarations move the figure down two rungs; everything else about the
   drawing is inherited from the rule above, which is the ladder's rule
   expressed as a cascade. The band is the arrival ghost's, at the same
   period and the same measured 0.18 cap, because a wait is a wait wherever
   it is drawn. */
.cf-btn:not(.cf-btn--glass)[aria-busy="true"] {
  --btn-rung-x: repeating-linear-gradient(90deg, var(--border-strong) 0 1px, transparent 1px 5px);
  --btn-rung-y: repeating-linear-gradient(0deg,  var(--border-strong) 0 1px, transparent 1px 5px);
  /* The strip the light is allowed into: the edge, whole, with no dash in it.
     A gradient and not a colour because a mask layer is an image. */
  --btn-edge: linear-gradient(#000, #000);
  position: relative;
  isolation: isolate;
}
/* THE LIGHT RUNS ALONG THE CONTOUR, NOT ACROSS THE PLATE, and both halves of
   that sentence were measured before it was written.

   ACROSS THE PLATE WAS TRIED FIRST — .cf-arrive__ghost's band, at the same
   --arrive-light-peak, over the whole button. On this component that band
   crosses TYPE, which is the one thing .cf-arrive__ghost is built never to do:
   its own note says the light sits above the contours it crosses and the
   status line is a SIBLING rather than a child for exactly this reason. A
   button's label is a text node of the button, so there is no sibling to move
   it to. Composited in sRGB and quantized the way the browser paints it, worst
   stop over --text-secondary on the page wash:

     band          label       plate       contrast
     none          72,72,72    207,207,207   5.87:1
     Weiss  0.18   105,105,105 216,216,216   3.85:1
     Weiss  0.18   105,105,105 207,207,207   3.52:1   <- band on the label only
     Glas   0.18    94,101,100 205,212,210   3.96:1
     Sky    0.18    92, 98,102 203,208,213   3.98:1

   Every frame of it is under the 4.5:1 a label owes, and the worst is the
   frame where the band has reached the words and not yet the edge. Lowering
   the peak does not rescue it: the same arithmetic run backwards puts the
   ceiling at 0.09 — 0.10 renders 4.43:1 and fails, the same integer cliff
   tokens.css records for the hero's glass — and half the ghost's light spread
   over a 48 px plate is a band nobody can see.

   SO THE FIGURE IS THE PROGRESS RAIL'S, NOT THE GHOST'S. .cf-progress--
   indeterminate already answers this exact question one component along — "a
   ghost line and a lit line is a difference in line TYPE, which is the
   difference this whole component is built on" — and it answers it in
   --gradient-foil-ink, the one leg of the family legible on this surface,
   because a 0.18 light band over an 8 px box "puts almost no ink on the one
   pixel that is the line". A 1 px contour is that argument at its limit.

   The mask is what confines it. Four layers, the same four the contour above
   paints, at the same sizes and the same positions — so the band paints on
   the dashes and nowhere else. The label keeps its full 5.87:1 and the lit
   dashes measure 5.24:1 against the wash at the ramp's palest stop — Glas 800;
   Sky 800 is 7.12 and Violett 800 is 8.53 — against the 3:1 a contour owes.

   INSET IS NEGATIVE FOR THE SAME REASON .cf-btn--glass::before's top is:
   `inset: 0` resolves against the PADDING box, and .cf-btn's contour is
   painted on the BORDER box (background-origin: border-box, 1 px reserved).
   Zero would put the mask one pixel inside the dashes it is meant to line up
   with, on every edge. */
@supports (mask-image: linear-gradient(#000, #000)) or (-webkit-mask-image: linear-gradient(#000, #000)) {
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"]::after {
    content: "";
    position: absolute;
    inset: calc(-1 * var(--stroke-1));
    z-index: 1;
    pointer-events: none;
    /* The foil's ink, in the foil's own order, in a travelling window. The
       window is the arrival band's — -22 % to +22 % of the sweep, so the pass
       reads as one crossing and a rest — and the colours are the ink ramp's
       three 800s, which tokens.css derives as clearing AA at any size across
       the whole page wash. Nothing here is invented for this component. */
    background-image: linear-gradient(var(--angle-b),
        transparent            calc(var(--arrive-sweep) - 22%),
        var(--glas-800)        calc(var(--arrive-sweep) - 8%),
        /* arc waypoint, Glas 800 -> Sky 800. See THE ARC in tokens.css. */
        #33494E                calc(var(--arrive-sweep) - 4%),
        var(--sky-800)         var(--arrive-sweep),
        /* arc waypoint, Sky 800 -> Violett 800. */
        #273650                calc(var(--arrive-sweep) + 4%),
        var(--violett-800)     calc(var(--arrive-sweep) + 8%),
        transparent            calc(var(--arrive-sweep) + 22%));
    -webkit-mask-image: var(--btn-edge), var(--btn-edge), var(--btn-edge), var(--btn-edge);
    mask-image: var(--btn-edge), var(--btn-edge), var(--btn-edge), var(--btn-edge);
    -webkit-mask-size:
      100% var(--stroke-1), 100% var(--stroke-1),
      var(--stroke-1) 100%, var(--stroke-1) 100%;
    mask-size:
      100% var(--stroke-1), 100% var(--stroke-1),
      var(--stroke-1) 100%, var(--stroke-1) 100%;
    -webkit-mask-position: 0 0, 0 100%, 0 0, 100% 0;
    mask-position: 0 0, 0 100%, 0 0, 100% 0;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    animation: cf-arrive-sweep var(--arrive-period) linear infinite;
  }
}
/* The band on the oklab path, the same way every other gradient in the system
   takes it; browsers without it keep the sRGB band above. A browser without
   the mask keeps the ghost contour and loses the light, which is the correct
   thing to lose: the plate still says the object is not here, and nothing is
   drawn over the label. */
@supports ((mask-image: linear-gradient(#000, #000)) or (-webkit-mask-image: linear-gradient(#000, #000)))
      and (background: linear-gradient(in oklab, red, blue)) {
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"]::after {
    background-image: linear-gradient(var(--angle-b) in oklab,
        transparent            calc(var(--arrive-sweep) - 22%),
        var(--glas-800)        calc(var(--arrive-sweep) - 8%),
        /* arc waypoint, Glas 800 -> Sky 800. See THE ARC in tokens.css. */
        #33494E                calc(var(--arrive-sweep) - 4%),
        var(--sky-800)         var(--arrive-sweep),
        /* arc waypoint, Sky 800 -> Violett 800. */
        #273650                calc(var(--arrive-sweep) + 4%),
        var(--violett-800)     calc(var(--arrive-sweep) + 8%),
        transparent            calc(var(--arrive-sweep) + 22%));
  }
}
/* The ghost variant has ONE edge, so it gets one mask layer and not four —
   otherwise the light travels round a box three sides of which are not drawn.
   The inset is one-sided for the matching reason: this variant's border box
   and padding box differ on the bottom alone, which is where its 1 px lives. */
.cf-btn.cf-btn--ghost[aria-busy="true"]::after {
  inset: 0 0 calc(-1 * var(--stroke-1)) 0;
  -webkit-mask-image: var(--btn-edge);
  mask-image: var(--btn-edge);
  -webkit-mask-size: 100% var(--stroke-1);
  mask-size: 100% var(--stroke-1);
  -webkit-mask-position: 0 100%;
  mask-position: 0 100%;
}

/* 50 % is the designed still — the band at the middle of the plate, the same
   rest the arrival ghost and the swinging foil land on. The ghost stays:
   nothing is moving, and the thing is still not done. */
@media (prefers-reduced-motion: reduce) {
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"]::after {
    animation: none;
    --arrive-sweep: 50%;
  }
}
/* Paper has no waits in it, and no unavailable controls either — but the two
   print differently. The ghost contour PRINTS, because it is a true statement
   about the control on the page that was printed; the light does not, because
   it is the half of the state that is still happening. Same split as
   .cf-arrive__ghost. */
@media print {
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"]::after { display: none; }
}
/* Forced colours drop every background image, which here is the whole
   contour — so the four edge gradients are redrawn as a border, the only
   paint that survives. GrayText is the mode's own word for an unavailable
   control and is the one place in this file where a system colour says
   something the palette cannot. The band goes for the same reason it goes on
   the arrival ghost: it is a background image and there is nothing left of
   it to see. */
@media (forced-colors: active) {
  .cf-btn:not(.cf-btn--glass)[disabled],
  .cf-btn:not(.cf-btn--glass)[aria-disabled="true"],
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"] {
    border: var(--stroke-1) dashed GrayText;
    color: GrayText;
    -webkit-text-fill-color: GrayText;
  }
  /* One edge here too. The shorthand above draws four, which on the variant
     that has no plate is a box the reader has never seen before appearing at
     the moment the control goes unavailable — the ladder's rule broken by a
     media query. */
  .cf-btn.cf-btn--ghost[disabled],
  .cf-btn.cf-btn--ghost[aria-disabled="true"],
  .cf-btn.cf-btn--ghost[aria-busy="true"] {
    border: 0;
    border-bottom: var(--stroke-1) dashed GrayText;
  }
  .cf-btn:not(.cf-btn--glass)[disabled] .cf-arrow,
  .cf-btn:not(.cf-btn--glass)[aria-disabled="true"] .cf-arrow,
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"] .cf-arrow { fill: GrayText; }
  .cf-btn:not(.cf-btn--glass)[aria-busy="true"]::after { display: none; }
}

/* --------------------------------------------------------------------------
   LOGO LOCKUP
   The logo sits on a black plate in the top-left. A plate, not a pill: the
   export draws it 152.09 x 35.25 at rx="2", where a pill would be rx 17.6.
   -------------------------------------------------------------------------- */
.cf-logo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-6);
  background: var(--surface-inverse);
  border-radius: var(--radius-sm);
  color: var(--grey-000);
}
.cf-logo img, .cf-logo svg { height: 1.25rem; width: auto; }
.cf-logo--bare { padding: 0; background: none; border-radius: 0; }

/* THE PLATE IS --surface-inverse, A SOLID background-color, AND FORCED
   COLOURS OVERRIDES THOSE WITHOUT BEING ASKED — tokens.css only has to name
   the glass tokens explicitly because gradients compute to `none` in this
   mode and would otherwise vanish outright; a plain background-color like
   this one is repainted Canvas by the browser regardless of what this file
   says, the same way .cf-footer's is. The mark on that plate is a fixed
   white asset with no idea what is under it, so on the light high-contrast
   theme (Canvas: white) it sits Canvas-on-Canvas. Measured in Chromium at
   1280 x 900 with the mode active and colour-scheme light: the mark's own
   pixels and the plate's, sampled either side of it, both read
   rgb(255, 255, 255) — no contrast at all, the mark gone. Under a dark
   high-contrast theme (colour-scheme dark, Canvas: black) the same white
   mark reads rgb(255, 255, 255) against a rgb(0, 0, 0) plate, 21:1, exactly
   as designed — so only the light theme needs correcting, and inverting a
   pure-white asset there lands it on black, which is CanvasText's own colour
   without shipping a second file to hold it. */
@media (forced-colors: active) and (prefers-color-scheme: light) {
  .cf-logo img { filter: invert(1); }
}

/* --------------------------------------------------------------------------
   NAVIGATION
   ONE PLATE, CENTRED IN THE BAND: the logo at its left, then the seven routes,
   then the language switch — mono uppercase, opaque — floating on the
   full-bleed sheet of frosted glass that is still the band itself.

   THE COMPOSITION IS THE SITE OWNER'S, against a reference frame: "alles in
   einen Balken, zentriert, Logo links weiß, dann die anderen Punkte, trotzdem
   natürlich der sicke Glas-Backdrop" (Website Feedback, "Menu, mal so
   ausprobieren", 2026-08-03). It used to be three plates spread across the
   full width — logo hard left at --gutter, links and switch hard right — with
   1 032 px of band between the logo and the links at 1440. One plate now, as
   wide as its own contents, centred on the viewport.

   THE GLASS IS THE BAND AND NOT THE PLATE, which is what it has always been
   and is the half of this the reference frame does not settle. The plate is
   opaque for the reason the section header has carried since the mockups were
   sampled: it holds its text at 10.6:1 (the dim link ink) and 21:1 (white) no
   matter which frame of the video is playing underneath, and the video is the
   one backdrop on this site nobody controls. The reference bar is dark glass —
   measured off the frame at black 80-85 %, which composites to 7.4-8.5:1 for
   that same dim ink over a pure-white backdrop. That is a real floor and it is
   AAA, so this is a design call and not a contrast one; it is the owner's to
   make, and it is one declaration on .cf-nav__bar the day they make it.

   The plate is a rectangle with a 2 px corner, not a pill. The link bar was
   417 x 41 at rx="2" in the export, where a pill would be rx 20.5 — the
   compressed JPG is what made it look like a capsule, and the vectors settle
   it. The same 2 px is on all three CTAs and on the team photos; see the note
   on the radius tokens. The reference frame's corner is nearer rx 10 and is
   deliberately not taken: one corner in this system, and it is 2 px.

   The mockups' sampling of the sheet stands: the top 80 px of the frame is
   blurred and desaturated against the hero artwork with a hard edge at its
   bottom, and the plate inside it reads #000. That is the manual's stacking
   order — Layer 03 (glass) under Layer 02 (opaque).
   -------------------------------------------------------------------------- */
/* The bar has to stay a POSITIONED element. Three absolutely positioned
   children resolve against it — the glass sheet, the edge, and the mobile
   menu — so overriding this to `static` sends all three to the viewport,
   and a viewport-sized backdrop-filter blurs the entire page. If a context
   needs the bar not to stick, give it `position: relative`, never `static`.

   It also has to stay OUT of a wrapper that sets both `overflow` and
   `border-radius`. Firefox drops backdrop-filter on a sticky element whose
   ancestor carries that pair, and it fails silently: the bar keeps its tint
   and its edge and simply stops blurring, which is the one broken state
   nobody notices in review. The bar is a body-level element on both pattern
   pages today, so nothing hits this — it is written down because the fix
   after the fact is to move the nav, and by then the wrapper is load-bearing. */
.cf-nav {
  position: sticky;   /* not fixed: a fixed backdrop-filter repaints the
                         blurred region on every scroll frame in iOS Safari */
  top: 0;
  z-index: var(--z-nav);
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--nav-height);
  /* THE BAND IS --nav-height EXACTLY, and it was 89.94 px for as long as the
     links carried their own plate: --space-5 above and below a 49.94 px plate
     is 89.94, and min-height never bound. Every offset on the site is written
     against the token — the sticky top, the panel's max-height, the
     scroll-margin on every heading, .cf-hero's own negative margin — so six
     pixels of bar that the token did not know about were six pixels wrong in
     all of them. The plate is 51.94 px inside --space-2 above and below, which
     is 67.94 against a floor of 84, so the min-height binds and the band
     measures what it says it measures — at every width, on every page. */
  padding: var(--space-2) var(--gutter);
}

/* THE PLATE. Everything the bar carries is inside it, and it is as wide as
   that content and no wider — `max-content` rather than a width, so the plate
   is the sum of its parts and centring is the flex line's job.

   POSITIONED, because the folded panel below resolves against it: the panel
   used to hang from the band's bottom edge at the band's own gutters, and the
   plate is now the thing it unfolds from. Relative and never static — see the
   band's note above for what a static ancestor does to the three absolutely
   positioned children.

   NOT A CONTAINER WITH `overflow` ON IT. The radius is 2 px and the temptation
   to clip to it is real; Firefox drops backdrop-filter on a sticky element
   whose ancestor carries overflow AND border-radius, and it fails silently.
   Nothing here clips. */
.cf-nav__bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: max-content;
  max-width: 100%;
  padding: var(--space-2);
  background: var(--surface-inverse);
  border: var(--stroke-1) solid var(--border-inverse);
  border-radius: var(--radius-sm);
}
/* The lockup is inside the plate now, so it brings no plate of its own: the
   mark, and the --space-2 that keeps it off the plate's own border. Its
   `background` and its --space-6 padding were the plate — .cf-logo is a
   standalone lockup everywhere else it appears and keeps them there. */
.cf-nav__bar > .cf-logo {
  padding: 0 var(--space-2);
  background: none;
  /* Free space only ever exists on the folded bar, where the plate is wider
     than logo + toggle + switch; there it puts the two controls hard right,
     which is where they have always been. Above 48.75 rem the plate is
     max-content, there is no free space, and this resolves to zero. */
  margin-right: auto;
}

/* The glass sheet. It reaches one blur kernel past the bottom of the bar and
   is masked back to the bar's own box: backdrop-filter only samples the
   pixels directly behind its element, so without the reach a vivid object
   scrolling up would appear at the boundary already sharp instead of
   arriving through the blur. The mask hides the reach; the blur has already
   read it.

   The reach is exactly --glass-lookahead and not a fraction of the bar,
   because blurred area is the whole cost here. Measured over a 3,000 px
   scripted scroll at 1280 px, 95th-percentile frame: 16.9 ms with no glass
   at all, 17.8 ms with no reach, 20.0 ms at a reach of a quarter bar,
   25.1 ms at half a bar. Everything past the kernel is spend with nothing
   to show for it.

   "ARRIVING THROUGH THE BLUR" DOES NOT HAPPEN FOR TEXT IN WEBKIT 26.5, and
   this is left unfixed rather than worked around. -webkit-backdrop-filter
   computes correctly (verified via getComputedStyle: blur(16px) saturate
   (1.08), identical to Chromium) and non-text backdrops — the tiled ground,
   a striped gradient — blur as designed. Text glyphs behind this sheet do
   not: measured on a minimal reproduction (sticky bar, ::before at inset:0,
   backdrop-filter only, no mask, no isolation, no z-index) and confirmed on
   this page's own FAQ text and on Expertise's .cf-pin__step title, both
   render pixel-sharp through the veil where Chromium renders them
   correctly smeared. Ruled out by direct test, each with no effect: the
   mask (--glass-lookahead's linear-gradient), z-index: -2 on this pseudo,
   isolation: isolate on .cf-nav, position: sticky vs fixed on .cf-nav, the
   view-transition-name relocation two files up (base.css, "THE BAR ITSELF
   CANNOT CARRY THE NAME"), and forced layer promotion (transform:
   translateZ(0) / will-change: transform) on the text, on this pseudo, and
   on .cf-nav itself. The failure tracks specifically with text content —
   a striped background under the same bar blurs while text laid over that
   same background does not — which points at how this WebKit build
   composites glyph layers rather than at anything this stylesheet controls.
   Not chased further because every candidate fix left to try either
   degrades Chromium/Firefox's identical, working render or forces every
   passing text run onto its own compositing layer sitewide on a hunch.
   Whether this reproduces on real Safari (Playwright's WebKit is a Linux
   GTK/WPE port, not Apple's Core Animation compositor) is untested here.
   → a human, with real Safari, decides whether this is worth chasing. */

/* NO CAPTURE THIS REPO'S TOOLING CAN TAKE SHOWS GECKO PAINTING THIS BLUR,
   and the trigger is not the mask, not the reach, not the bar's isolation,
   not anything else this rule can change — it is the screenshot pipeline
   itself. Audited 2026-08-04, a team portrait's head under the band at
   identical scroll offsets, mean per-channel pixel difference between the
   live sheet and the same sheet with backdrop-filter forced off, measured
   over glass-on-photo pixels: Chromium 151 reads 31.61 (peak 196), Firefox
   153 reads 0.00, WebKit 26.5 reads 0.00 — while getComputedStyle returns
   blur(16px) saturate(1.08) in all three. On a ten-line page with four
   bars over 6 px stripes — plain blur(16px), this rule's exact hard-stop
   mask, clip-path in the mask's place, veil and blur together — stripe
   gradient energy under every bar in Firefox is 36.65 against 36.65 on
   bare stripes, where Chromium reads 0.21-2.03. The veil alone does paint
   (35.16 under the veiled bar), so the sheet is present in the capture and
   only its filter is missing — and real Firefox 153.0.1, run headless with
   --screenshot on that same page, produces the identical 36.65. That is
   Bugzilla 1657997, open since 2020 and reconfirmed on Nightly 143:
   Gecko's snapshot path does not composite backdrop-filter, in any
   variant, on any page. The screen is a different pipeline — Firefox has
   shipped working backdrop-filter since 103 — so a Firefox-only fallback
   here (an opaque veil, a faked blur) would trade the render every real
   Firefox user sees for one only test tooling looks at. Wrong trade, same
   shape as the view-transition-name lesson in base.css.

   Ruled out by direct test, each still 0.00 in Firefox: mask-image: none;
   clip-path: inset(0 0 var(--glass-lookahead) 0) in the mask's place; the
   reach dropped entirely (height: 100%, no mask); isolation: auto on
   .cf-nav — the filter-effects-2 backdrop-root suspicion; this mask and
   that isolation are both inducers on paper, and an inducer on the element
   itself would legally empty its own backdrop, but removing both changes
   nothing — translateZ(0); will-change: backdrop-filter; contain: paint on
   the bar. Headed and headless capture pixel-identically.

   THE PARAGRAPH ABOVE INHERITS THIS CAVEAT. The same audit run on the two
   WebKit ports available here (26.4/2287 and 26.5/2336, macOS) captures no
   backdrop-filter either — plain, masked, clipped or veiled, text or
   stripes behind it (Playwright issue #28363 is the same family) — so a
   screenshot cannot separate text from non-text backdrops in WebKit: the
   split that paragraph reads off captures is not observable by this
   harness today, and its "blurs as designed" gradient does not reproduce
   on either build. What real Safari and real Firefox put on a real screen
   is exactly what none of this tooling can photograph.
   → a human, at an actual window, is the only instrument that closes
   this one. docs/firefox-audit/2026-08-04/ holds the frames. */
.cf-nav::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: calc(100% + var(--glass-lookahead));
  z-index: -2;
  pointer-events: none;
  background: var(--surface-glass-veil);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  -webkit-mask-image: linear-gradient(#000 0 calc(100% - var(--glass-lookahead)),
                                      transparent calc(100% - var(--glass-lookahead)));
  mask-image: linear-gradient(#000 0 calc(100% - var(--glass-lookahead)),
                              transparent calc(100% - var(--glass-lookahead)));
}

/* The edge the sheet ends at. Present by default, because a bar with
   content passing under it has to read as a separate plane; the scroll
   block below takes it away again while the bar is still part of the hero,
   which is the state both mockups are drawn in. */
.cf-nav::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: var(--stroke-1);
  z-index: -1;
  pointer-events: none;
  /* TWO LAYERS ON ONE PIXEL: the specular, parked off the left edge, over the
     designed rim. Written as background-image plus explicit repeat, size and
     position rather than the `background` shorthand, so the parked position
     is stated here instead of implied — and so a later reader can see that at
     rest this rule paints --glass-edge and nothing else.

     THE BAND AND ITS TWO ENDPOINTS ARE THE MATERIAL'S, AND ARE READ RATHER
     THAN RESTATED. The width and the two off-canvas positions were written out
     here, on .cf-btn--glass::before and on .cf-info-card--glass::before, and
     twice more inside three pairs of keyframes — eight copies of one geometry
     that had to stay equal to each other by hand, on three surfaces whose own
     token comment says they differ only in "the container and the clock". They
     are --glass-rim-band, --glass-rim-park and --glass-rim-cross now, and the
     arithmetic that ties the endpoints to the width — a band b wide is fully
     off the left at -b/(1-b) and fully off the right at 1/(1-b) — is stated
     once, in tokens.css, beside them. */
  background-image: var(--glass-rim-light), var(--glass-edge);
  background-repeat: no-repeat, no-repeat;
  background-size: var(--glass-rim-band) 100%, 100% 100%;
  background-position: var(--glass-rim-park) 0, 0 0;
}

/* Light arriving as the bar separates from the page, and then light crossing
   it for as long as the reader keeps going. Both scrubbed by scroll position
   rather than a timer, so they track the reader's hand and run backwards on
   the way up. No script; the un-enhanced state is the settled one.

   The two ranges are different on purpose and say different things. The rim
   FADES IN over the first 5rem, because that is the bar changing state —
   part of the hero, then a plane over the page. The specular CROSSES over the
   whole document, because that is not a state change at all: it is the sheet
   being a material the reader is moving past. A bar that is still part of the
   hero has not separated from anything, so at scroll 0 the rim is invisible
   and the band has not started; by the time the rim is fully lit the band has
   moved 80px worth of a document scroll, which is nothing.

   One pass per document, which is as quiet as this can be made while still
   being motion. It is proportional to the page rather than timed, so a long
   page gets a slow drift and a short one a brisk pass, and neither is a
   speed anybody chose.

   DELIBERATELY ON THE EDGE AND NOT ON THE SHEET, and this is now the reason
   for two things rather than one. Fading a backdrop-filtered layer
   re-rasterises its blur every frame, which showed up as a doubling of long
   frames during scroll; moving a gradient across one would do the same. The
   rim is one pixel of unblurred gradient, so both the fade and the travel are
   free. If a later run wants the sheet itself to respond to scroll, this is
   the layer to do it on — not that one. .cf-btn--glass::before is that run:
   the other frosted surface took the same band onto the same kind of layer,
   for the same reason.

   `screen and` IS NOT DECORATION HERE, and its absence was a real defect for
   as long as this rule has existed. foundations/motion.html states the rule —
   any scroll-driven animation must be scoped to screen — after a paged medium
   printed whichever isometric object happened to be at the current scroll
   position. This rule was the one scroll-driven animation in the system that
   did not obey it, and the failure is the sharper one: print has no scroll, so
   a `both`-filled animation holds its `from` keyframe, and cf-nav-edge's from
   is opacity 0. Measured under print emulation before the fix — the rim
   computed to opacity 0, meaning every printed page lost the hairline that
   separates the bar from the content under it. With the scope, print never
   applies the animation at all and .cf-nav::after falls back to the rest
   declaration above, which draws the edge. Same disposition as everything
   else here: the un-enhanced state is the designed one. */
@supports (animation-timeline: scroll()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-nav::after {
      animation: cf-nav-edge linear both, cf-glass-rim linear both;
      animation-timeline: scroll(root block), scroll(root block);
      animation-range: 0 5rem, normal;
    }
  }
}
@keyframes cf-nav-edge { from { opacity: 0; } to { opacity: 1; } }

/* THE CROSSING, ONCE, FOR EVERY LIT GLASS SURFACE IN THE SYSTEM. This was
   three keyframe pairs — cf-nav-rim here, cf-glass-rim-pass on the hero
   button, cf-info-card-rim on the plate — holding identical values under
   three names, on three surfaces whose token comment already said they share
   the band and differ only in the container and the clock. Three names is
   three chances for one of them to be edited alone, and nothing would have
   reported it: each rim renders correctly on its own.

   IT IS ONE ANIMATION AND STILL THREE CLOCKS, which is the part that makes
   the consolidation legal rather than merely tidy. A keyframe set says WHAT
   moves; animation-timeline says WHEN. The bar takes scroll(root block)
   because a bar is a plane the whole document moves under, the button and the
   plate take view() because each is one object the reader carries up the
   screen once, and the pinned plate on patterns/expertise.html takes the pin's
   own timeline. All three now name this.

   background-position is a list property, so both layers are restated in each
   keyframe; the second pair is the designed rim and never moves. The button's
   rim is a SINGLE layer and reads the same keyframes: a background-position
   list longer than the element's background-image list has its extra values
   ignored at used-value time, so the second pair is simply not consulted
   there. That is what lets one keyframe set serve a two-layer rim and a
   one-layer one without either restating the other's geometry. */
@keyframes cf-glass-rim {
  from { background-position: var(--glass-rim-park) 0, 0 0; }
  to   { background-position: var(--glass-rim-cross) 0, 0 0; }
}

/* THE SAME LOSS PRINT HAD, IN A SECOND MEDIUM. The rim above is two gradient
   layers on a 1 px pseudo-element with no border and a transparent
   background-color, and forced colours computes every gradient
   background-image to `none` — measured on the landing page at 1280,
   .cf-nav::after resolves to `none, none`. So the bar shipped with no bottom
   edge at all in that mode: the one hairline that says the sheet is a plane
   with the page moving under it, gone, on every page, exactly as it was gone
   on paper until the `screen and` scope was added above.

   Redrawn as a border, which is the only paint the mode keeps — the idiom
   .cf-arrive__line and .cf-progress__rail already use, and the same reason
   .material-glass-edge takes it in base.css.

   THE OPACITY ANIMATION IS LEFT ALONE ON PURPOSE. It still runs, so the edge
   still arrives over the first 5rem as the bar separates from the hero, and
   that reads correctly here rather than in spite of the mode: the sheet is an
   opaque Canvas plate under forced colours (see tokens.css), so a bar at
   scroll 0 is already legible as a bar and the hairline is still doing the job
   it does everywhere else — marking the moment content starts passing beneath
   it. What does NOT survive is the specular, and it should not: cf-nav-rim
   moves a background-position that now has no image to move. */
@media (forced-colors: active) {
  .cf-nav::after { border-top: var(--stroke-1) solid CanvasText; }
}

/* THE SAME LOSS IN THE OTHER TWO DOORS, and it is a boundary here rather than
   a hairline. tokens.css turns --glass-edge and --glass-rim-light off wherever
   the blur is off, because with no material there is no material light; the
   measurement is there. What is local to this rule is that this sheet ENDS at
   this edge instead of closing a perimeter — .cf-nav::before carries no border
   — so the bar's whole boundary against the page is this one pixel. Nothing
   else would draw it.

   The plate it has to separate from the page is the material's opaque
   stand-in, #E7E7E7 in the light theme, and the wash runs 207 to 255 on every
   screen at every scroll position: at some scroll the bar and the page are the
   same grey and this line is all there is. --border-default removes 24 % of
   whatever is behind it, which is a constant direction at both ends of that
   range (1.40:1 to 2.17:1 against the page) where the white rim inverts inside
   it (1.38:1 down to 1.05:1). Same argument as --glass-border one rule over,
   and the same ink.

   Two blocks and not one, because @supports and @media cannot be OR'd into a
   single query. Both say the same thing.

   THE BACKGROUNDS ARE GONE BY CONSTRUCTION, not by a second declaration. This
   element is `height: var(--stroke-1)` under a global border-box, so a 1 px
   top border leaves a zero-height padding box, and a background-image sized
   against that paints nothing. Exactly what the forced-colours rule above
   relies on, which is why neither needs to unset anything. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .cf-nav::after { border-top: var(--stroke-1) solid var(--border-default); }
}
@media (prefers-reduced-transparency: reduce) {
  .cf-nav::after { border-top: var(--stroke-1) solid var(--border-default); }
}

/* THE LIST IS NO LONGER A PLATE — .cf-nav__bar is the plate and this is the
   run of seven routes inside it. The fill, the border, the 2 px corner and the
   8 px inset all moved there, unchanged; what is left is the row itself.

   Its padding is the one that used to need `ul` to outweigh base.css:150's
   `ul[role="list"] { padding: 0 }` at (0,1,1) — the 8 px inset that computed
   to 0 px on every page in the tree until it was qualified, third occurrence
   of that reset, and scripts/check-reset-shadow.py is what runs it now. There
   is nothing left here for the reset to shadow, so the tag qualifier goes
   with it; the folded panel below states its own plate at (0,2,0) and is
   heavier than the reset on its own. */
/* AND IT WRAPS, WHICH IS THE ONLY REASON THE PLATE CAN BE CAPPED AT ALL.

   .cf-nav__bar is `width: max-content; max-width: 100%` — it asks for the one
   line its contents need and is then cut to the band. When the cap bites, flex
   has to take the difference out of the three children, and it cannot: each is
   `flex: 0 1 auto` with `min-width: auto`, and a nowrap flex row's automatic
   minimum is the SUM of its items. So the row could not shrink, and the
   surplus left the plate to the right — `overflow: visible`, so it went on to
   the document's scroll width and took the whole page sideways.

   The fold does not catch it. `@media (max-width: 48.75rem)` resolves `rem`
   against the root's INITIAL font size and never against the reader's — the
   same two meanings of one word scripts/check-rem-floor.py is named after — so
   the plate unfolds at 780 px at every text size while everything inside it
   goes on scaling. Measured on patterns/landing-page.html, document scroll
   width against viewport, and how far past its own plate the EN switch stood:

     viewport   root 16    root 20    root 22        root 24        root 32
       900       900 -9     900 -11    911 +60 ✗     989 +139 ✗    1302 +451 ✗
      1024      1024 -9    1024 -11   1024 -12       1024  +28     1308 +341 ✗
      1280      1280 -9    1280 -11   1280 -12       1280  -13     1323 +113 ✗

   900 x 22 px is the first failure and it is nothing exotic: 900 px is the
   width just above the fold, and 22 px is between Chrome's "Large" (20) and
   "Very large" (24). WCAG 1.4.4 asks for 200 % — root 32 — and there the page
   scrolled sideways at three of the seven widths this page is swept at.

   One declaration answers it, and only where the fault is: above the fold the
   seven routes take a second row inside the plate and the plate grows to hold
   them, which is what the no-script tier below has always done with the same
   property. At every width and text size that fits on one line today the
   layout is byte-identical — wrap only ever does something to a line that was
   going to overflow. → scripts/check-text-zoom.py

   WHO ELSE SEES IT: THE CENSUS THIS NOTE SHIPPED WAS ONE STATE SHORT. It read
   "below the fold this list is `display: none` (the panel is out of flow) and
   in the no-script tier it is `flex-direction: column`, so neither sees this",
   and the first clause is true of the closed panel and false of the open one —
   [data-cf-nav] .cf-nav__list[data-open="true"] puts `display: flex` back, and
   a wrap on a column with a bounded height makes columns instead of a
   scrollport. Four columns and a clipped route at 568 x 320; the numbers and
   the one-word fix are on the folded rule's own `flex-wrap: nowrap`.

   The second clause holds and for a reason worth keeping: `flex-direction:
   column` is not on its own enough — that is exactly what the folded panel
   also has — but the no-script list has no cap on its height, so its main size
   is its content's and there is never a line to break. Measured at 320 x 568
   and 667 x 375, roots 16/20/24, both engines: one column, seven rows, in
   every cell. */
.cf-nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  list-style: none;
}
.cf-nav__link {
  display: block;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);   /* 10.6:1 on black */
  transition: color var(--duration-fast) var(--ease-standard),
              background var(--duration-fast) var(--ease-standard);
}
.cf-nav__link:hover,
.cf-nav__link:focus-visible { color: var(--grey-000); }
/* The current page is marked by weight of ink, not by a lime ground — lime is
   light, and the one lime moment on the page belongs to the hero's button.

   The export has no marker at all: the nav is one black rect and five text
   runs, all the same grey — five being the export's count and not the site's,
   which has shipped seven routes since Expertise and Suche landed. That is the
   mockup showing a single page rather than a rule, and a bar that never says
   where you are is a real loss, so the marker stays — as a 2 px plate inside a 2 px plate, which is
   the same corner the bar itself carries. It was a capsule inside a capsule
   before, and that shape is what made the whole bar read as a pill.

   Two tokens, one drawing. aria-current="page" is the page itself;
   aria-current="true" is the section a subpage sits under (a vacancy under
   Karriere, an article under News), because "page" would announce a page the
   reader is not on. The marker means "where you are", not "which file", so
   both draw the same — scripts/check-a11y.py keeps the tokens honest. */
.cf-nav__link:is([aria-current="page"], [aria-current="true"]) {
  color: var(--grey-000);
  background: rgba(255, 255, 255, 0.14);
}
/* LIME PLUS AN INNER RING, AND THE INNER RING IS THE HALF THAT WAS MISSING.

   This rule takes the bar's indicator off --focus-ring, and the reason is the
   bar's own plate: black, so the system's black ring would land on black. Lime
   was the right answer for the five links, because their ring falls INSIDE
   .cf-nav__list's black plate — measured 18.51:1 on both edges at 1280 and
   1440. It was not the right answer for the two controls that stand on their
   own plate with nothing under them but the page: .cf-logo at every width, and
   .cf-nav__toggle below 48.75rem.

   THE TWO ARE INSIDE THE PLATE NOW, and this whole note is the reason the rule
   does not change with them. .cf-nav__bar is one plate carrying all four
   controls, so every ring in the bar falls on black at 18.51:1 — measured at
   1440, the lockup's ring reaches 358.7..521.5 x 28..60 inside a plate at
   353.7..1086.3 x 16..67.9, and at 390 the toggle's reaches 16..68 inside a
   plate at 11..73. The figures below are what the two controls measured when
   they stood on the artwork, and they are kept because THE COMPOSITION IS THE
   ONLY THING THAT CHANGED: the plate can be re-cut, the bar can be made glass
   (see the section header), and a control that leaves the plate takes its
   1.06:1 back the moment it does. The guarantee lives in the indicator, not in
   the layout that currently makes it unnecessary.

   The ring is 2 px at outline-offset 2 px, so BOTH of its edges land 2-4 px
   outside the border box — on the hero, which is a video nobody controls and
   whose dominant hue is this brand's lime. Measured against the poster, worst
   edge of eight sampled per control:

     .cf-logo         1.24:1 at 375   1.32 at 768   1.28 at 1280   1.40 at 1440
     .cf-nav__toggle  1.28:1 at 375   1.06 at 768   (not shown above 48.75rem)

   The floor is 3:1 (WCAG 1.4.11). Nothing there is close to it, and the 1.06
   is the control sitting over the artwork's lime passage — the indicator and
   the backdrop are the same colour.

   THE FIX IS THE ONE THIS FILE ALREADY WROTE, over .cf-btn--glass: a two-tone
   ring whose internal boundary is the guaranteed edge. box-shadow fills the
   offset — black from the border box to 2 px — and the lime outline sits from
   2 to 4 px on top of it. Black against lime is 18.51:1 and that boundary is
   INTERNAL to the indicator, so no frame of the video can reach it. The outer
   lime/artwork edge still varies and still carries the dark frames; it is no
   longer the only edge there is.

   ON THE WHOLE BAR AND NOT ON THE TWO. The links do not need it — their plate
   already gives them an edge — but black on black paints nothing, costs
   nothing, and means the guarantee does not depend on which of the bar's
   controls happens to sit inside a plate today. One rule, the same one that
   made the colour lime in the first place — and the composition change above
   is exactly the event it was written to survive.

   Dropped in forced-colors mode along with every other box-shadow, where the
   outline survives and the system takes over — same as .cf-btn--glass.
   → scripts/check-focus-ring-edge.py */
.cf-nav :focus-visible {
  outline-color: var(--cf-lime);
  box-shadow: 0 0 0 var(--stroke-2) var(--cf-schwarz);
}
.cf-nav__toggle { display: none; }

/* --------------------------------------------------------------------------
   THE LANGUAGE SWITCH
   One control, last in the plate, past the seven routes and past the toggle:
   the last thing in the bar because it is the one thing in it that does not
   move you through the site but between two copies of it.

   IT NAMES WHERE IT GOES, NOT WHERE YOU ARE, and that is the one place this
   control departs from the bar's own doctrine. Five links and a current-page
   marker answer "where am I"; a switch with two states would answer it twice,
   and at 375 px a DE|EN pair is 72 px the bar does not have — the logo plate,
   the toggle and the gutters already spend it. So the label is the
   destination, `hreflang` says which language that destination is in, and the
   aria-label says the whole sentence, because "EN" read aloud on its own is a
   letter pair and not an offer.

   `lang` ON THE LABEL, because the label is not in the document's language. On
   the German page it says EN; a German speech synthesiser reads that "eh en".
   The attribute hands those two letters to the English voice, which is the
   same reason patterns/karriere.html marks "Data Engineer" and not the whole
   link around it.

   IT TAKES THE ROUTES' METRIC NOW, AND IT USED TO TAKE THE LINK PLATE'S.
   --space-4 / --space-6 were not new numbers either: they were the list
   plate's --space-2 of padding plus .cf-nav__link's --space-2 / --space-4 of
   label padding, added up, because the switch was one link that had brought
   its own plate and the two plates had to stand level. There is one plate
   now and the switch is inside it, beside the seven routes, so the arithmetic
   that made it 49.94 px tall next to a 33.94 px label is the arithmetic that
   would now make it the tallest thing in the bar for no reason anybody can
   see. Same padding as .cf-nav__link, and the row is one row.

   It keeps its contour, and that is the whole of what still separates it
   from the six. A route is a label on the plate; this is a control on it.

   The lit state is that contour and not a fill — .cf-nav__toggle's note above
   settles why, and for the same reason: a plate standing on the page with no
   unlit sibling beside it cannot be distinguished by a 1.3:1 wash over its own
   black. --grey-000 on the border is this component's "on", already carried by
   .cf-nav__link:hover, by the current-page marker and by the open toggle.
   -------------------------------------------------------------------------- */
.cf-nav__lang {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-4);
  border: var(--stroke-1) solid var(--border-inverse);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--grey-000);
  text-decoration: none;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.cf-nav__lang:hover,
.cf-nav__lang:focus-visible { border-color: var(--grey-000); }

/* The open menu already used --radius-sm while the closed bar was a pill, so
   the bar changed shape on the way to the small screen. One corner now. */
@media (max-width: 48.75rem) {
  /* WITHOUT THE SCRIPT THE LINKS ARE SHOWN AND THE DEAD CONTROL IS NOT.

     A disclosure needs something to disclose it, so every rule that folds this
     bar is gated on [data-cf-nav] — the attribute assets/js/cf-nav.js writes on
     <html> before the body is parsed. Nothing below this comment can therefore
     take the five links away from a reader whose scripting is off, whose
     network dropped the file, or whose browser hit an error in it: they get the
     bar with its list stacked underneath, which is the same plate drawn with
     the same corner, and no button that does nothing.

     Not sticky in that state, and that is the whole reason the fallback is a
     stacked list rather than the open panel. The panel is out of flow, so it
     costs the bar no height; six rows of it pinned to the top of a 375 px
     screen would leave the page about a third of itself to be read in.

     The gate is an attribute on <html> rather than on the bar because it has to
     be true before the bar exists — see the file's own header for why that is
     the load-bearing part. */
  /* THE PLATE IS WHERE THE ROWS GO, and the band is where they used to go: the
     bar wrapped and the list became its second line. One plate now, so the
     plate wraps and the six rows stack inside it — which is also the only
     version that keeps them on a black ground with no plate of their own. */
  html:not([data-cf-nav]) .cf-nav { position: relative; }
  html:not([data-cf-nav]) .cf-nav__bar {
    flex-wrap: wrap;
    row-gap: var(--space-4);
  }
  html:not([data-cf-nav]) .cf-nav__list {
    width: 100%;
    flex-direction: column;
    align-items: stretch;
  }

  /* THE PLATE SPANS THE BAND DOWN HERE, and the band gives up its gutter to
     pay for it. At 320 the six things in the folded bar measure 279.9 px
     against 280 px of content box — the plate's own 8 px inset and 1 px
     border are 18 px the band does not have, so the two gutters come down to
     --space-2 and the plate stands in their place. Measured at 320: the
     lockup's first pixel lands at 20.05 px, which is the --gutter it stood on
     before, to a twentieth of a pixel. */
  .cf-nav { padding-inline: var(--space-2); }

  /* THE ROWS THE TOGGLE DISCLOSES MEET THE SAME FLOOR THE TOGGLE DOES.

     The toggle's note below settles what a primary control on a phone is owed:
     2.75rem, the 44 px touch convention (WCAG 2.5.5, Apple's HIG). What the
     toggle opens is six more of exactly that — the only rows that reach any
     other page — and they kept the bar's desktop metric instead: 11 px mono
     inside --space-2 padding, a 32 px row, six of them 4 px apart, in the
     panel and in the no-script stacked list alike. Measured at 375 on every
     pattern page: toggle 44, each row it discloses 32. One rule serves both
     folded states, because they are the same six rows drawn in and out of
     flow. Above 48.75rem the links are the bar again and this reaches
     nothing — the bar's height is spoken for by --nav-height, and a pointer
     is not a fingertip. → scripts/check-touch-floor.py */
  .cf-nav__link {
    display: flex;
    align-items: center;
    min-height: 2.75rem;
  }

  /* THE ONE PANEL IN THE SYSTEM THAT OPENS AND CLOSES, and until now the only
     state change on the site that simply appeared: measured at 375 px, the
     frame 60 ms after the click and the frame 560 ms after it were
     byte-identical.

     It is drawn out from under the bar rather than switched on — a clip edge
     travelling down the panel, which is the same gesture --sheen-panel and
     --glass-rim-light make, at the scale of a whole surface instead of one
     pixel. Downward and from the bar's own bottom edge, because that is where
     the panel comes from: it is the link bar unfolding, not a sheet arriving
     from somewhere else.

     A WIPE AND NOT A FADE, and that is a contrast decision rather than a taste
     one. This panel is opaque black over the hero video, which is the only
     backdrop on the site we do not control. Fading it in would put its labels
     over a partly transparent plate for the length of the transition, and the
     floor would then depend on which frame of the video was playing. Clipped,
     the plate is fully opaque in every frame of the motion and the labels hold
     16:1 from the first one — the same principle the glass button states as
     "light only adds", applied to a reveal: the guarantee never depends on how
     far the wipe has travelled.

     --duration-slow, matching .cf-accordion__item::details-content. Both are a
     panel opening, and foundations/motion.html already settles what that costs:
     a reveal is content arriving, not the control answering a click. Ease is
     --ease-standard for the same reason, and NOT --ease-in-out, whose entry in
     that page's curve table ("states that toggle back and forth") describes
     this panel almost exactly. It stays unused. Two panel reveals in one system
     should not move on two different curves, and the accordion got there first.

     Support is one lockstep pair — @starting-style and allow-discrete shipped
     together — so the whole thing degrades as one. Without them the transition
     shorthand is dropped at parse time, the open state's clip-path still
     applies, and the panel snaps exactly as it does today. The un-enhanced
     state is the settled one. */
  [data-cf-nav] .cf-nav__list {
    display: none;
    position: absolute;
    /* FLUSH WITH THE PLATE IT UNFOLDS FROM. The inset was --gutter on both
       sides when the panel hung off the band, because the band was
       full-bleed and the gutter was the only thing putting the panel under
       the plates rather than under the screen. It hangs off the plate now,
       and the plate is already inset — repeating the gutter here would inset
       it twice and stand a narrower panel under a wider bar.

       It carries the plate the bar's own fill can no longer give it: out of
       flow, so it is over the hero rather than on the black it dropped from.
       Same fill, same border, same 2 px corner, same 8 px inset — the block
       that used to be ul.cf-nav__list's, moved here whole. At (0,2,0) it
       outweighs base.css's `ul[role="list"] { padding: 0 }` on its own.

       The three --stroke-1 terms are the plate's border, which the offsets
       would otherwise sit inside: a percentage and a zero inset both resolve
       against the containing block's PADDING box, so `100% 0 auto 0` would
       stand a panel 2 px narrow, 1 px high, aligned to nothing a reader can
       see. These put the panel's border box on the plate's border box. */
    inset: calc(100% + var(--stroke-1))
           calc(var(--stroke-1) * -1)
           auto
           calc(var(--stroke-1) * -1);
    background: var(--surface-inverse);
    border: var(--stroke-1) solid var(--border-inverse);
    border-radius: var(--radius-sm);
    padding: var(--space-2);
    flex-direction: column;
    /* AND NOWRAP, WHICH IS THE HALF THE BASE RULE'S CENSUS MISSED. This list
       carries `flex-wrap: wrap` from .cf-nav__list, where it is right: above
       the fold the seven routes take a second line inside the bar's plate.
       That rule's note clears this tier in as many words — "below the fold
       this list is `display: none` (the panel is out of flow)" — and that is
       true of the CLOSED panel only. Open, the [data-open="true"] rule below
       puts `display: flex` back, and the wrap comes back with it, now on a
       column whose main size the max-height 30 lines down has bounded.

       A wrapping column flex container answers a full main axis by starting a
       new flex LINE, and a column's lines run sideways. So the cap never
       fills, `overflow-y: auto` never has anything to scroll, and the panel
       the note over that cap describes as scrolling inside itself is instead
       dealt out into two, three and four columns. Measured on the shipped
       panel, WebKit 26.5 and Chromium 141 agreeing to the tenth of a pixel on
       all 66 cells — 11 viewports x roots 16/20/24 x both engines, of which
       32 wrap:

         320 x 568  root 24   2 cols   rows 142.1 and 121.9 px wide
         360 x 640  root 24   2 cols   rows 172.1 and 131.9
         667 x 375  root 16   2 cols   rows 152.2 and 138.8
         740 x 360  root 20   3 cols   rows 133.3 / 124.9 / 99.7
         568 x 320  root 24   4 cols   "Suche" 58.4 px past the panel's right
                                       edge, clipped, and reachable only by
                                       scrolling a vertical menu sideways

       scrollHeight - clientHeight was 0 in all 66. 320 x 568 at a 24 px root
       is a small phone whose reader has turned the type up; 667 x 375 is the
       iPhone SE on its side, which is the exact viewport the cap's own note
       was written for. Neither is an edge case, and on the 568 x 320 row the
       primary control on a phone loses a route outright.

       IT IS ALSO WHY THE ROWS CAME OUT RAGGED. `align-items: stretch` below
       stretches an item to its own flex LINE's cross size and not the
       container's, so the moment there are two lines the columns take two
       different widths — 152.2 against 138.8 at 667 x 375 — and a menu whose
       rows are one plate wide is drawn as two unequal ones.

       nowrap rather than unsetting the base declaration: the panel is a single
       column by design, this states it where the direction is stated, and the
       bar above the fold is untouched because none of this rule reaches it. */
    flex-wrap: nowrap;
    align-items: stretch;
    /* Closed is the panel wiped away upward, back under the bar.

       THE THREE NEGATIVE SIDES ARE LOAD-BEARING AND THE OBVIOUS `inset(0)`
       IS A BUG. A clip-path clips everything the element paints, and the
       focus ring is painted outside the border box: measured at 375 px, the
       first link's ring reaches 3 px above and 3 px left of the panel's own
       box, so a clip sitting exactly on that box cuts the ring on the first
       and last links and on both edges of every one of them. 4 px of slop on
       every side that is not the travelling edge — 2 px of outline plus the
       2 px offset base.css sets — puts the whole ring inside the region while
       leaving the wipe visually unchanged.

       Those two numbers live in base.css and this one does not read them, so
       a heavier focus ring has to be answered here. */
    clip-path: inset(-4px -4px 100% -4px);
    transition: clip-path var(--duration-slow) var(--ease-standard),
                display   var(--duration-slow) var(--ease-standard) allow-discrete;
    /* Six 2.75rem rows plus the plate's padding put the open panel at 302 px,
       and the bar above it holds 84. A portrait phone has room to spare; the
       same phone on its side is 375 tall, and 84 + 302 leaves the Kontakt row
       starting 11 px past the bottom of the screen with no way to reach it —
       the panel is out of flow, so the page's own scroll never brings it up.
       So the panel scrolls inside itself, which is the rule the system
       already applies to any box bigger than the room it is given. The ring
       slop the clip-path holds (4 px) sits 9 px inside the padding edge where
       this scrollport clips, so no focus ring is cut by it. vh first and dvh
       on top: the dynamic unit tracks the browser chrome a phone collapses,
       and where it does not exist the older unit errs only that much tall. */
    max-height: calc(100vh - var(--nav-height) - var(--space-2));
    max-height: calc(100dvh - var(--nav-height) - var(--space-2));
    overflow-y: auto;
  }
  [data-cf-nav] .cf-nav__list[data-open="true"] {
    display: flex;
    /* An inset and not `none`: `none` is not an interpolable clip-path, so the
       wipe would have nothing to travel to. At rest every side is outside the
       panel, so an open panel is clipped in no direction at all. */
    clip-path: inset(-4px);
  }
  /* AFTER the rule it starts, never before. @starting-style adds no
     specificity of its own, so an earlier placement is overwritten by the open
     state and the entry wipe silently does not happen. */
  @starting-style {
    [data-cf-nav] .cf-nav__list[data-open="true"] { clip-path: inset(-4px -4px 100% -4px); }
  }

  [data-cf-nav] .cf-nav__toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    /* 44 px. The label's own metric — 11 px mono inside --space-3 padding —
       lands the plate at 42, and this is the primary control on a phone: the
       only way to reach any other page. 2.75rem is the 44 px touch convention
       (WCAG 2.5.5, Apple's HIG). It was also, exactly, the height the band
       had to give when the controls stood in it: --space-5 above and below
       inside a --nav-height of 5.25rem is 84 − 40 = 44. They are in the plate
       now and the band's padding is --space-2, so the ceiling is 84 − 16 − 18
       = 50 and the touch floor is what sets this instead; nothing anchored to
       --nav-height moves either way. Measured either side of the change: band
       84 px both. Above 48.75rem the toggle is not rendered and this reaches
       nothing. foundations/mobile.html carried this as its open question. */
    min-height: 2.75rem;
    background: var(--surface-inverse);
    color: var(--grey-000);
    border: var(--stroke-1) solid var(--border-inverse);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    transition: border-color var(--duration-fast) var(--ease-standard);
  }
  /* The control had no open state at all: aria-expanded flipped and the plate
     was identical either way, so the visual state and the announced one had
     drifted apart on the primary control of the whole mobile page.

     THE CONTOUR AND NOT THE FILL, which is the system's first rule and here it
     is also the only one that reads. The obvious move is the marker the link
     bar uses for the current page — white 0.14 over black — but that works
     there because it is a small plate inside a larger black field with unlit
     siblings beside it to compare against. The toggle is itself the plate and
     has nothing to be compared with, and white 0.14 over black composites to
     rgb(36,36,36): 1.3:1 against the plate it is supposed to distinguish
     itself from, which is the same near-invisible step the system already
     threw out when it dropped anthracite.

     So the contour lifts to --grey-000 instead. That is not a new value: it is
     already this component's "on" — .cf-nav__link:hover and the current-page
     link both take their ink to exactly it. One lit value in one component,
     now on a contour as well as on a label. --duration-fast matches
     .cf-nav__link too; the bar should not answer at two speeds. */
  [data-cf-nav] .cf-nav__toggle[aria-expanded="true"] {
    border-color: var(--grey-000);
  }

  /* THE SWITCH TAKES THE TOGGLE'S METRIC HERE, AND THE REASON IS A FINGERTIP.

     Above this width the switch is one more label in the plate and measures
     what a route measures — 33.94 px, the same row. Below it the seven routes
     are out of flow, and the switch is one of the two controls a phone reader
     has: it owes a fingertip the same 2.75 rem the toggle's note above
     derives from WCAG 2.5.5. Same padding, same floor, same 44.

     THE BAND NO LONGER PAYS FOR IT, and that is the part of this note that
     changed with the plate. This rule was written when the switch stood
     directly in the band: 49.94 px inside --space-5 above and below is 89.94
     against a --nav-height of 84, and every offset anchored to that token —
     the sticky top, the open panel's max-height calc, the scroll-margin on
     every heading — was wrong by the six pixels. The band's own padding is
     --space-2 now and min-height is what sets it, so a 44 px control inside
     the plate's 8 px inset and the band's own is 78 against 84 and the band
     does not move. Measured at 375 after: band 84, plate 62, toggle 44,
     switch 44. */
  .cf-nav__lang {
    padding: var(--space-3) var(--space-4);
    min-height: 2.75rem;
  }
}

/* --------------------------------------------------------------------------
   SECTION HEADER
   Mono label on the left, counter on the right, 1 px rule underneath.
   This is the connective tissue of every page — use it to open each section.

   The header owns the air beneath it, so every section on the site opens on
   the same axis. Two cases, both taken from the mockups:

     default   content sits --section-header-gap below the rule. For content
               that is not itself a ruled box: a logo wall, a team strip, a
               block of copy.
     --flush   the content below is a ruled container (process card, accordion,
               blog grid) and ITS top border is the rule. The header gives up
               its own border rather than drawing a second hairline 1 px away.
   -------------------------------------------------------------------------- */
.cf-section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--section-header-gap);
  padding-bottom: var(--space-3);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
/* The intrinsic minimum, on the item — the second of the two fixes
   foundations/layout.html#intrinsic-two-fixes tabulates, and the one .tiles
   and .subdivide already take. Both halves of this row are flex items with
   min-width: auto, so each floored at its own min-content and a German
   compound in either took the whole document with it. Measured at a 16 px
   default, one real compound, no enlarged type needed:

     .cf-section-header__count   "6 Fragen" -> 42 chars   320 -> 384 px
     .cf-section-header__label   "FAQ"      -> 42 chars   320 -> 356 px

   The count is the half that is written to change — "189 Beiträge",
   "01 / 04", "6 Fragen" — so it is the half a copy edit reaches. Not a track
   floor here: this row stays flex because the two parts are set on a baseline
   and the right one is sized by its own text, which is what space-between
   between two auto-width items says and what a two-track grid would have to
   restate. → base.css, .tiles > * */
.cf-section-header > * { min-width: 0; }
/* --flush IS A PROMISE ABOUT THE NEXT ELEMENT, and nothing kept it. The
   modifier gives up the only hairline the section has, on the understanding
   that the container below draws it instead — so written above anything that
   does not, it does not move the rule, it deletes it. That renders as a mono
   label floating with no rule under it, which is a picture nobody reads as an
   error: it is the section header device minus the device.

   Fifteen headers carried it and five of those stood over ordinary flow —
   .cf-prose, a .stack, .cf-contact, and the two zero states where
   .cf-error--inline replaces a register that would have brought the edge.
   karriere-stelle.html had 04 and 05 adjacent, one drawing a rule and one not,
   and impressum.html had it between an 01 and an 02 that both do.

   The set of containers that may stand under it is derived from this
   stylesheet rather than listed — any rule declaring a top border of stroke
   ink qualifies — so a sixth ruled register earns the right by existing.
   → scripts/check-section-header-rule.py */
.cf-section-header--flush {
  margin-bottom: 0;
  border-bottom: 0;
}
/* THE LABEL IS THE ANCHOR, so it carries the clearance. components/article
   .html states the rule in as many words — "Headings carry scroll-margin-top
   for the sticky navigation. Without it an anchor lands underneath the bar" —
   and .cf-prose :is(h2, h3, h4) is where it was kept. It is a rule about
   heading elements that own an id, and this is the other class in the system
   that does: the section header's label is the <h2> of every section on every
   designed page, and it is what a fragment in the URL resolves to. Thirty-three
   headings across thirteen pattern pages carry it.
   Without it the bar wins outright rather than nearly. The nav is 84 px and
   the label's own box is 16, so the heading does not land low — it lands
   entirely inside the band and is not on screen at all. Measured on the
   landing page, loading the address with the fragment already in it:
   #prozess, #partner, #faq and #blog each put 16 px of 16 behind the nav, and
   #team escaped only because the document had run out of scroll. The value is
   .cf-prose's, unchanged: the clearance is the same bar. */
.cf-section-header__label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
  scroll-margin-top: calc(var(--nav-height) + var(--space-6));
}
/* The other half of the same hairline row, so it is set the same way: mono,
   11 px, +0.01em, uppercase. Only the colour differs, which is what makes the
   label read as the heading and the counter as the annotation. Without the
   uppercase the two ends of one rule were in two different cases — and every
   counter the designer drew ("189 BEITRÄGE", "SEARCH") is uppercase on the
   plates. The transform is CSS, not markup, so the accessibility tree still
   carries "189 Beiträge".

   ONE REGISTER, TWO OCCUPANTS. The right end of this rule holds a counter or —
   the plate's own rare case, "FAQ … SEARCH" — an action. Both are the same
   mark in the same slot, so the type is declared once for both rather than
   copied; only what makes an action a control is added below. The classes stay
   separate because two gates read the counter by name and neither has anything
   to say about a control: check-section-counts.py holds the designed pages'
   figures, check-faq-count.py holds a quantity above an accordion to the number
   of rows. An action in the slot is simply not a count, and now cannot be
   mistaken for one. */
.cf-section-header__count,
.cf-section-header__action {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
/* THE AIR UNDER THE LABEL IS 12 px IN THIS FILE AND 16.94 px ON SCREEN, and
   the rule above says "every section on the site opens on the same axis"
   while two of them do not. Both facts have the same cause: a line of type
   is not a line, it is a line BOX, and the slot under the baseline that
   carries the descenders is part of it. padding-bottom starts at the bottom
   of that slot, so the drawn distance from the label's baseline to its rule
   is --space-3 plus a slice of the font that no token names.

   Measured — Chromium 141, 1280 px, every .cf-section-header on all
   38 pattern pages, the label trimmed to its baseline to read the
   distance off the rule:

     54 headers, 2 distinct axes
     11 px / 1.45   declared 12   drawn 16.94   53 of them
     32 px / 1.02   declared 12   drawn 17.63   the acts' own header

   The slot scales with the size and the leading, neither of which this
   component chooses — the label inherits both — so the axis this component
   claims to hold is a number it does not control. 0.69 px between the two
   today; a third size or a container at another leading moves it again.

   text-box-trim takes the slot off the box, so the padding lands on the
   baseline and the axis is the token. --space-4 rather than --space-3 is
   the drawn distance moved onto the space scale rather than changed: 16 px
   is within 0.94 px of what all 40 of these headers draw today and within
   1.63 px of the other one, so the composition the designer signed off does
   not move and the number under it becomes true. In the branch below the
   axis is 16 px at every size and every leading, which is the invariant the
   comment above has always claimed.

   trim-end and not trim-both, deliberately. The slot ABOVE the cap line is
   air between the header and whatever precedes it, and it is spent by
   --section-gap rather than by this rule; trimming it would tighten every
   page's rhythm to fix a distance nothing measures. One edge, the one the
   invariant is about. Whatever cannot trim keeps today's drawing exactly.
   → foundations/capline.html */
/* ALL THREE OCCUPANTS OF THE ROW, and the third is the one this branch was
   written without. The rule two blocks up says the count and the action are
   "the same mark in the same slot, so the type is declared once for both" —
   and then the trim, which is a type metric like every line in that
   declaration, was declared for two of the three. It is not a nicety on the
   third: this row is `align-items: baseline`, so the container's content box
   ends at the LOWEST item's bottom, and one untrimmed item holds the whole
   header's descender slot open on behalf of the two that gave theirs up.
   Measured, Chromium 141, 1280 px, components/section-header.html, the ten
   headers on the page, distance from the label's baseline to the top of the
   rule and the header's own height:

     right end is a count    16.00 px   28.00 px high   6 headers
     right end is a count    16.00 px   27.00 px high   1, the flush one,
                                                        which draws no border
     right end is an action  20.94 px   32.94 px high   3 headers

   So the axis this component exists to hold was the token on every header
   whose right end is a number and 4.94 px below it on every header whose
   right end is a control — and everything below the rule moved with it. The
   defect is invisible today for the reason it is worth fixing now rather
   than later: the action has one consumer and it is the chapter documenting
   the component, so the two axes stand three rows apart on the only page
   that draws both, and the first pattern page to put a control in the slot
   would have shipped a section opening on a different line from its
   neighbours with nothing in the markup to say why.

   Trimming it does not touch the drawn hit area. The ::after below is sized
   `max(100%, 1.5rem)` in both directions precisely so the target cannot be
   walked under the floor by a change to the type: 100 % goes from 15.9 px to
   11, the `max()` holds 24, and the target measures 40.27 x 24 either way.
   → scripts/check-hover-focus-parity.py is the other half of this slot's
   symmetry, on the states rather than the metrics. */
@supports (text-box-trim: trim-end) {
  .cf-section-header { padding-bottom: var(--space-4); }
  .cf-section-header__label,
  .cf-section-header__count,
  .cf-section-header__action {
    text-box-trim: trim-end;
    text-box-edge: cap alphabetic;
  }
}

/* THE ACTION, AND WHY IT COULD NOT STAY A <span>. components/section-header
   .html has always tabulated four forms for the right end of this rule — a
   position, a quantity, a bare count, and an action — and its own row for the
   fourth says "yes, and then as a <button>". The demo two rows above it drew a
   <span>. That is the documentation disagreeing with itself on the one page
   whose subject is this component, and following either half got you nothing:
   base.css resets <button> to `background: none; border: 0; padding: 0` and
   `font: inherit`, so a button dropped into the slot came out PIXEL-IDENTICAL
   to the static counter beside it. Measured on expertise.html at 1280 —
   40.3 x 15.9 px, colour rgb(72,72,72), no background, no border, no
   underline: the same drawing as the annotation next to it, and the reader has
   been taught by every other section on the site that this slot is text.

   SO THE RULE ANNOUNCES ITSELF AND KEEPS THE SLOT. The underline is the mark,
   at the .cf-footer__meta offset so the system says "control" one way; the
   colour stays --text-secondary at rest, because this is still the annotation
   half of the rule and not a second heading, and sharpens to --text-primary on
   hover and focus. No fill, no box, no lime: contour before fill, and one lime
   moment per screen is not spent on a link in a rule.

   THE TARGET IS THE OTHER HALF OF THE FIX, and it is the half a screenshot
   cannot show. 11 px mono in a 16 px line box is 15.9 px tall, and WCAG 2.2
   SC 2.5.8 (AA) asks 24 x 24 CSS px of pointer target. Neither exception
   covers it: the inline one is for links inside a sentence, and the spacing
   one only holds while nothing lands within 24 px, which is an accident of
   whatever the next section ships, not a property of this component.

   Padding would meet the floor and move the drawing: this row is
   `align-items: baseline`, so padding-block grows the flex line and takes the
   hairline down with it — the one axis every section on the site shares. The
   hit area is drawn instead, by an ::after that is centred on the text and
   sized `max(100%, 1.5rem)` in both directions. Layout, baseline, row height
   and the rule's y all stay exactly where a counter leaves them; the target
   goes from 40.3 x 15.9 to 40.3 x 24. The `max()` is why there is no number
   to keep in sync: a longer label keeps its own width, a two-letter one is
   floored, and a change to --text-xs or the line height cannot walk the target
   under the floor.

   THE RING IS NOT ENLARGED WITH IT. focus-visible draws base.css's outline on
   the element's own box, which is the text — the ring marks what the reader
   sees, and a ring 4 px clear of the glyphs on three sides would read as a
   second box in a system whose section headers have none. */
.cf-section-header__action {
  position: relative;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: var(--stroke-1);
  text-underline-offset: 0.25em;
}
.cf-section-header__action::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  inline-size: max(100%, 1.5rem);
  block-size: max(100%, 1.5rem);
}
.cf-section-header__action:hover,
.cf-section-header__action:focus-visible {
  color: var(--text-primary);
}

/* --------------------------------------------------------------------------
   HERO
   Full-bleed isometric artwork, headline set solid in Publica Sans,
   a hairline rule, then one mono line of positioning copy.
   -------------------------------------------------------------------------- */
.cf-hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* The nav floats over the artwork, so the hero starts where the page starts —
     pulled up by exactly the nav's height, never by a rounded-off guess. */
  margin-top: calc(var(--nav-height) * -1);
  /* AND IT ENDS WHERE THE OTHER FLOATING LAYER BEGINS, which is the same rule
     as the line above it, paid to the second layer instead of the first. The
     nav floats over the artwork and the hero reserves --nav-height for it. The
     consent banner floats over the artwork too — `position: fixed` on the
     viewport's bottom edge — and nothing reserved anything, so the hero's
     bottom row went under it: the kicker, and the one call to action on the
     page. 92vh + the banner's height is more than the viewport at every height
     below ~1800 px, so this was every reader's first screen. The numbers, and
     why the banner's height is measured rather than written down, are in
     cf-consent.js over publishHeight().

     THE THIRD TERM SHRINKS THE HERO, IT NEVER GROWS IT, and that is why it is
     a third term in the min() rather than bottom padding. Padding would push
     the same content down into the same banner on a short viewport; a cap
     moves the hero's own bottom edge up to the banner's top edge and the
     absolutely-positioned .cf-hero__action, which hangs off that edge, comes
     with it. The hero's content is ~400 px at its tallest, so the cap never
     binds against it: 375 x 812 leaves 478.5 px for ~400.

     UNSET IT AND THIS DECLARATION IS THE OLD ONE, exactly. --cf-consent-height
     exists only while the banner does; `var(…, 0px)` makes the third term
     100vh, which can never be the smallest of the three. So a page with no
     banner, a reader with a stored decision, and a browser with no JS all get
     min(92vh, 56rem) — the same box, from the same line. */
  min-height: min(92vh, 56rem, calc(100vh - var(--cf-consent-height, 0px)));
  /* THE TOP PAD RESERVES THE NAV IT IS PULLED UP BY, whichever of the two is
     larger. The line above pulls this box up by --nav-height (84 px) so the
     artwork starts at the page's top; --space-16 is 64, so for as long as the
     top pad was that rung alone the hero's first line began 20 px INSIDE the
     nav — at every width, on every page with a hero.

     It never showed, because the hero is a flex column justified to flex-end
     and 92vh tall: the content sits against the BOTTOM pad and the top one is
     slack that nothing ever takes up. It shows the moment the box is short
     enough for the content to reach the top of it, which is what the min-height
     term above does at 375 x 812 — 478.5 px of hero for 473 px of content, and
     the title's first line went back under the nav plate, 20 px of it, hit test
     returning nav.cf-nav. Trading the banner's overlap for the nav's is not a
     fix, so the slack is spent here instead: max() takes the rung while the
     rung is the larger, and the nav's real height when it is not. Measured, the
     only box on any page whose height this changes is that one — everywhere
     else the min-height binds first and the top pad is still slack. */
  padding-block: max(var(--space-16), var(--nav-height)) var(--space-12);
  /* CLIP, NOT HIDDEN, AND THE DIFFERENCE IS NOT COSMETIC. What this line is
     for is cropping the artwork to the hero's box — a paint decision. But
     `overflow: hidden` also makes the element a SCROLL CONTAINER, and a
     view timeline resolves against its subject's nearest scroll container.
     So every view() inside this header was silently measured against a box
     that never scrolls: .cf-btn--glass::before below reported a live
     ViewTimeline whose progress sat at 0.116 and did not move at any scroll
     position on the page. No error, no warning, no visible difference from
     an animation that simply had not been written.

     `overflow: clip` crops without creating a scrollport, so the timeline
     resolves to the document and the pass runs. Both declarations are kept:
     a browser too old for `clip` drops the second and keeps the crop, and
     the same browser has no animation-timeline either, so it lands on the
     parked state the pass is designed to degrade to.

     Audited across all six pattern pages and the scroll-driven documentation
     pages before changing this: 40 scroll-driven animations, and the one
     below was the only one whose source was anything but the document. It
     is the first scroll-driven thing ever placed inside this header, which
     is why the trap had never been sprung. Anything else put in here later
     inherits the fix rather than rediscovering it. */
  overflow: hidden;
  overflow: clip;
}
/* NO BACKGROUND ON .cf-hero, and its absence is the point. It carried an
   opaque --surface-base, which was invisible while the artwork covered the
   box and became the thing the artwork dissolved into once it stopped. That
   would have swapped one seam for another: the hero's flat CF-Grau against
   the page wash, which at the hero's lower edge has already travelled most
   of its ramp and is several levels lighter. The wash is fixed-attachment
   and viewport-sized, so no colour declared here can match it at that
   height. Let it through instead and the junction is not a junction.

   The fallback colour the hero used to carry moves onto .cf-hero__media
   below, where it belongs: it is what stands in for the artwork before the
   loop has decoded, so it should be masked away with the artwork rather
   than survive it. */
.cf-hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--surface-base);
  /* See --hero-dissolve in tokens.css. Both spellings: Safari before 15.4
     only knows the prefixed one, and the same pair is used on .cf-nav. */
  -webkit-mask-image: var(--hero-dissolve);
  mask-image: var(--hero-dissolve);
}
.cf-hero__media img,
.cf-hero__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The reading scrim — see --hero-scrim in tokens.css. Inside the media
   rather than over it, so the dissolve takes the scrim away at the same
   rate it takes the artwork away: the scrim exists to be read through, and
   past the bottom of the picture there is nothing left to read through.
   Below .cf-hero__body's z-index 1 by being in the media's box at all, so
   no stacking order has to be stated for the type. */
.cf-hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--hero-scrim);
}
/* THE COLUMN IS AS WIDE AS ITS OWN TYPE, and it used to be `max-width: 44ch`.
   `ch` is font-relative and resolves against the element it is declared on —
   this box is set at 14 px, so the clamp landed at 342.59 px, and then governed
   a headline set at 64 px. The same trap .cf-prose already documents at the
   prose track, one component further down, sprung here in the opposite
   direction: there the prose measure on an h2 came out too wide, here a 44ch
   on a 14 px wrapper came out far too narrow for the display face it was
   sizing.

   What shipped: the headline's three authored <br> breaks were re-wrapped into
   SIX lines at every width from 768 up, and the kicker, one line on the plate,
   ran to two at every width including 375. The mockup draws three lines and one
   kicker line. The markup already says exactly where the headline breaks; the
   stylesheet was overruling it with a number that had nothing to do with the
   type.

   fit-content sizes the box to the longest line the type actually sets, so the
   breaks the markup declares are the breaks that ship, at 64 px and at the
   40.775 px the clamp scales down to on a phone. Nothing in here has to be kept
   in sync with the copy. */
.cf-hero__body {
  position: relative;
  z-index: 1;
  width: fit-content;
  max-width: 100%;
}
.cf-hero__title {
  font-family: var(--font-display);
  font-size: var(--text-display-1);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  /* Only reachable once the line no longer fits — the <br>s are the desktop
     answer. Below about 700 px it is what keeps the fold from leaving one word
     alone on the last line. */
  text-wrap: balance;
}
/* THE RULE IS THE KICKER'S TOP EDGE, not a box of its own, and the plate is
   what settles it: measured off landing-page.jpg the rule and the mono line
   below it start and end on the same two x — 42 → 463 in the 1200 px render —
   while the headline above overhangs both by 141 px. A separate empty <div>
   cannot know that. It fills whatever box it is given, so it ended with the
   headline instead of with the line it belongs to, and it did so in a box that
   was itself the wrong width. Drawn as the kicker's own border it is the
   kicker's width by construction — at every viewport, and in a language whose
   translation of that sentence is longer.

   Same 32/16 rhythm the standalone rule carried, so this changes the line's
   width and nothing about where it sits. */
.cf-hero__kicker {
  width: fit-content;
  max-width: 100%;
  margin-top: var(--space-8);
  padding-top: var(--space-4);
  border-top: var(--stroke-1) solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
}
/* On the column, not on the window — see --column-inset. The vertical half of
   this was already right and hid the horizontal half: --space-12 equals the
   hero's own bottom padding, so the button's lower edge lands exactly on the
   kicker's, and at the 1280 frame the two axes coincided. They stop coinciding
   the moment the container caps, and then the CTA walks away from the headline
   it belongs to while still sitting flush with its baseline. */
.cf-hero__action {
  position: absolute;
  right: var(--column-inset);
  bottom: var(--space-12);
  z-index: 1;
}
/* THE FOLD IS 56.25rem AND NOT 48.75rem, AND THE 90 px BETWEEN THEM WERE THE
   BUG. The line above hangs this box off the hero's bottom edge at the column
   inset; .cf-hero__kicker one rule up is `width: fit-content` on the same
   bottom band, and nothing reserves the horizontal room the action takes. So
   from the width where the action starts floating up to the width where the
   hero is finally wide enough to hold both, the kicker's last words run UNDER
   the glass — under a backdrop-filter, which does not merely stack them, it
   smears them.

   The fold used to be 48.75rem because that is where the NAV collapses, and
   the action was folded with it. But 48.75rem is a measurement of the nav's
   links, not of this row: measured on this page, at a 16 px default the kicker
   ends at x=560 and the action's left edge does not clear it until 870. The
   band and the worst overlap in it, by the reader's own default font size:

     browser default   overlapping widths   worst overlap
     16 px             781 – 869            70 px at 790
     20 px             971 – 1089           94 px at 980
     24 px             1171 – 1309         109 px at 1180

   It gets WIDER and WORSE with larger type, which is the failure mode the
   register's rem rule exists for: the kicker is mono at --text-xs and grows
   with the reader while the gutter it sits in is 5.5vw and does not.

   56.25rem IS AN EXISTING REGISTER ENTRY, not a new number — the same width
   the page-header figure is dropped at and the consent banner goes single
   column at. A second consumer for a number the register already carries, the
   way 44rem carries .grid--early and 48rem carries .cf-logo-wall. It clears
   the band at every default measured, with the slack constant because the
   threshold scales with the type that widens the kicker: 31 px at a 16 px
   default, 36 at 20 px, 41 at 24 px.

   Below it the action is the form it has always been below 48.75rem — in the
   flow, under the kicker, on the container's gutter. That form now covers 120
   px more of the ladder rather than being a new one. → tokens.css, THE
   BREAKPOINT REGISTER */
@media (max-width: 56.25rem) {
  /* Dropped back into the flow it has to pick up the container's gutter, or it
     sits a gutter-width left of the headline it belongs to. */
  .cf-hero__action {
    position: static;
    margin-top: var(--space-8);
    padding-inline: var(--gutter);
  }
}

/* --------------------------------------------------------------------------
   BREADCRUMB
   The line above a subpage title that says where the page sits. Mono uppercase
   like every other label in the system, a list of links, and the page you are
   on as a <span> — the same decision .cf-pagination already made one section
   down, for the same reason: a link to where you already are is a target that
   answers nothing.

   THE SEPARATOR IS DRAWN, NOT TYPED, and that is the one decision here worth
   the paragraph.

   Every other system reaches for a character — "/", "›", "»" — set in a
   ::before. Three things are wrong with that here. It is a glyph from the
   body face standing in a row of mono labels, so it is the only mark on the
   line drawn by somebody else. Its angle is whatever the typeface felt like:
   a solidus leans about 70°, which is none of the four the brand sanctions.
   And CSS `content` with text in it is announced by some screen readers, so
   the trail reads "News slash Analyse slash" — which is why every system that
   uses one then has to work out how to hide it again.

   `content: ""` on a 1 px box rotated to a brand angle has none of those
   problems. There is no text, so there is nothing to announce and nothing to
   suppress. It is a hairline, which is what this system draws everything
   with. And it lands on --angle-a, 63.43°, the steep isometric — the same
   line the lattice runs. The rotation is written as --angle-b because the box
   starts vertical and the two angles are complements: 90 − 26.57 = 63.43.
   Turn --angle-b and the separator follows it, which is the point of naming
   it.

   IT PASSES THE LINE TEST. `README.md` > Where a line may go bans a line that
   is neither an edge, a division, nor a label rule — and a separator looks
   exactly like the decoration that rule is aimed at. It is not: there is a
   crumb on each side of it, which is the definition of a division. The banned
   shape is the bar with nothing on its far side. Judge the drawing.

   THE FOLD KEEPS THE LAST TWO. Below 30rem of container every crumb but the
   parent and the current page is removed. Not an ellipsis and not an overflow
   menu: the first needs a control to expand it and the second needs a script,
   and both put a second interactive thing in a component whose whole job is
   one line of orientation. The parent is the crumb a reader on a phone
   actually uses — it is the way back up — and the current page has to stay
   because it is the only thing carrying aria-current. Everything above the
   parent is reachable from the parent.
   -------------------------------------------------------------------------- */
.cf-breadcrumb {
  container-type: inline-size;
  /* The component owns the air under it, the way .cf-pagination owns the air
     above it: the distance belongs to the shape and every page wants the same
     one. */
  margin-bottom: var(--space-4);
}
.cf-breadcrumb__list {
  display: flex;
  align-items: center;
  /* No wrap. A trail that wraps to a second line stops reading as one path
     and starts reading as two rows of labels; the fold below is the answer
     instead. */
  flex-wrap: nowrap;
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.cf-breadcrumb__item {
  display: flex;
  align-items: center;
  /* Ancestors keep their full width; only the current page may shrink, and it
     is the last child. A truncated ancestor is a link whose destination you
     can no longer read. */
  flex: none;
  min-width: 0;
  white-space: nowrap;
}
.cf-breadcrumb__item:last-child { flex: 0 1 auto; }

.cf-breadcrumb__item + .cf-breadcrumb__item::before {
  content: "";
  flex: none;
  height: 0.75rem;
  margin-inline: var(--space-3);
  /* A BORDER, NOT A BACKGROUND, and that is a forced-colours fix rather than a
     style. Both draw the identical 1 px line, but forced colours overrides
     background-color to Canvas — so the background version rendered a hairline
     everywhere and nothing at all in Windows high contrast, where the trail
     lost its separators and read as three labels with gaps between them.
     Border colours are mapped to the forced palette instead of erased, so the
     line survives. currentColor keeps it the same ink as the row it divides. */
  width: 0;
  border-inline-start: var(--stroke-1) solid currentColor;
  rotate: var(--angle-b);
}

.cf-breadcrumb__link {
  display: block;
  /* 11 px type in a nav list is not the "inline" exception to WCAG 2.2
     SC 2.5.8 — that covers a link inside a sentence. Padding takes the target
     to 28 px tall, and the separator's own margins hold the row's targets
     ~29 px apart. */
  padding-block: var(--space-2);
  color: inherit;
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-standard);
}
/* Hover and focus are one state, as everywhere else in this file. The
   underline arrives with them rather than sitting at rest: at rest the row is
   uniform mono ink and the one thing that has to be distinguishable is which
   crumb is NOT a link, which the ink step below does. */
.cf-breadcrumb__link:is(:hover, :focus-visible) {
  color: var(--text-primary);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}

.cf-breadcrumb__current {
  display: block;
  padding-block: var(--space-2);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-primary);
}

@container (max-width: 30rem) {
  .cf-breadcrumb__item { display: none; }
  .cf-breadcrumb__item:nth-last-child(-n + 2) { display: flex; }
  /* Whatever is now first has no crumb to its left, so it has nothing to
     divide. */
  .cf-breadcrumb__item:nth-last-child(2)::before { content: none; }
}

/* --------------------------------------------------------------------------
   PAGE HEADER
   The opening of every subpage: display title, a rule that runs to the page
   edge, and a mono line split left/right underneath. An isometric object may
   sit behind the rule on the right.
   -------------------------------------------------------------------------- */
.cf-page-header {
  position: relative;
  padding-block: var(--space-24) var(--space-16);
}
/* THE RULE IS THE OBJECT'S AXIS, so the rule is what the object is measured
   against — not the container, and not the header.

   Both earlier attempts positioned the figure against a box and hoped the
   axis would land somewhere sensible. Against .cf-page-header (full-bleed,
   right: 0) it ran past the content column entirely. Against .container it
   was centred on the container's middle — but the container holds a 65 px
   title above the rule and a 16 px meta row below it, so its middle sits 31
   px above the rule and the page line cut the drawing across the nucleus
   instead of continuing its axis. Measured at 1440: rule centre 270, figure
   centre 239.

   The figure is now a child of the rule, and the rule is the only thing it
   is measured from: `top: 50%` of a 1 px element is that line, and the
   viewBox is padded at the bottom so the drawing's own dotted axis sits at
   exactly 50 % of the SVG box. Rule and axis are then collinear by
   construction at every width, with nothing to keep in sync by hand.

   The width is the drawing's own, not a grid fraction — the designer's
   source composition spans x 540..1125 of a 1440 page whose content column
   is 80..1360, which is 45.703 % of that column wide. Centred on the column
   rather than inset from its right edge: an inset held the object to where
   the source mockup happened to place it, and at the column's full width
   that read as the drawing floating in the right two thirds with the first
   third permanently empty. Centred, inset 27.1485 % on both sides. */
.cf-page-header > .container { position: relative; }
.cf-page-header__figure {
  position: absolute;
  right: 27.1485%;
  top: 50%;
  translate: 0 -50%;
  width: 45.703%;
  /* The viewBox is the drawing's bounding box, so a 1 px non-scaling contour
     at the extremes is half outside it. SVG clips to the viewport by
     default; without this the outermost shell loses half its stroke. */
  overflow: visible;
  pointer-events: none;
}
.cf-page-header__body { position: relative; z-index: 1; }
.cf-page-header__title {
  font-family: var(--font-display);
  font-size: var(--text-display-1);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
}
.cf-page-header__rule {
  position: relative;
  height: var(--stroke-1);
  margin-top: var(--space-6);
  background: var(--border-strong);
}

/* THE RULE STOPS WHERE THE OBJECT STARTS AND RESUMES WHERE IT ENDS. In the
   mockup the page line and the object's dotted axis are one continuous line
   that changes character as it passes through the drawing: solid page rule,
   then the object's own 1-2 axis across the gap, then solid rule again. A
   solid line drawn straight through the nucleus — which is what a full-width
   rule does — reads as a line lying ON the object rather than as its axis.

   Same two stops as the figure's own placement, so the line meets the
   drawing's bounding box exactly. Scoped with :has() because every other
   page header on the site carries an uninterrupted rule.

   90deg AND NOT `to right`, which is the same direction spelled the other way
   and was the only gradient in the four shipping stylesheets spelled that way.
   Twenty-four others state a rake as an angle, check-gradient-angle.py's
   register reads angles, and a family whose stops are held to one hand should
   not describe its own directions in two vocabularies. Nothing renders
   differently; it is the register that gets shorter. */
.cf-page-header__rule:has(.cf-page-header__figure) {
  background: linear-gradient(90deg,
    var(--border-strong) 0 27.1485%,
    transparent 27.1485% 72.8515%,
    var(--border-strong) 72.8515%);
}
.cf-page-header__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
/* THE SECOND ITEM IS THE QUIET ONE, and it takes the ink step because it is
   the second — not because it is the last. The row is "a mono line split
   left/right": the left item says what the page holds, the right one is the
   aside beside it, and only a split has a right-hand side at all.

   `:last-child` alone does not say that. With one child the only item is both
   first and last, so the left-hand line took the right-hand colour and the
   page's one piece of metadata was set in --text-secondary while the identical
   line on every other page is full ink. It is not a hypothetical: news.html's
   meta is generated — build-news.py writes "the post count and, WHEN THERE IS
   MORE THAN ONE PAGE, which page this is" — so the archive drops to one span
   whenever the register fits on a single page, which is the state it ships in
   today. Measured at 1440 x 900, patterns/news.html: the lone span computed
   rgb(72,72,72); patterns/ueber-uns.html's first of two computed rgb(0,0,0).
   Same component, same slot, two inks, decided by how many posts exist.

   `:not(:first-child)` is the whole fix, and it says the rule in the same
   words the note above the component does. A single-item row is now ink, a
   split row is unchanged. */
.cf-page-header__meta :last-child:not(:first-child) { color: var(--text-secondary); }
/* THE INTRINSIC MINIMUM, ON THE ITEMS — and this is the row .cf-info-card__
   chips' note named and left alone. That note closes by counting what its own
   floor did not reach: "what is left at 320 x 32 is 27 px of
   .cf-page-header__meta, a two-span flex row that cannot wrap, which is a
   different rule and is not touched here". This is that rule, and the count
   was low because it was taken on one page.

   Both spans are flex items with the default `min-width: auto`, so each is
   floored at its own min-content and the pair cannot be squeezed below the sum
   of those two floors plus the gap. Past that sum the line neither wraps nor
   shrinks — it overflows, to the right, out of the header's column and off the
   page. Measured at 320 px of viewport, the reflow width WCAG 1.4.10 asks for,
   at a 32 px root: 200 % text, which is WCAG 1.4.4 at level AA and what a
   Windows reader arrives with from Edge's own font-size control.

     page                        right-hand span             outside   document
     stellen/data-engineer.html  "Ausgeschrieben am 06.07."   61 px     361
     expertise.html              "Vier Felder"                47 px     347
     suche.html                  "Seiten, Beiträge, …"        34 px     342
     suche-leer.html             "Seiten, Beiträge, …"        34 px     342
     kontakt-danke.html          "Antwort in der Regel …"     20 px     342
     bewerbung.html              "Rückmeldung auf den …"       7 px     342
     bewerbung-danke.html        "Rückmeldung auf den …"       7 px     342
     datenschutz.html            "Stand: Juli 2026"            7 px     342

   "outside" is past the right edge of .cf-page-header__meta's own 280 px
   column. On the first two the span cleared the viewport as well and took the
   document with it; the job page still went 13 px over its column at 375. The
   342 in the last column is NOT this row — it is .cf-nav__lang leaving the
   bar's plate at the same default, which is the nav's rule and is untouched
   here. Every one of the eight is 0 px outside after, and the two documents
   this row had widened past it — 347 and 361 — are 342.

   The component opens 74 of the 76 shipped pages, so the fault is the page
   header's rather than any one page's: what changes page to page is only which
   words are in the aside.

   `min-width: 0` is the second of the two fixes foundations/layout.html
   #intrinsic-two-fixes tabulates, and it is the SAME DECLARATION on the same
   shape one component along: .cf-section-header > * takes it for a mono row
   set on a baseline with space-between and two auto-width items, which is this
   row's construction exactly. The floor comes off, the pair shrinks to what
   the column has, and base.css's net breaks the word that then cannot be set
   — `span` is in that list.

   WHAT IT COSTS, AND IT IS NOT NOTHING. The floors were also what decided how
   the shrink was shared, so a line already AT both min-contents redistributes
   rather than staying put. Seven rows do, all of them at 320 x 24, none of
   them outside their column before or after: each takes one more line, and on
   expertise — where the two floors summed to exactly the 280 px column —
   "Vier Felder" was set VIER / FELDER in 60 px and is now VIER / FELDE / R in
   55 px, a break inside a word where the line had, barely, fitted. That is
   base.css's own trade in the smaller direction ("a word set badly inside the
   panel is the smaller fault than a word set well outside it"), paid here to
   take eight pages back inside their column.

   NOTHING ELSE MOVES. 640 measurements — 20 pages x roots 16/20/24/32 x
   320/375/414/768/853/1024/1280/1440 — comparing the row's box and each item's
   x, width and height: 37 differ, and all 37 are at 320 x 24, 320 x 32,
   375 x 32 and 414 x 32. Identical to the pixel at a 16 px and a 20 px default
   at every width, and identical at every default from 16 to 32 at 768 and
   above — which includes 853 and 1024, the CSS viewports a 1280 px screen has
   at Windows' 125 % and 150 % display scaling.
   → base.css, .tiles > *; .cf-section-header > * */
.cf-page-header__meta > * { min-width: 0; }

/* THE HEADER HAS TO BE TALL ENOUGH TO HOLD THE OBJECT, and only the header
   that carries one — every other page header keeps the padding above.

   The figure's box is symmetric about its axis by construction, so it hangs
   exactly as far below the rule as it rises above it: 45.703 % of the content
   column wide, 428.76 / 586 as tall, halved — 16.72 % of the content column,
   214 px at 1440. The rule itself sits --space-3 plus one mono line above the
   header's own content edge, so that much of the drop is already paid for.

   BOTH ENDS OF THE REACH HAVE TO BE PAID FOR, and this rule used to pay only
   the bottom one. The note here claimed the top was free — "it passes behind
   a transparent nav bar, clear of both pills" — and the nav bar is not
   transparent. It is a glass sheet: --glass-blur behind it, a lit rim along
   its bottom edge. Measured in Chromium at 1440 x 900 the bar's box ends at
   89.9 and the figure's box begins at 63.9, so the top 26 px of the drawing —
   the crown of the outer shell and of the wide one behind it — sat under the
   blur, and the rim cut across them. The one thing the drawing is composed
   around is the reach above the axis being equal to the reach below it; a
   quarter of the upper reach veiled is the one way to break that read.

   The mockup does not do this either, and it is measured rather than
   remembered: on ueber-uns.jpg (1200 wide) the nav plates run y 16..49 inside
   equal air, so the bar's box ends at 65, and the topmost mark of the drawing
   is at y 75 — the page rule is at 253.5 and the reach is 0.3658 x 488, which
   lands on the same 75. Ten pixels of clearance at 1200, twelve at 1440.
   --space-3, and it is the same --space-3 the bottom already names.

   Upward the room is bought with the header's own top padding, because the
   rule is the figure's axis and cannot move without it. Between the header's
   top edge and the rule sit the title — one line, so exactly
   --text-display-1 x --leading-tight — and the rule's own --space-6. What is
   left of the reach after those two is what the padding owes:

     padding-top = reach - title - --space-6 + --space-3

   under a floor of --space-24, the padding every other page header carries.
   The two arms meet where the reach has eaten all of the 96: below that width
   the floor gives MORE than 12 px of clearance and above it exactly 12, so the
   clearance never drops below the mockup's and never steps. Crossover is a
   content column of 1036 px, a 1165 px viewport. Measured, the clearance comes
   out at 12.5 rather than 12 at every width from there up, because the figure
   is centred on the rule's CENTRE and half of --stroke-1 is below its top
   edge — half a pixel the rule pays out of the reach, not a rounding.

   → scripts/check-figure-clearance.py holds all of this, at every width and
   both scrollbar widths, with the reach recomputed from the figure's viewBox.

   Downward the object would run 42 px into the value table's top border.

   The header is full-bleed, so its own 100 % is the window, not the column —
   the exact case --column-inset was added for. Two of them off the window is
   the column, and the reach is a fraction of that. */
.cf-page-header:has(.cf-page-header__figure) {
  --cf-figure-reach: calc(0.1672 * (100% - 2 * var(--column-inset)));
  padding-top: max(var(--space-24), calc(var(--cf-figure-reach)
    - var(--text-display-1) * var(--leading-tight) - var(--space-6) + var(--space-3)));
  padding-bottom: calc(var(--cf-figure-reach) - var(--space-3) - var(--space-4) + var(--space-16));
}

/* THE ROOM THE REACH BUYS IS EMPTY ON BOTH SIDES OF THE DRAWING, and on one
   page that emptiness is the whole of what the reader sees below the fold.
   The rule above reserves a band as deep as the drawing hangs; the drawing
   is centred on the column at 45.703 % wide, so 27.1485 % of that band on
   each side is air by construction, and the header is the last thing on the
   page with anything in it. Measured at 1440 x 900 on patterns/ueber-uns.html:
   the header ends at y 589, the section under it is act 4's pinned track, and
   a pinned track at scroll 0 has drawn nothing yet — 311 px of viewport under
   a page that looks finished. The cue is what stands in that air and says the
   page is not.

   THE CUE IS PLACED HERE AND DRAWN ELSEWHERE. Everything about what the object
   looks like is .cf-scroll-cue in the section below; this is the one rule that
   knows about this header — the band's floor, and its own horizontal position.

   NOT THE FIGURE'S BOUNDING-BOX CENTRE — that was tried first, at `left: 50%`,
   and it put the cue's label across the shells' own tangent-arc path: the
   drawing is not symmetric inside its box, the shells reach to x 857.6 of a
   539.5..1125.5 viewBox while the reflector starts at x 936.3, so the box's
   midpoint (832.5) and even the 50 % mark of the CENTRED figure both fall
   inside the shells' own reach rather than in the gap between the two halves.

   THE GAP IS WHERE THE AXIS ALREADY CROSSES, dotted, from the shells to the
   reflector — the one place in the composition drawn empty on purpose. Its
   centre, (857.635 + 936.294) / 2 = 896.965 of the viewBox, is 61.00 % of the
   figure's own width from its left edge; the figure sits at 27.1485 % ..
   72.8515 % of the column, so the gap's centre is 27.1485 + 0.6100 * 45.703 =
   55.03 % of the column — column-inset plus 0.5503 of the space between the
   two insets, the same construction --cf-figure-reach already uses to turn a
   column fraction into a full-bleed one. The cue's dotted stem now continues
   the axis downward instead of crossing the drawing above it.

   AND THE FLOOR IS THE SEAM, not an inset off it. The header's bottom edge is
   where the section under it begins, so a cue standing on that edge has its
   chevron pointing at the thing it is asking the reader to go and find, and
   the object's own foot marks where the next thing starts. An inset was tried
   first, at --space-12: it puts the mark in the middle of an empty band, which
   is the one place in that band that means nothing. */
.cf-page-header__cue {
  position: absolute;
  inset-block-end: 0;
  left: calc(var(--column-inset) + 0.5503 * (100% - 2 * var(--column-inset)));
  /* Off the floor by --space-8 rather than on it: flush with inset-block-end
     the chevron's tip lands exactly on the header/track seam, which reads as
     the mark colliding with the rule it is pointing past. The band below is
     311 px of nothing at 1440 x 900 (see the HTML's own comment), so the drop
     still lands well clear of act 4's pinned stage. */
  translate: -50% var(--space-8);
}

/* Below 900 px the object would sit behind the title instead of beside it.

   THE SELECTOR CARRIES THE PARENT DELIBERATELY, and this is a bug fix rather
   than a style. `.cf-iso { display: block }` is declared further down this
   file at the same specificity (0,1,0), a media query adds none of its own,
   and the figure on Über uns is `.cf-page-header__figure.cf-iso` — so the
   plain class lost on source order and the object never disappeared. It was
   painting on every phone the rule was written to protect: measured at a 375
   viewport, 172.5 px of isometric drawing sitting behind the page title.
   Naming the parent puts the hide at (0,2,0), which wins wherever .cf-iso
   ends up in the file. → foundations/mobile.html */
@media (max-width: 56.25rem) {
  .cf-page-header { padding-block: var(--space-16) var(--space-12); }
  .cf-page-header .cf-page-header__figure { display: none; }
  /* :has() matches on the element existing, not on it being painted, so the
     gap in the rule and the room reserved on both sides of it outlive the
     object they were cut for. Same specificity as the rules above, later in
     the file, so these win — and they have to, because the padding rule above
     is (0,2,0) and the padding-block reset three lines up is (0,1,0). */
  .cf-page-header__rule:has(.cf-page-header__figure) { background: var(--border-strong); }
  .cf-page-header:has(.cf-page-header__figure) {
    padding-top: var(--space-16);
    padding-bottom: var(--space-12);
  }
  /* AND THE CUE GOES WITH THE OBJECT, because the band it stands in is the
     object's. Two lines above, this tier takes the reserved band back to
     --space-12; there is no air left to place anything in, and none is needed
     — the reason the cue exists is a pinned stage that has drawn nothing at
     scroll 0, and below the pin gate act 4 is a stacked column already in its
     finished state. The section under the header is showing a photograph, so
     the page says it continues by continuing. Same parent-carrying selector as
     the figure's hide directly above, at (0,2,0), and for the same reason. */
  .cf-page-header .cf-page-header__cue { display: none; }
}

/* THE PAGE LINE IS PAINT, AND FORCED COLOURS DOES NOT KEEP PAINT.

   This file already states the rule, one component over, in the note above
   .lp-ev-card__rule: "forced-colors mode discards background-color and maps
   border colours into its own palette, so a mark made of border survives
   Windows high contrast and a mark made of fill does not." That note names
   the offending shape as `height: var(--stroke-1); background: …` and then
   adds, in as many words, "the shape .cf-page-header__rule uses" — and fixed
   the evidence card's hairline only. The rule under the H1 was left as it
   was, on sixteen of the site's eighteen pages, which makes it the most
   widely shipped instance of the exact defect that note was written about.

   MEASURED, Chromium's forced-colors emulation at 1440 x 900, before this
   block existed:

     patterns/news.html        plain rule    background-color forced to
                               rgb(255,255,255) — Canvas, on Canvas. Absent.
     patterns/ueber-uns.html   interrupted   background-image is a gradient,
                               and a gradient is not a url(), so it is forced
                               to `none`. background-color rgba(255,255,255,0).
                               Absent, with nothing left behind it.

   Both schemes fail the same way and for the same reason: on a light forced
   palette the line is Canvas on Canvas, on a dark one it is Canvas on Canvas
   again. The title keeps its ink, the mono row keeps its ink, and the one
   line between them — the page line the whole header is composed around, and
   the thing .cf-page-header__figure uses as its axis — is not drawn at all.

   THE FIX IS THE SHAPE, NOT THE COLOUR. A border survives, so the rule is
   redrawn as one: height to 0 so the box does not double, background off so
   nothing is left claiming to paint it, and CanvasText for the ink, the way
   .cf-nav::after and .cf-info-card--glass::before already take theirs.

   THE GAP SURVIVES TOO, and it has to. .cf-iso's own forced-colors block
   three thousand lines down forces every stroke in the figure to CanvasText,
   so the drawing is still on the page in high contrast — and a solid line
   ruled straight through its nucleus is the exact read the interrupted rule
   exists to prevent. A border cannot carry a gradient's transparent middle,
   so the two lit ends become the rule's own ::before and ::after, at the two
   stops the gradient names — 0 → 27.1485 % and 72.8515 % → 100 %. The rule is
   already position: relative for the figure's sake, so they have their box.

   AND BELOW 56.25rem THE GAP GOES BACK, because there the figure is
   display: none and the tier above has already restored an uninterrupted
   line. :has() matches on the element existing rather than on it painting,
   so without this the phone would get two segments and a hole where nothing
   is drawn. Nested rather than a fifth top-level selector: the width rule is
   a detail of the forced-colours rule, not a peer of it. */
@media (forced-colors: active) {
  .cf-page-header__rule {
    height: 0;
    background: none;
    border-block-start: var(--stroke-1) solid CanvasText;
  }
  .cf-page-header__rule:has(.cf-page-header__figure) { border-block-start-style: none; }
  .cf-page-header__rule:has(.cf-page-header__figure)::before,
  .cf-page-header__rule:has(.cf-page-header__figure)::after {
    content: "";
    position: absolute;
    inset-block-start: 0;
    border-block-start: var(--stroke-1) solid CanvasText;
  }
  .cf-page-header__rule:has(.cf-page-header__figure)::before { inset-inline: 0 72.8515%; }
  .cf-page-header__rule:has(.cf-page-header__figure)::after  { inset-inline: 72.8515% 0; }

  @media (max-width: 56.25rem) {
    .cf-page-header__rule:has(.cf-page-header__figure) { border-block-start-style: solid; }
    .cf-page-header__rule:has(.cf-page-header__figure)::before,
    .cf-page-header__rule:has(.cf-page-header__figure)::after { content: none; }
  }
}

/* --------------------------------------------------------------------------
   SCROLL CUE
   The page saying, once, that it continues. A mono label, a hairline dropping
   from it, and the "keep reading" chevron at the foot of the hairline; a short
   mark of ink travels the hairline three times and then the object stands
   still. Three parts, one of which moves.

   IT IS FURNITURE FOR A HERO THAT ENDS IN AIR, and it is not general licence.
   A page whose next section is visible under the fold already says it
   continues, by continuing, and a cue over the top of that is a label on a
   thing the reader can see. The one page in this system that does not have
   that is patterns/ueber-uns.html: the header reserves a band as deep as its
   isometric object hangs, and the section under it is act 4's pinned track,
   which at scroll 0 has built nothing. Measured at 1440 x 900, that is 311 px
   of empty viewport below a header that reads as the end of the page.

   THREE PASSES, AND THEN IT STOPS. This is the one decision in the component
   and it is made twice over.

   The first reason is the bill check-idle-motion.py states in full: an
   `infinite` animation is on the DOCUMENT clock, which means for the life of
   the tab, and the landing page's 128 of them cost 1 351 ms of style recalc in
   every 5 000 — half the main thread — on a page nobody was touching. A cue
   at the top of a page is the worst possible address for that, because the top
   of the page is where a reader who has not decided anything yet is sitting.

   The second is WCAG 2.2.2, which is the same rule the hero's still switch
   exists for: content that moves automatically for MORE THAN FIVE SECONDS
   alongside other content needs a mechanism to pause, stop or hide it. The
   hero loop is 12 s and buys its own switch. This one is --duration-slow of
   delay plus three passes of --duration-scene — 4.08 s — so it is not
   conformance-relevant at all, and there is no control to put on the page for
   a thing that has already finished.

   And the timing is honest rather than a dodge: the moment the cue is for is
   the moment the page arrives. A reader still on this screen after four
   seconds has read the label, which does not stop being legible when the ink
   stops moving. What is left is the settled object — hairline, chevron, word —
   which is a scroll cue drawn the way this system draws everything else.

   NO LIME. foundations/colors.html allows one lit element per screen and this
   screen has already spent it: the nucleus of the demon core carries the
   #cf-core-light bloom, 300 px to the right of where this object stands. So
   the travelling mark is INK on a light wash, which is the same construction
   the assembly uses — two strokes, one line; the mark travels, the contour
   remains — with the two roles the right way up for the ground it is on.

   THE SETTLED STATE IS THE BASE TIER, not something the animation leaves
   behind. Under prefers-reduced-motion, in print and in forced colours the
   mark is never declared at all, and what is left is the hairline, the chevron
   and the word — a cue that still says scroll, without moving. Same rule the
   motion chapter states for the assembly: where nothing is driving the front,
   there is no front.

   AND THERE IS NOW ONE TIER BELOW EVEN THAT, which is no object at all: the
   cue paints in Blink and WebKit and nowhere else. That is a scope decision
   rather than a capability one — Gecko renders the settled object pixel for
   pixel — and it is stated in full at the @supports under .cf-scroll-cue.

   "THE MOMENT THE CUE IS FOR IS THE MOMENT THE PAGE ARRIVES" ASSUMED ONE
   ARRIVAL. On patterns/ueber-uns.html a reader who scrolls seventeen thousand
   pixels down and back up to the header a minute later meets the same settled
   object a reader four seconds late does — the run already spent. → [data-idle]
   .cf-scroll-cue__stem::after below cancels the animation while the header is
   off screen, so `animation-name` goes from none back to cf-scroll-cue-run on
   every return rather than staying finished: still one bounded 4.08 s run at a
   time, never the `infinite` this section spent three paragraphs ruling out,
   just paid again on each arrival instead of only the first. `animation-name`
   rather than `animation-play-state`, because a paused run holds at its last
   frame — which for a finished `both` fill is the end state forever — and the
   point is to replay from the top, not to thaw. Wired through data-cf-idle,
   the same gate check-idle-motion.py and check-idle-reach.py already hold the
   landing page's tickers to; unwired instances (components/scroll-cue.html)
   never carry [data-idle] and keep the rule above exactly as written.
   -------------------------------------------------------------------------- */
.cf-scroll-cue {
  /* NOT PAINTED UNTIL AN ENGINE IS NAMED, see the @supports directly below.
     `grid` is what the object is when it is drawn at all — three rows, centred
     — and the block below turns it back on; everything else in this rule and in
     the four that follow is the same either way, because a hidden box costs
     nothing to have described. */
  display: none;
  justify-items: center;
  gap: var(--space-3);
  /* The object is as wide as its widest part — the word — and the hairline and
     the chevron are concentric on that. A grid cell as wide as its containing
     block would size the object to the column instead, and .cf-page-header__cue
     centres it by its own box, not by a wide cell's midpoint. */
  width: max-content;
}

/* THE CUE SHIPS IN TWO ENGINES, and this is a decision about where the object
   is signed off rather than anything it needs in order to run.

   IT IS NOT A BUG FIX, and that was measured before it was written. On
   patterns/ueber-uns.html at 1440 x 900, all three engines put the cue's box at
   54 x 128, x 757-758, y 493, with the stem's 2-on-4-off gradient and
   cf-scroll-cue-run on the mark: Chromium 151, WebKit 26.5 and Firefox 153
   render the settled object identically. Gecko draws it as drawn. It is simply
   not one of the two browsers this object is looked at in, and the cue is the
   one piece of furniture on the site whose whole job is a nudge — furniture
   that nudges is exactly what you do not ship into an engine you do not watch.

   AN ALLOW-LIST AND NOT A FIREFOX EXCLUSION. "Everything that is not Gecko"
   also admits every engine nobody here has ever opened, which is the same
   licence by a quieter door. So the base tier above is `display: none` and
   PAINTING is the enhancement: an engine that fails this query — or one too old
   to have @supports to fail it with — is left with the header exactly as it
   stood before this component existed. The rule, the meta row, and the band the
   isometric object hangs into. The page is 18 305 px long there too and says so
   by scrolling, which is what every other page in this system says it with.

   `-webkit-box-reflect` IS THE DISCRIMINATOR, AND IT WAS PICKED BY MEASUREMENT.
   CSS.supports in the three engines above: box-reflect is true in Chromium and
   WebKit, false in Firefox. The obvious alternatives are not — `selector(::-
   webkit-scrollbar)` and `(-webkit-text-security: none)` are both TRUE in
   Firefox 153, because Gecko parses a good deal of the -webkit- surface for web
   compatibility, and `(-webkit-hyphens: none)`, the query acts.css reaches
   WebKit alone with, is false in Chromium. box-reflect has shipped in WebKit
   and Blink since Safari 4 and Chrome 4 and Gecko has declined it for the whole
   of that time, so both ends of the allow-list are one query.

   `and (not (-moz-appearance: none))` is the second half, for the reason
   .cf-btn--solid carries the same clause 1700 lines up: a Mozilla-only ident no
   other engine has ever recognised, holding the gate shut if Gecko ever adds a
   box-reflect alias the way it added the other two.

   AND IT DOES NOT REOPEN THE MOBILE HIDE. That rule is `.cf-page-header
   .cf-page-header__cue` at (0,2,0) and this one is (0,1,0), so below 56.25rem
   the cue stays gone in every engine regardless of which of the two comes
   later in the file. */
@supports (-webkit-box-reflect: below) and (not (-moz-appearance: none)) {
  .cf-scroll-cue { display: grid; }
}

/* The meta row's type, because that is what a micro-label is set in on this
   page and the cue stands directly under one. --text-secondary rather than
   the primary ink: it is an instruction about the page, not part of it, and
   the meta row's own second span already establishes that rung here. */
.cf-scroll-cue__label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* THE HAIRLINE, AT --presence-faint. It is a real line rather than reference
   geometry, so 1-4 is too absent for it; it is also not the drawing's subject,
   so solid is too present. 1-2 is the rung between, and the vertical gradient
   is the same construction .rule--dashed-1-2 in base.css realises for a
   horizontal one, at the same 2-on-4-off the token names — a border-style
   cannot state a dash ratio, which is the whole reason those three utilities
   are gradients.

   `clip` AND NOT `hidden`, per scripts/check-overflow-clip.py: the mark starts
   above this box and leaves below it, so the box has to crop, and a crop that
   also makes a scrollport is the failure that check exists for. */
.cf-scroll-cue__stem {
  position: relative;
  overflow: clip;
  width: var(--stroke-1);
  height: var(--space-16);
  background: repeating-linear-gradient(180deg,
    var(--border-strong) 0 2px, transparent 2px 6px);
}

/* The sprite's own "Expand all, keep reading" mark, which is what this object
   is asking for in one glyph. The act rail uses it for its jump to the foot of
   the landing page; a second consumer with the same sentence is the reason it
   is in the set.

   AT THE DEFAULT 24 AND IN THE PRIMARY INK, which is the one place this object
   departs from the meta row's register. The rail's jump draws the same glyph at
   the 16 px floor and can afford to: it sits inside the rail's own plate, and
   the plate is what the eye finds. This one stands on the page wash with
   nothing around it, at the foot of a 1-2 hairline, and 16 px of secondary grey
   at --stroke-1 measured as the faintest thing on the screen — fainter than the
   reference geometry of the drawing beside it. The label says what to do and
   the chevron is what gets looked at; it takes the ink. */
.cf-scroll-cue__glyph { color: var(--text-primary); }

/* THE MARK, and it is the only part of this that moves.

   `screen` because paper does not scroll and there is nothing on it to cue —
   the same prelude every animation in this system carries, and the reason
   check-print-fixed.py has anything to check. `no-preference` because the
   settled object above is already the whole cue; there is nothing to
   substitute for a reader who has asked for less.

   A COMET, HEAD DOWN. The gradient is transparent at the top and ink at the
   bottom, so the leading edge is the solid one and the trail is what fades —
   an object moving, rather than a dash blinking. `linear` for the same reason
   .cf-arrive__ghost::after is linear: a light crossing a surface travels at
   the speed it travels at, and an ease on it reads as the light hesitating.

   THE MARK IS HALF THE STEM, and that is the whole reason the travel can be
   written as two bare percentages. A percentage in `translate` resolves against
   the element's OWN box, so -100 % is exactly one mark-height above the stem's
   top edge and 200 % is exactly two below it — which is the stem's full height
   when --space-8 is half of --space-16, and the mark is therefore entirely
   past the crop at both ends. Both keyframe values stay plain percentages for
   that reason: motion.html's own note, "a calc() in a keyframe does not
   interpolate", was paid for once on this system's trace and there is no need
   to find out a second time whether the case it names generalises.

   `both` holds the first of those two through --duration-slow of delay and the
   second forever after the third pass — which is the settled state the section
   header describes, reached by arithmetic rather than by a second
   declaration. */
@media screen and (prefers-reduced-motion: no-preference) {
  .cf-scroll-cue__stem::after {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    height: var(--space-8);
    background: linear-gradient(180deg, transparent, var(--border-strong));
    animation: cf-scroll-cue-run var(--duration-scene) linear
               var(--duration-slow) 3 both;
  }
}
@keyframes cf-scroll-cue-run {
  from { translate: 0 -100%; }
  to   { translate: 0 200%; }
}

/* Cancels the run above rather than pausing it, so the header's return to
   view is a fresh `animation-name: none` -> `cf-scroll-cue-run` transition —
   a new animation instance at frame zero, not a held one resuming from
   wherever `both` had it parked. → the addendum on THE MARK, above, and
   assets/js/cf-idle.js for what sets and clears [data-idle] and why 0px is
   the margin on this cue's own data-cf-idle. */
[data-idle] .cf-scroll-cue__stem::after { animation-name: none; }

/* Forced colours discards background-image outright — the same loss
   .rule--dashed-* and .cf-nav-rim take, and base.css answers it the same way:
   a border-style the palette does repaint. `dotted` is what that file already
   maps 1-2 onto, so the two answers agree. The mark is a background-image too
   and is simply gone, which is correct: a system palette has no value for a
   light. */
/* AND THE CHEVRON HAD TO BE NAMED, which is the one icon on the site that
   does. .cf-icon strokes `currentColor` and that is normally the whole story —
   the mode forces `color`, so every icon in the system arrives on CanvasText
   without anyone writing anything. An <svg> is `forced-color-adjust:
   preserve-parent-color` in Chromium's UA sheet, though, which means it does
   not take its parent's FORCED colour but the author one, and the rule above
   is the only place in the system that hands an icon's parent an author colour
   of its own. Measured on ueber-uns.html with the dark palette active: the cue's
   label came through white and its chevron computed rgb(0, 0, 0) — the one part
   of the cue the note above says "is what gets looked at", drawn on Canvas
   black. It is also the only .cf-icon on the shipped site that does this;
   every other one inherits `color` and is repainted for free. */
@media (forced-colors: active) {
  .cf-scroll-cue__stem {
    background: none;
    border-inline-start: var(--stroke-1) dotted;
  }
  .cf-scroll-cue__glyph { stroke: CanvasText; }
}

/* --------------------------------------------------------------------------
   STATEMENT
   A single large claim standing in a field. Used for the
   "Tausende Sensoren erzeugen Daten" moment.

   It used to be a claim BESIDE a square object, and the square was the
   problem. At 1280 the figure cell measured 608 x 608 to carry five hairlines,
   a 14 px dot and a small grey mark, while the claim it was there to support
   filled 168 px of the 608 px row next to it. Seventy-two per cent of the
   component was reserved for nothing, and the section stood 848 px tall on a
   900 px viewport to say one sentence.

   The figure is a band now, spanning the whole block, and the claim sits
   inside it rather than next to it — over the part of the band the field has
   already thinned out of. Same content, roughly half the height, and the two
   halves of the composition finally touch.
   -------------------------------------------------------------------------- */
.cf-statement {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--space-8);
}
/* The band is 1200 x 288 and `slice`s, so a narrow block crops rather than
   shrinking the whole scene to a letterbox — the drawing keeps its scale and
   the frame keeps a share of it. THE SHARE IS THE WHOLE DECISION, and for as
   long as this rule existed it was the default one.

   `slice` fills the box and crops the surplus axis; the align keyword says
   WHICH surplus goes. This box is 288 units tall in every mode, so the crop is
   always horizontal and the visible window is exactly `288 x ratio` viewBox
   units wide. That is a CONSTANT, not a ramp: a ratio shows the same number of
   units at 320 px and at 1000 px alike, and the only thing a wider viewport
   buys below the fold is a bigger picture of the same units.

   THE BAND WAS 420 UNTIL scripts/gen-stmt-field.py, and every number in the
   working below is restated for 288. The reason it moved is in that script and
   in scripts/check-figure-fits.py: at 420 the whole statement-to-process
   drawing stood 871 px tall against 810 px of viewport below the nav, so there
   was no scroll position at which a reader could see it whole.

   Which 700 is the question, and the answer has to come from the drawing. The
   field is masked by #cf-stmt-reach, a userSpaceOnUse ramp over 0–1200 whose
   last stop is opacity 0 at offset 0.54 — so the data stops at x 648 and
   x 648–1200 is the void, 552 units, 46 % of the drawing. The void is the
   subject: the claim set on this band says the sensors produce data and no
   answers, and the void is where that sentence is true.

   xMin ANCHORED THE LEFT EDGE, which is the end with the data on it. The
   window was [0, 700]: the whole field, and 52 of the void's 552 units — 9 %
   of the absence, 7 % of the frame, against 46 % of the frame in the two-column
   mode the composition was drawn in. Below 56rem of container, which is every
   phone and every tablet in portrait, the figure was a lattice running edge to
   edge and thinning slightly at the right, over a sentence about there being
   nothing there. The picture argued the opposite of its own caption, and it
   did so at ten of the ten widths on the ladder because the crop never moved.

   xMid ANCHORS THE COMPOSITION INSTEAD. The window is centred on the drawing's
   own centre, so the field/void boundary lands at 0.5 + 48/(288 x ratio) of the
   frame — between 54 % and 56 % for ANY ratio the void survives, against the
   54.0 % the uncropped band puts it at. The anchor alone fixes the share; the
   ratio is then free to be chosen for the other constraint.

   THE OTHER CONSTRAINT IS THE VOID, and it is what moves 2/1 to 3/1. The
   window's left edge sits at 600 - 144 x ratio. The void is the circle the
   lattice is cut at — cx 330, r 84 — so its rim is x 246, and on a 288-unit
   band 2/1 puts the edge at 312: 66 units INSIDE the rim, a frame cutting the
   one absence the drawing is about, in a band whose own #cf-stmt-halo note asks
   for "an extent of its own instead of four edges cut by the frame". The
   clearance is the lattice's module: 3/1 puts the edge at 168, which is 78
   units — two modules and change — clear of the rim.

   Measured, below the fold, before and after:

     ratio  align  window        void in frame   void clearance   figure at 375
     2/1    xMid   [312, 888]    240 u / 41.7 %   -66 u (cut)      188 px
     3/1    xMid   [168, 1032]   384 u / 44.4 %    78 u           125 px
     1200/288      [0, 1200]     552 u / 46.0 %   246 u             90 px

   The third row is the two-column mode, for reference — 44.4 % against 46.0 %
   is the same composition, which is the point, and on a phone it is now bought
   WITH band height rather than against it: the band is shorter than the one it
   replaces at every ratio, which is the whole subject of
   scripts/check-figure-fits.py.

   scripts/check-slice-crop.py holds the align keyword, the ratio and the void's
   share to each other, and holds the three copies of the keyword — this
   component's two instances and the code sample beside one of them — to one
   value. */
.cf-statement__figure { min-width: 0; }
.cf-statement__figure .cf-iso {
  width: 100%;
  height: auto;
  /* 3 / 1 STACKED, AND THE THREE IS THE VOID'S CLEARANCE. The band's viewBox
     is 1200 x 288 now (scripts/gen-stmt-field.py: the field was 420 units tall
     and the whole drawing did not fit the viewport it plays in), so a `slice`
     box of ratio R shows 288 x R units of the 1200 and centres them on 600.
     2 / 1 would show 576 and put the window's left edge at 312 — 66 units
     INSIDE the void's rim at x 246, a frame cutting the one absence the drawing
     is about. 3 / 1 shows 864, edge at 168, and the clearance is 78 units: over
     two of the lattice's own 32-unit modules, the same way the ratio was chosen
     the last time this rule moved. It costs the void 1.6 points of the frame
     (44.4 % against the 46.0 % the uncropped band gives it), which is inside
     the five scripts/check-slice-crop.py allows. */
  aspect-ratio: 3 / 1;
}
.cf-statement__title {
  font-size: var(--text-h2);
  font-weight: var(--weight-medium);
  line-height: var(--leading-normal);
  text-wrap: balance;
}
.cf-statement__body {
  margin-top: var(--space-6);
  max-width: 46ch;
  font-size: var(--text-md);
  color: var(--text-secondary);
}
/* --iso-travel is a transform, so it is in viewBox units — which makes it a
   fact about the DRAWING's own coordinate window and not about the page the
   drawing happens to be on. This figure is 1200 units wide wherever it is used,
   so it takes 1200/40: the same 2.5 % of itself that the 640-unit process
   objects get from the token's default 16.

   The value lived in patterns/landing-page.html's own <style> block, keyed on
   .lp-statement-figure — a hook that exists on exactly one page. The identical
   drawing in components/statement.html therefore sat on the 640-unit default
   and arrived from 1.33 % of its own width, a bit over half the distance of the
   one that ships, on the page whose whole job is to be the specimen. Keyed on
   the component, both are right and neither can be forgotten again.

   THE FRACTION IN THAT SENTENCE USED TO READ 3.33 %, "a third further", which
   is 16/480 and belongs to a frame this drawing has never had. A bigger frame
   on the same token travels a SMALLER fraction, so the correction inverts: the
   default was too little here, not too much. foundations/motion.html carried
   the same inversion in the worked example that quotes this rule, and the
   sample is now read by check-iso-motion.py against this declaration rather
   than left to agree with it by hand.
   → foundations/motion.html#travel */
.cf-statement .cf-iso { --iso-travel: 30; }
/* THE LATTICE IS GROUND, AND IT WAS SHIPPING AS FIGURE. The statement's lattice
   is authored at less than full strength — a 1-4 dashed field across the whole
   1200-unit frame, written `opacity=".26"` on the group, because a field drawn
   at contour weight competes with the object standing in it. That attribute
   never once reached a pixel.

   `.cf-iso__ghost` runs `cf-iso-fade`, and that keyframe ended `to {opacity:1}`.
   A CSS animation resolves in the ANIMATION origin, and an SVG presentation
   attribute sits at the very BOTTOM of the author origin — below every
   selector, below the element's own style attribute. Between the two there is
   no contest and no warning, and `animation-fill-mode: both` made the
   replacement total: 0 before the range, 1 after it, .26 at no point on any
   scroll. Same failure shape as #224, one origin further up — the value is
   stated in one place, something else gets the last word, and the render is
   plausible enough that nobody re-reads the declaration.

   FOUR TIMES TOO DARK, and measured rather than judged. On the render at
   1440 x 900 at the scroll where the root is fully drawn, the darkest pixel
   inside the "12 480" label's own box is luminance 0.0341 — a lattice dash
   crossing the digits — against 0.0648 for the numeral's own --text-secondary
   ink. The ground was 1.37:1 from the figure standing on it: the dash and the
   digit are the same mark to a reader, and the dash is the darker of the two.
   At the .26 the markup asks for, it renders 0.4164 and the separation is
   4.06:1.

   --iso-rest IS THE OPACITY A PART COMES TO REST AT, defaulted to 1 in the
   keyframe, so every other part that fades — form, light, trace, node, orbit,
   scene — is untouched and needs to say nothing. Declared here rather than in
   the markup so ONE number governs both paths: the animated one through the
   keyframe, and the un-animated one (below the gate, reduced motion, print, no
   view-timeline support) through the `opacity` declaration, which is why the
   markup's dead attribute could be deleted rather than left to rot.
   Held by scripts/check-authored-opacity.py. */
.cf-statement .cf-iso__ghost { --iso-rest: .26; opacity: var(--iso-rest); }
/* AND THE FIELD WAS FLAT FOR THE SAME REASON, 69 TIMES OVER. The sensors run
   `cf-iso-fade, cf-stmt-collapse` — the same fade, so the same override — and
   every one of them carried its intensity as `opacity="0.1"`…`opacity="0.6"`.
   gen-stmt-field.py derives that number from the glow's radius and calls it one
   of "one quantity, three consequences"; the consequence never rendered. Read
   off the render at the field's peak (scroll 1225 at 1440 x 900): 69 sensors,
   SIX authored values, and ONE computed value, 1. A field of instruments of
   differing strength was rendering as instruments of identical strength, which
   is a field with no information in it, drawn at up to ten times the weight it
   asked for, under the numerals and the claim.

   So each sensor's intensity is emitted as `--iso-rest` beside its own
   --mx/--my/--m, where the keyframe reads it and this declaration falls back on
   it. Still one number per sensor, still derived rather than typed, and now it
   is the number that renders. scripts/check-void-departure.py weighs an
   instrument by it. */
.cf-stmt-sensor { opacity: var(--iso-rest); }
/* Two layers in one row once the component itself has room for them, not once
   the viewport does. Stacked is the default, so a browser without container
   query support gets the safe layout rather than a broken one.

   The figure spans BOTH columns and the claim sits over the second. That is
   the composition, not a trick to save space: the field is masked to nothing
   by 52 % of its own width, so the sentence occupies the frame exactly where
   the data stopped arriving. Ending the figure at the column edge instead
   would put a seam through the one idea the drawing has. */
@container (min-width: 56rem) {
  .cf-statement {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0;
  }
  .cf-statement__figure { grid-area: 1 / 1 / 2 / 3; }
  .cf-statement__text   { grid-area: 1 / 2 / 2 / 3; }
  /* Side by side the band is the block's full width at the drawing's own ratio,
     so the scale is width/1200 and the trace weight follows it: 1139 px of
     container at a 1280 px viewport puts the signal on 0.99 CSS px. */
  .cf-statement__figure .cf-iso { aspect-ratio: 1200 / 288; }
}

/* THE STATEMENT TAKES ITSELF APART ON THE WAY OUT. The assembly runs on the
   figure's entry — scene, ghosts, signals, sources, in that order — and until
   now leaving was nothing: the finished drawing simply slid off the top. Now
   the same timeline's exit phase runs the assembly's mirror, in reverse order
   of arrival: the sources go first, then each signal retreats the way it came,
   then the two rings break on their diagonals and fall toward the section
   below — where the process frame is drawing itself in as this leaves — and
   the lattice, which is ground and does not scatter, goes dark last.

   Geometry lives in the markup: each part carries --sx/--sy (its exit vector,
   on a brand angle, sized by one rule at every scale) and --scat-in (its slot
   in the sequence). This file only says THAT parts scatter, the same division
   of labour as the assembly above.

   Scrubbed, not played: scroll back up and the figure reassembles, which is
   what makes it a state of the drawing rather than an effect on it.

   The second fill-mode is `forwards`, not `both`, and it matters: under
   `both` the scatter would hold its from-state through the whole entry phase
   and pin every part's translate before the assembly had run. Same lesson,
   same wording, as the removed dim recorded on foundations/colors.html.

   Second animation, so every list is restated in full — animation-name is a
   replacement, not an addition, and a shorter list here would cycle wrong. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    /* THE ASSEMBLY WAS OVER BEFORE ANYONE ARRIVED. A `cover` range starts the
       instant the element's first pixel crosses the viewport bottom, so under
       the old `cover 20% -> 40%` this figure — 448 px tall in a 900 px
       viewport — finished at 40 % of a range whose 50 % mark is the moment the
       figure sits centred. It was done before it was looked at, and the reader
       met a still drawing and no animation at all.

       So the window straddles the centre instead. THE CENTRED MOMENT IS COVER
       50 %, EXACTLY, AT EVERY VIEWPORT — this note used to say 54 % and to
       present it as a measurement at 1440 x 900, which made a constant look
       like something to re-measure per size. It is arithmetic, and it falls
       out in one line: a cover range opens at scroll `top - vh` and closes at
       `top + h`, so it spans `vh + h`; the figure is centred at scroll
       `top + h/2 - vh/2`, which is `(h + vh) / 2` into a span of `h + vh`.
       Both the element's height and the viewport's cancel. Confirmed at
       1440 x 900, 1280 x 800 and 1920 x 1080: 50.00 % on all three, for this
       figure and for .lp-flow.

       These ranges put the lattice at about half its fade there — still moving
       as the eye settles on it, settled a little after, and a good margin
       before the scatter starts at about cover 92. Measured at the centred
       scroll: 36 animated parts in this figure, mean 17.4 % of their settled
       state, 14 of them still in motion. A reader wheeling past fast may miss
       it; a reader reading cannot.

       THE RANGE IS NOT THE PROGRESS, and that is what made the first retime
       miss. These animations carry --ease-out, so the visual state runs ahead
       of the linear range position: at the centre the lattice sat two thirds
       of the way through its window and read 78 % built. The numbers below are
       derived from the eased curve, not from the window, and the check is the
       rendered opacity at the centred scroll position rather than the fraction
       of the range spent. */
    .cf-statement .cf-iso__scene {
      animation-range: cover 14% cover 30%;
    }
    /* THE SCATTER OUTRAN THE HAND BY THIRTEEN TO ONE, and the timing function is
       the whole of it. Every list on this rule is restated in full because
       animation-name is a replacement — and the one list that was NOT restated
       is animation-timing-function, so cf-stmt-scatter inherited the --ease-out
       the block above sets for the whole isometric family. That is right for a
       fade and wrong for this, and the arithmetic says so without a browser:

         travel  hypot(393.12, 196.56) = 439.5 units, the farthest part's --sx/--sy
         window  40 % of `exit`, and `exit` spans the figure's OWN HEIGHT — 420 of
                 the same units. 0.40 x 420 = 168 units of scroll.

       Both quantities are in the drawing's units, so the ratio is a pure number
       and the same at every viewport: 2.62 px of travel per px of scroll. Then
       --ease-out multiplies it. cubic-bezier(0, 0, .2, 1) has dy/dx = 5 exactly
       at t = 0 (6s - 6s² over 1.2s + 1.2s², as s goes to 0), so the part leaves
       at 13.1 px per px and is 83 % of the way gone by the middle of its own
       window. Measured at 1440 x 900 in 20 px steps, which averages across that
       start and so reads under it: 5.8 units of travel per px of scroll, against
       1.14 CSS px/px for the next fastest thing on the page — card 01's tallest
       build part, 155 units over 374 px of the pinned track. Ten to one.

       On a page whose whole grammar is that scroll position IS the state, a part
       that moves thirteen times faster than the hand is not scrubbed, it is
       flung — and it is the transition INTO the section the page is built round.

       linear, therefore, on the second animation only. It is the same division
       the flow's two passes are written on one file over: "linear on both,
       because scroll position IS the draw". The fade keeps --ease-out: it is an
       object arriving, which is what that token is for. After: a constant 2.62
       px per px through the window, at every viewport, in both directions.

       NOT fixed by widening the window, and that is deliberate. --scat-in runs
       28 to 58 and the last part already ends at exit 98 %, two points before
       the figure's last pixel leaves the top — there is no room in `exit` to
       spend, and the leads are per-part markup that the drawing's own lane
       owns. The curve is this file's, and the curve is what was wrong.
       scripts/check-scrub-rate.py holds the ceiling. */
    /* The field as a whole sits under the claim, quieter than the lattice it
       stands on is dark; per-instrument opacity rides on top of this. */
    .cf-stmt-field { opacity: .92; }
    /* THE FIELD BUILDS INSIDE THE FIRST STOP, NOT ON THE WAY TO IT. The
       page's hold is three phases now (patterns/landing-page.html, the ramp's
       own note): a standstill for the cloud, a glide, a standstill for the
       root. These windows are fitted to the measured scroll band of the FIRST
       standstill — fade complete before the glide begins, collapse riding the
       glide into the second stop — so the reader watches the field arrive on a
       stage that is not moving. The numbers come from sampling the rendered
       page, not from range arithmetic; the mapping between scroll and cover
       has been wrong four times when derived.

       THE FIELD MUST BE WHOLE WHILE IT IS WHOLLY ON SCREEN, and it was not.
       The figure was 448 px tall in a 900 px viewport, so it was FULLY visible
       for exactly 452 px of scroll — page 502 to 954 at 1440 x 900 — and the
       field was reaching full strength at 980, twenty-six pixels after the
       figure had started leaving the top. Measured at the peak, a third of the
       cloud and the first line of the claim were above the fold: the reader
       never once saw the whole of either.

       THE BAND IS 288 UNITS NOW, not 420 — 307.2 px at 1440 x 900 against the
       448 the sentence above was written for, and every number in this note is
       from before that. scripts/gen-stmt-field.py has the reason: the whole
       drawing, field to root's foot, stood 871.15 px against 810.06 px of
       viewport and the reader could not see it whole at any scroll position.
       The window's ranges below are NOT re-tuned here — they are held by
       scripts/check-scrub-rate.py and they are the hold's lane — but their
       premise moved, and the visible band is wider than it was.

       These numbers are read off the rendered page, not off the range: sampling
       the aggregate opacity against scroll puts cover 0 near scroll 533 and a
       point at about 10.4 px, which is not what the box's own height predicts,
       and the arithmetic was wrong about it twice. Measure the mapping, then
       set the window inside the visible band.

       THE REAL FIX IS A PIN, and this is not it. 452 px of visible scroll
       cannot hold a cloud arriving, holding, and collapsing into a root; the
       collapse still runs while the figure leaves. A sticky stage would give
       the sequence as much room as it needs, and it is blocked on the anchor
       chain that stretches .lp-flow between the void and the rail — anchor()
       does not follow a sticky ancestor. That is the hold lane's brief and it
       has already found it independently. What this window buys is the one
       thing that was strictly broken: the field is now whole, on screen, with
       both lines of the claim, before anything moves.

       THE FIELD COLLAPSES INTO THE LINE. Two animations on one element and
       they must not be one: the glows arrive with the lattice, hold, and then
       every one of them travels to the trunk's head and goes out as it gets
       there — nearest first, the far edge of the field last, staggered by --m,
       which is each dot's own distance to the head normalised. The trunk grows
       downward out of that point while they are still arriving, so the line is
       made of them rather than replacing them.

       `forwards` on the collapse, not `both`: under `both` it would hold its
       from-state through the arrival and pin every glow at full opacity before
       the field had faded up. That is the third time this file has paid that
       lesson — see the removed dim on foundations/colors.html — and the second
       time in this block.

       The travel is `translate`, not `transform`: a keyframe setting the
       property would replace the transform ATTRIBUTE rather than compose with
       it, and half the parts in this system carry one. */
    .cf-stmt-sensor {
      animation-name: cf-iso-fade, cf-stmt-collapse;
      animation-range: cover calc((26 + var(--m) * 6) * 1%)
                       cover calc((40 + var(--m) * 6) * 1%),
                       cover calc((52 + var(--m) * 8) * 1%)
                       cover calc((64 + var(--m) * 8) * 1%);
      animation-fill-mode: both, forwards;
      animation-timing-function: var(--ease-out);
      animation-timeline: --cf-iso, --cf-iso;
      animation-duration: auto;
    }
    .cf-statement .cf-iso__ghost {
      animation-name: cf-iso-fade, cf-stmt-scatter;
      animation-range: cover 52% cover 70%,
                       exit var(--scat-in, 30%) exit calc(var(--scat-in, 30%) + 40%);
      animation-timing-function: var(--ease-out), linear;
      animation-fill-mode: both, forwards;
    }
  }
}
/* A part moves with `translate`, not `transform` — see the build's own note:
   half the parts in this system carry a transform ATTRIBUTE, and a keyframe
   that sets the property replaces the attribute instead of composing with it. */
@keyframes cf-stmt-collapse {
  to {
    translate: calc(var(--mx, 0) * 1px) calc(var(--my, 0) * 1px);
    opacity: 0;
  }
}
@keyframes cf-stmt-scatter {
  to {
    translate: calc(var(--sx, 0) * 1px) calc(var(--sy, 0) * 1px);
    opacity: 0;
  }
}

/* --------------------------------------------------------------------------
   PROCESS CARD
   The four-step "Was wir machen" sequence. Square illustration panel on
   the left, copy panel on the right, one hairline border around the pair.
   -------------------------------------------------------------------------- */
/* CONTOUR ON THE WASH, WITH NO PLATE UNDER IT. This card carried
   `background: var(--surface-card)` — grey-050, an opaque near-white plate —
   and foundations/materials.html has said all along that the process card
   "sits on the page wash with nothing complex behind it, so it is drawn with
   a contour instead". The page was describing a card that did not exist.

   The designer agrees with the page. Sampled off mockups/landing-page.jpg
   down the whole Discovery plate, the card's interior and the page margin
   beside it read 206.8 vs 207.0, 208.4 vs 208.0, 210.9 vs 211.0, 211.9 vs
   212.0 — within 0.2 at every row, and the only rows that differ at all are
   the ones the illustration's own light fill passes through. There is no
   plate in the material.

   It also could not hold still. The wash is viewport-fixed and runs the full
   207 → 255 down every screen, so measured at 1280x900 the plate stood +37.6
   off the page near the top of the viewport and +7.5 near the bottom — the
   card separating from its own page five times as strongly at one scroll
   position as at another, and dissolving into it entirely at the last. A
   contour is ink; it is the same line wherever the card happens to be. */
.cf-process {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  border: var(--stroke-1) solid var(--border-strong);
}
.cf-process__figure {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  min-width: 0;
  /* THE BOX STOPS WHERE THE DRAWING STOPS, and the number is not chosen: it is
     the drawing's own cap plus the air this rule declares around it. The svg
     below is capped at 22rem and this rule states --space-8 of padding on every
     side, so 22rem + 2 x --space-8 = 26rem is the tallest this panel can be and
     still be a drawing with its padding around it. One pixel more is air nobody
     asked for.

     WITHOUT IT THE 4:3 RAN AWAY FROM ITS OWN CONTENT. The note under the svg
     already records the crossover — "above 554 the 22 rem width cap binds first
     and the height never can" — but only the drawing was capped there, not the
     box holding it, so past that card width the panel kept taking three
     quarters of every pixel the card gained while the object inside it stayed
     352 x 352. Measured on the landing page's stacked "Was wir machen" column,
     dead height BEYOND the --space-8 this rule asks for:

       viewport   card    panel        drawing    dead height
       600        532     532 x 399    352 x 334  0     (height cap still binds)
       700        621     621 x 466    352 x 352  50
       768        682     682 x 511    352 x 352  95
       834        740     740 x 555    352 x 352  139
       1000       888     888 x 666    352 x 352  250

     At 834 — an iPad Air held upright — the panel was 63 % of the card's whole
     height to show an object covering 30 % of its area, and the four steps
     stood 3 678 px tall between them. The card is designed at 375, where the
     drawing fills its panel, and it is designed again at the two-column fold,
     where the panel is square beside the copy. The 360 px of viewport between
     those two designs had no design in it, only the 4:3 stretching.

     THIS ADDS NO THRESHOLD, and the register says why in its own words: it is
     "a list of QUERIES, not of numbers". There is no query here. The cap is a
     length derived from two the rule already states, and it engages where the
     4:3 box grows past it — binary-searched to a card width of 556 px, one
     pixel over 4/3 of the cap and 2 px above the 554 the register records for
     the crossover the SVG's own cap makes. Below it nothing changes: the box is
     shorter than the cap, and the svg's max-block-size still shrinks the
     drawing to fit exactly as the note below describes. At and above it the
     panel is 26rem plus its hairline, the content box is 22rem, and the drawing
     fills it to the pixel — measured 352 x 352 at 700, 768, 834, 900 and 1000.

     NOT IN THE TWO-COLUMN FORM, where it is reset: there the panel is square
     BESIDE the copy and its height is the row's, so a cap would leave the
     hairline that divides the card stopping short of the copy it divides.

     THE BORDER IS IN THE NUMBER AND THE WIDTH IS STATED, and both are here
     because of what a max size does to a box that also has a ratio. Everything
     is border-box (base.css:49), so the cap has to carry the border-bottom this
     rule draws or the content box lands on 351 and the drawing misses its own
     22rem cap by a pixel — which is the pixel --trace-weight: 1.82 is derived
     from, 640/352 = 1.00 CSS px on the contour. And a max size in one axis
     TRANSFERS through an aspect-ratio to the other whenever the box's size in
     that other axis is a stretch-fit rather than a stated length: with the cap
     and no width, the panel came out 555 x 416 — the ratio pulling the WIDTH in
     to match the clamped height, a card-wide band shrunk to a 555 px box with
     the copy's full width under it. `width: 100%` makes the inline size a
     definite length, the ratio resolves height from width as it always did, and
     the cap clamps that height and nothing else. */
  width: 100%;
  max-block-size: calc(22rem + var(--space-8) * 2 + var(--stroke-1));
  padding: var(--space-8);
  border-bottom: var(--stroke-1) solid var(--border-strong);
  background: transparent;
}
/* One size for every isometric object in a card, so the four steps read as a
   set rather than four drawings that happen to sit next to each other.

   THE HEIGHT CAP IS THE HALF THAT WAS MISSING, and it is a bug fix rather than
   a symmetry. The four objects are square drawings (viewBox 640 × 640) and this
   panel is 4:3 until the card goes two-column — so below a certain card width
   the object is taller than the box it is centred in, and a grid item is not
   shrunk to fit its area. Measured on the landing page: at a 375 px viewport
   the card is 332 wide, the panel 249 tall with --space-8 of padding on every
   side, and the drawing rendered 268 × 268. That is 83 px more than the air the
   padding declares and 19 px more than the panel's own border box — so the
   object spilled about 10 px past each end, over the card's top edge and down
   through the hairline into the copy panel. That hairline is an edge in the
   sense foundations/geometry.html means it: it closes a surface. Drawn through.

   The window is card width 256 → 554 px for the padding and 256 → 469 px for
   the border: below 256 the 4:3 box is tall enough, and above 554 the 22 rem
   width cap binds first and the height never can. Both intervals sit squarely
   in the phone range, which is why every phone had it and no desktop did.

   max-block-size: 100% resolves against the grid area — the panel's content
   box — so the drawing is bound by whichever of the two caps is tighter. The
   svg's own box stays as wide as the row allows and the drawing letterboxes
   inside it at preserveAspectRatio's default, centred: the object shrinks, the
   air comes back, and nothing moves horizontally. Above 56rem the width cap is
   still the tighter one and this changes nothing at all. */
.cf-process__figure > svg { width: 100%; max-width: 22rem; max-block-size: 100%; }
/* The trace weight belongs to this rule, because this rule is what sets the
   scale it is derived from — see --trace-weight in the isometric section.
   640 / 352 wherever the 22 rem cap binds: 1.00 CSS px, on the contour weight
   exactly. This literal is now the FALLBACK TIER — the value an engine that
   cannot evaluate atan2() over a container unit gets, which today is Gecko —
   and the rule that follows is the one that ships. */
.cf-process__figure .cf-iso { --trace-weight: 1.82; }
/* THE BAND WHERE THE OTHER CAP BINDS USED TO BE "THE ONE PLACE A NUMBER CANNOT
   FIX", and the comment that stood here was right about the number and wrong
   about the conclusion. Between 256 and 554 px of card width the height cap
   above is the tighter one, so the drawing is letterboxed into a box that grows
   with the card: measured 0.22 of scale at a 320 px viewport and 0.53 at 608, a
   2.4x ramp that no constant can land on 1 px. The old answer held a second
   constant, 2, behind a 34.625rem container query — 0.45 to 1.06 CSS px across
   the phone band, and the only fractional threshold in the register — and
   closed with "a weight that tracked it would have to be read off the rendered
   box, and only the box knows".

   THE BOX IS NOT THE ONLY THING THAT KNOWS. The drawn size is arithmetic over
   lengths the stylesheet already states, and a query container is what gives a
   stylesheet a length to do arithmetic on. Every card that ships stands in a
   query container that it fills to the pixel — .container on the landing page,
   .docs-demo on components/process-card.html, measured equal at every width
   from 320 to 1024 — so 100cqw IS the card, and preserveAspectRatio's default
   letterboxes a square viewBox into the smaller of the figure's content width
   and content height:

     content width    100cqw − 2 x card border − 2 x padding
     content height   3/4 x (100cqw − 2 x card border) − 2 x padding − border
     the cap          22rem, the svg's own max-width

   The weight is viewBox width over the smallest of the three. A length cannot
   be divided by a length in calc() — but a ratio of two lengths is an angle's
   tangent, and tan(atan2(a, b)) is exactly a / b as a <number>. It is written
   back into a length with `* 1px`, because inside an SVG a CSS px IS a user
   unit and stroke-width takes a length in every engine, where Gecko refuses a
   trig-function <number> there outright. Measured on the landing page after
   this rule, Chromium 141: 1.000 CSS px at 320, 360, 375, 414, 480, 540 and
   600, against 0.45 to 1.06 before, and still 1.000 with the root font at 20
   and 24 px.

   THE GUARD NAMES THE ONE CAPABILITY THIS NEEDS, not the function. Gecko has
   had tan() and atan2() since 108 and evaluates them over absolute lengths
   only: atan2(640px, 22rem) is invalid there, and so is anything with cqw, em
   or vw inside it — measured, Firefox 142, ten spellings. A guard on
   `tan(atan2(1px, 1px))` is therefore TRUE in Firefox and the rule behind it
   is invalid at computed-value time, which lands stroke-width on its initial
   1 user unit: 0.29 to 0.55 CSS px on a phone, worse than either constant.
   `atan2(1cqw, 1px)` is what the rule actually does, Gecko's parser rejects it
   (CSS.supports false, measured), and so Firefox keeps 1.82 above — which is
   what the band had within a tenth of a pixel. Chromium takes the ratio,
   measured; WebKit is expected to and was not measured — the sandbox this was
   written in could not launch one — so if Safari ever draws this trace thin,
   the guard is the first thing to check. Re-measure the guard before
   loosening it; a gate that passes an engine into a rule it cannot evaluate
   is the one failure here that renders as a hairline nobody can see.

   AND THE THRESHOLD IS GONE, not moved. min() reproduces the crossover the
   query used to spell out: above 554 the height term passes 22rem and the cap
   binds by itself, and the two-column form at 56rem is already past that, so
   one expression is exact in both forms and at every root font size — a 20 px
   default moves the crossover to 693 px of card and this follows it, where a
   rem query would have had to be re-derived. The register in tokens.css and
   foundations/layout.html had carried the number as "a question rather than an
   answer ... whether a derived geometric crossover should be a threshold at
   all". It was not: it was a min() written as an if.

   The three lengths are the ones .cf-process, .cf-process__figure and the svg
   rule above declare — edit any of those and edit this, which is the same
   contract the letterbox check holds max-block-size to. */
@supports (stroke-width: calc(tan(atan2(1cqw, 1px)) * 1px)) {
  .cf-process__figure .cf-iso {
    --trace-weight: calc(tan(atan2(640px, min(
      22rem,
      100cqw - 2 * var(--stroke-1) - 2 * var(--space-8),
      0.75 * (100cqw - 2 * var(--stroke-1)) - 2 * var(--space-8) - var(--stroke-1)
    ))) * 1px);
  }
}
/* No fill of its own, and it never had one that could be seen: this declared
   --surface-raised while the card behind it declared --surface-card, and both
   tokens resolved to grey-050. One plate painted twice, half of it dead.

   AND THE SENTENCE THAT FOLLOWED IS BACK, HAVING BEEN CALLED TOO BROAD ONCE
   AND THEN TESTED. It read: "with the card's plate gone the honest reading of
   that pair is that the copy column was never a second surface — the hairline
   between it and the figure is what separates them". Measuring
   mockups/landing-page.jpg looked like a refutation of the second half — the
   copy column IS a light plate there, +22 to +29 over the page, and only the
   FIGURE half reads as the page — so the panel was given that plate as a veil,
   and the veil shipped. Looked at on the card rather than in the source, it
   divides the halves a second time and more loudly than the hairline does. The
   original sentence was the right one: neither half is a second surface, and
   the edge between them is the whole of the difference.

   So there is no fill in either form. The two-column rule at the foot of this
   component adds padding and nothing else, and --surface-lifted, which existed
   for this one panel, is retired in tokens.css. */
.cf-process__panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--space-6);
}
.cf-process__title {
  font-size: var(--text-h2);
  font-weight: var(--weight-medium);
  line-height: var(--leading-normal);
  text-transform: uppercase;
}
.cf-process__subtitle {
  margin-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.cf-process__body {
  margin-top: var(--space-6);
  max-width: 62ch;
  font-size: var(--text-md);
  color: var(--text-secondary);
}
.cf-process__divider {
  height: var(--stroke-1);
  margin-block: var(--space-6);
  background: repeating-linear-gradient(90deg, var(--border-strong) 0 1px, transparent 1px 5px);
  opacity: 0.5;
}
.cf-process__benefit-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.cf-process__benefit {
  margin-top: var(--space-2);
  max-width: 56ch;
  font-size: var(--text-md);
  color: var(--text-secondary);
}
/* Optional note block, pinned to the bottom of the panel. The one thing in
   this card that still takes a surface, and the reason --surface-sunken had
   to become a veil rather than a grey: with the card's plate gone this block
   sits directly on the wash, and an absolute grey inside the wash's own range
   inverts as the reader scrolls past it. Measured before the change, walking
   this exact element down a 900 px viewport: +21 at the top of the screen,
   0 at 65 %, -10 at the bottom. The veil is -12 to -15 at every position.
   See tokens.css. */
.cf-process__note {
  margin-top: auto;
  padding: var(--space-4);
  background: var(--surface-sunken);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.cf-process__note strong {
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
/* AND A VEIL IS NOT A MARK IN FORCED COLOURS — it is provably no mark at all,
   which is a stronger statement than the one tokens.css used to make and the
   reason this block exists rather than a line in that file.

   Chromium does force `background-color` in that mode; what it preserves is
   the ALPHA, not the colour. (CSSWG css-color-adjust-1 #4175, shipped in
   Chromium and Edge: "the user-preferred background-color on all channels
   except for the alpha channel".) So an author veil is repainted in the
   reader's own surface colour AT THE AUTHOR'S ALPHA, and a veil of the
   backdrop over the backdrop is the backdrop. Measured on
   components/process-card.html at 1280, mode active:

                          authored            used                 parent
     light palette   rgba(0,0,0,0.06)   rgba(255,255,255,0.06)   rgb(255,255,255)
     dark  palette   rgba(0,0,0,0.06)   rgba(0,0,0,0.06)         rgb(0,0,0)

   1.000:1 in both, by construction rather than by bad luck, and the same in
   any palette the reader picks — the block had no boundary, no surface and
   nothing else to be seen by. It is the one thing in this card that takes a
   surface, so with the surface neutralised it needs the paint the mode keeps.

   A CONTOUR, WHICH IS WHAT THE CHAPTER ALREADY PRESCRIBES for a panel that
   needs a boundary rather than a surface, and the same idiom .cf-nav::after
   and .material-glass-edge take one door over. .cf-prose pre is the control
   case: identical veil, and it survives the mode intact because it already had
   a border. → foundations/materials.html#veil-boundary */
@media (forced-colors: active) {
  .cf-process__note { border: var(--stroke-1) solid CanvasText; }

  /* THE DIVIDER IS THE SAME LOSS ONE PROPERTY OVER, and it is worse: a
     background-IMAGE is not repainted in the palette, it is dropped. Forced
     colours computes this repeating gradient to `none`, so the hairline that
     separates the body from the benefit inside the copy column was gone
     outright — measured `background-image: none` on the same page, in both
     palettes. That is the loss this chapter already recorded for the two lit
     edges, arriving a third time through the same door.

     Redrawn as a border, which is the only line the mode keeps, and dashed
     rather than solid because the gradient it stands in for is a dashed rule
     and a solid one would be a different line type. The height goes to zero so
     the border is the whole px, and the opacity goes to 1: half a CanvasText
     line is half the contrast the reader turned this mode on to get. */
  .cf-process__divider {
    height: 0;
    border-top: var(--stroke-1) dashed CanvasText;
    background: none;
    opacity: 1;
  }
}
/* Square illustration panel beside the copy once there is room for both.
   Below that the panel goes full width and shortens to 4:3 so the card does
   not become a tower. Measured against the card's own container: on the
   documentation page a 901 px viewport leaves this component 533 px, and the
   old viewport query put it in two columns anyway. */
@container (min-width: 56rem) {
  .cf-process { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .cf-process__figure {
    aspect-ratio: 1;
    /* The stacked form's cap is released here, and the reason is the hairline.
       Square beside the copy, this panel's right border IS the card's divider,
       so its height has to be the row's — capped at 26rem it would stop short
       of the copy it divides on every card wider than 52rem. The stacked form
       has no such duty: its border-bottom is drawn at the panel's own edge
       wherever that edge lands. */
    max-block-size: none;
    /* AND THE SQUARE IS THE SECOND CAP, which releasing the first one did not
       reach. `aspect-ratio: 1` states a height as firmly as a max-block-size
       does: the figure is its column's width tall and stops, whatever the row
       does. A grid item is stretched to its row by default and that is what
       would have saved this — but a box with an aspect ratio and an auto block
       size takes the ratio's height instead of the stretch, in all three
       engines, so the release above bought nothing on any card whose copy is
       taller than the drawing beside it. And the copy is taller often: it is
       four paragraphs, a dashed divider, a benefit and sometimes a note,
       against a square.

       MEASURED ON THE SHIPPED PAGE, "Weniger Ausfälle" — the third card of
       "Was wir machen", the one carrying both a benefit and a note — figure
       height against the row it is supposed to divide:

         viewport     row    figure    hairline short by
         1280 x 800   709     569       140 px
         1366 x 768   688     607        81
         1440 x 900   688     639        49
         1920 x 1080  688     639        49

       Forty-nine pixels of a card's bottom-centre with no line in it, under a
       drawing whose whole job on that edge is to divide. It reads as a slightly
       open corner rather than as a fault, which is why it survived: the card's
       own outer border still closes the box underneath, so nothing is missing,
       something is only unfinished.

       `block-size: 100%` IS THE RELEASE, and it is a percentage on purpose.
       While the row is being sized it cannot resolve, so it behaves as auto and
       the ratio still makes its square contribution — the row is never SHORTER
       than the drawing wants. Once the row is sized, the percentage resolves
       against it, both axes are definite, and the ratio steps aside. The square
       becomes the figure's minimum rather than its size, which is what it was
       always meant to be: the drawing inside is capped at 22rem and centred, so
       a taller box is more air around the same object, never a bigger one.
       → scripts/check-figure-letterbox.py, which registers this release */
    block-size: 100%;
    border-bottom: 0;
    border-right: var(--stroke-1) solid var(--border-strong);
  }
  /* AND THE COPY HALF TAKES NO PLATE, WHICH IS A REVERSAL AND IS WRITTEN AS
     ONE. This rule carried `background: var(--surface-lifted)`, a 26 % white
     veil, and the case for it was a reading of mockups/landing-page.jpg: the
     drawing's right half is a flat 248 against a page margin climbing 219 to
     226 beside it, +22 to +29 and never negative, so the source draws a light
     plate on this half and the implementation drew nothing. The measurement
     stands. The conclusion does not, and the rendered card is where it fails —
     the veil arrives as a lit block occupying the right half of a card whose
     left half is the page, and what the eye picks up is the plate's own edge
     rather than the hairline that exists to divide the two. A card that is one
     surface cut in two read as two surfaces set side by side.

     SO BOTH HALVES ARE THE WASH AND THE DIVIDER IS THE WHOLE OF THE DIVISION.
     That is not a new material, it is the one the comment over
     .cf-process__panel already describes and the one the other three forms of
     this card were already in: the figure half at every width, and both halves
     below this breakpoint, where the columns stack and there is no second
     surface to differ from. The exception is gone rather than scoped — contour
     on both halves, in both forms.

     THE HAIRLINE IS UNCHANGED and is now unaccompanied. .cf-process__figure's
     border-right three rules up is the card's divider; it was drawn before the
     veil and it is what stays. Contour, without the fill. Measured across it on
     patterns/landing-page.html at 1440 x 900, one row inside each half:

       card row   before (left -> right)   after
       head       213 -> 233   +20         213 -> 214   +1
       middle     227 -> 241   +14         227 -> 230   +3
       foot       244 -> 248    +4         244 -> 243   -1

     The 1 to 3 that is left is the wash's own horizontal travel across 1,150 px
     of card, which the figure half carries too. There is no step at the seam
     any more, only a line.

     --surface-lifted GOES WITH THIS DECLARATION. The copy half was its only
     consumer in the system, so the token is retired rather than left standing
     for a panel that might want it — see tokens.css, where the retirement note
     keeps the derivation, and foundations/materials.html#copy-panel-gap, which
     holds the mockup's measurement and this answer to it. */
  .cf-process__panel {
    padding: var(--space-12);
  }
}

/* THE 1-4 HAIRLINE OUTSIDE .rule, WHERE .rule's FORCED-COLOURS BLOCK CANNOT
   REACH IT.

   base.css says the whole of this under the LINE UTILITIES block: every .rule
   is `border: 0` and painted entirely by background, forced-colors discards
   background-image outright, "nothing else on the box paints, so the whole
   line disappears". It then redraws the family as a border and maps the dash
   ratios onto the two styles a border has — .rule--dashed-1-4, whose gradient
   is `var(--border-strong) 0 1px, transparent 1px 5px`, becomes dotted.

   Two hairlines in this file are that same gradient, character for character,
   written out on a component instead of taken from the utility. Neither is a
   .rule, so neither is inside that block, and both are the defect it was
   written to fix. MEASURED in Chromium 1194's forced-colors emulation at
   1440 x 900, both schemes, before this block existed:

     .cf-process__divider   components/process-card.html   439 x 1
     .cf-process__divider   patterns/landing-page.html     451 x 1
     .cf-prose hr           patterns/blog-artikel.html     558 x 1

   background-image: none, background-color rgba(255,255,255,0) on the light
   palette and rgba(0,0,0,0) on the dark, border-top-width 0px. The box keeps
   its 1 px of height and paints nothing in it. Absent, in both schemes, on
   all three surfaces.

   THE HEIGHT IS LEFT ALONE, and that is worth writing down because the
   neighbouring fix does not leave it alone. .cf-page-header__rule's block
   sets `height: 0` so the box does not double under its new border. Here it
   would be a no-op: base.css puts `box-sizing: border-box` on everything, so
   a 1 px box with a 1 px border is a 1 px box — the border grows inward into
   height the element had already reserved. Measured on
   patterns/blog-artikel.html, getBoundingClientRect().height is 1 either way,
   with border-top-width 0px before and 1px after. Nothing moves in the
   rhythm around it (margin-block: var(--space-6) on the divider,
   var(--space-12) on the prose rule), so nothing needs restating to hold it.

   AND THE OPACITY GOES WITH IT, on the divider only. Forced colours does not
   reset opacity, so a 0.5 CanvasText border does not composite to a colour in
   the user's palette: measured against Canvas, rgb(127,127,127) at 4.00:1 on
   the light palette and rgb(128,128,128) at 5.32:1 on the dark, where the
   palette's own pairing is 21:1. Half-strength is a legitimate register on a
   brand ramp and is not one here — forced colours has two inks and the whole
   contract is that the reader chose them. .rule--muted is the same shape and
   is left alone deliberately: it is the demonstration swatch for the muted
   variant on foundations/geometry.html, where showing the register IS the
   content. */
@media (forced-colors: active) {
  .cf-process__divider {
    background: none;
    border-block-start: var(--stroke-1) dotted CanvasText;
    opacity: 1;
  }
}

/* --------------------------------------------------------------------------
   INFO CARD
   A field of copy on a plate of glass: a count, a title, a subtitle, a
   paragraph, the question the subject actually asks, and the facts under it.

   THE MATERIAL IS A MODIFIER, AND THAT IS NOT A CONVENIENCE. The base card is
   layout and type; --glass is what makes it a plate. The first version of this
   component was glass unconditionally and Expertise turned the material off
   again in its own stylesheet for the four cards that did not need it —
   which check-glass-budget.py counted, correctly, as six blurred layers on a
   two-layer page. That checker reads the HTML rather than resolving the
   cascade, on purpose: a census with an unstated exception in it is worse than
   no census. But it was right on the design too. A class that says "I am
   glass" and a page-local rule that says "except here" is exactly the drift
   these scripts exist to stop, and the budget should be legible from the
   markup. Carry the modifier where the material is spent.

   BEARING GLASS, NOT VEIL. Text sits directly on this, so --glass takes
   --surface-glass and its 4.5:1 floor — see the measurement in tokens.css
   section 9, which is the reason that tint has almost no room to move. The
   no-blur and reduced-transparency fallbacks are already in the tokens, so
   there is nothing to write here: the surface goes opaque and the card keeps
   its contrast.

   WHERE THE MODIFIER BELONGS. foundations/materials.html states the material's
   rule in one line — Milchglas "exists to calm a complex graphic beneath it,
   never to sit on a flat surface, where it is only a grey box". So --glass
   goes on a card that stands on the ground, on an illustration, on anything a
   reader can still see through it, and nowhere else. And a shipping page gets
   two blurred layers with the navigation band always one of them, so it goes
   on ONE card per page. The pinned Expertise stage is the case that came up
   first: the material is on a single empty card under the copy column and the
   four cards hand over on top of it, unfrosted. One lectern, changing copy —
   which is also the better drawing, because a lectern does not change when the
   page on it does.

   THE TITLE TAKES THE FOIL, and does not take it from here either.
   .text-foil--ink is authored on the element by the page, because the
   one-foil-moment-per-screen budget is a fact about the SCREEN and a component
   cannot know how many of itself are on one. Ink, never the lit half: this
   surface is light.

   The lit rim is the same one pixel of Weiss-Glas-Sky that .cf-nav::after and
   .cf-btn--glass end at — glass in this brand is edged with light, never
   outlined with ink. -------------------------------------------------------- */
.cf-info-card {
  position: relative;
  isolation: isolate;
  padding: var(--space-8);
  /* Contour, no surface. --surface-card is retired — see the note in
     tokens.css section 2 — and a panel that needs a boundary takes a contour.
     Which is also why the modifier below is a swap and not an addition. */
  border: var(--stroke-1) solid var(--border-strong);
}
/* The material, and it REPLACES the boundary rather than sitting inside it.
   --info-card-tint so a page that can NAME what is behind the glass can reach
   for the thinner pane; the default is the floor for a backdrop nobody
   controls. Both tints go opaque together in the two fallbacks. */
.cf-info-card--glass {
  background-color: var(--info-card-tint, var(--surface-glass));
  -webkit-backdrop-filter: var(--glass-blur);
          backdrop-filter: var(--glass-blur);
  border: var(--glass-border);
}
/* The rim, and it belongs to the material rather than to the card. Above the
   content rather than under it, so it survives a card whose first line starts
   at the padding edge, and pointer-transparent so it cannot eat a click on
   anything inside.

   TWO LAYERS ON ONE PIXEL, the same construction .cf-nav::after carries: the
   specular parked off the left edge, over the designed rim. -70 % and 170 %
   are the off-canvas endpoints for a band 40 % of the container wide — a
   layer positioned at P is offset by P x (container - image), so at 40 %
   width the band's edges sit at -0.42W and -0.02W. Written as background-image
   plus explicit repeat, size and position rather than the shorthand, so a
   later reader can see that at rest this rule paints --glass-edge and nothing
   else.

   THIS IS THE THIRD GLASS SURFACE JOINING THE OTHER TWO. The nav's rim fades
   in and its band crosses, both scrubbed by scroll; .cf-btn--glass answers the
   pointer with --cf-specular. This one had a rim that never moved — and it is
   the one surface in the system the reader is guaranteed to be moving past,
   because its only shipping use is a pinned stage they scroll through. The
   comment on cf-nav-rim asks a later run to put sheet response on the rim
   layer and not on the sheet; this is that run, and this is that layer. */
/* THE INSETS CANCEL THE SHEET'S BORDER-WIDTH, and for as long as they did not
   this card drew TWO lines where the material has one.

   `inset` on an absolutely positioned box resolves against its containing
   block's PADDING box, and .cf-info-card--glass reserves a border — the lit
   rim it takes from --glass-border. So `inset: 0 0 auto 0` put this strip one
   pixel INSIDE the perimeter it is the perimeter of, and two pixels short of
   it in width. Measured on components/info-card.html at 1440 x 900, device
   scale 1, card border box 608 px wide:

     row 0   253-254 flat        the border, drawn by the card
     row 1   251 .. (240,248,246) --glass-edge, drawn by this strip, x 1..607
     row 2+  251                 the bearing tint

   Two parallel lines a pixel apart, the outer one achromatic and the inner one
   carrying the material's whole Weiss-Glas-Sky travel — on the one surface in
   the system whose own comment three lines up says the rim "is the same one
   pixel of Weiss-Glas-Sky that .cf-nav::after and .cf-btn--glass end at".
   Nothing rendered wrong. There was simply a second line, and glass in this
   brand is edged once.

   .cf-btn--glass::before HAS ALWAYS DONE THIS RIGHT and says so in as many
   words — "painted on the BORDER box (background-origin is border-box, 1 px
   reserved)" — cancelling the width on `top` and `left` and carrying the same
   cancellation into its own 62 % window (`62% + 0.62 * 2 * var(--stroke-1)`).
   .cf-nav::after needs no cancellation because the band has no border to
   cancel. This was the third rim and the only one written against the padding
   box; the idiom existed, in this file, and it was not reached for.

   IT IS THE SAME DEFECT check-contour-box.py WAS WRITTEN FOR, one construction
   over. That script's rule — an absolutely positioned drawing that takes over
   a bordered parent's edge must cancel that border-width on its insets — was
   scoped to <svg> edge frames, because that is the form it was found in. A
   1 px pseudo-element is the same sentence in CSS. scripts/check-glass-rim-box.py
   now reads the other form.

   THE ENDS DO NOT GAP. --glass-edge is transparent at 0 % and 100 % and never
   exceeds alpha 0.42, so the strip composites ONTO the border it now stands on
   rather than replacing it: where the gradient has nothing, the card's own
   --glass-border shows through unchanged, and the two top corners still close
   against the side borders. That is what the button's rim does over the same
   border, and it is why this is a one-line move rather than a redraw. */
.cf-info-card--glass::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--stroke-1)) calc(-1 * var(--stroke-1)) auto;
  height: var(--stroke-1);
  background-image: var(--glass-rim-light), var(--glass-edge);
  background-repeat: no-repeat, no-repeat;
  background-size: var(--glass-rim-band) 100%, 100% 100%;
  background-position: var(--glass-rim-park) 0, 0 0;   /* parked off the left */
  pointer-events: none;
  z-index: 1;
}

/* The band crosses as the card passes the reader, once per pass, scrubbed by
   position rather than by a timer — so it tracks their hand and runs backwards
   on the way up. view() and not scroll(root), because this is a component: a
   card halfway down a long document would have had its light cross while it
   was still off screen. Each instance gets its own pass through its own
   viewport crossing.

   `screen and` is load-bearing, not decoration. foundations/motion.html states
   that every scroll-driven animation must be scoped to screen, because a paged
   medium has no scroll and a `both`-filled animation there holds its `from`
   keyframe — which here would print the band frozen mid-crossing on a surface
   that should be at rest. Print never applies it and the parked rest position
   above is what draws.

   ON THE RIM AND NOT ON THE SHEET, for the reason cf-nav-rim gives: fading or
   sliding anything across a backdrop-filtered layer re-rasterises its blur
   every frame. This is one pixel of unblurred gradient, so the travel is free.

   The plate on patterns/expertise.html overrides the timeline — it sits inside
   a sticky-pinned stage, so its own view progress stalls at the moment the
   reader starts moving through it, which is exactly the wrong moment. */
@supports (animation-timeline: view()) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-info-card--glass::before {
      animation: cf-glass-rim linear both;
      animation-timeline: view();
    }
  }
}
/* The crossing itself is @keyframes cf-glass-rim, declared once beside the
   navigation sheet's edge. This plate used to carry its own copy of it under
   its own name; what is local to this surface is the timeline above and the
   pin override on patterns/expertise.html, which is what "same band, different
   clock" means in a stylesheet. */
/* Forced colours computes every gradient background-image to `none`, so this
   element — which IS its background-image, 1 px tall with nothing else to
   paint — loses the rim entirely. Redrawn as a border, the only paint the mode
   keeps. Same idiom as .cf-nav::after and .material-glass-edge. */
@media (forced-colors: active) {
  .cf-info-card--glass::before { border-top: var(--stroke-1) solid CanvasText; }
}

/* --text-secondary and not --text-muted: colors.html says twice that muted
   may not sit on the wash, and a pane of glass over the wash is still the
   wash. Measured on the shipping plate at the thin tint, muted lands at
   2.32:1 and secondary at 6.73:1. */
.cf-info-card__count { color: var(--text-secondary); }
/* An id on a heading in the flow is an address, whatever else it is for. These
   four ids name their cards to aria-labelledby and are linked from nowhere —
   and expertise.html#feld-02 is still a URL anyone can be sent, so it still has
   to arrive somewhere the reader can see. Same clearance as .cf-prose and the
   section header; scroll-margin-top does nothing until something scrolls to
   this element, so the pinned stage these cards sit on is unaffected. */
.cf-info-card__title {
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-h1);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  scroll-margin-top: calc(var(--nav-height) + var(--space-6));
}
.cf-info-card__sub { margin-top: var(--space-2); color: var(--text-secondary); }
.cf-info-card__body { margin-top: var(--space-5); max-width: 52ch; color: var(--text-secondary); }
/* The question the subject actually asks, opened by a mono tag with the
   hairline running out of it — the .cf-section-header device at card scale,
   and the third place in the system to use it.

   It was a 2 px rule down the left side, under the reasoning that a line is
   how this system sets something apart and that a second box inside a card is
   a card inside a card. The second half of that is right and the first half is
   the exact move foundations/geometry.html bans by name: a bar beside a block
   is not an edge (the other three sides are not drawn), not a division
   (nothing is on its far side) and carries no label, so it says "different"
   without saying how. .cf-prose__note was the same violation with the same
   rationale in its own comment and was fixed this way; this was the last one
   left. The tag also does what the bar could not — it says what the block IS.

   The type step stays. It is the .cf-quote half of the same precedent: a
   question set one size up is already marked as not being body copy, so the
   tag ranks it rather than having to carry the whole distinction alone. */
.cf-info-card__ask {
  margin-top: var(--space-3);
  font-size: var(--text-lg);
  max-width: 44ch;
}
/* The tag's own air. Everything else about it is declared once, with
   .cf-prose__note-tag, in the ARTICLE + PROSE section — see the note there. */
.cf-info-card__ask-tag { margin-top: var(--space-6); }
/* .tiles carries the recipe; this only sets the two numbers that are its own.
   Needs the guard .tiles adds — a fact column with a 13rem floor overflows a
   narrow phone otherwise. Markup: class="cf-info-card__facts tiles tiles--fit". */
.cf-info-card__facts {
  margin-top: var(--space-8);
  --tile: 13rem;
  --flow: var(--space-6);
}
.cf-info-card__label { color: var(--text-secondary); }
/* The chips belong to the card rather than to the system: a global chip would
   be a fifth way of drawing a small bordered label, and the four that exist
   are all doing something this is not. */
/* Tag-qualified for the margin-top, which base.css:150's `margin: 0` outweighed
   at (0,1,1) against this rule's (0,1,0): the chips sat flush under the label
   instead of a rung below it. → scripts/check-reset-shadow.py */
ul.cf-info-card__chips {
  margin-top: var(--space-3);
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
/* THE INTRINSIC MINIMUM, ONE LEVEL BELOW THE GUARD THAT ALREADY EXISTS.
   `.tiles > * { min-width: 0 }` floors the fact COLUMN, which is the tiles
   grid's item; the chip is a flex item inside that column and carries its own
   `min-width: auto`, so it floors at its own min-content — one uppercase mono
   compound with --tracking-label on every letter — and a flex item is not
   allowed to shrink below that. The column then holds its width and the chip
   simply leaves it. It is the third instance of the case
   foundations/layout.html#intrinsic-two-fixes tabulates, after .tiles and
   .cf-section-header, and the one nothing had run: check-grid-tracks.py reads
   TRACK LISTS, and a flex row declares none.

   base.css's break-word net does not reach it either, and the layout chapter
   says why in its own words — overflow-wrap decides how a line breaks once the
   box has a width, and the box's width is what min-content has already
   decided. The word breaks visibly inside the chip on every page, which is
   exactly what makes this look handled.

   MEASURED, patterns/expertise.html, field 03, the widest chip in the tree
   ("Windgeschwindigkeit", 19 characters with no interior break) against the
   fact column it stands in, at the browser default font sizes a Windows
   reader arrives with:

     viewport  root   chip     column   outside the card   document
     320       16     fits     —        0 px               320
     320       20     fits     —        0 px               320
     320       24     229 px   182 px   47 px              320
     320       32     305 px   150 px   155 px             390  (70 px sideways)
     375       32     305 px   198 px   107 px             393  (18 px sideways)

   So it paints outside its own panel from a 24 px default — 150 % is the
   common low-vision setting and inside what WCAG 1.4.4 asks — and takes the
   whole document sideways at 32. With the floor, every one of those rows is
   0 px outside and the 375 document is 375. What is left at 320 x 32 is 27 px
   of .cf-page-header__meta, a two-span flex row that could not wrap. That is a
   different rule and it is now taken, on the same declaration, where the
   component is declared — see its own note for the eight pages and the trade.

   WHAT IT COSTS IS THE BREAK POINT, and the net decides it rather than a
   dictionary. Contained, a chip whose longest word is wider than the column
   breaks inside that word — "BATTERIESPEICH / ER, KLEIN UND GROSS" at 320 x 24,
   where the escaping version broke at its comma. That is base.css's net doing
   the job it says it does ("It is a NET, not typography ... Until then the page
   does not scroll sideways"), and the right answer for German is still
   `hyphens: auto` against these pages' lang="de", which that same note leaves
   to the typography lane and to a designer. A word set badly inside the panel
   is the smaller fault than a word set well outside it.

   NOTHING MOVES WHERE NOTHING WAS WRONG. min-width: 0 only lets a box shrink
   that the line was already too narrow to hold: measured identical to the
   pixel at 320, 375, 414, 853 and 1024 at a 16 px default, and at 853 and 1024
   — the CSS viewports a 1280 px screen has at Windows' 125 % and 150 % display
   scaling — at every default from 16 to 32.
   → base.css, .tiles > *; foundations/layout.html#intrinsic-two-fixes */
.cf-info-card__chips li {
  padding: var(--space-1) var(--space-3);
  border: var(--stroke-1) solid var(--border-default);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  min-width: 0;
}

/* --------------------------------------------------------------------------
   LOGO WALL
   Partner + technology marks, desaturated into the background so they read
   as texture rather than as claims.
   -------------------------------------------------------------------------- */
.cf-logo-wall {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* CENTRED IS THE DEFAULT, AND space-between IS THE EXCEPTION — which is the
     opposite of how this rule was written, and the reason the wall was wrong at
     every width a phone or a tablet has.

     `justify-content: space-between` justifies EVERY line, and the last line of
     a wrapped row is the one line that must never be justified: it holds the
     remainder, and space-between spreads that remainder across the full measure.
     Seven marks wrap to 6 + 1 at a 768 px viewport, and the one mark left over
     sat flush left with 614.9 px of empty row beside it — 90 % of the wall. At
     375 it was 3 + 3 + 1 and 265.1 px, at 320 2 + 2 + 2 + 1 and 211.4 px. None
     of it was visible above 862, where the seven stand-ins fit on one line and
     there was no last line to strand, which is why a rule that was wrong at
     three of the four widths this system tests looked right every time anybody
     opened it. Every figure in this paragraph is the wall AS TEXT — 862 is
     1222 now that the marks are artwork, binary-searched: one line at a
     viewport of 1222, wrapping at 1221 — and the paragraph is kept as the
     record of why the rule below is `center` with `space-between` as the
     exception, which is a fact about wrapping and not about any one width.

     The same declaration also meant the gap token never governed. space-between
     stretches each row to the measure, so the 48 px written here was only ever a
     minimum that nothing hit: the marks actually sat 133–162 px apart at 320,
     59–69 at 375 and 54 at 768 — a different rhythm on every row of every width.
     Centred, --space-12 is the real distance between two marks everywhere.

     THE THRESHOLD IS NOT A NEW ONE. Seven marks plus six gaps need 617.0 px —
     329.0 of type and 288 of gap. A size query measures the CONTENT box, and
     .container's content box IS this wall's width, so the number the query
     wants and the number measured on the wall are the same number: the wall
     wraps at 616.78 and holds one line at 617.69. 44rem — 704 px, already in
     the register for .subdivide and .grid--early — is the register's first
     number above that. Above it the wall is one line and justifying it to the
     measure IS the design; below it there is always a remainder.

     IT WAS 48rem, AND THE NUMBER WENT STALE UNDER IT WITHOUT MOVING. The
     paragraph this replaces measured the same seven marks at 767.2 px and put
     the threshold at 768 inside a 0.9 px window — correct when it was written.
     What it did not survive is the marks getting SMALLER. They are stand-in
     text until the wordmark files land, and patterns/landing-page.html declared
     them by restating .t-label by hand at --text-lg; the fix that gave them the
     real class put them on --text-xs, where every other mono label in the
     system sits. 16 px to 11 px is 0.6875, and 767.2 x 0.6875 = 527.5 of type
     plus the six gaps that did not change — 615.5 predicted, 617.0 measured.
     The wall's one-line width fell 150 px and the query that asks about it did
     not, and nothing in the diff that shrank the type mentioned a breakpoint.

     WHAT THAT COST, and it is the band this rule was written to have no band
     in. From a wall of 617.69 to 768 — a viewport of 694 to 862, 168 px of the
     ladder — the wall was a single line of seven marks with no remainder to
     strand, and it was centred anyway. The air the threshold exists to spend
     sat at the two ends instead: 0.4 px each at 694, 75.2 px each at 862. A
     fifth of the measure, on the widest tablet in portrait, under a rule whose
     own next sentence says justifying a line with nothing left over IS the
     design.

     44rem DOES NOT CLOSE THAT BAND, IT HALVES IT, AND THE REMAINDER IS ON
     PURPOSE. 704 clears the trip point by 86.3 px, reached at a viewport of
     791, so a viewport of 694 to 790 is still one line and still centred — 96
     px against 168, and 43.1 px of unspent measure at each end at the top of it
     against 75.2. The register has nothing between 34rem and 44rem, so
     closing the band exactly would mean minting a threshold at about 38.6rem.
     That is the one thing this particular number must not have: its trip point
     is a STRING, it has already moved once by 150 px without any query being
     edited, and a threshold minted two pixels above a string measurement is the
     one that strands a mark the day the wordmark files land. The register's
     existing number, clearing by 86 px on the safe side of an asymmetric error,
     is the better trade — and stating the residual band is how the next reader
     gets to disagree with that.

     It asks the CONTAINER, per the rule in the register: .container is the query
     container named `layout`, so the wall reads the room it is actually in. A
     wall placed outside a .container matches nothing and stays centred, which is
     the safe half of the pair.

     RE-MEASURE THIS ONE WHENEVER THE MARKS CHANGE. It was 44rem and it is 64rem,
     and the move is the thing the paragraph below always said would happen:
     "the wordmark SVGs replacing the stand-in text" was one of the three events
     listed as able to move this number, and it is the one that did.

     THE TRIP POINT IS A BOX AGAIN, WHICH IS THE REAL NEWS. What stood here said
     this was "the only threshold in the register that needs saying of ... every
     other one is a container query whose trip point is a box; this one's is a
     STRING", 617.0 px being seven mono words set in a fallback mono at
     --text-xs — so it moved when the type scale moved, from inside the same file
     that declared the stand-ins, with nothing connecting the two edits. That is
     over. The marks are seven SVGs with declared boxes, normalised to a shared
     optical height before they were committed
     (assets/img/partner/README.md), so the wall's one-line width is a sum of
     boxes and a gap that is a token: every browser gives the same number, and
     the number does not move when a font lands or a type scale is edited.

     THE MEASUREMENT. Seven marks at the 2.5rem height below are 132.70, 86.14,
     174.61, 60.72, 150.89, 99.41 and 94.27 px — 798.74 — and six --space-12 gaps
     are 288, so the wall holds one line at 1086.73 px and wraps below it. A size
     query measures the CONTENT box and .container's content box IS the wall's
     width, so that is the number the query wants: 1086.73 px is 67.92rem, and
     72rem is the register's first number at or above it.

     IT WAS 64rem FOR ONE RELEASE, and the 98.21 px that moved it are the two
     marks the first pass got wrong: Azure shipped as the glyph ALONE — a bare
     A, 15.19 px wide, next to six marks that all say their own name — and Spark
     shipped at the rule's own answer, which is the one place the rule
     under-reads (see the height note below). Naming the brand and matching its
     neighbours cost 84.22 px on Azure and 14.00 on Spark, and the wall stopped
     fitting above 64rem. This is the event the register says re-measure for:
     the SET changed.

     THE ERROR IS STILL ASYMMETRIC, AND 72rem IS THE SAFE SIDE OF IT. 68rem is
     1088 and clears 1086.73 by 1.27 px, which is the shape the register warns
     about — and the warning is weaker than it was, because the trip point is a
     sum of declared boxes now rather than a rendered string, so the number is
     the same in every browser. It is not gone: those box widths are fractional
     (132.70, 86.14, …), sub-pixel rounding differs between engines and zoom
     levels, and 1.27 px is inside that. 72rem clears by 65.27 px instead. What
     it costs is 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, which
     is the cheap half of an asymmetric error: too low strands a mark on a
     justified last line, which is the bug at the top of this note. If a
     re-measurement is ambiguous, round up. Binary-searched on the shipped page:
     one line from a viewport of 1222, query engaging at 1295.
     → foundations/mobile.html#logo-wall */
  justify-content: center;
  gap: var(--space-12);
  /* The section header above owns the air; the section owns the air below. */
  padding-block: 0;
}
@container layout (min-width: 72rem) {
  .cf-logo-wall { justify-content: space-between; }
}
/* 2.5rem, AND IT IS DERIVED FROM THE FILES RATHER THAN CHOSEN AGAINST THEM.
   It was 1.75rem, which was the box the stand-in TEXT reserved — a mono label at
   --text-xs with its line box round it — and a height picked to match a string
   has nothing to say about artwork. The marks are normalised so the median
   column ink-extent, which is x-height, is 0.29 of each file's box
   (assets/img/partner/README.md). At 1.75rem that renders every wordmark on the
   wall at an 8.1 px x-height: smaller than any type this system sets, under a
   heading in 11 px mono. 2.5rem puts it at 11.6 px, which is --text-xs — the
   size of the labels the marks replaced, and of every other label on the page.

   ONE HEIGHT FOR SEVEN MARKS IS THE CONTRACT AND IT IS LOAD-BEARING. A wall that
   carries a number per logo is a wall nobody can add an eighth mark to. The
   equalising is done once, in the files, where it can be measured and re-derived
   for the whole set; nothing about a particular brand is allowed in here.

   SIX OF THE SEVEN ARE ON THE RULE AND SPARK IS NOT, which is worth knowing from
   here because it is the kind of thing that reads as a mistake. Median column
   ink-extent stands in for x-height on the assumption that most columns of a
   wordmark are letter STEMS. That holds for six upright faces and fails for
   Apache Spark, whose wordmark is a script with a tall S, a k ascender and a p
   descender — most of its columns span ascender to descender, so the median
   over-reads its x-height and the rule scales the mark down to compensate for a
   height it does not have. Shipped it at the rule's own answer and "Spark" read
   visibly smaller than "kafka" and "Azure" beside it. It carries a stated x1.30
   correction, chosen against its neighbours rather than in isolation; the
   arithmetic and the trial are in assets/img/partner/README.md. One exception,
   named, is the honest form — six marks quietly nudged to taste is not.

   grayscale(1) STAYS THOUGH THE FILES ARE ALREADY ONE INK. It costs nothing and
   it is the guard: the day somebody commits a mark straight from a press kit,
   this is what stops a full-colour logo from being the only coloured thing on a
   grey wall. The tone is opacity's, not the filter's. */
.cf-logo-wall img {
  height: 2.5rem;
  width: auto;
  filter: grayscale(1);
  opacity: 0.38;
  transition: opacity var(--duration-base) var(--ease-standard);
}
.cf-logo-wall a:hover img,
.cf-logo-wall a:focus-visible img { opacity: 0.75; }

/* THE FILES ARE "ALREADY ONE INK" TWICE OVER — every mark in
   assets/img/partner/ ships fill:#000 (Kubernetes alone carries a second,
   fill:#fff, layer for its own inner wheel), and opacity composites that
   fixed colour against whatever is behind the image, not against the mode's
   idea of a colour. Fine over the wall's white Canvas: 0.38 of black onto
   white samples rgb(158, 158, 158), exactly what .cf-logo-wall img's own
   opacity is tuned for. Invisible over a dark one, because 0.38 of black onto
   black is still black. Measured in Chromium at 1280 x 900, forced-colors
   active + colour-scheme dark, on the built expertise.html: six of the seven
   marks sampled solid rgb(0, 0, 0), pixel-identical to the Canvas either side
   of them — gone, not dim, the same failure .cf-logo's own light-theme rule
   above already names for the opposite asset over the opposite Canvas.
   Kubernetes' white inner layer was the one survivor, at rgb(97, 97, 97) —
   0.38 of white onto black. Inverting the marks here gives all seven that
   same arithmetic. */
@media (forced-colors: active) and (prefers-color-scheme: dark) {
  .cf-logo-wall img { filter: invert(1); }
}

/* --------------------------------------------------------------------------
   ACCORDION (FAQ)
   Full-width rows separated by hairlines. Question left, arrow right.
   Built on <details> so it works without JavaScript.
   -------------------------------------------------------------------------- */
.cf-accordion { border-top: var(--stroke-1) solid var(--border-strong); }
.cf-accordion__item { border-bottom: var(--stroke-1) solid var(--border-strong); }
/* GRID AND NOT FLEX, FOR THE FLOOR AND FOR NOTHING ELSE. Two children, the
   question and the arrow, one pushed to each end — flex with space-between
   drew exactly that, and drew it with an UNFLOORED intrinsic minimum: the
   question is a bare text node, so it is an ANONYMOUS flex item, and an
   anonymous item is the one kind nothing can put min-width: 0 on. Its
   automatic minimum is its min-content width, which for German is the longest
   compound in the question, and that width becomes the flex line's, and the
   line's becomes the document's. Measured at a 16 px default with one real
   compound in the first FAQ question: 320 -> 384 px, 375 -> 384 px.

   overflow-wrap does not reach this. Intrinsic sizing runs before line
   breaking, so the reset breaks the word inside a box that is already too
   wide — the trap foundations/layout.html#intrinsic-minimum spells out in a
   note of its own.

   As a grid the same two children take the same two ends, and the question's
   track carries the floor the system has written twenty-eight times:
   minmax(0, 1fr). Same picture, one declaration different, and the component
   is now inside scripts/check-grid-tracks.py's reach instead of beside it. */
.cf-accordion__summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-6);
  /* 12 px block, not 16. The row then measures 48 px — 12 + 12 + the 23.2 px
     line box + the 1 px divider — which is what the mockup draws (six FAQ rows
     at exactly 48 px, measured off landing-page.jpg) and what this component's
     own Anatomy table has said all along. At 16 px it shipped 56.19 px: half a
     rung off the 8-point grid every other fixed measure in the system lands on.

     Right padding is 0 on purpose. The arrow is a mark on an axis, not text:
     the mockup lands its right edge on the same x as the end of the row's
     hairline, on every row. 16 px of padding held it 17 px short of that axis.
     .cf-accordion__content keeps its right padding — that is prose, and prose
     stopping at the container edge is a different question from a glyph
     aligning to it. */
  padding: var(--space-3) 0 var(--space-3) var(--space-20);
  list-style: none;
  cursor: pointer;
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  /* Not glass: these rows sit on the page wash, and frosted glass over a flat
     surface is only a grey box. Light crossing the row is the honest version.

     Declared here rather than in a second .cf-accordion__summary block below.
     That block set `transition: background-position`, and the shorthand reset
     the `transition: background var(--duration-fast)` this rule used to carry
     — so the row read as a 120 ms background fade in the stylesheet and moved
     as a 480 ms travelling light on screen. Nothing rendered wrong; the two
     halves of one component simply disagreed about what it does. One block,
     one transition. */
  background-image: var(--sheen-panel);
  background-repeat: no-repeat;
  /* The band and its two endpoints are the material's, and are read rather
     than restated — the same move --glass-rim-band made for the lit rim, one
     material over. --sheen-park is the gradient's own transparent half; what
     makes it transparent is an equality between the band and the gradient's
     head, stated once beside the token and re-derived by claim 10. */
  background-size: var(--sheen-band) 100%;
  background-position: var(--sheen-park) 0;
  transition: background-position var(--duration-slow) var(--ease-standard);
}
.cf-accordion__summary::-webkit-details-marker { display: none; }
.cf-accordion__summary:hover,
.cf-accordion__summary:focus-visible { background-position: var(--sheen-cross) 0; }
.cf-accordion__item[open] .cf-accordion__summary .cf-arrow { transform: rotate(90deg); }

/* The panel slides open instead of jumping. interpolate-size is Chromium-only
   as of mid-2026, so everywhere else this block does not apply and the panel
   opens exactly as it does today — the fallback is the current behaviour, not
   a degraded one. Scoped to the accordion rather than :root so it does not
   quietly change every other auto-sized transition in the system. */
@supports (interpolate-size: allow-keywords) {
  @media (prefers-reduced-motion: no-preference) {
    .cf-accordion__item {
      interpolate-size: allow-keywords;
    }
    .cf-accordion__item::details-content {
      block-size: 0;
      overflow-y: clip;
      transition: block-size var(--duration-slow) var(--ease-standard),
                  content-visibility var(--duration-slow) allow-discrete;
    }
    .cf-accordion__item[open]::details-content { block-size: auto; }
  }
}
/* The one <div> in the tree that holds its own copy, so it is the one that
   carries base.css's net itself. The net is a list of elements that hold text
   and deliberately does not include div — see the note there for why a single
   inherited declaration on div or body would have been the worse fix — which
   makes this the other half of that decision rather than a stray property.
   → base.css, "THE LIST IS THE WHOLE NET"; scripts/check-wrap-net.py */
.cf-accordion__content {
  padding: 0 var(--space-4) var(--space-6) var(--space-20);
  max-width: 76ch;
  font-size: var(--text-md);
  color: var(--text-secondary);
  overflow-wrap: break-word;
}
@media (max-width: 48.75rem) {
  .cf-accordion__summary { padding-left: var(--space-4); font-size: var(--text-md); }
  .cf-accordion__content { padding-left: var(--space-4); }
}

/* --------------------------------------------------------------------------
   BLOG GRID — the subdivision layout
   The recursive square-subdivision grid from the manual, applied as an
   editorial layout: newest article is largest, older ones halve down to
   a title-only list. Anchored by a mono timeline axis.
   -------------------------------------------------------------------------- */
/* Built on the .subdivide primitive in base.css — this rule only adds the
   contours and the rank each column carries. The even form is what the
   manual's application plate shows: equal columns, hierarchy from the rows.

   Two edges on the container, the other two on the cells — same construction as
   .cf-team-grid. Bordering all four sides here would lay a second hairline over
   the right and bottom edges and the grid would read 2 px there and 1 px on the
   other two. */
.cf-blog-grid {
  border-top: var(--stroke-1) solid var(--border-strong);
  border-left: var(--stroke-1) solid var(--border-strong);
}
.cf-blog-grid > * {
  border-right: var(--stroke-1) solid var(--border-strong);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
.cf-blog-col--1 { --rank: 1; }
.cf-blog-col--2 { --rank: 2; }
.cf-blog-col--3 { --rank: 3; }
.cf-blog-col--6 { --rank: 6; }

/* THE PORT — the archive's own form of the grid, where a column is a year and
   the year is longer than the box.
   ------------------------------------------------------------------------
   The grid used to be as tall as its content and the content was capped at
   eighteen cards: post nineteen went to a second page under a tick naming the
   same year as the first. A year does not end because a page does. So the
   archive's grid is a fixed-height port and each column scrolls its own year;
   nothing is paged away, and the pagination under it went with the pages.

   36.5rem / 584 px is twice what this grid drew. Measured on news.html at 1440
   and 1920: 293.66 px, six compact rows stretched to 48.6 each — the six-row
   tail column set the height, because it was the longest. Twice that is 587.3,
   and 584 is the number on the 4 px unit under it.

   AND IT IS DELIBERATELY NOT A WHOLE NUMBER OF ROWS. A compact row is 40.59 px
   of its own accord — 12 + 12 of padding around one line of --text-sm, measured
   identically at 1024, 1280, 1440 and 1920, because none of those three numbers
   is fluid. The port's own two hairlines take 2 px of the 584, and the 582 left
   hold fourteen rows and 14.7 px of the fifteenth: a year with
   more posts than fit always shows one cut off by the bottom edge, which is the
   only thing on a static page that says "keep going" before anyone has touched
   it or hovered anything. A port ending flush on a hairline reads as a list
   that has ended, and the reader is not told twice.

   Only the archive wears it. The Weiterlesen grid under an article is the same
   component at three columns and three cards — a port there is a fixed height
   with air under it. → scripts/build-news.py */
.cf-blog-grid--port { height: 36.5rem; }
.cf-blog-grid--port .cf-blog-col {
  overflow-y: auto;
  /* A grid item's automatic minimum size is its content: without this the
     column refuses to be shorter than its cards and the port never clips, so
     nothing ever scrolls and the grid simply grows past its own height. */
  min-height: 0;
  /* The rank's equal 1fr rows are what fills a content-sized column. In a port
     they would divide 576 px by six and hand each single-line headline 96 px of
     box to float in, so the rows are the cards' own heights instead and the
     column fills from the top. What is left over at the bottom of a thin year
     is that year being thin — which is what an archive looks like. */
  grid-template-rows: none;
  grid-auto-rows: min-content;
  align-content: start;
}
/* Except the lead, which is one card and takes the whole port: its picture is
   written to absorb the slack, and this is the cell the plate draws large. */
.cf-blog-grid--port .cf-blog-col--1 { grid-auto-rows: minmax(0, 1fr); }

/* FOLDED, THERE IS NO PORT. Below the fold every column spans the whole grid
   and they stack — a fixed height there would put the entire archive inside one
   576 px box nested in the page's own scroll, which is the pattern that takes a
   thumb hostage. The two queries are base.css's two fold widths, read from the
   same place and for the same reason: 44rem is the primitive's own number and
   56rem is the one five columns take with .subdivide--late. */
@container (max-width: 44rem) {
  .cf-blog-grid--port { height: auto; }
  .cf-blog-grid--port .cf-blog-col { overflow: visible; }
}
@container (max-width: 56rem) {
  .subdivide--late .cf-blog-grid--port { height: auto; }
  .subdivide--late .cf-blog-grid--port .cf-blog-col { overflow: visible; }
}

/* THE PORT CROPPED THE FOCUS RING OFF EVERY CARD IN IT, ON THE ONE AXIS IT
   NEVER SCROLLS.

   `overflow-y: auto` above is one axis of intent and two axes of effect: CSS
   Overflow 3 does not let a box scroll on one axis and overflow visibly on the
   other, so `overflow-x` computes from `visible` to `auto` and the column
   becomes a scrollport horizontally as well. Measured on news.html at 1280,
   both columns: scrollWidth 568 = clientWidth 568, scrollWidth 569 =
   clientWidth 569. Nothing ever scrolls sideways — the horizontal scrollport
   is pure cost, and what it costs is the ring.

   A card is the column's whole width, edge to edge: the column has no padding
   and the card no margin. base.css draws the ring at `outline-offset: 2px`, so
   all 4 px of it are outside the card's border box and therefore outside the
   scrollport's padding box on both sides. Measured with the card focused: left
   edge cut by 4 px of 4, right by 3 of 4 — the whole indicator, on every card.
   The lead loses all four sides, because its column is one card tall and its
   block axis has nothing to scroll either. 130 links across the eight archive
   pages and this component's own page, and a keyboard reader gets no ring on
   any of them. WCAG 2.4.7.

   Padding on the column cannot buy the room back — the card is sized by the
   column and would shrink with it — and no overflow value gives one axis a
   scrollport and the other a bleed: `overflow-y: auto` forces a specified
   `clip` on x to compute to `hidden`, which crops just the same and takes
   `overflow-clip-margin` with it.

   SO THE RING TURNS INWARD, which is this system's answer already: .cf-team
   -strip does exactly this, for exactly this reason, where the strip's own
   full-bleed box put two of the ring's four sides off the screen. That note
   says nothing else in the system needs it. This is the second thing, and the
   difference is worth naming: the strip is cropped by the viewport, these
   cards by a scrollport their own component declared.

   AND IT IS TWO-TONE, BECAUSE THE EDGE IT LANDS ON IS A PHOTOGRAPH.
   `.cf-blog-card--media` is `padding: 0` and its picture is `object-fit:
   cover` across the full cell, so an inward ring's outer band sits on a news
   photo — content, not a surface this stylesheet owns or can compute. That is
   the case .cf-btn--glass derives over the hero video and the case
   check-focus-ring-edge.py registers: no single tone clears 3:1 against an
   arbitrary image, and two tones whose own boundary is internal to the
   indicator do. Same pair and the same order as the glass button — lime
   outside where the artwork is, black inside where it cannot reach, 18.51:1
   between them — and here the lime band lands against the grid's own black
   hairline as well, which is the second reason for that order: black on black
   reads as the rule having thickened, which is the failure .cf-team-strip
   names.

   The inner tone is a pseudo-element and not `box-shadow: inset`, which is
   what the glass button uses. An inset shadow paints above the background and
   below the content, and on a --media card the content is the picture: the
   shadow would be under it and invisible on exactly the cards that need it.
   `::after` paints above, at the same two bands. */
.cf-blog-grid--port .cf-blog-card { position: relative; }
.cf-blog-grid--port .cf-blog-card:focus-visible {
  outline-color: var(--cf-lime);
  outline-offset: calc(var(--stroke-2) * -1);
}
.cf-blog-grid--port .cf-blog-card:focus-visible::after {
  content: "";
  position: absolute;
  inset: var(--stroke-2);
  border: var(--stroke-2) solid var(--cf-schwarz);
  pointer-events: none;
}
/* Folded, the crop is released — the two queries above say so — so the ring
   goes back outside with it, and a card in a folded port indicates exactly as
   the same card does in the Weiterlesen grid under an article, which is the
   same component without the port. The inward ring is a consequence of the
   scrollport, so it lives and dies with it rather than with the modifier. */
@container (max-width: 44rem) {
  .cf-blog-grid--port .cf-blog-card:focus-visible {
    outline-color: var(--focus-ring);
    outline-offset: 2px;
  }
  .cf-blog-grid--port .cf-blog-card:focus-visible::after { content: none; }
}
@container (max-width: 56rem) {
  .subdivide--late .cf-blog-grid--port .cf-blog-card:focus-visible {
    outline-color: var(--focus-ring);
    outline-offset: 2px;
  }
  .subdivide--late .cf-blog-grid--port .cf-blog-card:focus-visible::after {
    content: none;
  }
}
/* ASPECT-RATIO IS NOT A HEIGHT, THE ONE TIME THAT DIFFERENCE COSTS TEN SHEETS.
   .cf-blog-card__image is `aspect-ratio: 3 / 2; flex: 1 1 auto` a few rules
   down — a size that depends on the flex container's own final size, which on
   paper is exactly the box the fragmenter is still trying to solve. Chromium's
   print pass treats that circularity as "cannot be less than the rest of the
   page": every `.cf-blog-card` this port holds is `break-inside: avoid`
   (below, the print law's own register), and rather than measuring the aspect-
   ratio picture at its resolved size and testing whether the card fits in
   what is left of the sheet, it gives the card the whole remaining page and
   starts the next one fresh.

   Measured on the built news.html, ten posts, page.pdf() through Chromium:

     unmodified                 11 sheets, one post per sheet, ink covering
                                 roughly a third of each — the other two thirds
                                 are the .cf-blog-grid border's own left rule,
                                 drawn down the blank rest because nothing
                                 after the first post ever shares its page
     height fixed, ratio auto    5 sheets, three to four posts a page

   Proven by isolation, not just observation: dropping the images entirely
   (image `display: none`, avoid rule untouched) also collapsed the archive to
   two sheets, and restoring them at a plain fixed `height` — no aspect-ratio,
   nothing else touched — reproduced the five-sheet result on its own. The
   `flex: 1 1 auto` was not the trigger; measured with it left alone and only
   `aspect-ratio` and `height` overridden here, the count did not move.
   `aspect-ratio` on a flex item is the one property in this rule that cannot
   be resolved without first knowing the box it sits in, and a fragmenter
   choosing a page boundary is asking the same question the property is
   waiting on.

   200px / 12.5rem is not the screen's 3 / 2 — at the port's own column widths
   (roughly 330–390 px folded, up to the full measure for the lead) 3 / 2 would
   ask for 220–520 px per picture. 200 keeps every picture legible under
   object-fit: cover while holding three ordinary rows plus a lead to a sheet,
   which is what bought the count above. Nothing here touches the screen rule:
   this is scoped to print and does not change `aspect-ratio: 3 / 2` outside
   it. → scripts/check-print-law.py, .cf-blog-card's break-inside a few rules
   down */
@media print {
  .cf-blog-grid--port .cf-blog-card__image { aspect-ratio: auto; height: 12.5rem; }
}

.cf-blog-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-4);
  min-width: 0;
  min-height: 4rem;
  padding: var(--space-3);
  border-bottom: var(--stroke-1) solid var(--border-subtle);
  /* Same light-across-a-panel treatment as the accordion row. These cells
     are contour on the page wash — there is nothing complex underneath for
     glass to calm, so glass would be dishonest here. */
  background-image: var(--sheen-panel);
  background-repeat: no-repeat;
  background-size: var(--sheen-band) 100%;
  background-position: var(--sheen-park) 0;
  transition: background-position var(--duration-slow) var(--ease-standard);
}
/* The last card in a column would otherwise draw its own hairline 0 px above
   the column's, doubling the grid's bottom edge. */
.cf-blog-card:last-child { border-bottom: 0; }
.cf-blog-card:hover,
.cf-blog-card:focus-visible,
.cf-blog-card:focus-within { background-position: var(--sheen-cross) 0; }
.cf-blog-card__title {
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
}
.cf-blog-card--lead .cf-blog-card__title { font-size: var(--text-lg); }
.cf-blog-card__meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--text-secondary);
}
/* AN ENTRY WITH NO ARTICLE BEHIND IT. The archive lists what has been
   published; scripts/build-articles.py writes a page for every post that
   carries text, and build-news.py draws the rest as a <span> rather than as a
   link to somebody else's article. Every rule above is written for the class
   and not for <a>, so the row keeps its geometry, its hairline and its type.
   What it drops is the light that travels across a card on hover: that light
   is the affordance, and there is nowhere for this one to arrive. */
.cf-blog-card--listing { background-image: none; }

/* THE TITLE PICTURE. A post names one — `bild:` in its file, the Titelbild
   property in Notion — and scripts/build-news.py draws it on the cards that
   have room for one: the lead and the two- and three-row columns. The single
   line rows carry none, and that is the archive's own argument about getting
   terser to the right rather than an omission: a 2rem row would paint a 48 px
   stamp out of a file sized for a 1008 px plate, which is bytes a reader pays
   for a picture nobody can read. → scripts/check-image-scale.py

   No alt text, by construction. The picture sits inside the link whose text is
   the headline; a description of the photograph would have a screen reader
   read every card twice. → design-system/components/blog-grid.html */
.cf-blog-card__image {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  /* The picture takes the slack the text does not need. Without it the card's
     justify-content: space-between spreads three items down a tall lead cell
     and the headline floats in the middle of it; with it the cell is a
     photograph with a headline under it, which is what the plate draws. */
  flex: 1 1 auto;
  /* A flex item's automatic minimum size is its content, and an image's content
     is its intrinsic height — without this the picture cannot shrink below
     1067 px and every card in the column grows to fit it. */
  min-height: 0;
}
/* A card that carries a picture pays its padding on the text instead of on the
   box, so the photograph meets the cell's own hairline rather than floating in
   a 12 px frame. The gap keeps the air between picture and headline. */
.cf-blog-card--media { padding: 0; }
.cf-blog-card--media > .cf-blog-card__title { padding-inline: var(--space-3); }
.cf-blog-card--media > .cf-blog-card__meta { padding: 0 var(--space-3) var(--space-3); }

/* Older entries collapse to a single line — the tail of the subdivision. */
.cf-blog-card--compact {
  flex-direction: row;
  align-items: center;
  min-height: 2rem;
}
/* The title is nowrap + ellipsis, so without min-width: 0 the card's
   min-content width is the full untruncated headline and the whole grid
   refuses to shrink below it. This is what pushed the landing page 17 px
   sideways at 375 px. */
.cf-blog-card--compact .cf-blog-card__title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The axis carries no tracks of its own — it is a .subdivide__row and adopts
   the grid's. See base.css for why that is structural rather than a tidy-up:
   a repeat(5, …) here was a second, independent declaration of the same track
   set, and blog-artikel.html already runs the grid at three.

   No compensation for the grid's 1 px left contour, deliberately. A subgrid
   subtracts its own border from its FIRST track only; every line after that
   still falls on the parent's. So ticks 2..n stand exactly on the rules they
   name, and tick 1 stands on the grid's left edge rather than 1 px inside it —
   which is the plate: the label sits against the rule, not against the cell. */
.cf-blog-axis__tick {
  padding-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  /* The mono label in this system is four properties, not three, and this was
     the one axis label keeping only the first three. Mono + 11 px +
     --tracking-label is the recipe thirty-two selectors share and every one of
     them that carries WORDS carries the uppercase too — the section header's
     own label and counter six rules above this one included, which is why the
     Blog section read "BLOG / 189 BEITRÄGE" over a tick reading "Aktuell".
     The plate settles it as well as the family does: on
     mockups/landing-page.jpg the first tick is drawn AKTUELL. The four ticks
     beside it are years, so nothing about them could ever have shown the gap.

     The three mono labels that legitimately stay mixed-case are the ones whose
     content is not a label — .cf-blog-card__meta is a person's name,
     .cf-result__path is a URL, .cf-consent__meta is a sentence. A year band is
     none of those: it is the axis naming its own column. */
  text-transform: uppercase;
  color: var(--text-secondary);
  writing-mode: vertical-rl;
  /* A tick labels the column it starts, so it stands on that column's leading
     rule — which is where the application plate draws every one of them.
     Without this it stands on the TRAILING rule, and reads as labelling the
     column to its right: "Aktuell" ends up over the 2026 boundary. The cause
     is vertical-rl, not the grid. Block direction is right-to-left there, so a
     stretched tick starts its text at the right edge of its own box, and a grid
     item stretches by default. justify-self: start shrinks the box onto the
     leading edge; the text still starts at that box's right edge, which is now
     one glyph-height inside the column instead of a whole column away. */
  justify-self: start;
}
/* The grid folds itself (see .subdivide in base.css); the axis is one of its
   rows, so it folds on the same element's width by construction. */
@container (max-width: 44rem) {
  .cf-blog-axis { display: none; }
}
/* AND IT FOLDS ON THE SAME ELEMENT'S WIDTH IS ONLY HALF OF IT — it has to fold
   at the same NUMBER too. .subdivide--late moves a five-column grid's fold to
   56rem (base.css carries the measurement); left out of this rule the axis
   would keep the 44rem above and, between the two, draw five vertical year
   ticks under a grid that had stacked into one column — a timeline naming
   columns that are not there. scripts/check-subdivide-fold.py holds the two
   preludes to each other so the next move of either one cannot orphan it. */
@container (max-width: 56rem) {
  .subdivide--late .cf-blog-axis { display: none; }
}

/* --------------------------------------------------------------------------
   PAGINATION
   The closing bracket of a listing, and deliberately the section header read
   backwards. The header opens a section with a mono label on the left, a
   counter on the right and a rule underneath; the pagination closes it with
   the rule on top, the counter on the left and the controls on the right.
   Same three parts, same order, mirrored — so a listing is framed by one
   shape rather than by two unrelated ones.

   Four decisions worth their comment:

   1. THE CURRENT PAGE IS NOT A LINK. It is a <span aria-current="page">.
      A link to where you already are is a target that does nothing, and
      taking it out of the tab order is the difference between eleven stops
      and ten. This is also why the current slot needs no :hover — it cannot
      be one.

   2. NO CONTOUR AT REST. Contour before fill governs how an OBJECT is drawn,
      and at rest a page number is type, not an object. The contour arrives on
      hover and focus, which is also the only thing that can tell you the
      target is larger than the glyph — the slots are 44 px and the numerals
      are 11 px, well past WCAG 2.2 SC 2.5.8's 24 px floor.

      The one slot drawn as an object at every moment is the current one, and
      it is drawn the way this system draws "this is the one": an opaque black
      plate, the same move as .cf-btn--solid. Not lime. Lime is light and there
      is one lime moment per screen, which on a listing page belongs to the
      call to action, not to a number.

   3. THE NUMBERS LEAVE BEFORE THE ROW WRAPS. Under 34rem every slot except
      the current one is hidden and Zurück/Weiter carry the component, with the
      status line saying where you are. A pagination that has wrapped onto
      three lines has stopped being one control. The container query measures
      the component, not the viewport, so it also folds correctly in a column.

   4. THE STATUS LINE IS NOT DECORATION. "Seite 1 von 11" is the one part that
      states position without asking anyone to infer it from which numeral is
      filled — which is what the ellipsis costs you. It carries no ARIA: it is
      the first thing inside the labelled <nav>, so it is read on entry.
   -------------------------------------------------------------------------- */
.cf-pagination {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-6);
  margin-top: var(--space-8);
  padding-top: var(--space-4);
  border-top: var(--stroke-1) solid var(--border-strong);
}
/* The margin is the component's, not the page's, for the reason
   .cf-footer--detached carries its own: the distance belongs to the shape and
   every page wants the same one. It also stops a rule of its own from landing
   on top of somebody else's. patterns/news.html is the case that found this —
   its blog timeline axis separates the grid from the pagination at desktop, so
   the pagination keeps its rule; below the fold the axis is display: none and
   without this margin the two hairlines would meet and read 2 px. */
/* Same contract as .cf-section-header--flush, the other way up: the ruled
   container ABOVE — a blog grid, a table — already draws this hairline with
   its own bottom border, so drawing a second one 0 px below it would read
   2 px there and 1 px everywhere else. The air stays; only the rule goes.

   IMMEDIATELY above, though. Anything in between — patterns/news.html puts
   the blog timeline axis there — and --flush deletes the only rule the
   component had, because the one it was deferring to is no longer adjacent.
   Then the pagination floats. If something sits between, do not use it.

   Flush also drops the margin: it is not standing off the container above, it
   is continuing it. */
.cf-pagination--flush { margin-top: 0; border-top: 0; }

.cf-pagination__status {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* margin-inline-start: auto rather than relying on the parent's
   space-between — without a status line there is only one flex child, and
   space-between would park it on the left.

   And it never once did that, because base.css:150's `margin: 0` is (0,1,1)
   and this rule was (0,1,0): the auto margin computed to 0 px, so the case
   this comment exists for — a pagination with no status line — parked on the
   left exactly as if the declaration had not been written. The two-child case
   looked right the whole time on the parent's space-between, which is why the
   comment describing the fallback outlived the fix. It is the same reset
   winning against `margin-inline: auto` that .cf-pin__inner's note records,
   one component over. → scripts/check-reset-shadow.py */
ul.cf-pagination__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  margin: 0;
  margin-inline-start: auto;
  padding: 0;
  list-style: none;
}

.cf-pagination__page,
.cf-pagination__step,
.cf-pagination__gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 2.75rem;
  border: var(--stroke-1) solid transparent;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* Tabular figures so the slots stop shuffling sideways between 1 and 11. */
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  transition: border-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}
.cf-pagination__page { min-width: 2.75rem; padding-inline: var(--space-2); }
.cf-pagination__step { padding-inline: var(--space-3); }
/* The gap is punctuation, not a target: no border box, no minimum width past
   the glyph, and aria-hidden in the markup. */
.cf-pagination__gap  { min-width: 1.5rem; border: 0; }

/* The `a.` prefix is what keeps the current page out of this: it is a <span>,
   so neither state can ever land on the slot that already carries the plate. */
a.cf-pagination__page:hover,
a.cf-pagination__page:focus-visible,
a.cf-pagination__step:hover,
a.cf-pagination__step:focus-visible {
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.cf-pagination__page[aria-current] {
  background: var(--surface-inverse);
  border-color: var(--surface-inverse);
  color: var(--grey-000);
}

/* The chevron leans the way it points, the same 240 ms nudge .cf-btn gives
   the arrow. Transform only — nothing here reflows. */
.cf-pagination__step .cf-icon {
  transition: transform var(--duration-base) var(--ease-standard);
}
a.cf-pagination__step--prev:hover .cf-icon,
a.cf-pagination__step--prev:focus-visible .cf-icon { transform: translateX(-3px); }
a.cf-pagination__step--next:hover .cf-icon,
a.cf-pagination__step--next:focus-visible .cf-icon { transform: translateX(3px); }

@container (max-width: 34rem) {
  .cf-pagination__page:not([aria-current]),
  .cf-pagination__gap { display: none; }
}

/* The plate is the whole state here, and forced-colours throws backgrounds
   away. Highlight/HighlightText is the pair the mode reserves for exactly
   this — "the selected one of a set" — and it is the pair below, but only
   because the element is first taken out of the mode's own repaint.

   THE INK IS NOT LAID ON THE PLATE. IT IS LAID ON A BACKPLATE THE COLOUR OF
   THE PAGE. That one fact is behind every failure this comment has recorded,
   and it is the fact the earlier readings were missing. Chromium paints an
   opaque Canvas-coloured rectangle behind a run of text in forced-colours
   mode — the guarantee that text over an image stays readable — and it lands
   ON TOP of whatever background the element declared. So the plate has to
   clear Canvas and the ink has to clear Canvas, separately, and a matching
   system pair answers neither question: every pair is built to contrast with
   its own plate, and its own plate is not what the glyphs land on.

   ON TOP OF WHICH ELEMENT'S BACKGROUND IS THE PART THAT DECIDES WHO IS HIT.
   The backplate belongs to the BLOCK CONTAINER that lays the text out, and it
   is painted in that container's own background phase — so it goes down before
   the inline backgrounds inside it and after its own. An inline <mark> in a
   paragraph is not that container: the paragraph's backplate goes down first
   and the mark's plate covers it, which is why the found state's element rungs
   render correctly in both palettes and this slot does not. This slot is an
   inline-flex box, so it IS its own block container and the backplate lands on
   the plate the state is made of. Measured across the structural cases at
   base.css's found state, where the same mark inline and blockified are two
   different results.

   Measured by render and not by computed style, which cannot see any of this:
   Chromium 149 and 151 driven by playwright-core, forced-colors emulated
   active against `color-scheme` light and dark, this page served over http,
   the current slot screenshotted and its pixels counted. The two builds agree
   to the byte. Shares are of the slot's own box:

                          light                      dark
     ButtonFace/Text      98 % #ffffff               98 % #000000
     Highlight/Text       81 % #37336d, 8 % #ffffff  81 % #00b8cc, 8 % #000000
     Mark/MarkText        89 % #ffff00, 6 % #ffffff  89 % #ffff00, 8 % #000000

   The second figure in each cell is the backplate, and it is the page's own
   colour in every one of them. Read across it and the three findings this file
   has accumulated all fall out of the one mechanic:

   HIGHLIGHTTEXT IS CANVAS. White under the light palette, black under the dark
   one — the same value as the backplate it is printed on, both times. The
   glyphs are painted and then cannot be told from what is behind them, which is
   what the note here used to call "HighlightText does not paint".

   MARKTEXT IS BLACK IN BOTH, because Mark and MarkText hold the same two values
   in either scheme rather than flipping with it. On the light backplate that is
   legible; on the dark one it is black on black. Hence a failure only under the
   dark theme, which is how it shipped: verified once, in the palette that
   works.

   AND BUTTONFACE IS CANVAS IN BOTH, so the pair that was put here to escape
   those two draws no state whatsoever. 98 % of the slot is the page's own
   colour, the border went with it because it was set to ButtonFace as well, and
   the current page renders as a bare numeral between two links that keep their
   boxes. ButtonText against the backplate is legible in both palettes and that
   is exactly what was measured and recorded. Legible was true. Selected was
   never asked.

   FORCED-COLOR-ADJUST: NONE IS THE ESCAPE, AND IT DOES NOT LEAVE THE PALETTE.
   It stops the backplate and the repaint for this element alone; the values
   below stay system colours, so the user's theme still chooses them — the plate
   resolves to #37336d under the light palette and #00b8cc under the dark one,
   flipping with their scheme and not with ours. Rendered the same way: 97 % of
   the slot's interior is plate, and the numeral paints in HighlightText on it —
   17 px of ink clearing 4.5:1 against the plate in light, 14 px in dark, where
   the same pair under the mode's own repaint put a backplate there and no ink
   at all. The plate stands 11.31:1 off the page in light and 8.73:1 in dark.
   It is also the only answer here that does not depend on the backplate
   existing — the platform guarantees Highlight against HighlightText — so it
   still draws in an engine that paints no backplate of its own, which an ink
   of CanvasText on this plate would not.

   It also puts this rule back beside the other one that means it. .cf-annot--
   lit::before fills with Highlight under forced colours for "the selected one
   of a set", and says so pointing here — a claim that was false for as long as
   this slot was a button face. That rule needs no escape and this one does,
   which is the whole of what the escape is for: the annot rung is a pseudo-
   element with no text in it, so it has a plate to draw and no ink to lose.

   Mark/MarkText survives the same escape and is still not taken: it is the
   found state's pair (base.css, .cf-mark--current, and the calendar's lit day),
   "currently matched" is not "currently selected", and yellow stands 1.07:1 off
   a white page — a hue with no luminance under it.

   The Mark row of the table above is this element's measurement and not the
   found state's, and that distinction has since been settled by render. The
   found state's two ELEMENT rungs never failed: an inline <mark> keeps its
   plate, in both palettes, for the reason two paragraphs up. What did fail is
   the calendar's lit day, which is a flex item and therefore this element's
   shape rather than a <mark>'s; it takes the same escape now, and so do
   base.css's element rungs, where the escape changes nothing that ships and
   covers the day a match is blockified. The half that is still open is the
   found state's three highlight PSEUDO-elements, which take no author
   declaration under forced colours at all — measured against three candidate
   declarations, byte-identical every time. base.css records it. */
@media (forced-colors: active) {
  .cf-pagination__page[aria-current] {
    forced-color-adjust: none;
    background: Highlight;
    border-color: Highlight;
    color: HighlightText;
    -webkit-text-fill-color: HighlightText;
  }
}

/* --------------------------------------------------------------------------
   ERROR & EMPTY STATE
   The other terminal state of a listing, which is why it sits next to the
   pagination: one closes a page that has content, this one stands in for a
   page that has none. Three cases, one shape:

     404 / 410  the address resolves to nothing        patterns/404.html
     500        the address resolves and the server failed
     empty      the address is fine and the filter matched nothing

   Four decisions worth their comment:

   1. THE STATUS CODE IS A LABEL, NOT THE HEADLINE. A 96 px "404" is the one
      thing every error page does, and it says nothing to the person who
      cannot find the page — the code is for whoever reads the logs. So the
      code goes in the mono label slot the whole system already uses for
      "what kind of thing is this", and the headline is a sentence in plain
      German. USWDS says the same thing from the content side: concise,
      non-technical, apologetic, never funny.

   2. THE WAY OUT IS A ROW, NOT A CARD. A dead end needs somewhere to go, and
      the routes list is that somewhere — hairline rows, mono label, one line
      of copy, an arrow. Contour before fill: at rest a route is two rules and
      some type, and the only thing that moves on hover is the arrow and the
      ink. Rows also stack without a media query, which a card grid does not.

   3. NO LIME DOWN HERE. On the 404 page the screen's one lime moment is the
      lit socket in the header figure — the place the missing object should be.
      So the primary action is .cf-btn--solid, the black plate whose label is
      the foil, the same "this is the one" move the pagination's current page
      makes. Reach for .cf-btn--primary here only on a page that carries no
      figure.

   4. THE BLOCK IS NOT THE PAGE. .cf-error is a block of copy and actions; the
      page furniture around it — nav, page header, footer — is the ordinary
      furniture, deliberately. An error page that does not look like the site
      is a second dead end, and on a German site the footer is also what keeps
      the Impressum two clicks away from every address, including the ones
      that do not exist (DDG § 5).
   -------------------------------------------------------------------------- */
/* No max-width on the block. Every part that is read as prose carries its own
   measure — the title 26 ch, the body 56 ch — and capping the block on top of
   that would cap the ROUTES too, which are not prose: they are a 12 rem label
   column beside a description, and 66 ch is narrower than the 34 rem their
   two-column form needs. A cap here would mean a routes list that can only
   ever be seen folded. */
.cf-error { container-type: inline-size; }

/* The page case. It deliberately does NOT sit under a .cf-page-header: an
   error page has no title/rule/meta triple to draw, because the error IS the
   title, and there is no long headline for an object to sit behind. So the
   object comes out from behind the type and stands beside it, on a grid that
   folds on its own — where the page header's figure is absolute and has to be
   switched off under 900 px. The full-ink rule that opens the routes list is
   the page's hairline; it is drawn once, by the thing it belongs to. */
.cf-error--page {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-12);
  align-items: center;
}
.cf-error--page .cf-error__title  { font-size: var(--text-display-1); }
.cf-error--page .cf-error__routes { margin-top: 0; }

/* THE ONLY SHIPPING COMPONENT THAT ASKED THE VIEWPORT, and the comment above
   already said it should not: "a grid that folds on its own" is the container
   claim, written over a `@media (min-width: 62rem)`. The register's rule is
   that only page furniture pinned to the viewport — nav, hero, footer, consent
   — may ask the viewport, and an error block is none of those. It also went
   unregistered: 62rem appeared exactly once in the whole repository and in
   neither copy of the breakpoint register.

   Folding it onto 56rem REMOVES a threshold rather than converting one,
   because the two are 0.82rem apart. A size query measures the CONTENT box,
   so at the viewport 992 where 62rem fired, a .container inside it is
   992 − 2 × 5.5vw = 882.9 px = 55.18rem. That is the two-column threshold in
   all but name, reached 15 px of viewport earlier. Snapping to the real one
   moves the split from a viewport of 992 to about 1007 — later, so the
   two-column state is 13 px roomier when it arrives, never tighter.

   Unnamed on purpose, matching .cf-statement and .cf-process, the other two
   members of the 56rem family: it measures the nearest ancestor container, so
   the same block folded into a column folds at the width the column gives it.
   It cannot resolve against .cf-error's own container-type — an element is
   never its own query container — so this asks the .container it sits in,
   which is what the media query was reaching for the long way round. */
@container (min-width: 56rem) {
  .cf-error--page {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--grid-gap);
  }
  .cf-error--page .cf-error__routes { grid-column: 1 / -1; }
}

/* Sized off the copy beside it rather than off the viewport, and centred when
   it drops under the copy so a 640-unit drawing does not sit hard left in a
   column it is twice as short as. */
.cf-error__figure {
  display: block;
  width: 100%;
  max-width: 34rem;
  margin-inline: auto;
}

.cf-error__code {
  margin: 0 0 var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* Never --text-muted: #919191 on the page wash is 2.0:1. → colours */
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The last id-bearing heading in the tree that sits in the flow, and the same
   clearance for the same reason — see .cf-info-card__title. */
.cf-error__title {
  margin: 0;
  max-width: var(--measure-tight);
  font-family: var(--font-display);
  font-size: var(--text-display-2);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  scroll-margin-top: calc(var(--nav-height) + var(--space-6));
}

.cf-error__body {
  margin: var(--space-6) 0 0;
  max-width: var(--measure);
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}

.cf-error__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-8);
}

/* The inline case: a listing whose filter matched nothing. It sits INSIDE the
   ruled container the results would have filled, so it draws no rule of its
   own — the container's edges are already the box — and it drops to the
   heading size the section it interrupts is set in. It is never an <h1>:
   the page has a title and it is still correct. */
.cf-error--inline { padding: var(--space-12) 0; }
.cf-error--inline .cf-error__title { font-size: var(--text-h3); }
.cf-error--inline .cf-error__body  { margin-top: var(--space-4); }
.cf-error--inline .cf-error__actions { margin-top: var(--space-6); }

/* The routes.
   A ruled list, drawn the way the accordion is drawn: one rule on top and one
   under every row, so a run of them reads as a single ruled block rather than
   as n boxes. The rules are --border-default rather than full ink because
   there are up to five of them in a column and five black hairlines is a
   fence; the row that is being pointed at gets the ink instead. */
.cf-error__routes { margin-top: var(--space-12); }

.cf-error__routes-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: var(--stroke-1) solid var(--border-strong);
}

.cf-error__route {
  display: grid;
  grid-template-columns: 12rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-2) var(--space-6);
  padding: var(--space-5) 0;
  border-bottom: var(--stroke-1) solid var(--border-default);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.cf-error__route:hover,
.cf-error__route:focus-visible { border-bottom-color: var(--border-strong); }

.cf-error__route-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
}
.cf-error__route-desc {
  /* The description sits in the row's minmax(0, 1fr) track, and a 1fr track
     has no opinion about how long a line should be — it takes whatever the
     column has. On this page that is the full 1280 content column, so the
     track grew with the window and the copy grew with it:

         viewport   375     768    1024    1280    1920
         before    37.6    54.8    84.0   113.3   131.4   characters per line
         after     37.6    54.8    56.0    56.0    56.0

     The longest of the five descriptions is 94 characters, so above 1024 they
     stopped wrapping altogether and each route read as one 94-character line —
     the failure is not that the line is ugly, it is that there is no second
     line for the eye to return to. Below 1024 nothing changes: 54.8 is already
     inside the band, so the phone and the tablet render exactly as before.

     The cap is on the copy, not on the track. The arrow stays pinned to the
     right edge of the row, because the row is the target and the arrow marks
     where it ends — that is the same full-bleed row .cf-blog-card--compact
     and .cf-vacancy draw, and it is not the measure's business. */
  max-inline-size: var(--measure);
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}
/* The arrow is the only thing that moves, and it moves the 4 px .cf-btn
   already moves it — the row is a link to somewhere else, so it points the
   same way and by the same amount. align-self keeps it on the label's line
   when the description wraps to three. */
.cf-error__route .cf-arrow {
  align-self: center;
  color: var(--text-secondary);
  transition: transform var(--duration-base) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}
.cf-error__route:hover .cf-arrow,
.cf-error__route:focus-visible .cf-arrow {
  transform: translateX(4px);
  color: var(--text-primary);
}
/* Both of those inks are named on the <svg>, so forced colours preserves them
   rather than mapping them — the trap the note beside .cf-icon--light sets out
   in full. Measured on patterns/404.html with the dark palette: the five rows
   and their labels came through in LinkText #FFFF00 and every arrow stayed
   rgb(72, 72, 72), 2.29:1 against Canvas, at the far right of a row that is
   otherwise the loudest thing on the page.

   `inherit` and not CanvasText, because the arrow belongs to the row: the row
   is the link, the arrow is where it ends, and taking the link's own ink is
   what every other .cf-arrow in the system already does by not naming one. The
   quiet-grey-to-black step is a second ink and this mode has one, so hover is
   listed here as well — it carries a higher specificity than the base rule and
   would otherwise reintroduce the escape on the row under the pointer. */
@media (forced-colors: active) {
  .cf-error__route .cf-arrow,
  .cf-error__route:hover .cf-arrow,
  .cf-error__route:focus-visible .cf-arrow { color: inherit; }
}

/* One fold, measured on the component rather than the viewport, so a routes
   list inside a column folds at the same width it folds at full bleed: below
   34rem the label sits above its description instead of beside it, and the
   arrow keeps the right edge. */
@container (max-width: 34rem) {
  .cf-error__route {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: var(--space-1);
  }
  .cf-error__route-label { grid-column: 1; }
  .cf-error__route-desc  { grid-column: 1; }
  .cf-error__route .cf-arrow { grid-column: 2; grid-row: 1 / -1; }
}

/* --------------------------------------------------------------------------
   SEARCH & RESULTS
   The surface the brand is named after. foundations/found.html drew the four
   registers a match can be marked in and shipped none of them on a page; this
   is the page.

   Two parts, and the second is the one with an argument in it:

     .cf-search    the control. A <form role="search"> around one .cf-field
                   and one button — no new field drawing, because a search box
                   is a text input and the system already has one.
     .cf-result    one answer. NOT a card: no box, no fill, no corner. It is a
                   block of type standing on a rule, which is the same claim
                   foundations/found.html makes about a marked word — an
                   isometric object has no bounding box, what places it is the
                   lattice edge under it. Every result carries its own bottom
                   rule INCLUDING the last one, so the register closes; the
                   interior rules are divisions and the closing one is an edge,
                   which is both of the sanctioned uses in
                   foundations/geometry.html#where-lines-go.

   No lime anywhere in here, and that is the design rather than an omission.
   A result set has no current match — the reader has not chosen one yet — so
   .cf-mark--current has nothing to attach to and every hit on this page is
   contour. The light is spent on the page the reader lands on, through the
   #:~:text= fragment each result link carries and ::target-text in base.css.
   Contour here, light there. → components/search.html#light
   -------------------------------------------------------------------------- */
.cf-search {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-4);
}
/* Wrap rather than a container query. The row has exactly two parts and the
   second one is a fixed width, so "does it still fit" is a question flex can
   answer on its own — and the breakpoint register in tokens.css is a list to
   add to deliberately, not one to grow every time two things sit in a row.
   16rem is the basis at which the field still holds a readable query. */
.cf-search__field { flex: 1 1 16rem; }
.cf-search__submit { flex: 0 0 auto; }

/* THE SHIPPED ANSWER IS NOT THE READER'S ANSWER, AND THIS IS THE ONE FRAME IT
   MUST NOT BE SEEN IN.

   patterns/suche.html ships a worked result set — six hits for "Telemetrie",
   which check-register-count.py holds to its own claim and which a reader with
   no scripting gets as an honest, if not personal, page. On a site with a
   server that is what the server would have replaced before sending. There is
   no server here, so assets/js/cf-search.js replaces it in the page, and
   between first paint and that replacement there is a window in which the
   register on screen answers a question nobody asked.

   The window is closed the way cf-nav.js closes its own: the script writes
   data-cf-search on <html> from the <head>, BEFORE the body is parsed, so this
   rule is already matching when the register is laid out. `live` is the script
   admitting it has not answered yet; `ready` is it having answered, and the
   rule stops applying with no second declaration to keep in step.

   Two properties, and the difference is what each part costs while hidden. The
   claim is one span on a two-span meta line, so it goes out by `visibility`
   and the line keeps its shape. The register is six results tall, so it goes
   out by `display` — reserving that height for a set that will be a different
   size is a hole in the page and then a jump, rather than one settle.

   With scripting off nothing writes the attribute, neither rule ever matches,
   and the page is the specimen. → components/search.html#behaviour */
html[data-cf-search="live"] [data-cf-search-claim] { visibility: hidden; }
html[data-cf-search="live"] [data-cf-search-region] { display: none; }

/* THE REGISTER HAS THREE CONSUMERS, AND IT IS DECLARED ONCE.
   .cf-vacancy is the open-position list on patterns/karriere.html and .cf-event
   is what stands on a day of .cf-calendar, and neither is a second drawing —
   both are this one. A register is a stack of type blocks standing on
   hairlines, and a job opening, a search hit and a date in a month are the same
   object at that level: a mono line, a linked title, a couple of sentences.
   So every rule below names all three, the way .cf-prose table names itself
   alongside .cf-table rather than being written out twice. One drawing, and
   it cannot drift.

   What the vacancy adds is the one part the other two have no use for: a <dl>
   of facts (Standort, Anstellung, Umfang, Start). That is declared on its own,
   below, and nothing else is. An event's own facts — where, and at what hour —
   are two short strings and go in the mono line the register already has, which
   is why the third consumer costs three words in seven selector lists and not
   one declaration. → components/vacancy.html, components/calendar.html */
.cf-results,
.cf-vacancies,
.cf-events {
  list-style: none;
  margin: 0;
  padding: 0;
  /* The top edge belongs to the section header's rule when the header carries
     --flush, exactly as .cf-blog-grid takes it. Same weight, same ink. */
  border-top: var(--stroke-1) solid var(--border-strong);
}

.cf-result,
.cf-vacancy,
.cf-event {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-block: var(--space-6);
  border-bottom: var(--stroke-1) solid var(--border-strong);
  /* The same light-across-a-panel move the blog card and the accordion row
     make. Contour on the page wash with nothing complex underneath, so glass
     would be dishonest here and a sheen is what is left.

     THE THIRD OF THE TOKEN'S THREE DRAWINGS, and for a long time the one
     foundations/materials.html had never heard of: its panel table named the
     other two and its prose closed "it is on nothing else in the system". It
     is honest by that chapter's own rule — light is a response, and every row
     of this register carries a link in its title, which is why the answer is
     on :focus-within and not on :focus-visible: the focus lands on the link
     inside the box, never on the box. The rule is now enforced rather than
     asserted; the table is held to whatever paints this token.
     → foundations/materials.html#panel-verdicts;
       scripts/check-glass-budget.py, claim 7 */
  background-image: var(--sheen-panel);
  background-repeat: no-repeat;
  background-size: var(--sheen-band) 100%;
  background-position: var(--sheen-park) 0;
  transition: background-position var(--duration-slow) var(--ease-standard);
}
.cf-result:hover,
.cf-result:focus-within,
.cf-vacancy:hover,
.cf-vacancy:focus-within,
.cf-event:hover,
.cf-event:focus-within { background-position: var(--sheen-cross) 0; }

/* The row is the container its own fold is measured in: the square below
   shrinks with the row and not with the viewport, so a register drawn inside a
   column folds at the same width it folds at full bleed — the argument
   .cf-pagination makes for its own slots. Results never fold, so this is the
   vacancy's alone. */
.cf-vacancy { container-type: inline-size; }

.cf-result__meta,
.cf-vacancy__meta,
.cf-event__meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* --text-secondary, not --text-muted: this label sits on the page wash and
     #919191 there is 2.0:1. Same rule every mono label on the wash follows. */
  color: var(--text-secondary);
}

.cf-result__title,
.cf-vacancy__title,
.cf-event__title {
  font-family: var(--font-display);
  font-size: var(--text-h4);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
}
/* The link is the title and only the title. A stretched ::after over the whole
   result would make the excerpt unselectable, and the excerpt is the part
   carrying the marks the reader came to read. */
.cf-result__link,
.cf-vacancy__link,
.cf-event__link {
  color: inherit;
  text-decoration: none;
}
.cf-result__link:hover,
.cf-result__link:focus-visible,
.cf-vacancy__link:hover,
.cf-vacancy__link:focus-visible,
.cf-event__link:hover,
.cf-event__link:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: var(--stroke-1);
}

.cf-result__excerpt,
.cf-vacancy__excerpt,
.cf-event__excerpt {
  max-inline-size: var(--measure-prose);
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}

/* The address, stated rather than implied. A result list that hides where each
   answer lives asks the reader to trust the ranking; this brand's whole claim
   is that things can be located. Truncated at the end, never in the middle:
   the host and the first segment are what the reader is checking. */
.cf-result__path {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--text-secondary);
}

/* The one part the register's other consumer has no use for: the facts of an
   opening. Standort, Anstellung, Umfang, Start — name/value pairs, which is
   what <dl> is for and what nothing else in HTML is. It is the drawing
   .cf-contact already makes, mono term over its value, laid along the row
   instead of down a column, because four short facts read as a strip and a
   four-row stack under every opening would be longer than the opening.

   No rules between the pairs. A line there would be neither an edge nor a
   division nor a label rule — it would be a separator between two things that
   are already separated by 32 px — and that is the test in
   foundations/geometry.html#where-lines-go. The register's own hairline is
   the only line here. */
.cf-vacancy__facts {
  display: flex;
  flex-wrap: wrap;
  /* Column gap is the wide one: pairs must read as pairs before they read as
     a row, so the air between two facts has to beat the air inside one. */
  gap: var(--space-3) var(--space-8);
  margin: var(--space-2) 0 0;
}
.cf-vacancy__fact { min-width: 0; }
.cf-vacancy__term {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* --text-secondary, never --text-muted: this sits on the page wash, where
     #919191 is 2.0:1. Same rule every mono label on the wash follows. */
  color: var(--text-secondary);
}
.cf-vacancy__value {
  margin: var(--space-1) 0 0;
  font-size: var(--text-md);
  line-height: var(--leading-normal);
}

/* THE SQUARE, WHEN AN OPENING HAS A TITELBILD. The modifier lays the row in
   two — a square photograph at the start, the register's own text in a column
   beside it — and an opening without one is emitted without either, byte for
   byte as before. The picture is optional and the layout does not reserve a
   hole for its absence: an opening with no image is the row this page has
   always drawn, and the two forms sit next to each other in content/jobs/
   without the absent image reading as a fault.

   DECORATIVE, by the same rule as the blog card: `alt=""` and no link, because
   the title beside it is the link whose text is the whole content — a
   description would have the row read twice. → components/vacancy.html

   A WIDTH AND NO HEIGHT, and deliberately: this rule must never pin both axes,
   or it joins fixed_boxes() in scripts/check-image-scale.py and demands a
   192–288 px derivative of a photograph that has to be 1008 px wide to exist.
   aspect-ratio states the square instead, and the width/height attributes on
   the <img> keep their job — they are the aspect-ratio box, from the file's
   own numbers.

   THE WIDTH IS FIXED, NOT THE ROW'S OWN HEIGHT, and that is a limit of flex
   layout rather than a choice: a square whose side tracks a sibling's content
   height, in CSS alone, is circular — the row's height comes from its
   tallest child, so the square cannot both take its size from that height and
   contribute to it. --space-40 is chosen because it sits close to a register
   row's usual height (title, a two- to three-line excerpt, the facts strip),
   so the square reads as matched to the text beside it without literally
   measuring it. align-items: center absorbs the difference on rows whose text
   runs shorter or longer than that. */
.cf-vacancy--image {
  flex-direction: row;
  align-items: center;
  /* The wide gap separates the square from the text; the body's own
     --space-2 below separates the text from itself, so picture and text read
     as two parts of one entry rather than two facts of it. */
  gap: var(--space-4);
}
.cf-vacancy--image > .cf-vacancy__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* The row's column gap, moved onto the text so it still separates title,
     excerpt and facts the way it does in an entry without a picture. */
  gap: var(--space-2);
}
.cf-vacancy__image {
  flex: 0 0 auto;
  width: var(--space-40);
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--surface-raised);
}
/* Under 34rem the square pays some of what a narrow column is short of: the
   row folds the square down instead of letting it crowd the text the reader
   came for. Measured on the row itself, → the container-type above. */
@container (max-width: 34rem) {
  .cf-vacancy--image { gap: var(--space-3); }
  .cf-vacancy--image > .cf-vacancy__image { width: var(--space-20); }
}

/* --------------------------------------------------------------------------
   ARTICLE
   The reading page the blog grid has been linking to. Two parts, and they
   are deliberately separable:

     .cf-article   the page furniture — the rail, the byline, the tail
     .cf-prose     the running text itself, styled by element and not by class

   .cf-prose styles bare HTML on purpose. Article bodies come out of an
   editor, and an editor does not add classes; anything that needs a class
   per paragraph is a system that will be bypassed the first time someone is
   in a hurry. Paste markup in, and it is already set.
   -------------------------------------------------------------------------- */
.cf-article {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-12);
  align-items: start;
}
/* The rail comes first in the source, which is also the order it should be
   read in: what is in this piece, then the piece. Below the two-column
   threshold it simply stays there as a block above the text. */
.cf-article__rail { min-width: 0; }
.cf-article__body { min-width: 0; }

@container (min-width: 60rem) {
  .cf-article { grid-template-columns: 13rem minmax(0, 1fr); gap: var(--space-16); }
  /* Clears the sticky navigation, not the top of the viewport. */
  .cf-article__rail {
    position: sticky;
    top: calc(var(--nav-height) + var(--space-6));
  }
}

/* Contents. A numbered index of the piece — the same counter treatment the
   section header uses, because it is the same idea at document scale. No
   script: there is no scroll-spy, so there is nothing to go wrong and
   nothing to load. */
.cf-article__toc-title {
  padding-bottom: var(--space-3);
  border-bottom: var(--stroke-1) solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.cf-article__toc {
  counter-reset: cf-toc;
  margin: 0;
  padding: 0;
  list-style: none;
}
.cf-article__toc li { border-bottom: var(--stroke-1) solid var(--border-subtle); }
.cf-article__toc a {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  gap: var(--space-2);
  padding-block: var(--space-3);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  transition: color var(--duration-fast) var(--ease-standard);
}
/* --text-secondary and not --text-muted: #919191 is 2.0:1 on CF-Grau, and the
   index sits at the top of the page where the wash is still CF-Grau. The
   system's rule is that no label on the wash is ever muted. */
.cf-article__toc a::before {
  counter-increment: cf-toc;
  content: counter(cf-toc, decimal-leading-zero);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}
.cf-article__toc a:hover,
.cf-article__toc a:focus-visible,
.cf-article__toc a:hover::before,
.cf-article__toc a:focus-visible::before { color: var(--text-primary); }

/* Byline. The round avatar is the system's one true curve: everything else
   that is not square is 2 px, and a circle cannot be expressed as 2 px. */
.cf-article__byline {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-bottom: var(--space-6);
  border-bottom: var(--stroke-1) solid var(--border-default);
}
/* The byline's rule owns the air under it, the same way a section header owns
   the air under its own hairline. Both the pattern page and this component's
   documentation page were setting it inline, in the same value, twice. */
.cf-article__byline + .cf-prose { margin-top: var(--space-8); }
.cf-article__avatar {
  flex: none;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-full);
  object-fit: cover;
  filter: grayscale(1);
}

/* The tail: what this piece was filed under, and the way back. */
.cf-article__tail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  margin-top: var(--space-16);
  padding-top: var(--space-6);
  border-top: var(--stroke-1) solid var(--border-strong);
}
.cf-article__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* A full-ink contour, like every other outlined control in the system, and
   for the same reason: rgba(0,0,0,0.24) over CF-Grau resolves to 1.7:1, which
   is under the 3:1 a UI contour has to clear. Hover inverts, exactly as
   .cf-btn--outline does. */
.cf-article__tag {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: var(--stroke-1) solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: background var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}
a.cf-article__tag:hover,
a.cf-article__tag:focus-visible { background: var(--cf-schwarz); color: var(--grey-000); }
/* A tag row used as a filter needs a selected state, and the selected chip is
   a <span>, not a link — same reasoning as the current page in .cf-pagination.
   It lands on the same inverted plate that hover lands on, which is safe here
   precisely because the two can never be the same element. */
.cf-article__tag[aria-current] { background: var(--cf-schwarz); color: var(--grey-000); }
/* Highlight/HighlightText under forced-color-adjust: none — the same pair and
   the same escape as the identical selected state on .cf-pagination__page[aria-
   current], whose note carries the measurement and the mechanic: forced colours
   prints the ink on a backplate the colour of the page rather than on the plate
   the element declared, so plate and ink each have to clear Canvas on their own.
   ButtonFace stood here before and IS Canvas in both palettes. Measured the same
   way, Chromium 149 and 151, this chip in the row it sits in: 82 % of its
   interior was the page's own colour, in both palettes, with the word legible
   on top. A legible
   word is not a filter state — six of these are links, and the chip is the only
   thing saying which topic you are reading. It is now a plate of #322f69 against
   the light palette and #2ee7fb against the dark one, 12.03:1 and 13.94:1 off
   the page, with the word in HighlightText on it.

   THE BORDER IS DECLARED HERE AND IS NOT DECLARED IN THE UNFORCED RULE ABOVE.
   That asymmetry is the one thing the escape changes about writing this rule:
   the contour comes from .cf-article__tag and is --border-strong, which unforced
   is right — the plate is brand black and the contour vanishes into it. With the
   repaint switched off, the mode no longer maps that translucent brand ink onto
   the user's palette, and it would draw a contour of ours around a chip of
   theirs. It takes the plate's colour instead, and the chip stays one shape. */
@media (forced-colors: active) {
  .cf-article__tag[aria-current] {
    forced-color-adjust: none;
    background: Highlight;
    border-color: Highlight;
    color: HighlightText;
    -webkit-text-fill-color: HighlightText;
  }
}

/* --------------------------------------------------------------------------
   PROSE
   Running text. Everything here is an element selector inside .cf-prose.

   Two sizes are in play. The system's body copy is 14 px, which is right for
   copy that supports a layout; text that is read for minutes at a time steps
   up to 16 px. The system already does this — .cf-value-row__body goes to
   --text-lg once it has the room — so prose is the same move, made explicit.

   The measure is a GRID TRACK, not a max-width on each element, and that is
   the whole trick. `ch` is font-relative: 53ch declared on an h2 resolves
   against the h2's own 32 px and comes out at 1166 px, while the paragraph
   under it resolves the same token at 16 px and comes out at 583 px. Every
   element would get a different column and the headings would overhang the
   text they head — measured, not theorised.

   (The token was 66ch for a year and this paragraph glossed it as 587 px —
   arithmetic done with the fallback font's zero. Geist's zero is 11 px at
   16 px, so the browser drew 726 px and the gloss was 139 px of fiction;
   tokens.css § measure has the measurements. 53ch is the same column the
   587 px figure always described, drawn for real.)

   Declaring the track once on .cf-prose resolves 53ch once, at the prose's
   own size, and every child sits in the same 583 px column. The second track
   takes the remainder, so a figure can span both and step outside the text.
   That difference in width is the rhythm of the page: type in a column,
   plates stepping out of it.
   -------------------------------------------------------------------------- */
.cf-prose {
  display: grid;
  grid-template-columns: min(100%, var(--measure-prose)) minmax(0, 1fr);
  font-size: var(--text-lg);
  font-weight: var(--weight-light);
  line-height: var(--leading-relaxed);
  color: var(--text-primary);
}
.cf-prose > * { grid-column: 1; }
/* Plates run the full width. A quote is type, so it stays in the column even
   though it is also a <figure>. */
.cf-prose > figure:not(.cf-quote),
.cf-prose > .cf-prose__table-scroll { grid-column: 1 / -1; }
.cf-prose > figure.cf-figure--inline { grid-column: 1; }
.cf-prose > * + * { margin-top: var(--space-6); }

/* The standfirst. One step up in size, one step back in colour: it is the way
   into the piece rather than the first paragraph of it. --text-secondary is
   5.9:1 on the page wash, so it clears AA at this size without being body ink. */
.cf-article__lead {
  font-size: var(--text-h4);
  font-weight: var(--weight-light);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}

/* Headings open on a hairline, the way every section of every page does.
   The rule is the width of the text column, so the type and the line share
   one left edge and one right edge. */
.cf-prose h2 {
  margin-top: var(--space-16);
  padding-top: var(--space-4);
  border-top: var(--stroke-1) solid var(--border-strong);
  font-family: var(--font-sans);
  font-size: var(--text-h2);
  font-weight: var(--weight-medium);
  line-height: var(--leading-normal);
}
.cf-prose h3 {
  margin-top: var(--space-12);
  font-size: var(--text-h4);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-normal);
}
/* This is .t-label-lg with a margin on it, and it was four of that utility's
   five declarations plus a fifth taken from the other rung. --tracking-wide is
   not decoration on the 12 px form; it is what makes 12 px mono read as the
   larger label rather than as the 11 px one set slightly big, which is the
   only difference a reader can see between the two rungs at a glance. Set
   --tracking-label it stood 0.84 px narrower per character than every button
   on the same page — one word of drift across a heading, under a rule that
   claims the two are the same style. */
.cf-prose h4 {
  margin-top: var(--space-8);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
/* The navigation is sticky, so an anchor that lands flush with the top of
   the viewport lands underneath it. */
.cf-prose :is(h2, h3, h4) { scroll-margin-top: calc(var(--nav-height) + var(--space-6)); }

.cf-prose strong { font-weight: var(--weight-semibold); }

/* A link inside running text is underlined at all times — colour alone never
   carries it. The line starts at the same weight as every other hairline in
   the system and goes to full ink on hover.

   :not(.cf-btn), AND IT IS THE TRANSITION THAT FORCES IT, not the underline.
   `transition` is a shorthand: the rule that wins REPLACES the whole list
   rather than merging with it. This selector is (0,1,1); .cf-btn declares its
   own background / color / border-color, all at --duration-base, at (0,1,0),
   and therefore lost the entire declaration — not just the properties the two
   rules share. What was left on the button was
   `transition-property: text-decoration-color`, which is neither of the two
   properties its :hover moves.

   ONE ANCHOR IN THE SYSTEM CARRIES A COMPONENT CLASS INSIDE .cf-prose: the
   "Fall besprechen" / "Discuss your case" CTA at the foot of Expertise, one
   in each edition. Censused by dumping the full computed style of every
   `.cf-prose a` on every page this repo serves, before and after this line —
   148 anchors over 214 pages, the 76 shipped and the 138 of the manual — and
   diffing: FOUR move, and they are that one CTA in both editions plus the two
   pattern sources it is generated from. The other 144 are byte-identical, and
   `color` is not among the nine properties that move on the four. So this is
   the whole blast radius, and it was the whole defect. Measured in
   Chromium 1194 at 1440 x 900 by reading the CSSTransition objects Blink
   actually builds on the frame the pointer enters, which is exact where a
   sample is not. BEFORE: one, `text-decoration-color`, 120 ms — and none on
   either property the :hover moves, so background transparent -> #000 and
   colour #000 -> #fff were both already at their END value in the first
   sample, 19 ms in on / and 21 ms on /en/. AFTER: two, `background-color`
   and `color`, 240 ms each, cubic-bezier(0.2, 0, 0, 1), which is
   --duration-base and --ease-standard as declared. Sampled on a ~22 ms grid
   the ramp now lands at 271 ms, against 266 ms for the same .cf-btn--outline
   one page over on datenschutz — a <button>, which this selector could never
   match, and therefore the control that says what the button should do.

   EXCLUDED HERE RATHER THAN OUT-SPECIFIED IN THE BUTTON BLOCK, and in the
   same idiom the list rules immediately below already use for .cf-plot and
   .cf-block — `.cf-prose li:not(.cf-plot__col, …)` — under the sentence this
   file writes there: the exception belongs where the general rule is written.
   Weight goes (0,1,1) ->
   (0,2,1) and the :hover pair (0,2,1) -> (0,3,1), so nothing this rule used
   to win it now loses; it only stops matching the one element it should never
   have styled.

   The underline goes with it, and that is the correction and not a side
   effect: `a { text-decoration: none }` in base.css is what every other
   .cf-btn in the system renders under, and this one was drawing a
   running-text underline through a bordered CTA for no reason other than
   standing in prose. Its colour does not move — .cf-btn--outline sets
   `color: var(--text-primary)`, which resolves to the #000 this rule was
   handing it by inheritance, measured identical before and after. */
.cf-prose a:not(.cf-btn) {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: var(--stroke-1);
  text-underline-offset: 0.2em;
  text-decoration-color: var(--border-default);
  transition: text-decoration-color var(--duration-fast) var(--ease-standard);
}
.cf-prose a:not(.cf-btn):hover,
.cf-prose a:not(.cf-btn):focus-visible { text-decoration-color: var(--border-strong); }

/* Lists are drawn, not bulleted: an unordered marker is a 6 px contour
   square, an ordered one is a mono ordinal. Both hang outside the text
   column so the left edge of the copy stays straight.
   0.725em is half of --leading-relaxed, i.e. the centre of the first line.

   A .cf-plot is an <ol> whose items are columns rather than list items, and
   these three rules would give each column a hanging ordinal, a 32 px indent
   and a 12 px top margin that steps every column but the first out of the row.
   Excluded here rather than fought with specificity inside the plot block, for
   the same reason .cf-quote is excluded from the figure rule below: the
   exception belongs where the general rule is written.

   .cf-block carries FOUR list-item names for the same reason and is the
   sharper case, because three of the four are flex items whose width is the
   drawing: a 32 px hang on a block inside a group does not merely decorate it,
   it makes the block narrower than the value it stands for. The legend's items
   are the fourth, and an ordinal in front of every key would be the figure
   numbering its own series in a mark that means something else here. */
.cf-prose :is(ul, ol) { margin-left: 0; padding-left: 0; list-style: none; }
.cf-prose li:not(.cf-plot__col, .cf-block__group, .cf-block__bar, .cf-block__step, .cf-block__legend-item) { position: relative; padding-left: var(--space-8); }
.cf-prose li + li:not(.cf-plot__col, .cf-block__group, .cf-block__bar, .cf-block__step, .cf-block__legend-item) { margin-top: var(--space-3); }
.cf-prose ul > li:not(.cf-block__step)::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(0.725em - 3px);
  width: 6px;
  height: 6px;
  border: var(--stroke-1) solid var(--border-strong);
}
.cf-prose ol { counter-reset: cf-ol; }
.cf-prose ol > li:not(.cf-plot__col, .cf-block__group, .cf-block__bar, .cf-block__step, .cf-block__legend-item)::before {
  counter-increment: cf-ol;
  content: counter(cf-ol, decimal-leading-zero);
  position: absolute;
  left: 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}
/* Nested lists restart the count and step in by the same hang. */
.cf-prose li > :is(ul, ol) { margin-top: var(--space-3); }

/* Figures run the full width of the column. The plate carries the contour;
   the caption hangs under a lighter hairline so the two rules do not read as
   the same line.

   A plot is a figure whose drawings are not plates: .cf-plot holds one small
   SVG per column, and framing each of them would put a boxed, filled panel
   round every single column. The plot draws its own contour and stands on the
   page wash, so it is excluded. */
.cf-prose figure { margin-top: var(--space-12); }
.cf-prose figure :is(img, svg, video):not(.cf-plot__draw) {
  width: 100%;
  border: var(--stroke-1) solid var(--border-strong);
  background: var(--surface-raised);
}
.cf-prose figcaption {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}
/* .cf-figure--inline keeps a figure inside the reading column — a portrait, a
   small diagram. The grid rule above does the work; this is the escape hatch
   for a figure used outside .cf-prose. */
.cf-figure--inline { max-width: var(--measure-prose); }

/* Quote. Type, not quotation marks and not a bar: the words go one step up
   the scale and the attribution closes the block under a hairline. The
   attribution is not part of the quote, so it goes in a figcaption OUTSIDE
   the blockquote — putting it inside would make the source read as something
   the source said about itself.

   It used to be marked by a hairline down its left side, which is the one
   mark this system does not make — see foundations/geometry.html, "Where a
   line may go". Two things changed when the bar came off. The type moved from
   --text-h4 to --text-h3, because the bar was carrying half the distinction
   and something had to take it over: 24 px Light against 20 px SemiBold for
   .cf-prose h3 and 32 px Medium for h2, so the quote now sits in a slot of
   its own by size AND weight rather than by a decoration. And the caption
   stopped suppressing the figcaption rule it inherits from .cf-prose figure —
   an attribution under a hairline is what every other caption in an article
   already is, so the quote stops being a special case. The rule is declared
   here rather than left to that inheritance because .cf-quote is usable
   outside .cf-prose, where nothing would draw it.

   Declared after the figure block on purpose: .cf-quote is a <figure>, so
   both blocks reach its caption at equal specificity and the later one has
   to be this one. */
.cf-prose blockquote,
.cf-quote {
  font-size: var(--text-h3);
  font-weight: var(--weight-light);
  line-height: var(--leading-snug);
}
.cf-quote { margin: 0; max-width: var(--measure-prose); }
.cf-quote > blockquote { font: inherit; }
.cf-quote figcaption,
.cf-quote__source {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* A break in the argument, not a divider between sections: the finest of the
   four sanctioned dashes, at the width of the text column.

   The dash is the 1-4 gradient written out rather than taken from
   .rule--dashed-1-4, so forced colours drops it and the break is absent —
   measured at 558 x 1 on patterns/blog-artikel.html, both schemes. It is
   redrawn as a dotted border below, on the argument set out over
   .cf-process__divider's block, which carries the same gradient and fails the
   same way. */
.cf-prose hr {
  width: 100%;
  height: var(--stroke-1);
  margin-block: var(--space-12);
  border: 0;
  background: repeating-linear-gradient(90deg, var(--border-strong) 0 1px, transparent 1px 5px);
}
@media (forced-colors: active) {
  .cf-prose hr {
    background: none;
    border-block-start: var(--stroke-1) dotted CanvasText;
  }
}

/* An aside inside the argument. Sunken rather than outlined, so it reads as
   a step back from the page instead of another box on it, and opened by a
   mono tag with the hairline running out of it to the far edge — the
   .cf-section-header device at prose scale, which is what makes the aside
   look like part of this site rather than like a callout from any other.

   It used to be marked by a 2 px rule down its left side, and that is the
   mark this system does not make: a bar beside a block is the only line on
   the page that is not an edge of anything, does not divide two things, and
   carries no label. See foundations/geometry.html, "Where a line may go".
   The tag replaces it and does more than mark the block — it says what the
   aside IS, which the bar never could.

   The tag holds one word of plain text. It is a flex row whose ::after is
   the rule, so an inline element inside it would become a second flex item
   and push the rule along; nothing breaks, but the tag is not the place for
   a link. Anything with structure in it belongs in the body. */
.cf-prose__note {
  padding: var(--space-5) var(--space-6);
  background: var(--surface-sunken);
  font-size: var(--text-md);
}
.cf-prose__note > * + * { margin-top: var(--space-3); }
/* One rule, two components. .cf-info-card__ask opens with the same device for
   the same reason — it was the other block in the system marked by a bar down
   its left side — and the declaration is shared rather than copied so the two
   cannot answer at two different label sizes. The info card supplies only the
   tag's margin-top, next to the paragraph it opens. */
.cf-prose__note-tag,
.cf-info-card__ask-tag {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
}
.cf-prose__note-tag::after,
.cf-info-card__ask-tag::after {
  content: "";
  flex: 1;
  height: var(--stroke-1);
  background: var(--border-strong);
}
/* THIS ASIDE LOST BOTH OF ITS MARKS IN FORCED COLOURS, and the rule is the
   clean proof of the mechanism the note on .cf-process__note above states.
   --border-strong is OPAQUE BLACK here, painted as a background rather than as
   a border because a flex ::after is the tidiest way to run a hairline out of
   a label. Measured on components/article.html at 1280, mode active: it
   computes rgb(255,255,255) in the light palette and rgb(0,0,0) in the dark
   one — the reader's own Canvas, both times, on a ground that IS Canvas. An
   opaque black line becoming an opaque white one is not an alpha surviving; it
   is the colour being replaced and the alpha kept, which is exactly what the
   mode does and what tokens.css and foundations/materials.html both said it
   did not, until this measurement corrected them.

   So the block had no surface (its veil, same arithmetic as the process note)
   and no rule (this line), leaving one mono word and two paragraphs of body
   text — an aside indistinguishable from the argument it steps out of.

   Both marks come back as the paint the mode keeps. The height goes to zero
   under the border so the line stays one pixel and the label's row does not
   grow. The info card's ask shares the declaration for the reason the rule
   above is shared: two copies of one device answer at two sizes eventually. */
@media (forced-colors: active) {
  .cf-prose__note { border: var(--stroke-1) solid CanvasText; }
  .cf-prose__note-tag::after,
  .cf-info-card__ask-tag::after {
    height: 0;
    border-top: var(--stroke-1) solid CanvasText;
    background: none;
  }
}

.cf-prose :is(code, kbd, samp) {
  padding: 0.1em 0.35em;
  background: var(--surface-sunken);
  font-family: var(--font-mono);
  font-size: 0.875em;
}
.cf-prose pre {
  overflow-x: auto;
  padding: var(--space-4);
  border: var(--stroke-1) solid var(--border-default);
  background: var(--surface-sunken);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}
.cf-prose pre code { padding: 0; background: none; font-size: inherit; }

/* Tables in an article are the TABLE component below, not a second drawing of
   it — `.cf-prose table` is named in every one of its selectors. The one thing
   that belongs here is where the block sits in the prose grid, which is the
   rule at the top of this section.
   → the TABLE section */

/* --------------------------------------------------------------------------
   NOTICE
   One message, standing in the flow: a qualification inside an article, a
   warning above a form, the sentence a failed POST comes back with. Not a
   toast, not a dialog, nothing that moves or disappears on its own.

   THE PROBLEM THIS COMPONENT HAS THAT OTHER SYSTEMS DO NOT. Every notice
   component in the field carries its severity in a fill: USWDS paints a
   green, a yellow and a red band across the whole box, Carbon tints the
   surface, GOV.UK fills the header. This system has one non-brand colour in
   it — `--feedback-error`, and tokens.css calls it "the one colour outside
   the brand palette, and the only one" — so there is no green to be
   successful in and no amber to warn in, and inventing them would cost the
   palette more than the component is worth.

   So severity is carried by the PRESENCE LADDER instead, which is the axis
   the brand already has for exactly this: "how present the thing it draws
   is", four line types read as one scale. A notice is a hairline box with a
   4 px band on its leading edge, and the band's LINE TYPE is the tone.

     hinweis   --presence-faint (1-2)    a qualification. Read it or don't.
     achtung   --presence-near  (2-1)    something will go wrong if you don't.
     fehler    --presence-present (solid, in --feedback-error)  it already did.

   Which is the ladder's own rule obeyed rather than quoted: one object, three
   rungs, moving ALONG the scale and never across it. The box, the geometry,
   the padding and the glyph column are identical in all three; what changes
   is how solidly the leading edge is drawn.

   And the tone is never only the line. WCAG 1.4.1 is about colour, but the
   principle generalises to any single channel a reader may not resolve — a
   1-2 band and a 2-1 band are four pixels apart at arm's length. Every notice
   therefore states its tone as a WORD in the title (Hinweis / Achtung /
   Fehler), which is the same conclusion USWDS reaches from the colour side:
   "start alert text with the alert type descriptor". The glyph is the third
   channel and the band is the fourth.

   No fill, no radius, no lime. Lime is the light layer and a notice is not a
   light source; a notice drawn in lime would spend a screen's one lime moment
   on an interruption.
   -------------------------------------------------------------------------- */
.cf-notice {
  /* The tone, as two custom properties. Everything else is shared. */
  --notice-ink: var(--text-primary);
  --notice-edge: var(--border-default);
  /* --presence-faint, 1-2. The ratio is written out because CSS cannot read a
     two-value `stroke-dasharray` token into a gradient — the same reason
     base.css writes .rule--dashed-1-2 as literals rather than as var(--dash-1-2),
     and the period is the identical 6 px. */
  --notice-band: repeating-linear-gradient(180deg,
                 var(--notice-ink) 0 2px, transparent 2px 6px);

  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "glyph title" ". body" ". actions";
  column-gap: var(--space-3);
  row-gap: var(--space-2);
  /* The glyph and the label are one row and are centred in it, so a 16 px mark
     and an 11 px mono line share a middle rather than a top edge. Every other
     row holds a single item, where centring and starting are the same thing. */
  align-items: center;
  padding-block: var(--space-5);
  /* The leading pad clears the band, which sits ON the contour rather than
     inside it — so the edge of the box thickens rather than gaining a stripe. */
  padding-inline: calc(var(--space-5) + var(--stroke-4)) var(--space-5);
  border: var(--stroke-1) solid var(--notice-edge);
  border-radius: var(--radius-none);
}

/* The band. Inset by the contour's own 1 px on three sides so it covers the
   corner it stands in; a band that stopped short would read as two lines
   meeting rather than as one edge drawn heavier. */
.cf-notice::before {
  content: "";
  position: absolute;
  inset-block: calc(var(--stroke-1) * -1);
  inset-inline-start: calc(var(--stroke-1) * -1);
  width: var(--stroke-4);
  background: var(--notice-band);
}

/* Hinweis is the base and takes no modifier — the quietest tone is what a
   notice is unless it says otherwise, the same way .cf-btn is the plain
   button and only the loud ones are named. */
.cf-notice--achtung {
  /* --presence-near, 2-1. Same 6 px period, twice the ink. */
  --notice-band: repeating-linear-gradient(180deg,
                 var(--notice-ink) 0 4px, transparent 4px 6px);
}
.cf-notice--fehler {
  /* --presence-present. The top rung is solid, and it is the one tone that
     also takes the error ink — on the band, on the label and on the contour,
     so the box is findable at a glance on a page that is otherwise all
     hairlines. Same weight as every other notice: the rung does the work,
     not a second stroke width. */
  --notice-ink: var(--feedback-error);
  --notice-edge: var(--feedback-error);
  --notice-band: var(--notice-ink);
}

/* The glyph is a required part, not a decoration: it is the third channel the
   tone is stated on, after the word and the line type. It takes .cf-icon--sm —
   16 px is the icon set's floor and it is what stands beside an 11 px label
   without becoming the loudest thing in the box. */
.cf-notice__glyph {
  grid-area: glyph;
  color: var(--notice-ink);
}
.cf-notice__title {
  grid-area: title;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--notice-ink);
}
/* No font-size here on purpose. A notice inherits the size of what it stands
   in — 16 px inside .cf-prose, 14 px beside a form — because it is a sentence
   in that text, not a panel with a type scale of its own. */
.cf-notice__body { grid-area: body; }
.cf-notice__body > * + * { margin-top: var(--space-3); }
.cf-notice__body :last-child { margin-bottom: 0; }

/* Optional. One or two ways out of what the notice just said — the vacancy
   that closed linking to the register, the failed submission linking to the
   address it could have been sent to instead. */
.cf-notice__actions {
  grid-area: actions;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-2);
}

/* Under forced colours a background-image is discarded outright, so the band
   would vanish and all three tones would render as the same box — the exact
   failure base.css documents on .rule--dashed-*. Same answer as there: the
   band becomes a real border, whose colour forced colours maps rather than
   erases, and the two dashed rungs take the two dash styles CSS actually has.
   1-2 is dotted and 2-1 is dashed, which is base.css's own mapping. */
@media (forced-colors: active) {
  .cf-notice::before {
    width: 0;
    border-inline-start: var(--stroke-4) dotted CanvasText;
  }
  .cf-notice--achtung::before { border-inline-start-style: dashed; }
  .cf-notice--fehler::before  { border-inline-start-style: solid; }
  /* AND THE THIRD CHANNEL WAS NOT THERE AT ALL, which is worse than the band
     the paragraph above rescued. The glyph names --notice-ink on the <svg>
     itself, and an ink named there is preserved rather than mapped — see the
     note beside .cf-icon--light. Measured on components/notice.html in the
     dark palette: Hinweis and Achtung drew rgb(0, 0, 0) on a Canvas of
     rgb(0, 0, 0), 1:1, absent; Fehler drew rgb(167, 32, 25) at 2.86:1; the
     title beside each of them is an ordinary <p> and came through at
     CanvasText. So the box a reader was meant to read on three channels was
     reading on two, and the one that went missing is the one the component's
     own note calls required.

     The shape is what survives here, and it is enough: the three tones carry
     #cf-diamond, #cf-triangle and #cf-triangle-solid, so the glyph still says
     which notice this is in a palette with one ink in it. `inherit` puts it on
     the title's colour, which is where the tone put it before the mode took
     the colour away. */
  .cf-notice__glyph { color: inherit; }
}

/* --------------------------------------------------------------------------
   TEAM
   Two forms: a horizontal strip (landing page) and a portrait grid
   (Über uns). Both share the name/role typography.
   -------------------------------------------------------------------------- */
.cf-person__name {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-normal);
}
.cf-person__role {
  margin-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
/* THE FOURTH LINE, and only the grid has room for it. Name and role say who a
   person is and leave open the thing a portrait wall is actually asked: what
   do they work on. The strip cannot carry it — a 56 px round avatar in a
   120 px cell has no third register — so this is grid-only in practice while
   staying under the shared .cf-person prefix, the same way __name and __role
   are typography both forms happen to share rather than a promise both use.

   PROSE, NOT CHIPS. The note over .cf-info-card__chips rules out a fifth way
   of drawing a small bordered label in this system, and a focus is a sentence
   anyway: "Historians, Steuerungen und Feldbusse als lückenlose Zeitreihen" is
   a claim, and the same words as three chips are a tag cloud.

   --text-md and not --text-sm, because the scale writes "14 — smallest size
   allowed for prose" against that rung and this is prose. The hairline over it
   is --border-default, not the --border-strong the cells are ruled with: it
   divides two registers inside one cell and must not read as a cell edge. */
.cf-person__focus {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: var(--stroke-1) solid var(--border-default);
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}

/* The strip is a scroll box wrapping a list, for the same reason
   .cf-table__scroll is a scroll box wrapping a table: the box carries
   tabindex="0" and role="region" so it is reachable and scrollable by
   keyboard, and the list inside keeps its list semantics, which role="region"
   on the same element would have overwritten. WCAG 2.1.1 — measured at 375
   and 768, where four cells at their 15rem floor overflow the viewport by
   585 px and 192 px and a keyboard-only reader could reach neither.

   It is deliberately not an APG carousel. That pattern is for rotating
   content with slide controls and a rotation switch; this is a list that
   happens to be wider than the screen, and the APG says so — a static
   scrollable list wants semantic list markup, not carousel roles. */
.cf-team-strip {
  overflow-x: auto;
  border-block: var(--stroke-1) solid var(--border-strong);
  /* Mandatory is safe here and would not be everywhere. It strands content
     when a cell can outgrow the box, and these cannot: the track is capped at
     1fr, so a cell is never wider than the scroll box, and every cell is the
     same width. Measured at 375, 768, 1280 and 1920 — the last cell's right
     edge lands exactly on the box's at maximum scroll, no tail cut off. */
  scroll-snap-type: x mandatory;
}
/* TWO OF THE RING'S FOUR SIDES WERE OFF THE SCREEN. This is the only focusable
   thing in the system that is genuinely full-bleed — measured at 375, 1280 and
   1920, the box runs 0 to the viewport width exactly — so base.css's ring, at
   its 2 px outward offset, drew its left and right edges at -2 and viewport+2.
   A keyboard reader got two horizontal lines 2 px outside the strip's own two
   hairlines, which reads as the rules having thickened rather than as a ring,
   and there was nothing to close it at either end.
   Turning the offset inward is the whole fix: the ring keeps its weight and
   its colour, all four sides land inside the box, and the 1 px of wash between
   it and the strip's own hairline is what tells the two apart.

   THIS USED TO CLOSE BY SAYING NOTHING ELSE IN THE SYSTEM NEEDS IT, on the
   grounds that nothing else is focusable AND flush with both edges of the
   screen. The premise still holds and the conclusion never did: the viewport
   is not the only box that crops a ring. `.cf-blog-grid--port .cf-blog-card`
   is flush with a scrollport its own component declared, and lost the whole
   indicator to it on 130 links — measured, and fixed by this same move a few
   hundred lines up. .cf-table__scroll, named here as the counter-example, is
   still one: it sits inside a .container and its focusable is the box itself,
   not something flush inside it. The rule to carry forward is about the crop
   and not about the screen — a ring drawn outward needs 4 px of somewhere to
   be drawn, and any box that clips is a box that can take it away. */
.cf-team-strip:focus-visible { outline-offset: calc(var(--stroke-2) * -1); }
.cf-team-strip__list {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(15rem, 1fr);
}
.cf-team-strip__item {
  display: flex;
  align-items: center;
  /* 32 px of padding and a 24 px gap, both read off the mockup and both
     confirmed twice over. The card measures 120 px tall there — 32 + the
     56 px avatar + 32 + the 1 px rule — and the portrait's left edge sits
     32 px in, which is the same number arrived at from the other direction.
     The name then starts at 32 + 56 + 24 = 112 px, which is where the mockup
     puts it. At 16 px and 16 px the card came out 90 px tall: the avatar with
     the light shaved off either side of it, and the one fixed measure in this
     strip sitting off the 8-point grid. */
  gap: var(--space-6);
  padding: var(--space-8);
  border-right: var(--stroke-1) solid var(--border-strong);
  scroll-snap-align: start;
}
.cf-team-strip__avatar {
  flex: none;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: var(--radius-full);
  object-fit: cover;
  filter: grayscale(1);
}

/* Photo, name, role, focus. The four lines have to sit on the same baselines
   right across a row — a portrait wall whose role labels stagger reads as a
   mistake, and it only takes one name that wraps to two lines. The card boxes
   are equal height already (grid stretch), so nothing looks wrong until real
   names land, and the README says real names are still to come.

   Each card spans four row tracks of the parent and adopts them with
   `subgrid`, so a two-line name grows that track for every card in the row at
   once. Without it, a single wrapped name pushes its own role label 21 px below
   its neighbours' and nothing else moves. The fourth track is the focus line,
   and it is the one that most needs the shared track: the six texts do not
   wrap to the same number of lines, so its rule would otherwise be drawn at
   six different heights across a row of six cells. */
/* THE FIELD IS THREE WIDE AT THE REFERENCE NOW, NOT SIX, and that is what
   settles the open question the docs page has carried since the grid was
   written: the mockup draws 294 px cells and this grid drew 213, because the
   track minimum was pinned to keep all six people on one row at 1280. Six on
   one row is what made the cell too small to be the mockup's. A cell wide
   enough to hold a portrait and a line of prose under it cannot also be a
   sixth of 1280, so the row count is what gives.

   The divisor rule the old minimum was chosen for is kept, not dropped. Six
   people is still the shipping count and a ruled field still only looks right
   when the column count divides it — 6, 3, 2 or 1 — and every count this
   minimum produces is one of those: 3 above 1008 of content, 2 from 673, 1
   below. Four and five never appear, so the ragged last row the old comment
   called unavoidable is now avoided outright rather than moved out of the way
   of 1280. What that costs is the single row, and what it buys is a cell that
   doubles: 426.3 px at the 1440 frame against 213.2, with the portrait at
   297.4 against 148.2.

   ONE RUNG, AND THE WINDOW IT HAS TO LAND IN IS 16 PX WIDE. Two constraints
   close on it from either side, and the count that satisfies both exists only
   because the field is allowed to wrap. Three columns at a full .container
   wants a minimum ABOVE 1279 / 4, or the fourth track fits and the row goes
   ragged; two columns at a 768 viewport — 683 of content, and one of the four
   widths this system is tested at — wants one at or BELOW 682 / 2, or the
   field drops to a single 683 px card with a 478 px portrait in it. That is
   319.75 px to 341 px, and 21rem is 336: sixteen pixels clear of the ragged
   edge and five clear of the single-card one.

   THE 1279 AND THE 682 ARE NOT TYPOS. The field draws border-left on the grid
   and border-right on each cell, so what the tracks divide is the content
   width minus one. Every boundary in this component sits a pixel above where
   the plain floor(content / minimum) puts it — measured, not derived: three
   columns start at 1009 of content, two at 673. Reading it the other way is
   how the first pass at this rule put a 527.5 px cell where a 351 was
   expected.

   SWEPT RATHER THAN SPOT-CHECKED. Every content width from 300 to 1280:
   3 columns above 1008, 2 from 673, 1 below, and 4 and 5 do not occur at any
   width. So the ragged last row the old comment called unavoidable is now
   absent from the whole range rather than merely moved off 1280. */

/* THE ROW TRACKS ARE min-content, AND THAT IS A FIREFOX BUG RATHER THAN A
   PREFERENCE. Gecko feeds the portrait's INTRINSIC height — the pixel height
   of the file, not the height it is drawn at — into the block-size
   contribution the subgrid cell makes to this grid, and spreads whatever that
   leaves over the four shared tracks equally across all four of them. The
   fault is the FILE, so it is invisible until the lazy portrait arrives: the
   card is drawn correctly, and then it grows. That is also why no capture in
   this repo caught it — the grid is far below the fold on the shipping page,
   so a screenshot run that never scrolls to it never loads a portrait.

   MEASURED IN FIREFOX 154 on patterns/ueber-uns.html at 1440, with the seven
   portraits loaded. With this line the four tracks are
   451.97 / 20.8 / 19.95 / 463.2 px; without it they are
   542.98 / 111.82 / 110.97 / 554.22 — every one of them 91.0 px too tall — and
   the 16 px this stylesheet asks for under the picture is drawn as 107. The
   cell goes from 955.9 px to 1320.0.

   91.0 IS NOT AN ARBITRARY NUMBER. simon.jpg is 640 x 800 and the box it is
   drawn in here is 297.7 x 372.17, so 800 - 372.17 - 63.8 (the 15 % inset the
   first track already holds) is 364.03, and a quarter of that is the 91.01
   each track gains. Swept over 375, 768, 1024, 1280 and 1440 the excess per
   track is 115.0, 112.8, 83.6, 103.0 and 91.0 px, and that same subtraction
   over four predicts every one of them to a hundredth of a pixel.

   THE ARITHMETIC WAS CHECKED BY CHANGING THE ONLY TERM IN IT. The same card
   fed a 320 x 400 copy of the same photograph needs no fix at all — an
   intrinsic height under what the track already holds has nothing to
   spread — and a 1280 x 1600 copy puts 296.5 px in every track, which is the
   subtraction run again. Shipping a smaller file is therefore not the fix:
   640 x 800 is 2 x the 297.7 x 372.17 the widest cell draws, which is the
   retina file this grid is supposed to have.

   AND IT COSTS THE OTHER TWO ENGINES NOTHING. A block box's min-content block
   size IS its height, so the tracks min-content produces are the tracks auto
   produced; Chromium and WebKit never had the fault and do not move. Measured
   at 375, 768, 1024, 1280 and 1440 — the four widths this system is tested at
   plus the two-column boundary — all three engines now agree on all four
   tracks within a tenth of a pixel, and Chromium and WebKit draw the same
   tracks after this line that they drew before it.

   THAT FIX WAS HALF OF ONE, AND THE OTHER HALF SHIPPED AS A BLANK BAND UNDER
   THE LAST PERSON — three days, #459 to this. min-content corrected the
   TRACKS; the grid container went on taking its own block size from the same
   inflated contributions, and nothing recomputed it from the tracks that were
   now right. Measured in Firefox 154 at 1440: twelve tracks summing 2 908.35
   px inside a <ul> drawn 4 001.45 px tall — 1 092.10 px of dead space below
   the last card, which is 3 x the 364.03 a row group over-contributes. The
   band is per ROW, so it grew as the cells narrowed and the rows multiplied:
   1 092.10 px at 1440 and above, 1 236.50 at 1280, 1 337.93 at 1024,
   1 804.93 at 768 and 3 219.65 at 375, where seven rows of it made the page
   half again as long as its content. `block-size: min-content` and `fit-content`
   on the grid do not shrink it — Gecko's min-content block size for this
   container IS the inflated figure — and neither does align-content,
   contain, or an explicit grid-template-rows.

   THE CONTRIBUTION IS FIXED AT THE SOURCE NOW, in the cell geometry below:
   the card's inline size is indefinite while Gecko measures it, so every
   percentage inside it resolved against nothing, and the two that land in the
   BLOCK axis are what the container was adding up. --cell is a container
   length instead, and both halves of the fault go with it — 0.00 px of slack
   at 375, 768, 1024, 1280, 1440, 1700 and 1920, in all three engines.

   SO THIS LINE NO LONGER CHANGES A MEASUREMENT. With --cell in place,
   grid-auto-rows: auto draws the same twelve tracks to the hundredth of a
   pixel at all seven widths in all three engines. It stays because it is the
   cheap guard on the half that shows in the card: if a contribution is ever
   wrong again, min-content keeps the wrong number out of the portrait and the
   three lines under it, and the damage is once more a band this comment
   already describes how to find. */
.cf-team-grid {
  /* The cells are drawn by their own borders, so the tiles must touch. */
  --tile: 21rem;
  --flow: 0;
  grid-auto-rows: min-content;
  /* Named, and read by the two rungs under .cf-team-grid__item. The cell
     geometry needs the grid's own content box — .container's `layout` is one
     element out and one border wide, which is a 1 px window at each rung
     where the ladder and auto-fill would disagree about the column count. */
  container: cf-team / inline-size;
  border-top: var(--stroke-1) solid var(--border-strong);
  border-left: var(--stroke-1) solid var(--border-strong);
}
/* THE SAME 32 PX .cf-team-strip__item CARRIES, and for the same reason: both
   are one person in one ruled cell, and two components drawing that cell with
   different insets is the drift this file is meant not to have. It used to be
   24 px block and 16 px inline — a three-value shorthand whose third value
   repeated its first, which is what a padding looks like when nobody measured
   it.

   Measured on mockups/ueber-uns.jpg, where the cell is 294 px and the portrait
   inside it is inset 48 px at the top and on both sides — 205 px of photo in a
   294 px cell, 70 %. That 70 % is the thing the designer decided; 48 px is only
   what it came to at their cell width. The rule above used to carry it as a
   token anyway — 32 px, the rung that landed 69.5 % at the one cell width the
   grid then had — with the ratio written out in this comment as the reason.
   A ratio a comment has to restate at every cell width is a ratio the
   declaration should be stating itself, and the cell width is a RANGE now
   rather than one number — swept from 320 to 1280 of content it runs 319.0 px
   at its narrowest to 671.0 at its widest, over three column counts. At a
   fixed 32 the 426.3 px cell of the 1440 frame
   would draw an 85.0 % portrait, which is the exact failure this paragraph was
   written to record — it was 84.5 % at 16 px, a portrait pressed against the
   rule with no cell left around it.

   So the inset is the ratio. 15 % of the cell on each side leaves 70 % for the
   portrait at every width the field can reach, and a percentage length here
   resolves against the grid area's inline size, so it tracks the cell without
   a query. Measured at the four widths this system is tested at — 1440, 1280,
   768, 375 — the cell is 426.3, 379.4, 341.3 and 332.8 px and the portrait
   297.7, 264.9, 238.2 and 232.2. Swept every content width from 300 to 1280
   the ratio never leaves 69.76–69.90 %, which is the plate's 69.7, and no
   width in that sweep puts a line over the picture. Percentages are outside
   the spacing scale on purpose and check-spacing-scale.py does not flag them —
   the scale governs distances between things, and this is a proportion of one
   thing.

   THE RATIO IS STILL THE THING DECLARED; WHAT CHANGED IS WHAT IT IS A RATIO
   OF, and the reason is that "the grid area's inline size" is a quantity
   Gecko does not have while it is measuring this card. .tiles' column is
   `minmax(min(var(--tile), 100 %), 1fr)`, and under an fr maximum the cell is
   not definite during intrinsic sizing — the same sentence the Chromium note
   below is built on. So every percentage in the card resolved against
   nothing in that pass, and the two that land in the BLOCK axis are the ones
   that reached the parent: a percentage block margin resolves against the
   INLINE size, so `margin-top` on the portrait and `margin-bottom` on the
   focus line went to zero, and the portrait — width: calc(100 % - …) on a
   replaced element — fell back to the FILE, 640 x 800, and offered 800 px of
   height for a box drawn 372.17. Isolated: with the portrait alone in
   container units the card contributes 63.8 px per row too little, with the
   margins alone it contributes 427.8 too much, and with both it is the
   364.03 the note above the grid takes apart.

   --cell IS THE SAME NUMBER AS A CONTAINER LENGTH, which is definite in that
   pass because a container query is answered after its container is laid out.
   100cqi rides `cf-team`, the grid's own content box, so --cell is one cell's
   content box and the three declarations under it read exactly as they did:
   0.15 of it four times over, and the portrait asking for what is left. The
   column count is the one term cqi cannot supply — hence --cols and the two
   rungs, which are auto-fill's own arithmetic and not a second opinion about
   it: gap is 0 here, so the track count is floor(grid / --tile), and 2 x and
   3 x 21rem are 42 and 63rem exactly. The grid never reaches four columns —
   the content column stops at 1280 and 4 x 21rem is 1344 — which is the same
   fact the sweep above records as "4 and 5 do not occur at any width", and
   the rung to add if that ceiling ever moves.

   MEASURED AGAINST THE GEOMETRY IT REPLACES, at 375, 768, 1024, 1280, 1440,
   1700 and 1920, in Firefox 154, Chromium and WebKit: same column count, same
   cell, same portrait width, same left edge for the picture and the prose —
   nothing moves by more than 0.03 px in any engine at any of the seven, and
   the ratio at those widths is the same 69.79–69.85 %. Swept finer, every
   viewport from 300 to 1600 in Firefox: --cols and the count auto-fill
   actually lays agree at all 651, including both crossings, and the portrait
   holds 69.73–69.90 % — the band the percentage held. What moves is Firefox,
   where the dead space under the last card is 0.

   FOUR MARGINS AND NOT A PADDING, and the reason is a Chromium bug this cell
   walked straight into. The cell is a subgrid spanning four of the parent's
   row tracks (the rule below), and a subgrid's own padding has to be added to
   the first and last of those tracks or the tracks cannot fit inside it.
   Chromium does add it — unless the padding is a PERCENTAGE and the column
   track is flexible, which is both halves of this component: `padding: 15%`
   inside .tiles' `minmax(min(var(--tile), 100%), 1fr)`. Under an fr maximum
   the cell's inline size is not definite while the rows are being sized, the
   percentage resolves to zero there, and the four shared tracks come out one
   padding short at the block start. Reduced to a minimum: the same three
   children, percentage padding, `repeat(3, 1fr)` breaks and `repeat(3, 200px)`
   does not; a px padding never breaks under either.

   WHAT IT DREW IS THE NAME ON THE PORTRAIT. Measured at 1280 on the shipping
   page: four tracks summing 482.36 px — the cell's BORDER-box height — laid
   out inside a 367.55 px content box, so the photo's track is squeezed by
   exactly the 56.91 px of padding that never reached it, the 330.73 px photo
   overflows the track, and the name is drawn 40.9 px above the picture's
   bottom edge. It is 33.9 px at 375 and 52.3 px at 1024; there is no width
   where the cell is right. Firefox and WebKit size the tracks correctly from
   this same stylesheet, so the fault was invisible in two engines out of three
   and visible in the one nearly everyone reads the site in.

   A margin carries the ratio where the padding could not. A grid item's margin
   is part of its outer size and is measured in the same pass as its content,
   so the four lines' own margins reach the tracks in every engine. --inset is
   therefore stated once and spent four times — inline on every line,
   block-start on the first, block-end on the last — and the portrait asks for
   what is left, calc(var(--cell) - 2 * var(--inset)). That subtraction IS the
   70 % this note is about, written as the arithmetic rather than as a second
   number somebody has to keep in step with the first. (It subtracted from
   100 % when this paragraph was written; the term is --cell now and the
   subtraction is unchanged — see the note over the item rule for why the
   percentage had to go.)

   Measured against the two engines that were already right: Chromium now draws
   the cell 596.22 px tall at 1280 where Firefox draws 596.20 and WebKit
   596.17, and the three agree within 0.05 px at 375, 768, 1024, 1280, 1440 and
   1920. Across those six the insets hold 14.95–15.00 % and the portrait
   69.79–69.84 %, and neither of the two engines that were already right moves
   by more than a tenth of a pixel.

   The mockup only insets the top and the sides; the bottom edge there is set
   by the name, and here by the focus line under it — so the block-end margin
   goes on :last-child, which is what "the line that closes the cell" means
   without naming a fourth class. */
.cf-team-grid__item {
  --cols: 1;
  /* One cell's CONTENT box: the grid's content box over the column count,
     less this cell's own border-right. That subtraction is what keeps the
     picture's edges on the prose's — without it --cell is the border box and
     the portrait is drawn 1 px wider than the three lines under it. */
  --cell: calc(100cqi / var(--cols) - var(--stroke-1));
  --inset: calc(0.15 * var(--cell));
  border-right: var(--stroke-1) solid var(--border-strong);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
/* auto-fill's own arithmetic, said in the one place cqi cannot reach it. */
@container cf-team (min-width: 42rem) { .cf-team-grid__item { --cols: 2; } }
@container cf-team (min-width: 63rem) { .cf-team-grid__item { --cols: 3; } }
.cf-team-grid__item > * { margin-inline: var(--inset); }
.cf-team-grid__item > :first-child { margin-top: var(--inset); }
.cf-team-grid__item > :last-child { margin-bottom: var(--inset); }
/* rx="2" on all five portraits in ueber-uns-full.svg (200 x 300 each). A photo
   is an object with a body of its own, so it takes the system corner; the cell
   around it is structure and stays square. */
.cf-team-grid__photo {
  /* The 70 %, as the subtraction rather than as a second number. A replaced
     element does not stretch to its grid area the way the three <p> above do,
     so this one has to state the width its margins leave it — and it states
     it against --cell rather than against 100 %, because a percentage here is
     the term that put 800 px of file into a 372 px box while Gecko was
     measuring the card. See the note over .cf-team-grid__item. */
  width: calc(var(--cell) - 2 * var(--inset));
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-4);
}
/* The enhancement, behind a support query: the fallback above is the current
   shipping layout, so a browser without subgrid loses the cross-card alignment
   and nothing else changes.

   row-gap is 0 and every gap in this card is a margin on purpose. The three
   interior gaps are not equal — 16 under the photo, 4 under the name, 16 over
   the focus rule — and a single row-gap cannot express that. Margins are part
   of a grid item's outer size, so the tracks size to content plus margin and
   the card is drawn exactly as it is without subgrid; the only difference is
   that the four tracks are now shared. That same property is what carries the
   cell's own block inset, for the reason set out above the item rule: a
   percentage padding on this element would not have reached these tracks in
   Chromium, and a margin on the lines does.

   THE COLUMN IS DECLARED BECAUSE THE ROWS ARE SUBGRID, and leaving it out was
   a live overflow. `display: grid` with only the ROW axis stated leaves the
   column axis implicit, and an implicit column is `auto` — which as a MINIMUM
   is the item's automatic minimum size, its min-content width. That is exactly
   the intrinsic-minimum defect check-grid-tracks.py was written for, one axis
   over: the script reads track LISTS, and a rule that declares no track list
   at all has nothing for it to read.

   It cannot overflow anywhere else in the system, and the reason is this
   element's other half. A grid whose own width is free absorbs a wide track by
   growing. This one cannot: the row axis is subgrid, so the card stands in a
   cell of .cf-team-grid whose width auto-fill has already decided, and the
   track grows THROUGH it. Measured on patterns/ueber-uns.html with the
   browser's default font at 24 px — 150 %, a common low-vision setting and
   well inside what WCAG 1.4.4 asks for — where the longest word of the focus
   line stops fitting the 15 % inset:

     viewport   cell   widest line   document      sideways
       320      258       289.3      320 -> 359       39 px
       340      278       289.3      340 -> 362       22 px
       360      298       289.3      360 -> 365        5 px
       380      318       289.3      380 -> 380          0

   Nothing at a 16 or 20 px default, at any width, on any page — which is why
   a sweep run at the default alone reported the system clean. The floor costs
   nothing there: a single `auto` column in a definite-width grid already
   stretches to fill it, so minmax(0, 1fr) draws the identical card and only
   removes the minimum that was never wanted. The word wraps inside the cell
   the way the reset's overflow-wrap always meant it to — break-word does not
   enter intrinsic track sizing, so it could not help while the track was
   still floored at min-content. → foundations/layout.html#intrinsic-minimum */
@supports (grid-template-rows: subgrid) {
  .cf-team-grid__item {
    display: grid;
    grid-row: span 4;
    grid-template-rows: subgrid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0;
  }
}

/* --------------------------------------------------------------------------
   TABLE
   Ruled rows, never a boxed grid: one hairline under the head, one under each
   row, nothing vertical. The manual has no table plate — the 22 plates in
   assets/source/manual/ do not include one and neither mockup carries a table —
   so this is derived from the language rather than measured, the same way
   .cf-plot is: the row rule is the system's hairline, the column head is the
   mono label the section header already uses, and the figures are tabular
   because every number in this system is.

   Vertical rules are the thing deliberately not drawn. A grid of boxes says
   "spreadsheet"; a stack of rules says "reading order", and reading order is
   what a row is. The columns are already held apart by the space between
   them — a line there would be neither an edge nor a division but decoration,
   which foundations/geometry.html bans by name.

   Every colour here is a semantic token, so the same table inverts by being
   put on a dark surface — [data-theme="inverse"], or the consent dialog, which
   rebinds the same four. There is no --inverse modifier and there should not
   be one.
   -------------------------------------------------------------------------- */

/* A table is the one block that legitimately outgrows its column, so it gets a
   scroll container instead of being allowed to widen the page. That container
   is scrollable content with no focusable child, which without a tabindex is
   unreachable by keyboard: mouse users drag it, everyone else never sees the
   last column. WCAG 2.1.1. The markup carries tabindex="0" and takes its
   accessible name from the caption — see components/table.html.

   The focus ring comes from base.css, which already rings :focus-visible on
   anything carrying [tabindex]. Nothing to declare here.

   Three names, one declaration, for the reason the register three rules above
   .cf-vacancy gives: the scroll box is not part of any of the three drawings,
   it is the one mechanism a block that outgrows its column gets, and a second
   copy of it is a second thing to move. → components/calendar.html#narrow */
.cf-table__scroll,
.cf-prose__table-scroll,
.cf-calendar__scroll { overflow-x: auto; }

/* AND THE BLOCK AXIS OF THAT BOX CROPS TOO, which `overflow-x` alone does not
   say. A box that scrolls on one axis cannot overflow visibly on the other, so
   `overflow-y` computes to `auto` and the box clips top and bottom as well —
   the same clause that costs .cf-blog-grid--port its ring, arrived at from the
   other axis.

   Only the calendar has a focusable INSIDE this box: a table's cells hold
   prose and the box itself is the tab stop, but .cf-calendar__day is an <a>
   sitting in a cell with `padding: 0`, so the last row's days end 1 px above
   the scrollport's edge. The ring wants 4 px there and gets 1. Measured on
   components/calendar.html at 1280: the bottom of the indicator cut by 3 px on
   every reachable day in the final week, on all four specimens.

   The room is bought below the table rather than taken out of the ring,
   because there is room to buy: this edge is the box's own, nothing is drawn
   on it, and 4 px of wash under the last hairline is not a spacing decision
   anyone reads. The port could not do this — a card there is sized BY the
   scrollport, so padding on the box comes straight back out of the card — and
   that is the whole reason these two answers differ.

   Block-end only, and deliberately not `padding-block`. The first row of days
   has the head row above it inside the same box, which is air the ring already
   has; 4 px on top would be air nothing needs, above a rule the head sits on.
   → components/calendar.html */
.cf-calendar__scroll { padding-block-end: calc(var(--stroke-2) * 2); }

/* A SCROLL BOX ON PAPER IS A CROP. Nothing scrolls a sheet, so every column
   past the box's width is not hidden-until-you-drag, it is gone — and it goes
   silently, mid-word, with no rule or ellipsis to say a column was dropped.
   Printed through Chromium at a page box narrow enough to force it, the head
   of the last column came out "LETZTE" from LETZTE SPALTE and its cell came
   out "KENNW" from KENNWORTENDE.

   This is the failure mode check-print-fixed.py opens by naming — invisible in
   every screenshot this repository takes, because every screenshot is `screen`
   — and neither gate covers it: that one reads `position: fixed`, and
   check-overflow-clip.py reads `overflow: hidden`. This is `overflow-x: auto`.

   Today's tables all fit A4 and print whole; datenschutz's five columns were
   checked page by page. So this is the latent case, fixed before it lands
   rather than after: a narrower sheet, a larger print stylesheet, or one more
   column, and the cookie inventory the site owes under TDDDG § 25 prints with
   a column missing and no sign that it did.

   Two declarations, because releasing the crop alone only moves the cut from
   the box's edge to the paper's. The head row has to be allowed to wrap before
   the table can compress into the sheet — which is the one place the nowrap
   above binds, and the one place it must not. */
@media print {
  .cf-table__scroll,
  .cf-prose__table-scroll { overflow: visible; }

  .cf-table thead th,
  .cf-table th[scope="col"],
  .cf-prose thead th,
  .cf-prose th[scope="col"] { white-space: normal; }
}

.cf-table,
.cf-prose table,
.cf-calendar {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-md);
  font-variant-numeric: tabular-nums;
  /* The table claims its own ink rather than inheriting whatever surrounds it.
     That is what makes the inverse case work without a modifier: dropped on a
     dark surface it takes the ink of that surface's theme, not the ink of the
     page the surface is sitting on. Inheriting instead left the head row and
     every ordinary cell black on black — the row headers survived only because
     they set a colour of their own. */
  color: var(--text-primary);
}
.cf-table :is(th, td),
.cf-prose :is(th, td) {
  padding: var(--space-3) var(--space-4) var(--space-3) 0;
  text-align: left;
  vertical-align: top;
  border-bottom: var(--stroke-1) solid var(--border-subtle);
}
/* The last cell drops its right padding so the row's content ends on the same
   axis as the rule under it, the way the accordion's arrow sits flush right.
   A ruled row that stops 16 px short of its own hairline reads as unfinished. */
.cf-table :is(th, td):last-child,
.cf-prose :is(th, td):last-child { padding-right: 0; }

/* A column head is a label — mono, uppercase, on the strong hairline.

   It does not wrap, and this comment used to give two reasons for that, of
   which it said the second was the one that mattered: a label broken over two
   lines is read as two words, AND the nowrap is what floors the table's width,
   without which a wide table compresses instead of overflowing and the scroll
   box it sits in never engages. The second reason is not true, and it is the
   one the do/don't was derived from.

   Measured — min-content width of every table in the system, with the heads
   nowrapped and with them allowed to wrap:

     datenschutz, the four inventory and Fristen tables    0 px difference
     table.html, three of the four demos                   0 px difference
     table.html, "Angebundene Quellen"                    15 px difference

   The body cells are the floor, everywhere the system actually draws a table.
   German data words are longer than the labels over them — INSTANDHALTUNG is
   14 characters of 11 px mono under a cell holding the same word at 14 px —
   so the nowrap buys nothing on screen and the do/don't's "a long label here
   is a scroll bar everywhere" was guarding a cost of zero.

   The first reason stands on its own, and it is now the only one: this is a
   typographic rule about labels, the same one the section header and the
   accordion follow, and not a mechanism holding the scroll box open. The
   scroll box engages because the CONTENT is wider than the phone.

   Where the nowrap does bind is paper, and there it binds the wrong way —
   see the @media print block under the scroll box. */
.cf-table thead th,
.cf-table th[scope="col"],
.cf-prose thead th,
.cf-prose th[scope="col"],
.cf-calendar thead th {
  border-bottom-color: var(--border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  white-space: nowrap;
}
/* A row head is not a label, it is the row's subject — content, set in the
   body face at body size. Uppercasing it would shout a cookie's name at the
   reader and cost the mono face its one job, which is marking the things that
   are not prose. */
.cf-table th[scope="row"],
.cf-prose th[scope="row"] {
  font-family: inherit;
  font-size: inherit;
  font-weight: var(--weight-medium);
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-primary);
}

/* The caption is the table's accessible name and its visible title at once,
   which is why it is a caption and not a heading above the block. */
.cf-table caption,
.cf-prose caption,
.cf-calendar caption {
  margin-bottom: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  text-align: left;
  color: var(--text-secondary);
}

/* THE TITLE LEFT THE FRAME IN THE ONE STATE THE SCROLL BOX EXISTS FOR. A
   caption is a child of the table, the table is what scrolls, so scrolling to
   the last column carried the title off the left edge with it. Measured on
   datenschutz at 320: of a 151.2 px caption, 42.2 px was still inside the box
   at full scroll — 72 % of the visible title gone. At a 20 px browser default
   the scroll range is longer than the caption is wide, so it leaves entirely.

   The component's own argument for a caption over a heading is that it stays
   attached to the table. It does — semantically, and the region still takes
   its accessible name from it, so this was never a screen-reader defect. It is
   the sighted phone reader who scrolls to read column five and loses the line
   saying what the rows are.

   THE FIX IS A SPAN AND IT HAD TO BE. `position: sticky` on the caption itself
   does nothing — a table-caption box is not stickyable, measured: still
   42.2 px — and giving the caption `display: inline-block` or `block` to make
   it stickyable drops it into the table's anonymous cell structure and renders
   it UNDER the head row. So the sticky box is one level in, and the caption
   stays a caption.

   A bare <caption> keeps exactly today's rendering. The selector is `> span`,
   so this is an enhancement over the markup .cf-prose promises an author —
   write the bare element and it behaves as it always has, wrap the line and
   the line stays put. */
.cf-table caption > span,
.cf-prose caption > span {
  position: sticky;
  left: 0;
  display: inline-block;
}

/* A numeric column aligns on its right edge so the digits stack, which is the
   whole reason the figures are tabular. Set on the head as well as the cells —
   a right-aligned column under a left-aligned label is a misaligned column. */
.cf-table :is(th, td).cf-table__num,
.cf-prose :is(th, td).cf-table__num { text-align: right; }

/* --------------------------------------------------------------------------
   CALENDAR — one month, and nothing that is not one month

   The system could rule a row, mark a word and number a slot, and it had no
   drawing for a date. This is that drawing, and the whole of it is borrowed:
   there is no new geometry here, no new colour, no new token, and — apart from
   the two names below — no new declaration that is not one of the three
   components it is made of, restated with its reason.

     the frame     .cf-table's own. Ruled rows, nothing vertical, tabular
                   figures, the caption as the accessible name, the scroll box
                   with its three attributes. A month IS tabular: the column is
                   a weekday, the row is a week, and <th scope> is what says so
                   without a line of script.
     the slot      .cf-pagination__page's own. A 2.75rem target with a mono
                   numeral centred in it, no contour at rest, the contour
                   arriving on hover and focus — because at rest a number is
                   type, and under the pointer it is a target larger than its
                   glyph.
     the mark      the found state's own, at numeral scale. A day something
                   stands on is a numeral on its ground line; TODAY is the same
                   numeral with the light behind it. Two rungs, one drawing,
                   held together by --found-* rather than by a shared selector —
                   which is the mechanism base.css states for exactly this.

   SO THE CALENDAR IS THE TABLE READ ON TWO AXES AND FIND-IN-PAGE READ OVER
   TIME, and the second half is the part that is not a pun. The brand is named
   after finding a thing in a page; a month is a page of days; the day the
   reader is standing on is the match they are standing on. foundations/
   found.html settles what that looks like and settles the lime budget with it —
   one lit rung, every other match in contour — and a month has exactly one
   today, so the budget is discharged by arithmetic rather than by a rule
   somebody has to keep.

   WHAT IS NOT DRAWN, and each absence is a rule from somewhere else:

     no vertical rules, no cell boxes. .cf-table's own first sentence. The
     columns are held apart by the space between them; a line there would be
     neither an edge nor a division, which foundations/geometry.html bans by
     name. The week rule is the whole drawing.
     no weekend column. It could only be a vertical rule or a column fill, and
     a fill doing a hairline's job is the zebra stripe .cf-table refuses. SA
     and SO are named in the head row, which is where the difference lives.
     no days of the neighbouring month. A cell holding "31" in a table captioned
     September contradicts its own caption, and the only way to say it is
     another month is to grey it — #919191 on CF-Grau is 2.0:1, the one step
     this system does not have. The cells before the first and after the last
     are empty, and the month step above the grid is how a reader leaves.
     no month stepper inside the component. .cf-table's own don't, unchanged:
     that is the page's navigation and .cf-pagination already draws it.
     no entry titles in the cells. A 44 px cell cannot hold a sentence, and a
     filled chip is the fill this brand spends contour instead of. The grid is
     an index; what stands on a day is a register, and .cf-events below is the
     register's third consumer rather than a fourth drawing.
   -------------------------------------------------------------------------- */
.cf-calendar {
  /* THE ONE MEASUREMENT IN THE COMPONENT, and it is not a new one: 2.75rem is
     .cf-pagination__page's floor, which is .cf-nav__toggle's floor, which is
     WCAG 2.5.5 and Apple's HIG. It is written once here because four
     declarations read it — the slot's two axes and both kinds of cell — and
     four copies of a floor are four floors.

     It is also what gives the table its minimum width. The cells carry it as a
     min-width, so auto table layout resolves every column's min-content to the
     same 2.75rem and the columns come out equal without table-layout: fixed
     being asked for; below the sum of them the table overflows and the scroll
     box engages. That is the same mechanism .cf-table describes for its own
     head labels — a floor the browser cannot compress past — reached by a
     different property because a calendar's floor is a fingertip and a table's
     is a word. */
  --calendar-slot: 2.75rem;
}

/* A day is centred in its own slot, so the cell has no padding of its own and
   nothing to align: the slot is the cell's whole content and it does its own
   centring. This is where the calendar leaves .cf-table's cell rule, which is
   top-aligned, left, and padded on three sides for prose that wraps. */
.cf-calendar :is(th, td) {
  min-width: var(--calendar-slot);
  padding: 0;
  text-align: center;
  vertical-align: middle;
  border-bottom: var(--stroke-1) solid var(--border-subtle);
}
/* The head sits on the strong rule — the shared rule above sets that — and
   needs its own air, since the cell rule it would have inherited padding from
   is the one rule the calendar does not take. */
.cf-calendar thead th { padding-block: var(--space-2); }

/* THE WEEK NUMBER IS OPTIONAL AND IS STYLED WHEREVER IT APPEARS. Nothing here
   turns it on: a calendar that carries the column has the column in its
   markup, and one that does not is the same component with seven cells to a
   row instead of eight. A modifier would be a class saying what the markup
   already says.

   It is the row's subject, which is what makes it a <th scope="row"> rather
   than a first cell — .cf-table's own test. It departs from that component's
   row head in one property and this is the reason: a row head there is set in
   the body face because it is CONTENT, and .cf-table says so about a cookie's
   name. KW 36 is not content, it is a figure, and every figure in this system
   is mono. The same two-part decision .cf-table__num already makes.

   --text-secondary rather than the full ink: the week number names the row, it
   is not in it. Never --text-muted — #919191 on the page wash is 2.0:1, the
   rule every mono label on the wash follows. */
.cf-calendar th[scope="row"] {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-label);
  color: var(--text-secondary);
}

/* THE SLOT. An <a> where the day has somewhere to go and a <span> where it has
   not — the same split .cf-pagination__page makes between a page you can reach
   and the page you are on, and it is what lets every rule below use an `a.`
   prefix to say "only the reachable ones" without a modifier class.

   No contour at rest. A day with nothing on it is a numeral in a month, which
   is type; the box arrives when there is something to point at. Same sentence
   .cf-pagination writes about its own numerals, and the same consequence: the
   target is 44 px and the glyph is 14, so the contour is also the only thing
   that can say the target is larger than what is drawn in it. */
.cf-calendar__day {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--calendar-slot);
  min-height: var(--calendar-slot);
  border: var(--stroke-1) solid transparent;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  /* Tabular, so the 1 and the 30 in one column stand on the same axis — the
     reason .cf-pagination gives for its own slots, and the reason every number
     in this system is set this way. */
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  text-decoration: none;
  transition: border-color var(--duration-fast) var(--ease-standard);
}
a.cf-calendar__day:hover,
a.cf-calendar__day:focus-visible { border-color: var(--border-strong); }

/* THE MARK, and it is the found state at numeral scale.

   The line is under the NUMERAL and not under the slot, which is the whole
   difference between a mark and a cell edge. foundations/found.html draws a
   match as "a word standing on a ground line"; a rule the full 44 px of the
   slot would be the bottom edge of a box, and a box is what this component
   spends its whole header refusing.

   THE TOKENS ARE WHAT KEEP IT ONE DRAWING. base.css states the same five
   declarations five times over — <mark>, .cf-mark--current, ::target-text and
   two ::highlight() names — and says why the repetition is load-bearing and
   why --found-* is what holds them together. This is the sixth statement of
   it, in a component rather than a pseudo-element, and it is held by the same
   thing: move --found-offset and the calendar moves with the search results.

   A LINK IS WHAT MAKES A MARK, structurally rather than by convention. There
   is no class for "something stands on this day": the mark is on `a.` and
   nothing else, so a day cannot be marked unless there is somewhere to go, and
   the drawing cannot be spent on a day the reader can do nothing with. */
a.cf-calendar__day > time,
.cf-calendar__day[aria-current="date"] > time {
  text-decoration-line: underline;
  text-decoration-color: var(--found-rule);
  text-decoration-thickness: var(--found-rule-weight);
  text-decoration-skip-ink: none;
  text-underline-offset: var(--found-offset);
}

/* TODAY. The second rung, and the light — which is the one thing in this
   component that is lime, and the one lime moment on any page carrying it.

   [aria-current="date"] and not a class, because ARIA has the word: "represents
   the current date within a calendar" is the token's own definition, and the
   drawing following the semantic fact is what .cf-pagination__page[aria-current]
   already does one component over. It is also the honest boundary of a static
   site — a claim about the day the page is SERVED is the server's to write,
   the same standing components/arrival.html's states have.

   Not the black plate .cf-pagination gives its current slot, and the two are
   not in conflict. That plate says "the one of this set you chose"; today is
   not chosen and there is no set to have chosen it from. The reader is
   standing on it, and foundations/found.html has already drawn standing on
   something.

   The fill is restated as -webkit-text-fill-color for the reason base.css
   gives at .cf-mark--current: inside a clipping context the inherited fill is
   transparent and beats color, and a lit day would render as an empty lime
   block. Contrast is carried by the rule and not by the fill in any case —
   lime on CF-Grau is 1.37:1 and cannot hold a boundary; --found-rule measures
   13.48:1 there. */
.cf-calendar__day[aria-current="date"] > time {
  background-color: var(--found-light);
  color: var(--found-ink);
  -webkit-text-fill-color: var(--found-ink);
}

/* Forced colours has the pair for this meaning, and this takes it — the same
   call base.css makes for the found state's own current rung, because a lit day
   and a current match are one drawing and may not have two answers here.

   THE PAIR WAS NEVER THE FAULT HERE; THE BOX WAS. What this note used to say,
   and what .cf-pagination__page[aria-current] one component over was read as
   saying, is that Mark/MarkText does not paint under the dark scheme and that
   the fix belonged in base.css, to all five rungs at once. Measured properly —
   Chromium 151, forced-colors: active, both schemes, this component and
   foundations/found.html served and screenshotted — the found state's own two
   element rungs paint correctly in BOTH palettes and always did, and this one
   does not:

                              light                    dark
     .cf-mark (inline)        ink + ground line        ink + ground line
     .cf-mark--current        yellow plate, black ink  yellow plate, black ink
     [aria-current="date"]    WHITE plate, black ink   BLACK BLOCK, no ink
     .cf-pagination[current]  navy plate, white ink    cyan plate, black ink

   The pagination's row is the shape this one is, not the shape a <mark> is, and
   it reached its plate the same way: Highlight/HighlightText under the same
   escape. Its note carries the mechanic and the figures.

   base.css now carries the mechanism in full. Chromium paints an opaque
   Canvas-coloured backplate behind each run of text in this mode, and the
   backplate belongs to the BLOCK CONTAINER that lays the text out, painted in
   that container's own background phase. A <mark> inline in a paragraph is not
   that container, so its yellow goes down on top of the backplate and survives.
   This <time> is a flex item of an inline-flex day — blockified, its own block
   container — so the backplate goes down AFTER its background and takes the
   plate with it: a white plate in the light scheme, where MarkText still reads
   on it, and a black plate in the dark one, where it does not.

   `forced-color-adjust: none` is the one declaration that stops the backplate,
   and it does not leave the reader's palette — every value below is a system
   colour keyword and still resolves to the theme under it. Verified by render:
   the lit day is a yellow plate with a black numeral and a black rule in both
   schemes, which is the drawing every other rung of the found state already
   makes. base.css carries the same escape on `mark` / `.cf-mark`, where it
   changes nothing today and covers the day someone blockifies a match; it
   cannot reach this rule, which is why the declaration is restated here rather
   than inherited from there. What was deferred to base.css was one declaration,
   not a change of pair.

   The three highlight PSEUDO-elements are still the half nothing reaches:
   Chromium discards every author declaration on ::target-text and
   ::highlight(), and forced-color-adjust inside a highlight pseudo is not on
   the closed property list. base.css records that measurement. It costs this
   component nothing — a day is an element. */
@media (forced-colors: active) {
  .cf-calendar__day[aria-current="date"] > time {
    forced-color-adjust: none;
    background-color: Mark;
    color: MarkText;
    -webkit-text-fill-color: MarkText;
    text-decoration-color: MarkText;
  }
}

/* On paper there is no reader to be standing on a day, so the light has nothing
   to rank against and lime prints as a pale wash over the ink. The rule stays,
   which is the rung that was carrying the meaning. base.css prints the found
   state this way for the same reason. */
@media print {
  .cf-calendar__day[aria-current="date"] > time {
    background-color: transparent;
    color: inherit;
  }
}

/* --------------------------------------------------------------------------
   VALUE TABLE
   Icon cell + copy cell, hairline ruled. The Über uns "wer wir sind" block.
   -------------------------------------------------------------------------- */
.cf-value-table {
  /* The icon cell is a SQUARE, and that is what sets the row height. In the
     mockup the icon column is 240 px wide and all three rows are 240 px tall
     to the pixel, though the three blocks of copy are nothing like equal —
     so the cell is a fixed measure the copy grows past, not a box that
     shrink-wraps it. Shipped content-height it gave 214 / 169 / 174 px: three
     different heights, none of them on the 8-point grid, and a ruled table
     whose rows disagree about how tall a row is.

     One custom property because the width and the height are the same
     decision. Split into two literals they drift, and the cell stops being
     square the first time either one is touched. */
  --value-figure: 15rem;   /* 240 — the icon cell, both ways */

  /* The component owns its own list reset, like .cf-nav__list and
     .cf-article__toc do. base.css zeroes ul[role="list"], but relying on that
     would make an ARIA attribute load-bearing for layout: strip the role a
     validator calls redundant and the UA's 40 px padding and 14 px margin come
     back inside the hairline frame. role="list" is then free to do only its own
     job — restoring the list semantics Safari drops when list-style is none. */
  margin: 0;
  padding: 0;
  list-style: none;
  border: var(--stroke-1) solid var(--border-strong);
  container-type: inline-size;
}
.cf-value-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}
.cf-value-row + .cf-value-row { border-top: var(--stroke-1) solid var(--border-strong); }
.cf-value-row__figure {
  display: grid;
  place-items: center;
  min-width: 0;
  padding: var(--space-6);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
/* --iso-travel is a transform, so it is in viewBox units and means something
   different in every frame. The convention foundations/motion.html sets is
   viewBox width / 40 — the same 5 % of the drawing's own width at 640, at 480
   and here. These three figures are 160 units wide, so left at the 640-frame
   default they would arrive from 32 units away: a fifth of their own width,
   where every other object in the system travels a twentieth. */
.cf-value-row__figure .cf-iso { --iso-travel: 4; }
.cf-value-row__body {
  min-width: 0;
  padding: var(--space-6);
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
}
.cf-value-row__body > * + * { margin-top: var(--space-4); }
/* The line stops before the cell does. --measure is 56ch and tokens.css says
   what it is for in as many words: "right for a block of copy that sits beside
   something else", which is this row's whole shape — an icon and a paragraph.
   The cap is on the children rather than on .cf-value-row__body because the
   body carries --space-6/-8 of padding, and a max-inline-size on the box would
   spend the measure on that padding instead of on the line.

   Without it the copy has no measure at any width above a phone, and the
   tablet is the worst point rather than the widest one. Measured on
   patterns/expertise.html, characters per line:

       viewport   375     768    1024    1280    1920
       before    36.5    81.4    68.1    93.7   109.6
       after     36.5    56.0    56.0    56.0    56.0

   768 is worse than 1024 because the two move against each other: the 48rem
   container query has not fired yet, so the copy is still a full-width band —
   and it is still at --text-md, where the split steps it up to --text-lg. The
   widest column and the smallest type land on the same width, and it is the
   one width nobody opens. */
.cf-value-row__body > * { max-inline-size: var(--measure); }
@container (min-width: 48rem) {
  .cf-value-row { grid-template-columns: var(--value-figure) minmax(0, 1fr); }
  .cf-value-row__figure {
    /* Square, so the row is at least as tall as the icon column is wide.
       Only inside the container query: below 48rem the figure is a full-width
       band above the copy, not a column, and 240 px of it would be a wall. */
    min-height: var(--value-figure);
    padding: var(--space-8);
    border-bottom: 0;
    border-right: var(--stroke-1) solid var(--border-strong);
  }
  /* THE COPY SITS ON THE FIGURE'S AXIS, NOT ON THE ROW'S TOP EDGE. The figure
     cell above is place-items: center, so the drawing stands at mid-row — and
     the copy beside it stood at the top, which put the two halves of one row
     on two different axes: a three-line block hanging off the top rule with
     170 px of empty cell under it, next to a figure centred 120 px down.
     Optically the row read as a caption that had slipped.

     The mockup centres both. Pixel-measured on assets/source/mockups/
     ueber-uns.jpg, scaled to the 1440 frame: all three rows 240.0 px tall,
     the figure's mid-line 0.6 px off the row's, the copy's mid-line -3.6 and
     +1.8 px off it on the two rows whose copy is short enough to move — one
     construction drawn three times. A row whose copy outgrows the square
     stretches to the copy and the alignment is a no-op, so nothing changes
     where the content already set the height.

     The inline inset is measured off the same plate: the copy starts 49.2 px
     right of the divider, which is --space-12 (48) and not the --space-8 the
     block edges keep. The figure column is a 240 square, and 48 into the
     copy column is what lets the drawing and the first letter breathe by the
     same amount; the block padding stays at 32 because it only ever governs
     the rows the copy has already overgrown, where the mockup has no case to
     measure. → components/statement.html#value-table */
  .cf-value-row__body {
    align-self: center;
    padding: var(--space-8) var(--space-12);
    font-size: var(--text-lg);
  }
}

/* --------------------------------------------------------------------------
   CULTURE BAND
   A photograph and a claim about the place, hairline ruled, the picture
   changing sides down the band. The Über uns "Wie wir arbeiten" block.

   THE PHOTOGRAPH IS BOUNDED BY THE CELL AND BY NOTHING ELSE, which is
   foundations/photography.html in as many words: "Photos get no rounded
   corners (except round avatars), no frame and no shadow. When an image
   needs to be bounded, the 1 px contour of the surrounding cell does it."
   So there is no --radius-sm here and no padding in the figure cell — the
   frame is the same hairline that rules the band, and the picture runs to
   it. That is the one place this component deliberately differs from
   .cf-team-grid__photo above, which carries the corner because the mockup
   draws its five portraits at rx="2" INSET in their cells; a portrait is an
   object on a plate, and this is a window cut in one.

   THE WIDE TRACK HOLDS THE WIDE PICTURE. Two ratios come out of the shoot —
   the working frames are 3:2 landscape and the close ones 4:5 — and a band
   that forced both into one window would crop half of them to fit a rule
   nobody asked for. Instead the tracks are fixed at 3fr / 2fr for every row
   and the FIGURE moves: a landscape frame takes the 3fr side, a portrait
   frame the 2fr side. In the 1280 content column that is 768 x 512 against
   512 x 640 — two row heights 128 px apart, which reads as rhythm rather
   than as two components. It also means the picture changes sides on its
   own, without an :nth-child rule deciding it, because orientation and side
   are the same fact stated once.

   minmax(0, Nfr) and not a bare Nfr: base.css warns twice that an fr floors
   at min-content, and the copy cell holds German compound nouns.

   THE FIGURE'S RATIO IS A FLOOR, NOT A BOX. aspect-ratio yields to a
   definite height, so on a row whose copy runs longer than the picture the
   cell stretches and the img — width and height 100 % under object-fit:
   cover — fills the taller cell instead of letterboxing inside it. The
   check-figure-letterbox.py failure needs a drawing that stops growing
   while its box does not; a cover crop never stops.
   -------------------------------------------------------------------------- */
.cf-culture {
  /* Same list reset and the same reason as .cf-value-table above: role="list"
     restores the semantics Safari drops with list-style: none, and must not
     also be load-bearing for the UA padding inside a hairline frame. */
  margin: 0;
  padding: 0;
  list-style: none;
  border: var(--stroke-1) solid var(--border-strong);
  container-type: inline-size;
}
.cf-culture__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}
.cf-culture__row + .cf-culture__row { border-top: var(--stroke-1) solid var(--border-strong); }
.cf-culture__figure {
  /* <figure> carries a 40 px UA inline margin that would stand the picture
     off its own cell wall on both sides. */
  margin: 0;
  min-width: 0;
  /* display: grid so the img can take a definite height off the cell whether
     that height came from the ratio or from a stretched row. */
  display: grid;
  aspect-ratio: var(--ar);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
.cf-culture__photo {
  display: block;
  width: 100%;
  height: 100%;
  /* A grid item's automatic minimum size is its CONTENT, and an <img>'s content
     is its intrinsic box — so the 1067 x 1600 portrait frame pushed its own
     cell to 449 x 672 and the aspect-ratio above was decoration. Measured at a
     1265 viewport before this line: figure 449 x 672 against the 449 x 561 the
     4:5 ratio asks for, with the copy beside it 178 tall. Same automatic
     minimum, same one-line fix as the fr track lists in base.css. */
  min-block-size: 0;
  object-fit: cover;
  /* Per frame, because a cover crop throws away a real part of a real
     picture and which part is a decision. The default is the centre; a row
     states --pos when the faces are not there. Same argument as the align
     keyword in check-slice-crop.py, one medium over. */
  object-position: var(--pos, 50% 50%);
}
.cf-culture__body {
  min-width: 0;
  padding: var(--space-6);
}
.cf-culture__title {
  font-size: var(--text-h4);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}
.cf-culture__body p {
  margin-top: var(--space-3);
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}
/* The line stops before the cell does — .cf-value-row__body's note carries the
   measurement and the same argument for putting the cap on the children rather
   than on the padded box. --measure is 56ch: in the 2fr track the cell is
   narrower than that and does the capping itself, and in the 3fr track this is
   what stops a 673 px cell from setting 90-character lines. */
.cf-culture__body > * { max-inline-size: var(--measure); }

/* 56rem is the system's two-column threshold — see the breakpoint register in
   tokens.css. The band splits where every other two-column split on the site
   splits, and below it each row is a picture with its claim under it. */
@container (min-width: 56rem) {
  .cf-culture__row { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .cf-culture__figure { border-bottom: 0; }
  .cf-culture__body {
    padding: var(--space-8);
    /* Short copy centres against a 512 px picture; long copy fills the cell
       and this does nothing. */
    align-self: center;
  }
  /* BOTH CELLS STATE grid-row: 1, and the second one is the load-bearing half.
     A grid item with a definite COLUMN and an auto row is placed by the sparse
     auto-placement cursor, which has already moved past the figure — so on the
     rows where the picture is drawn second the copy fell to a row of its own
     underneath it, 561 px below where it belongs, and the row grew to hold
     both. It renders as a paragraph adrift at the bottom of an empty cell,
     which is exactly what it was. */
  .cf-culture__row > * { grid-row: 1; }
  /* Landscape frame: the 3fr side, picture first. */
  .cf-culture__row--wide > .cf-culture__figure { grid-column: 1; border-right: var(--stroke-1) solid var(--border-strong); }
  .cf-culture__row--wide > .cf-culture__body   { grid-column: 2; }
  /* Portrait frame: the 2fr side, so the picture crosses to the right and the
     copy leads. The figure stays FIRST in the source on both kinds of row —
     the picture is the subject of the claim beside it, and a reader on a
     screen reader should meet it in that order whichever side it is drawn on. */
  .cf-culture__row--tall > .cf-culture__figure { grid-column: 2; position: relative; }
  .cf-culture__row--tall > .cf-culture__body   { grid-column: 1; }
  /* THE RULE BELONGS TO THE BOUNDARY, NOT TO THE CELL DRAWN BESIDE IT, and a
     border cannot say so on this band. The picture changes sides row by row, so
     a border on the figure changes WHICH SIDE OF THE COLUMN LINE the hairline
     falls on: `border-right` in a wide row is the last pixel INSIDE track 1,
     `border-left` in a tall row is the first pixel INSIDE track 2. Same
     boundary, opposite sides of it, and the line steps sideways at every row.

     Measured at 1440 on a 2x capture before this rule: the vertical hairline
     stood at x 847 down both wide rows and at x 848 down both tall rows — four
     1 px jogs from the top of the band to the bottom, and with them a 2 px
     stagger between the pictures' facing edges (the wide frame ended at 846.8,
     the tall one began at 848.8). One pixel is not a rounding artefact here:
     it is the whole stroke, so the eye reads the line as broken rather than as
     thin. It is what got reported, in those words: "the pictures are not
     aligned".

     So the tall row draws the rule where the wide row draws it — the last pixel
     of track 1 — by reaching back across the line for it. `right: 100%` against
     the figure's own padding box is that pixel stated as a position rather than
     as a second number: no track width is repeated here, so the rule cannot
     drift from the 3fr / 2fr split above it. A hairline on a cell's ::before is
     the construction .cf-subdiv__cell already uses for the same job ("every
     cell draws its own leading rule"), and it is deliberately NOT a box-shadow:
     foundations/photography.html gives photographs "no frame and no shadow",
     and a shadow on a figure is that sentence's exact subject however
     structural the intent.

     No border-left on the figure any more, so the picture reaches the rule
     instead of stopping a pixel short of it. Under the global border-box the
     figure's own box is unchanged either way, so the row heights the band's
     rhythm is measured in — 511 against 640 — do not move.
     → scripts/check-band-rule-continuity.py */
  .cf-culture__row--tall > .cf-culture__figure::before {
    content: "";
    position: absolute;
    inset-block: 0;
    right: 100%;
    width: var(--stroke-1);
    background-color: var(--border-strong);
  }
}

/* The group shot that closes the team grid. 16:9 and full container width, per
   the crop table on foundations/photography.html, which sanctions the group
   frame "only as a section closer" — this is the section it closes.

   The source frame is 3:2, so the cover crop takes 25 % off its height. The
   studio group stands in the upper two thirds with headroom above it, so the
   crop is taken at 42 % rather than at the centre: at 50 % it ate the front
   row's hands and left backdrop above the heads. */
.cf-team-closer {
  display: grid;
  margin-top: var(--space-12);
  border: var(--stroke-1) solid var(--border-strong);
  aspect-ratio: 16 / 9;
}
.cf-team-closer__photo {
  display: block;
  width: 100%;
  height: 100%;
  /* The same automatic minimum as .cf-culture__photo, and it bit here too:
     without it the 3:2 file held the box at 1124 x 750 and the 16:9 above
     never cropped anything. */
  min-block-size: 0;
  object-fit: cover;
  object-position: 50% 42%;
}

/* --------------------------------------------------------------------------
   FORM
   Underlined fields, mono labels. No boxes — the hairline is the input.
   -------------------------------------------------------------------------- */
.cf-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* WHAT ONE ROW OF A FORM IS TALL: one line of text at the document's own
     leading, the field's padding twice, and the rule under it — 16 x 1.45 + 24
     + 1 = 48.2. An <input> arrives at this by itself and never needed it
     written down. A <select> does, and so does the chevron that has to sit in
     the middle of one; both are below. */
  --field-line: calc(var(--text-lg) * var(--leading-relaxed) + 2 * var(--space-3) + var(--stroke-1));
}
/* The <legend> of a choice group is named here rather than given its own copy
   of these six declarations: it is the same thing one level up — the name of
   what is being asked — and two rules would let the face of a label and the
   face of a legend drift apart. See CHOICE GROUP below. */
.cf-field__label,
.cf-choice__legend {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.cf-field__input,
.cf-field__textarea,
.cf-field__select,
.cf-field__file {
  width: 100%;
  padding: var(--space-3) 0;
  background: transparent;
  border: 0;
  border-bottom: var(--stroke-1) solid var(--border-strong);
  border-radius: var(--radius-none);
  font-size: var(--text-lg);
  font-weight: var(--weight-light);
  transition: border-color var(--duration-fast) var(--ease-standard);
}

/* THE ONE CONTROL THE OPERATING SYSTEM DRAWS, and while it does, half of the
   rule above it is advisory. A <select> left at appearance: auto is a native
   menulist, and a menulist answers to its own UA stylesheet first. Measured on
   the shipping page, one engine each:

     Chromium   line-height computes to "normal" whatever is declared — set one
                inline and read it back and it is still "normal" — so the field
                came out 47.5 px against 48.2 for the inputs beside it.
     WebKit     padding computes to 0, min-height to the UA's 18 px, and the
                field came out 25 px. Not three quarters of a pixel: half a
                field. On patterns/kontakt.html, where two columns of fields
                stand side by side, THEMA's hairline sat 23.94 px above
                ZEITRAHMEN's while the three pairs above them were exact, and
                on patterns/bewerbung.html the position field was a squat box
                in a column of lines.

   appearance: none hands the box back — padding, min-block-size and leading
   all apply, and all three engines then measure the same --field-line as the
   inputs. It also takes the disclosure triangle with it, which is why the
   markup around a select is not just the select: .cf-field__control below, and
   the system's own #cf-chevron-down in it. foundations/iconography.html has
   listed that glyph's use as "expand, open a select" since the set was drawn.

   -webkit-appearance as well as appearance, because Safari answered only to
   the prefixed property until 15.4 and this is the declaration that decides
   whether the field has a height at all. */
.cf-field__select {
  appearance: none;
  -webkit-appearance: none;
  /* Room for the chevron: 16 px of glyph and 8 px of air, so a long option
     ("Machine Learning Engineer (m/w/d)") stops before it rather than under
     it. Only the right side — the block padding is the shared rule's. */
  padding-right: var(--space-6);
  min-block-size: var(--field-line);
}

/* The box a select shares with its chevron. Nothing else in this form needs
   one: every other control draws its whole self.

   The chevron is absolute and aria-hidden, so where it sits in the markup does
   not matter — which is what makes room for the one thing that also lands in
   here. worker/render.js writes a field's error message in after the control
   it belongs to, and for a select that is now inside this box rather than
   beside it; the second rule is that message keeping the gap it would have had
   as a child of .cf-field.

   The chevron hangs off the top of the box and not its middle for the same
   reason: with an error under it the box is two rows tall, and a chevron
   centred on that would drift down to the message. Half a --field-line from
   the top is the middle of the select whatever is under it. */
.cf-field__control { position: relative; }
.cf-field__control > .cf-field__error { display: block; margin-top: var(--space-2); }
.cf-field__chevron {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(var(--field-line) / 2);
  transform: translateY(-50%);
  /* The click belongs to the select underneath. Nothing here is a target. */
  pointer-events: none;
}

/* A FILE FIELD IS STILL A LINE, and that is the whole of the design decision.
   The browser's own control is a button plus a filename, which is a box in the
   middle of a form made of underlines; what is styled here is the button back
   down to the system's button idiom and the filename down to the field's own
   type, so the row reads as the same kind of thing as the four rows above it.
   Nothing is hidden and nothing is replaced by a label: a file input that is
   visually removed and driven by a <label> loses its focus ring in Safari and
   its filename readout everywhere, and this site has no script to put either
   one back.

   --text-md and not --text-lg, alone among the controls: the value in this
   field is a filename the reader did not choose the length of, and at --text-lg
   a normal "Lebenslauf_Nachname_2026.pdf" runs into the button. */
.cf-field__file {
  font-size: var(--text-md);
  color: var(--text-secondary);
}
/* THE BUTTON IS .cf-btn--outline, so it is drawn and it reacts like one. A
   transparent plate with a --border-strong contour and a mono label in caps is
   that button's construction exactly; what it was missing was that button's
   behaviour, and both halves of what it had instead were inert.

   THE RADIUS WAS A DANGLING TOKEN. This rule asked for --radius-pill, which
   tokens.css deleted — "Nothing in the source is a pill … It is gone; do not
   reintroduce it. If a rounded control is needed, it is 2 px." An unresolvable
   var() with no fallback is invalid at computed-value time, so border-radius
   fell back to its initial 0 and the button has been rendering square by
   accident, off a declaration asking for the one shape the system forbids.
   It is --radius-sm now, which is what .cf-btn carries and what the token note
   prescribes: this is a control, and a control is the thing that takes the
   2 px. The corner is a decision again rather than a fault that happened to
   land on the right answer.

   AND THE HOVER PAINTED NOTHING. `border-color: var(--text-primary)` sat on a
   border already declared `var(--border-strong)`, and both tokens resolve to
   #000000 — so the state changed the contour from black to black. Measured on
   patterns/bewerbung.html at 1280, the field's own box captured at rest and
   under the pointer: 0 differing pixels. There is nothing under this contour
   to go darker to, which is the same wall .cf-btn--solid names ("the plate is
   black at rest, so hover has to LIFT"); an outline button answers it by
   inverting instead, and that is the declaration borrowed here rather than
   restated. --duration-base and not --duration-fast, because this is now the
   control acknowledging rather than a hairline changing weight.

   The inversion is background and colour, so forced-colors mode drops it —
   correctly. Hover is not an indicator that mode has to preserve, it has its
   own conventions for one, and the contour that survives there is the border
   this rule no longer touches. The FOCUS indicator is the part that may not
   depend on a background, and it does not: see the focus rule below. */
.cf-field__file::file-selector-button {
  margin-right: var(--space-3);
  padding: var(--space-1) var(--space-3);
  background: transparent;
  border: var(--stroke-1) solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
  cursor: pointer;
  transition: background var(--duration-base) var(--ease-standard),
              color var(--duration-base) var(--ease-standard);
}
.cf-field__file::file-selector-button:hover {
  background: var(--cf-schwarz);
  color: var(--grey-000);
}
.cf-field__textarea { resize: vertical; min-height: 7rem; }
.cf-field__input::placeholder,
.cf-field__textarea::placeholder { color: var(--text-muted); }
/* The line goes to 2 px and the padding gives the pixel back, so focusing a
   field does not push everything under it down by one. The thickening is the
   whole indicator here — and it may be, for the three controls named below,
   because on each of them the line IS the control: the caret lands on it and
   the reader's own next keystroke appears above it.

   THERE ARE TWO CONTROLS THAT LINE CANNOT SPEAK FOR, not one, and the second
   was in this list for six characters' worth of resemblance. The read-only
   field is the case the rule below already names — no line left to thicken, so
   it keeps the global ring. THE FILE FIELD IS THE MIRROR OF IT: it has a line,
   which is why it looked like it belonged here, but the line is not where the
   control is. What a reader operates is the button at the far left; there is
   no caret, and nothing they type appears anywhere. So the indicator was a
   hairline gaining one pixel, the width of the whole field, 22 px below and
   ~110 px to the right of the only thing on the row that does anything.

   Measured on patterns/bewerbung.html at 1280, the CV field's box captured at
   rest and again after a real Tab onto it, differenced pixel by pixel: 655
   pixels changed, every one of them inside a single 655 x 1 row at the bottom
   edge. Zero of them on the button. WCAG 2.4.11 asks a focus indicator for the
   area of a 2 px perimeter of the component or sub-component and for 3:1
   against its own unfocused state; a detached 1 px line clears neither test,
   and 1 px also forfeits the "no thinner than 2 CSS pixels" alternative.

   So .cf-field__file comes out of this list, and that is the entire fix: with
   the override gone, base.css's ring — the system's one focus indicator, 2 px
   of --focus-ring at 2 px offset, around the input the reader is actually on —
   applies to it the way it applies to every other input, and the border stays
   1 px so nothing gains a second indicator. Removed rather than overridden
   after the read-only rule's shape, deliberately: an override here would tie
   with this rule at (0,2,0) and be decided by source order, which is the trap
   the .cf-gantt__mark--lit note at the foot of this file was written about.

   Outline and not the inversion the button takes on hover: an outline colour
   is mapped by forced-colors mode and a background is discarded, so this is
   the half of the component that survives Windows high contrast. */
.cf-field__input:focus,
.cf-field__textarea:focus,
.cf-field__select:focus {
  outline: 0;
  border-bottom-width: var(--stroke-2);
  padding-bottom: calc(var(--space-3) - var(--stroke-1));
}
.cf-field__hint { font-size: var(--text-sm); color: var(--text-secondary); }
.cf-field--invalid :is(.cf-field__input, .cf-field__textarea, .cf-field__select, .cf-field__file) {
  border-bottom-color: var(--feedback-error);
}
.cf-field__error { font-size: var(--text-sm); color: var(--feedback-error); }

/* --------------------------------------------------------------------------
   NOT AVAILABLE, AND NOT EDITABLE — two states, two different claims

   A field is a line. So the two states a field can be in without being
   *wrong* are two things that can happen to a line, and the ladder in
   components/arrival.html already has the rungs:

     disabled   the field is here and is not available. That is rung 2,
                --presence-near (2-1) — the rung the ladder reserved for
                "a claim about the object, not about the wait" and which
                nothing in the system had ever claimed. The drawing is the
                same field one rung down: same box, same size, same place.
     readonly   the value is here and is not yours to change. It is not a
                control at all any more, so it loses the line entirely and
                becomes what .cf-contact already is — a mono term with its
                value under it. Nothing is drawn as a place to type, because
                there is nowhere to type.

   The two are never the same drawing and never the same markup, and the
   difference is not cosmetic: a disabled control is skipped by assistive
   technology and does not submit, a read-only one is announced, focusable,
   copyable and submits. Reach for readonly whenever the value still matters
   to whoever is reading the form.

   Written as [disabled] / [readonly] rather than :disabled / :read-only on
   purpose. :read-only matches every element that is not user-editable — a
   disabled input is one, and a <select> is one *always*, since selects
   cannot be read-only — so a :read-only rule here would silently strip the
   line off every select on the site.

   CSS gives no control over the dash-to-gap ratio of border-style: dashed
   and the four line types are ratios, so 2-1 is a repeating gradient sized
   to the border box — the same construction as .rule--dashed-2-1 in
   base.css and .cf-arrive__plate below. Change one, change the others. */
/* AND THE FILE FIELD IS IN THIS LIST, which it was not. Four controls carry
   .cf-field__*, three were named here, and the ladder is doctrine rather than
   decoration: "a disabled field is the same field one rung down". Measured by
   building one of each disabled and reading them back, the file field kept a
   SOLID black 1 px line where the other three drop to the 2-1 gradient, and
   kept `cursor: default` where the other three say `not-allowed`. Its ink was
   the only part that matched, and by coincidence — .cf-field__file already
   sets --text-secondary at rest, so the one property that looked right was the
   one this rule was not supplying.

   The failure shape is the hand-written selector list, which this repository
   has now named four times in three files: the overflow-wrap net, the focus
   ring's five tags plus [tabindex], the fluid record, and this. Each was a
   list of the things a person thinks of as belonging rather than of the things
   that actually match the class. */
.cf-field__input[disabled],
.cf-field__textarea[disabled],
.cf-field__select[disabled],
.cf-field__file[disabled] {
  border-bottom-color: transparent;
  background-image: repeating-linear-gradient(90deg,
    var(--border-strong) 0 4px, transparent 4px 6px);
  background-origin: border-box;
  background-clip: border-box;
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% var(--stroke-1);
  color: var(--text-secondary);
  cursor: not-allowed;
}

/* No line, so the focus indicator cannot be the line getting thicker: this is
   the one field state that keeps the global :focus-visible ring from base.css,
   which the rule above turns off for every field that still has one. */
.cf-field__input[readonly],
.cf-field__textarea[readonly] {
  border-bottom-color: transparent;
  cursor: default;
}
.cf-field__input[readonly]:focus-visible,
.cf-field__textarea[readonly]:focus-visible {
  outline: var(--stroke-2) solid var(--focus-ring);
  outline-offset: 2px;
  border-bottom-width: var(--stroke-1);
  padding-bottom: var(--space-3);
}

/* "(optional)" rather than an asterisk on everything else. Most fields on a
   contact form are required, so marking the exception is the smaller mark —
   and it needs no legend explaining what a symbol means. Lowercase and in the
   body face so it reads as a word inside the label, not as a second label.

   AND IT MAY NOT MAKE THE LABEL TALLER. It is a 12 px word inside an 11 px
   line, and a line box is as tall as its tallest inline box — so at the
   inherited 1.45 the marker, not the label, decided how tall the label was.
   Measured on patterns/kontakt.html at 1280, an unmarked label against a
   marked one:

                     unmarked   marked
       Chromium        15.94    16.94
       WebKit          15.94    16.31

   One field, nobody sees it. Two columns of fields beside each other — which
   is what that page is — and the marked column walks away from the unmarked
   one a pixel a row: by the fourth pair of hairlines the two columns stood
   2 px apart in Chromium and 0.75 in WebKit, which is a grid that has stopped
   being one. --leading-tight puts the marker's own box (12.2 px) back inside
   the strut, where it decides nothing, and all nine labels measure 15.94. The
   baseline it sits on is the label's either way, so nothing about the drawing
   moves. */
.cf-field__optional {
  margin-left: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-light);
  line-height: var(--leading-tight);
  letter-spacing: 0;
  text-transform: none;
}

/* A FORM THAT IS A ROW, AND THE TAIL THAT FOLLOWS IT DOWN.

   patterns/kontakt.html is the one form on this site whose fields stand in two
   columns, so the <form> itself carries the grid — .grid .grid--early
   .grid--sections — and the columns are cells of it. Three cells: the required
   fields (7), the optional ones (5), and everything that comes after the last
   field of BOTH (7 again, in the row below).

   That third cell is why this class exists. .grid--sections gives the row
   --section-gap-sm between its rows, which is the right distance for the one
   boundary the STACKED form has — two sections under one another, each opening
   with its own .cf-section-header — and the wrong one for the boundary the
   TWO-COLUMN form has, which is not a boundary at all: the third cell is the
   tail of the first, the hint and the button that belong under the last field
   of the left column. Between a field and the button under it the distance is
   --space-8, the same rhythm .flow-8 keeps inside both columns, and it was
   --space-8 for as long as the button was a child of the left column.

   So the row gap is a section gap while the row is stacked and the form's own
   rhythm once it is not. The threshold is the row's own: .grid--early takes
   its columns at 44rem of container, and this takes its rhythm at the same
   width, because it is the same event seen from the other side.

   The class sits on the form and not on `form` itself: patterns/bewerbung.html
   has two columns and a form as well, and there the <form> is INSIDE the left
   cell — its submit button is already the last thing in it, and the right cell
   is prose standing beside the form rather than a second half of it. One
   arrangement, one class, and the other page keeps saying what it says. */
@container layout (min-width: 44rem) {
  .cf-form { row-gap: var(--space-8); }
}

/* --------------------------------------------------------------------------
   ERROR SUMMARY
   The list of everything wrong, above the form, each item linking to its
   field. Server-rendered: it exists in the response to a failed POST, so it
   needs no script — the browser lands on it because the form's action carries
   the fragment, and role="alert" announces it when a script does inject it.

   Drawn as a contour box like every other framed thing in the system, but in
   the error colour and at 2 px. It is the one place the system raises a
   contour above 1 px on screen: it has to be findable at a glance on a page
   that is otherwise all hairlines, and colour alone cannot carry that.
   -------------------------------------------------------------------------- */
.cf-error-summary {
  padding: var(--space-6);
  border: var(--stroke-2) solid var(--feedback-error);
  border-radius: var(--radius-none);
}
.cf-error-summary__title {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--feedback-error);
}
.cf-error-summary__list {
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}
.cf-error-summary__list li + li { margin-top: var(--space-2); }
.cf-error-summary__list a {
  color: var(--feedback-error);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* --------------------------------------------------------------------------
   CHECKBOX
   A real <input type="checkbox">, restyled. `appearance: none` keeps the
   element — and therefore its role, its state and its keyboard behaviour —
   and only takes the paint away, so nothing has to be reimplemented in ARIA.

   Square, 1 px contour, no radius: the same box the rest of the system draws.
   Checked fills with the foil rather than with lime, because a checkbox is
   never the one thing happening on a screen. The tick is two borders on a
   45° rotation — 45° and 135° are both sanctioned angles.
   -------------------------------------------------------------------------- */
/* One number, two scopes. The box is 20 px, and the meta line under a consent
   row hangs off the label rather than off the row — so its indent is that box
   plus the row's gap. Declared on a selector both can read, so the sum below is
   composed rather than hand-added. It used to be a literal `2rem`, which was
   correct only for as long as nobody resized the box: either half could move
   and the meta would have gone quietly out of alignment with no failing check
   and nothing visibly broken. A derived value cannot drift from what it derives
   from — that is the whole reason it is written as arithmetic. */
.cf-check, .cf-radio, .cf-consent__row { --check-box: 1.25rem; }

.cf-check,
.cf-radio { display: flex; align-items: flex-start; gap: var(--space-3); }
.cf-check__box,
.cf-radio__box {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  position: relative;
  width: var(--check-box);
  height: var(--check-box);
  margin: 0;
  background: transparent;
  border: var(--stroke-1) solid var(--border-strong);
  border-radius: var(--radius-none);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-standard);
}
.cf-check__box:checked,
.cf-radio__box:checked { background: var(--gradient-foil); }
.cf-check__box:checked::after {
  content: "";
  position: absolute;
  left: 0.375rem;
  top: 0.125rem;
  width: 0.3125rem;
  height: 0.625rem;
  border: solid var(--cf-schwarz);
  border-width: 0 var(--stroke-2) var(--stroke-2) 0;
  transform: rotate(45deg);
}

/* THE RADIO IS THE SAME BOX WITH A DIFFERENT MARK, and the box stays square.
   Every other system draws a circle here, and this one may not: corners are
   square and the three exceptions the manual grants are the logo, the nav bar
   and a round avatar — none of which is a control. So single choice and
   multiple choice are told apart by the mark inside the box rather than by the
   shape of the box: a tick for "and", the lattice rhombus for "or". The
   rhombus is the system's own unit — the cell every isometric object is cut
   from, and the same mark .cf-progress fills with light at its head.

   Drawn entirely out of border, like the tick above, and for the same reason:
   forced-colors mode discards background-color and maps border colours into
   its own palette, so a mark made of border survives Windows high contrast
   and a mark made of fill does not. A zero-sized box with a border on all
   four sides paints a filled square of twice the border width; turned 45° it
   is the rhombus. 0.3125rem of border is a 10 px square, 14.1 px across the
   diagonal, inside a 20 px box. */
.cf-radio__box:checked::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  border: 0.3125rem solid var(--cf-schwarz);
  transform: translate(-50%, -50%) rotate(45deg);
}

/* The box carries "not available" as ink where the field above carries it as
   a line type, and the reason is that a box is four edges: a controlled dash
   ratio has to be a gradient (see the field), and the checked state has
   already spent this element's background on the foil. Composited, the
   contour reads 7.30:1 on CF-Grau and 9.23:1 at the white end of the wash —
   both well over the 3:1 a UI contour owes, and a disabled control is exempt
   from that floor anyway. The cursor goes on the row, not on the input, so
   the label reads as unavailable too.

   The label keeps its full ink at every state. On a consent row the disabled
   control is `Notwendig`, whose copy is the part a supervisory authority
   would read; dimming legally required text to signal that its switch is
   fixed would be trading the wrong thing away. */
.cf-check__box:disabled,
.cf-radio__box:disabled { cursor: not-allowed; opacity: 0.72; }
.cf-check:has(:disabled),
.cf-radio:has(:disabled) { cursor: not-allowed; }

.cf-check__label,
.cf-radio__label { font-size: var(--text-md); line-height: var(--leading-relaxed); }
.cf-check__label > strong,
.cf-radio__label > strong {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   CHOICE GROUP
   A set of radios or checkboxes that answer one question. <fieldset> and
   <legend>, which is the whole reason this exists: a radio is labelled by its
   own row and a screen reader reading the third row of five has no way to know
   what the question was. The legend is announced with every control in the
   group, so it supplies it. role="radiogroup" + aria-label is the fallback for
   when the native element cannot be used, and it is not needed here — the
   native pair is the recommended construction and it brings the arrow-key
   model, the wrapping and the one-tab-stop-per-group behaviour for free.

   The legend is the same mono label a .cf-field carries, because it is the
   same thing one level up: the name of what is being asked. Its rules are
   written as a selector list with .cf-field__label rather than copied, so a
   change to the label's face reaches both.

   Three UA resets. A <fieldset> ships a border, padding and a margin nothing
   here wants; and `min-inline-size: 0` is the one that is not cosmetic —
   fieldset resolves its min width to min-content, which stops its children
   ever shrinking below their longest word and puts a horizontal scrollbar on
   any group holding a long line on a phone. A <legend> is not an ordinary
   box either, so its own padding is zeroed rather than assumed.
   -------------------------------------------------------------------------- */
.cf-choice {
  margin-inline: 0;      /* the UA's 0 2px, and only that — a blanket `margin: 0`
                            here would also beat .stack > * + * from base.css,
                            since this file loads second, and every choice group
                            in a stacked form would close up against the field
                            above it. Reset what the UA sets, nothing more. */
  padding: 0;
  border: 0;
  min-inline-size: 0;
}
.cf-choice__legend { padding: 0; }
.cf-choice__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-4);
}
.cf-choice > .cf-field__hint,
.cf-choice > .cf-field__error { display: block; margin-top: var(--space-3); }

/* An invalid group marks the group once, not five identical rows — the answer
   is wrong, not any one of the options. A field carries that on its line and a
   group has no line, so the legend carries it and the message states it. The
   obvious drawing — a 2 px red rule down the left of the list — is the one
   thing the system bans by name: it is not an edge, since the other three
   sides are not drawn, and not a division, since nothing is on its far side.
   See "Where a line may go" in foundations/geometry.html. */
.cf-choice--invalid .cf-choice__legend { color: var(--feedback-error); }

/* --------------------------------------------------------------------------
   CONTACT LIST
   Address, mail, phone. A <dl>, because these are term/value pairs and that
   is the element that says so — a stack of <p> throws the pairing away.

   Term above value, not beside it, which is the same arrangement .cf-field
   already uses for a mono label over its input. It also means the component
   needs no width query: a two-column form of it would have to know how wide
   its column is, and inside a .col-* the nearest query container is the whole
   page container, which cannot tell.

   No top border, so the section header above it must be the plain one, which
   draws its own. This used to read "the section header above it draws that
   hairline; giving the list one of its own is what --flush exists to avoid" —
   true in its first clause and backwards in its second, since --flush is
   precisely the header that gives its border UP. patterns/impressum.html read
   the second clause and wrote --flush, and the two correct-sounding halves
   cancelled into a section that opened on no rule at all.
   → scripts/check-section-header-rule.py
   -------------------------------------------------------------------------- */
.cf-contact { margin: 0; }
.cf-contact__row {
  padding-block: var(--space-5);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
.cf-contact__term {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.cf-contact__value {
  margin: var(--space-2) 0 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-light);
  line-height: var(--leading-relaxed);
}
.cf-contact__value address { font-style: normal; }
.cf-contact__value a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: var(--stroke-1);
  text-underline-offset: 0.25em;
}
.cf-contact__value a:hover,
.cf-contact__value a:focus-visible { text-decoration-color: var(--border-default); }

/* --------------------------------------------------------------------------
   STAT
   Big mono number + label. For the "10 Mitarbeiter" style facts.

   The number and its label are one fact, so they are one element — a single
   block with two spans in it — rather than two loose paragraphs a page has
   to space itself.

   The number and the label reach the accessibility tree as two separate text
   runs. Measured in Chromium: inline spans, a block label, and two block
   elements all produce the same two StaticText leaves, so no arrangement of
   this markup merges them, and the GOV.UK trick of stacking with a block
   pseudo-element buys nothing here while costing the space between the two
   in the flattened text. The label therefore has to read on its own — see
   components/statement.html.
   -------------------------------------------------------------------------- */
.cf-stat { display: block; }
.cf-stat__value {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-h1);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.cf-stat__label {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   FOOTER
   Black. Isometric logo object, one CTA, then a mono meta row.
   -------------------------------------------------------------------------- */
/* THE LAST BLOCK PAYS FOR THE LAST SCREEN. base.css reserves the consent
   banner in the scroll port — `scroll-padding-bottom` on <html> — which moves
   every focused control the page can still scroll for out from behind the
   notice. It cannot move the ones in the final viewport, because there the
   document has run out of scroll: measured on patterns/landing-page.html with
   the scroll-padding alone, 11 / 10 / 10 / 10 stops at 375 / 768 / 1280 / 1440
   were still entirely behind the banner, and all of them were this footer's —
   its ten links, the contact button and the wordmark.

   The only thing that adds scroll at the end of a document is height at the end
   of a document, so the reservation is the same term added to the padding that
   is already there. It is ADDED and not substituted: --space-8 is the footer's
   own bottom rhythm and is unchanged, and the banner's height is a strip on top
   of it that exists exactly as long as the banner does. Fallback 0px, so every
   page without the banner — and this page one decision later — measures what it
   always did, to the pixel.

   ON THE FOOTER AND NOT ON body, which is what makes it invisible: the footer
   is --surface-inverse and the body is --surface-base, so the same padding on
   body would open a strip of light wash under a black footer on every first
   visit. Growing the footer's own box grows the footer's own black.

   After both halves, first visit, banner up: 0 of 45 / 45 / 50 / 50 shipping
   tab stops entirely hidden, at 375 / 768 / 1280 / 1440.
   → base.css html; scripts/check-scroll-reserve.py */
.cf-footer {
  background: var(--surface-inverse);
  color: var(--text-inverse);
  padding-block: var(--space-24) calc(var(--space-8) + var(--cf-consent-height, 0px));
}
/* INK ON PAPER HAS NO PLATE TO SIT ON — the same sentence that governs
   .cf-btn--solid and .text-foil above, unapplied here until now. The footer
   is the one section every page ends on, so an unconverted --surface-inverse
   is not a corner of one page but a full-bleed black sheet at the foot of
   all fifteen.

   And .text-foil already believed the conversion had happened: its own print
   rule (base.css, PRINTED FLOOR) forces "Jetzt Projekt starten!" to
   var(--cf-schwarz), unconditionally, on the assumption that print never
   leaves a dark plate under it. Nothing here paid that half. Printed to A4
   through Chromium, patterns/expertise.html: black text, --surface-inverse
   still black behind it, 0:1 contrast — the headline is not faint, it is
   gone. --text-inverse-dim on .cf-footer__meta and the white stroke of
   .cf-footer__mark read the same way, one contrast step short of it.

   var(--cf-schwarz) AND NOT var(--text-primary), because --text-primary is
   not the light theme's black here: .cf-footer carries data-theme="inverse",
   and tokens.css redeclares --text-primary to var(--grey-000) inside that
   scope — the same reason .text-foil's own print rule two files up reaches
   for the literal and not the semantic name. Asking for "primary text" on an
   inverse-themed element still answers white; this rule wants the paper's
   ink regardless of which theme the section was drawn in. */
@media print {
  .cf-footer {
    background: none;
    color: var(--cf-schwarz);
  }
}
/* THE RULE ABOVE DOES NOT REACH A BUTTON THAT NAMES ITS OWN COLOUR. Two of
   sixteen pages — kontakt.html, kontakt-danke.html — put .cf-btn--outline
   in the footer CTA slot instead of the .cf-btn--primary every other page
   carries there: both offer "Lieber direkt sprechen?" and a phone link,
   because a reader already on or just past the contact form is the one
   reader the form itself is no longer the answer for. .cf-btn--primary was
   checked and cleared when the rule above was written — its label is the
   foil, not a themed token, so nothing about it depended on the plate.
   .cf-btn--outline was not: it
   sets `color: var(--text-primary)` and `border-color: var(--border-strong)`
   directly, and .cf-footer's own data-theme="inverse" redeclares both to
   (near-)white for exactly the reason .cf-footer's background rule above
   has to route around them. Print drops the plate but not the theme scope,
   so the button keeps asking for white.

   Measured on kontakt-danke.html, printed, before this rule: color
   rgb(255, 255, 255), border-color rgba(255, 255, 255, 0.72), against the
   sheet's own white — both at 1:1 contrast, the same "gone, not faint"
   failure the footer background rule describes, just reached through a
   property that names its own colour instead of inheriting the section's.
   var(--cf-schwarz) and not var(--text-primary) for the reason given
   above: .cf-arrow inside the button is fill: currentColor, so the arrow
   is carried by the same declaration. */
@media print {
  .cf-footer .cf-btn--outline {
    color: var(--cf-schwarz);
    border-color: var(--cf-schwarz);
  }
}
/* For pages whose last section is .section--flush and so carries no bottom
   padding of its own. Keeps the gap before the footer identical on every page
   instead of leaving it to a per-page margin.

   IF AND ONLY IF. This margin REPLACES the padding --flush gave up; it does not
   add to a normal .section's. patterns/kontakt.html carried it over a plain
   .section and stood its footer off at 240 where every other page measures 120,
   which nothing caught because neither modifier was written down anywhere but
   here. Both are on foundations/layout.html now. Change one, check the other.

   THE NUMBER IS --section-gap-sm AND NOT --section-gap, for the reason base.css
   gives over the rule that takes the same distance off every other page's last
   section: this edge meets a black plate that has already ended the page and
   spends 96 px of its own before the CTA, not a second section of content. The
   two modifiers still make one gap rather than two — it is just a smaller one,
   and it is the one number all sixteen pages now stand off at. */
.cf-footer--detached { margin-top: var(--section-gap-sm); }
/* Mark and copy are one centred lockup, which is how the footer is drawn on
   both plates: on landing-page.jpg the pair runs 334-865 in a 1200 frame and
   on ueber-uns.jpg 404-795 — both centred on 600 to within a pixel, not
   aligned to the container's left edge.
   `justify-content: center` was already here and did nothing, because a
   minmax(0, 1fr) second track eats every spare pixel and leaves the grid
   nothing to centre. Sizing both tracks to their content is what makes the
   declaration true. minmax(0, auto) keeps the title free to wrap rather than
   forcing the track to the width of the unbroken headline. */
.cf-footer__cta {
  display: grid;
  grid-template-columns: auto minmax(0, auto);
  align-items: center;
  gap: var(--space-20);
  justify-content: center;
  padding-bottom: var(--space-24);
}
/* THE PLATE CATCHES THE LIGHT AS THE READER ARRIVES AT IT. The footer is the
   site's one conversion CTA and the last block on every route, and it was the
   only place a reader arrives at a control and nothing happens at all: the
   button's specular rests off the right edge until a pointer finds it, and on
   a page this tall a pointer is the one thing that has not moved for the last
   several screens.

   TWO PARTS, ONE ARRIVAL, both scrubbed by the reader's own hand over the
   footer's `entry` range — from the moment its first pixel crosses the
   viewport's bottom edge to the moment its own bottom edge reaches that same
   edge, which on every page is exactly the document's last screen of scroll.
   Nothing here is on the document clock, so a footer nobody has scrolled to
   costs nothing. → scripts/check-idle-motion.py

   THE LIGHT. --cf-specular is the registered percentage :hover and :active
   already move on this button, and this is that same gesture on that same
   property rather than a second one: the specular travels in from 150 % — off
   the right edge, the rest value — crosses the plate at the hover position of
   64 %, and leaves again by the time the page comes to rest.

   IT ENDS WHERE IT RESTS, and that is the load-bearing part. `animation-fill-
   mode` is left at `none`, so past the range the declared value is in charge
   again and the hover state is the reader's. An animation that filled would
   outrank :hover for the life of the page — animations beat author
   declarations — and this button would never light under a pointer again.
   Returning to 150 % is what makes the hand-back invisible: the animated value
   and the declared one are the same number at the moment the animation stops
   applying, so there is no step at the boundary to see.

   THE FOIL. .text-foil swings on a line-of-sight model whose vertical term is
   1 only when the type is at the middle of the screen, and base.css records
   what that costs this one element — "a headline the reader cannot bring to
   the centre of the screen is a headline they never look at square on",
   measured between 90° and 108° here against the designed 116.57°. The footer
   is the one foil moment on the site that CANNOT reach the middle of a
   viewport, because there is no document under it left to scroll. So it takes
   the per-element range that note weighed and set aside: the swing opens over
   the footer's arrival and stays open, instead of turning back at a rake it
   never reached.

   --sight-h is untouched, and the two squarenesses multiply rather than add,
   so cf-sight.js still flattens this headline for a reader sitting off to the
   side of it and a reader with no script is exactly where they were. Firefox
   has view() and no animation-range, so the gate hands it the swing base.css
   already ships rather than half of this one. → base.css THE LINE OF SIGHT;
   scripts/check-range-gate.py */
@supports (animation-timeline: view()) and (animation-range: entry 0% entry 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    /* THE TIMELINE IS THE FOOTER'S AND NOT EACH PART'S OWN, which is the whole
       difference between an arrival and a flick. `animation-timeline: view()`
       reads the ELEMENT's own view progress: on the button that is a 55 px box
       whose entry range is 55 px of scroll, so the light crossed it while it
       was still a sliver at the bottom edge and was over before the label
       could be read — measured, the sweep finished 236 px of scroll before the
       button was wholly on screen. A named timeline on .cf-footer gives both
       parts the same range, and it is the range the reader experiences as
       arriving at the footer. It needs no timeline-scope: a view-timeline-name
       is visible to the declaring element's descendants, which both of these
       are. Same mechanism .cf-pin names one act with. */
    /* AND IT MEASURES THE WINDOW, NOT THE WINDOW MINUS THE BANNER, because
       this footer has already paid for the banner once. A view timeline reads
       the scrollport the scroll container's `scroll-padding` leaves — acts.css
       records the same fact for --sp, "a view timeline measures the scrollport
       that padding leaves" — and base.css sets
       `scroll-padding-bottom: var(--cf-consent-height, 0px)` on <html>. So on
       a first visit the end edge this range is measured against sat 144 px
       above the real one at 1280 and 1440, 233 at 768, 334 at 375. The footer
       ALSO adds that same term to its own bottom padding (see THE LAST BLOCK
       PAYS FOR THE LAST SCREEN above), which is what puts its content clear of
       the notice. Reserving it twice asks the footer's bottom edge to stop
       two banners short of the viewport's — and this is the last block in the
       document, so the second reservation is scroll that does not exist.

       Measured at the document's last pixel, first visit, banner up, before
       this line — the paragraph below says "progress arrives at exactly 1 and
       stays there", and it did not:

                        footer h   entry progress   --foil-angle   --cf-specular
           1440 x 900       538         0.7325        109.461deg      69.96 %
           1280 x 800       578         0.7501        109.930deg      78.37 %
           1024 x 700       578         0.7806        110.740deg      92.95 %
            768 x 1024      897         0.8387        112.283deg     120.70 %
            390 x 844      1055         1             116.570deg     150 %

       Two faults in one number. The foil stopped at 109.46deg instead of the
       116.57deg the swing opens to — inside the 90-108 band base.css weighed
       and rejected for this element, which is the whole reason it took a range
       of its own. And the invite never left its ACTIVE phase, so the animated
       --cf-specular kept outranking the declared one: the light parked at
       69.96 % mid-plate and :hover and :active moved it nowhere. Measured with
       a pointer held on the CTA at all four of those sizes, --cf-specular read
       back the rest column above unchanged, where it should read 64 %. That is
       the exact regression the paragraph below was written to prevent,
       arriving by a second route — an end the timeline cannot reach rather
       than a range that runs to it.

       With the inset, all five: entry progress 1 (0.999 at 1280 and 768, one
       fractional pixel of document short of it), --foil-angle 116.57deg, the
       invite past its range, --cf-specular 150 % at rest and 64.9 % under a
       pointer.

       THE PHONE IS THE ONE SIZE THAT WAS ALREADY RIGHT, and the reason says
       which screens this is about. At 390 x 844 the footer is 1055 px against
       an 844 px window, and `entry` on a subject TALLER than the scrollport
       ends when the subject's own start edge reaches the scrollport's start
       edge — the top — rather than when its end edge reaches the bottom. No
       bottom inset can move that, so the phone always landed. Everything from
       768 up has a footer shorter than its window, and every one of those was
       short by exactly the banner.

       `auto 0px` zeroes the END inset only; the start stays `auto` and keeps
       reading scroll-padding-block-start, which is what a footer arriving from
       below never uses anyway. It is the one property that separates "where a
       Tab press may land a control" from "where this box counts as arrived",
       and those two are the same edge on every screen of the document
       except the last one, which is the only screen this element is ever
       measured on.
       → base.css scroll-padding-bottom; scripts/check-scroll-reserve.py */
    .cf-footer {
      view-timeline-name: --cf-footer-arrival;
      view-timeline-axis: block;
      view-timeline-inset: auto 0px;
    }
    .cf-footer__title {
      animation-name: cf-footer-foil;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: --cf-footer-arrival;
      animation-range: entry 0% entry 100%;
    }
    /* THE RANGE STOPS AT 90 % SO THE BUTTON IS THE READER'S AGAIN AT THE
       BOTTOM OF THE PAGE. `entry 100%` on this element is the document's own
       last pixel of scroll: progress arrives at exactly 1 and stays there, and
       an animation at progress 1 is still in its ACTIVE phase — not its after
       phase — so it keeps outranking :hover for as long as the reader sits at
       the foot of the page. Measured: --cf-specular read 150 % under a pointer
       that should have made it 64 %, on the one control the page is asking
       them to press. Ending at 90 % puts the timeline past the range while
       there is still scroll left, which is what puts the declared value —
       and with it :hover and :active — back in charge. */
    .cf-footer__cta .cf-btn--primary {
      animation-name: cf-footer-invite;
      animation-timing-function: linear;
      animation-timeline: --cf-footer-arrival;
      animation-range: entry 0% entry 90%;
    }
  }
}
@keyframes cf-footer-foil {
  from { --sight-v: 0; }
  to   { --sight-v: 1; }
}
/* The crossing is late in the range and not at its middle, because the range
   is the footer's own height in scroll and the button is behind the viewport's
   bottom edge for the first half of it. Measured, footer height and the point
   in the range at which the button's own bottom edge clears that edge — twice
   per viewport, because the consent banner is part of the footer's height on a
   first visit and the reader who has answered it gets a shorter range:

                  banner up          answered
     1440 x 900   538 px  43.9 %   394 px  59.9 %
     1280 x 800   578 px  40.8 %   434 px  54.4 %
     1024 x 700   578 px  40.8 %   434 px  54.4 %
      768 x 1024  897 px  45.5 %   664 px  61.5 %
      390 x 844  1055 px  37.7 %   721 px  55.1 %

   61.5 % is the worst of the ten, so a sweep centred on the range would spend
   half its travel on a plate the reader cannot see whole. 80 % of an animation
   that runs to `entry 90%` is 72 % of the footer's own arrival: the light
   crosses a button that is entirely on screen in every one of the ten, and
   still has the last fifth of its travel left to leave by. */
@keyframes cf-footer-invite {
  from { --cf-specular: 150%; }
  80%  { --cf-specular: 64%; }
  to   { --cf-specular: 150%; }
}
.cf-footer__mark { width: 12.5rem; opacity: 0.9; }
/* THE MARK IS A FIXED RASTER-LIKE ASSET ON A PLATE FORCED COLOURS REPAINTS
   OUT FROM UNDER IT, the same mismatch .cf-logo carries in the nav (see that
   rule's own note) and for the same reason: .cf-footer's background is
   `var(--surface-inverse)`, a plain background-color, and forced-colors mode
   repaints that to Canvas regardless of what this file says, while an <img>'s
   own pixels are exempt from the mode and keep the colours the file was
   exported with — here `stroke="#FFFFFF"` on all three isometric planes, each
   at 0.85 opacity, plus a `fill="#E1FF00"` arrow.

   Measured in Chromium with the mode active and colour-scheme light: the
   lattice's own rasterised pixels read rgb(255, 255, 255) at alpha 162/255
   (the 0.85 layer) and the footer plate under it reads rgb(255, 255, 255)
   solid — white stroke over white Canvas composites to white, so all three
   planes vanish and only the arrow (rgb(225, 255, 0), enough luminance
   apart from white to stay visible) is left floating with no lattice to
   give it scale. Under a dark high-contrast theme (colour-scheme dark,
   Canvas: black) the same white stroke reads rgb(0, 0, 0) plate beneath it —
   unchanged from this component's ordinary near-black plate — so only the
   light theme needs correcting.

   Inverting the whole asset there restores the lattice (white → CanvasText's
   own black) at the cost of the arrow's colour (rgb(225, 255, 0) inverts to
   rgb(30, 0, 255), a blue): the same trade this mode already makes elsewhere
   in this file when a component's refinement is dropped so its shape reads
   at all — .cf-annot--lit gives up its ramp for Highlight above, .rule gives
   up its dash ratio in base.css. A blue arrow inside a legible lattice beats
   a lime arrow with no lattice around it. */
@media (forced-colors: active) and (prefers-color-scheme: light) {
  .cf-footer__mark { filter: invert(1); }
}
/* THE SAME WHITE-ON-WHITE THE RULE ABOVE ALREADY NAMES, reached by paper
   instead of by the mode: .cf-footer's own print rule now drops the plate to
   `none`, which for an <img> is exactly the Canvas repaint above — the
   asset's pixels are unaffected, so `stroke="#FFFFFF"` sits on a white sheet
   the same way it sat on light-theme Canvas. Paper has no dark theme to fall
   back to, so unlike the mode above this is unconditional.

   .cf-logo--bare img RIDES THE SAME PLATE AND THE SAME REMOVAL. .cf-logo's
   own plate (`background: var(--surface-inverse)`, LOGO LOCKUP above) is
   what .cf-logo--bare drops — "bare" meaning it never had a plate of its
   own, only .cf-footer's, which this rule's neighbour has just cleared.
   cf-logo-horizontal-white.svg is the same all-white export .cf-logo's own
   forced-colors rule already inverts for exactly this reason; scoped to
   --bare rather than to .cf-logo so the nav's copy, which keeps its own
   black plate in print, is untouched. */
@media print {
  .cf-footer__mark,
  .cf-logo--bare img { filter: invert(1); }
}
.cf-footer__title {
  font-family: var(--font-display);
  font-size: var(--text-display-2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  margin-bottom: var(--space-6);
  /* `anywhere` AND NOT THE NET'S `break-word`, and the difference is the whole
     reason this line exists. The net in base.css covers this element already —
     computed overflow-wrap here is break-word, inherited correctly, and it does
     not stop the overflow. Measured on the landing page at a 16 px default with
     one real compound (Grundstücksverkehrsgenehmigungszuständigkeits-
     übertragungsverordnung, 66 characters) substituted for this line:

       viewport   track   this <p>   document scrollWidth
       320         280      1150      1170   (+850 sideways)
       375         328      1165      1185   (+810)
       768         405      1437      1480   (+712)
       1280        405      1437      1788   (+508)

     The floors are not the fault and adding another does not help: at 320 the
     grid is already the single minmax(0, 1fr) column the 48.75rem rule below
     gives it, the track measures 280 and the grid item measures 280. Both
     floors the manual names are in place and the <p> inside them still computes
     1149.95 px wide. min-width: 0 on the item changes nothing — measured.

     What overflows is this element's MIN-CONTENT, and break-word is defined not
     to shrink it: it opens a break opportunity when a word cannot fit a line,
     while leaving the intrinsic minimum at the width of the unbroken word.
     `anywhere` shrinks the intrinsic minimum too, which is the only property of
     the two that reaches this. scripts/check-wrap-net.py says so in as many
     words — "`anywhere` also shrinks min-content ... a component that has
     chosen `anywhere` is covered, not flagged" — so this is the choice the net
     is written to allow, taken by the one component that needs it.

     NOTHING MOVES AT THE REAL STRING. "Jetzt Projekt starten!" has spaces in
     it, so its min-content is its longest word either way, and the track is
     max-content sized wherever there is room. Measured across the ladder before
     and after: the title box is 280 / 328 / 405 / 405 px at 320 / 375 / 768 /
     1280, identical in both states. → base.css, THE WRAP NET;
     foundations/layout.html#intrinsic-minimum */
  overflow-wrap: anywhere;
}
.cf-footer__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-inverse-dim);
}
/* --text-inverse-dim is rgba(255, 255, 255, 0.62) — legible dimmed white on
   .cf-footer's black plate, and against the plate this rule drops to `none`
   for print (above) it is 62 % white on the sheet's own paper, the address
   and the link row both under it.

   var(--grey-700) AND NOT var(--text-secondary), for the same reason the
   rule above reaches for var(--cf-schwarz) instead of var(--text-primary):
   .cf-footer's data-theme="inverse" redeclares --text-secondary to
   rgba(255, 255, 255, 0.72) too, so the semantic name still answers white
   here. --grey-700 is what --text-secondary itself resolves to outside an
   inverse scope — the system's ordinary dimmed ink, used unscoped for
   exactly this row's job elsewhere (.cf-page-header__meta, the blog card
   byline). */
@media print {
  .cf-footer__meta { color: var(--grey-700); }
}
.cf-footer__links { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.cf-footer__meta a { text-decoration: none; }
/* THE SEPARATOR IS THE SYSTEM'S DOT, CENTRED, AND SILENT.
   Three things were wrong with `content: "• "` and each is small enough to
   have survived every review of this component.

   THE MARK. U+2022 BULLET appeared in exactly one rule in the whole system —
   this one. Every other separator the site sets is U+00B7 MIDDLE DOT: the blog
   card's meta line, the page header's meta line, the address in the row
   directly below this list. The plate agrees: sampled off
   mockups/landing-page.jpg the footer row is drawn with the small mid-height
   dot, not the heavy one. One glyph, and it was the only place the system
   spoke with two voices about the same punctuation.

   THE CENTRING. The dot was the first thing in the second <li>, so the list's
   --space-3 gap sat in front of it and whatever the content string ended with
   sat behind. A trailing space is one mono advance — 6.6 px at 11 px — so the
   mark stood 12 px from the link on its left and 6.6 px from the one on its
   right, leaning into the word it introduces. The space is a margin now, which
   is the same 12 px on both sides: mathematically equal and, since the dot is
   symmetric about its own centre, optically equal too.

   THE SIDE. It is an ::after on every item but the last rather than a ::before
   on every item but the first, and the two are only the same drawing while the
   row fits on one line. This row does not: at 375 px it took three. A leading
   separator wraps with the item it precedes, so both continuation lines opened
   with a dot standing 12 px inside the margin with nothing on its far side.
   That is the bar beside a block one mark down — not a division, because a
   division has content on both sides of it, and there the left side is the end
   of the line above. Bound to the item it follows, the mark can only ever
   close a line, which is the ordinary reading: this list continues. The last
   item gets none, so no line ends on a mark separating the list from whatever
   comes after it. Six items pack into two lines at 375 px now rather than
   three, because the dot travels with a word instead of needing room of its
   own at the head of the next one. → foundations/geometry.html#where-lines-go

   THE ANNOUNCEMENT. foundations/geometry.html and the breadcrumb both argue
   against typed separators partly because screen readers speak generated
   content, and a trail that reads "Über uns bullet News bullet Karriere" is
   the failure named there. The breadcrumb's answer is to draw the mark instead
   of typing it; that answer is not available here, because the designer drew a
   dot and the material wins. The alt-text form of `content` is the other
   answer: the glyph renders and the accessibility tree gets the empty string.
   Declared twice on purpose — a browser without the `/ alt` syntax drops that
   whole declaration, so the plain one underneath is what it keeps, and the
   drawing degrades to the drawing rather than to nothing. */
.cf-footer__links li:not(:last-child)::after {
  content: "·";
  content: "·" / "";
  margin-inline-start: var(--space-3);
}
/* "Cookie-Einstellungen" opens a dialog rather than navigating, so it is a
   button. It is dressed as a link because it belongs to that row, but the
   element stays honest about what it does. */
.cf-footer__linkbtn {
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
}
/* One rule, not two. The link and the button sit in the same row and lit the
   same way, and the declaration was written out twice — identical, and one
   edit away from the row answering at two different underline offsets. */
.cf-footer__meta a:hover,
.cf-footer__meta a:focus-visible,
.cf-footer__linkbtn:hover,
.cf-footer__linkbtn:focus-visible {
  color: var(--grey-000);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}
@media (max-width: 48.75rem) {
  .cf-footer__cta { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
}

/* --------------------------------------------------------------------------
   CONSENT
   Two layers. The banner is the first one: a non-modal dialog pinned to the
   bottom of the viewport, on black so its contrast never depends on what
   it happens to be covering. The settings panel is the second, and it is a
   real modal <dialog>.

   Black, not frosted glass: a consent notice can be over a video, a
   photograph or a white section, and glass would put the text on a background
   the page controls. The material rules allow both; only one of them is
   readable everywhere.

   The banner spends no lime. Whatever page it covers has already spent the
   screen's one lime moment, so the light layer arrives here as the foil —
   the cool half of the ramp, which exists for exactly this case — and it
   arrives as an edge, not as a fill.
   -------------------------------------------------------------------------- */
/* The dark context is declared by the component, not asked for in the markup.
   `data-theme="inverse"` would do the same job, but a consent notice that
   silently becomes black-on-black because someone dropped an attribute is not
   a risk worth carrying. Same mappings as the theme block in tokens.css. */
.cf-consent,
.cf-consent__dialog {
  --text-primary:   var(--grey-000);
  --text-secondary: rgba(255, 255, 255, 0.72);
  --border-strong:  rgba(255, 255, 255, 0.72);
  --border-default: rgba(255, 255, 255, 0.18);
  /* The fourth border rung was missing from this list while the theme block it
     copies has always carried it, so anything ruled at --border-subtle inside
     the dialog was drawn in 12 % BLACK on black — invisible. Nothing used it
     until the inventory table did. */
  --border-subtle:  rgba(255, 255, 255, 0.10);
  --focus-ring:     var(--cf-lime);
}
.cf-consent {
  position: fixed;
  inset: auto 0 0 0;
  z-index: var(--z-overlay);
  background: var(--surface-inverse);
  color: var(--text-inverse);
  border-top: var(--stroke-2) solid transparent;
  border-image: var(--gradient-foil) 1;
  transition: transform var(--duration-base) var(--ease-out),
              opacity   var(--duration-base) var(--ease-out);
  /* THE BACKSTOP IS THE COMPONENT'S, NOT THE SHORT SCREEN'S, and for two
     revisions it was the short screen's. The cap below is unchanged — the nav
     band, one control at .cf-btn's own 3rem floor, and a rung of air, stated
     against what it LEAVES rather than as a share of the screen — and what
     moved is only the condition it was behind.

     WHY IT CANNOT BE BEHIND A HEIGHT QUERY. What makes this banner outgrow the
     screen is the reader's text size: every term of its height is rem — the
     title, the body, the three controls, the block padding — so at a 24 px
     browser default the banner is 644 px where it is 354 at 16. The screen it
     has to fit in does not move with that setting, and `@media (max-height:
     35rem)` cannot see it either: rem in a MEDIA QUERY resolves against the
     root's INITIAL font size and never against the reader's, which is the
     distinction check-rem-floor.py exists for. So the one query that carried
     this cap was gated on the only quantity in the arithmetic that stands
     still while the banner grows.

     Measured on patterns/landing-page.html, first load, nothing dismissed,
     Chromium 141 and WebKit 26.5 agreeing to the decimal at all 60 cells —
     CLEAR is the band between the two fixed layers, which is a constant no
     scroll offset moves, and the CTA is .cf-hero's one button:

       viewport    root   banner   CLEAR   CTA      CLEAR after this rule
       320 x 568    16     353.8   130.2    48      130.2  (cap inert)
       320 x 568    20     466.9    -3.9    60       90.0
       320 x 568    24     644.4  -202.4    72      108.0
       320 x 812    24     644.4    41.6    72      108.0
       360 x 640    24     590.1   -76.1    72      108.0

     At 320 x 568 and a 24 px default the banner is 113 % of the screen and its
     top edge sits 76 px ABOVE the viewport: the notice's own heading and the
     two links inside its sentence are off screen with no scroll that reaches
     them, because the box is `position: fixed` and was clipped by nothing. On
     the other three rows the nav is clear and the hero's one call to action is
     not: there is no scroll offset at which it can be pressed whole, which is
     the same finding the short-screen block was written for, arrived at
     through type rather than through landscape.

     INERT AT THE DEFAULT SIZE, EVERYWHERE, which is what makes it a backstop
     and not a layout rule. Re-measured at 320/360/390/414/768/1024/1280/1440
     across roots 16/20/24: every 16 px row is byte-identical to before —
     scrollHeight equals clientHeight, so the cap engages on none of them — and
     no cell above 414 px of width engages at any root either. It fires only
     where the banner had already outgrown the room it is allowed.

     vh FIRST AND dvh ON TOP, the pair .cf-nav__list[data-open="true"] carries
     for the identical shape: this box is bottom-anchored and clipped rather
     than scrolled, so iOS Safari's chrome-collapsed `vh` would let it grow
     past the true visible screen and push its top edge into the very band the
     cap reserves. A browser with no dvh keeps the line above.
     → scripts/check-consent-clearance.py link 5c */
  max-height: calc(100vh - var(--nav-height) - 3rem - var(--space-6));
  max-height: calc(100dvh - var(--nav-height) - 3rem - var(--space-6));
  overflow-y: auto;
}
.cf-consent[hidden] { display: none; }
/* AND IT DOES NOT PRINT, which is not a nicety — a fixed box in paged media is
   repeated on EVERY sheet, and this one is an opaque black plate the full width
   of the page.

   Measured before this rule existed, first visit, landing-page.html printed to
   A4 through Chromium: nine pages, and all nine carry `0 0 794 253 re f` filled
   `0 0 0` painted last in the content stream. The page content is laid out at
   794 units wide and scaled to the 595.92 pt MediaBox, so 253 units is 189.9 pt
   of an 842.88 pt sheet — 22.5 % of every page, over the bottom edge, opaque,
   on top of whatever the flow had put there. Pages 2 to 9 end with 4,540 bytes
   of byte-identical drawing operators: the plate, the foil rule above it, the
   11 px "Datenschutz-Einstellungen" run and the 14 px body. Page four of the
   proof lost "Ihr Vorteil" and the benefit line under it mid-card.

   It is also the one layer on the page that paper cannot answer. The banner
   asks a question and offers three buttons; printed, it is three dead buttons
   stamped nine times over the content the reader was trying to keep. The
   decision itself is not lost — it lives in localStorage and the banner is
   back on screen on the next load.

   The dialog goes with it for the same reason and one more: an open
   <dialog> is `position: fixed` by the UA stylesheet, so a reader who prints
   with the settings panel open gets the modal, its ::backdrop and none of the
   page. Both layers are chrome; neither is the document.

   AND IT IS WRITTEN `dialog`, NOT `.cf-consent__dialog`, which is the only
   place in this file where the bare element is the more precise selector. The
   fix on the second layer is not declared here at all — it comes from the UA
   stylesheet's `dialog:modal`, which is keyed on the element. Every <dialog>
   in the tree is this one component (ten pattern pages, one each), so the two
   selectors name exactly the same boxes today; the element name is the one
   that stays true the day a second dialog is written, and it is the key
   check-print-fixed.py can see.

   → scripts/check-print-fixed.py, which holds this for every fixed layer the
   shipping stylesheets declare, not only for these two. */
@media print {
  .cf-consent,
  dialog { display: none; }
  /* AND THE SPACE IT WAS RESERVED, which `display: none` does not give back.
     --cf-consent-height is an INLINE property on <html>, written by
     cf-consent.js, and an inline custom property outranks any rule that would
     zero it from here — so the reservation has to be revoked at its READER,
     which is this declaration.

     WHAT LOOKS LIKE IT ALREADY HANDLES IT, AND DOES NOT. Hiding the banner
     resizes it to zero, the file's ResizeObserver fires, and the property is
     republished as 0px: measured under Chromium's print emulation, the property
     does read 0px without this line. That is a script racing a paint, and it is
     load-bearing in neither direction it can fail. cf-consent.js says in its own
     words that where there is no ResizeObserver "the fix degrades to the first
     measurement" — the screen one — and a real print snapshot is not obliged to
     wait for an observer callback either. The correction has to be in the
     cascade, where it cannot arrive late.

     .cf-footer buys the document's last screen of scroll with its own bottom
     padding, which is the right trade on a viewport and meaningless on paper:
     paper has no scroll, and the layer being cleared is the one the block above
     has just removed from the sheet. Left in, a reader printing on a first
     visit gets 144 to 334 px of extra black under the footer — the same
     opaque plate, in the same place, that the block above exists to stop.
     → base.css html; scripts/check-scroll-reserve.py */
  .cf-footer { padding-block-end: var(--space-8); }
}
/* Entry state. Removed on the frame after insertion, so the banner rises into
   place; with reduced motion the durations collapse to 1 ms and it just is. */
.cf-consent[data-enter="true"] { transform: translateY(100%); opacity: 0; }

.cf-consent__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-12);
  padding-block: var(--space-6);
}
.cf-consent__title {
  /* fit-content so the focus ring hugs the words. The heading takes focus when
     the banner appears, and a block-level h2 would draw a ring the width of
     the whole column — a box around nothing. */
  width: fit-content;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  /* The other rung's tracking, and it was the only 11 px mono label in the
     shipping tree wearing it: forty-seven others take --tracking-label. The
     banner is the first thing a first-time reader sees, so its title was the
     one place the label register introduced itself in a voice the rest of the
     site never uses again. */
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  margin-bottom: var(--space-2);
}
.cf-consent__title:focus-visible { outline: var(--stroke-2) solid var(--focus-ring); outline-offset: 4px; }
.cf-consent__body {
  max-width: 68ch;
  font-size: var(--text-md);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}
.cf-consent__body a { color: var(--text-inverse); text-underline-offset: 0.25em; }

/* Accept and reject are one control repeated, never a strong one and a weak
   one: German supervisory authorities read unequal prominence as a dark
   pattern. Same size, same weight, same row. Settings is the third option,
   and it is allowed to be quieter because it is not the reject path. */
.cf-consent__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}
.cf-consent__actions .cf-btn { flex: 1 1 auto; justify-content: center; min-width: min(13rem, 100%); }
.cf-consent__actions .cf-btn--ghost { flex: 0 0 auto; min-width: 0; }
/* --outline inverts on a dark surface: its default hover paints black on
   black, which is no change at all. */
.cf-consent :is(.cf-btn--outline, .cf-btn--ghost):is(:hover, :focus-visible) {
  background: var(--grey-000);
  color: var(--cf-schwarz);
}
.cf-consent .cf-btn--ghost:is(:hover, :focus-visible) { background: none; color: var(--grey-000); }

/* VH FIRST AND DVH ON TOP, THE THIRD BOX OF THE SAME SHAPE AND THE LAST ONE
   STILL MISSING THE SECOND LINE. .cf-nav__list[data-open="true"] and the
   .cf-consent backstop a few rules down both cap on a viewport height with an
   inner scroll behind it, and both were given the dvh line for the reason
   written out at length over the backstop: iOS Safari's `vh` is the
   chrome-collapsed viewport, larger than what is on screen while the toolbar
   is still showing. This dialog is the same shape — a max-height cap over
   <dialog>'s own UA `overflow: auto` — and it kept vh alone.

   AND UNLIKE THE BACKSTOP, THIS CAP IS NEVER NOT ENGAGED. The backstop's own
   note calls itself "still a backstop and still rarely reached". This one
   binds on every phone at every text size: measured on the landing page's
   dialog, both engines, the panel's content against the scrollport the cap
   leaves it —

     viewport      root 16      root 20      root 24     cap
     375 x 667    837 / 532   1167 / 532   1634 / 532   533.6  (80vh)
     390 x 664    785 / 529   1167 / 529   1604 / 529   531.2  (80vh)
     412 x 839    765 / 669   1116 / 669   1538 / 669   671.2  (80vh)

   46rem never wins on a phone, so the rendered height of this box IS 80vh,
   and an error in what vh means is an error in the box, not in a term that
   might one day bind.

   WHAT THE ERROR COSTS, STATED AT ITS REAL SIZE. A modal <dialog> is centred
   in the viewport, so at 80vh it takes the middle four fifths and leaves a
   tenth of the vh-viewport above it and a tenth below. Safari's toolbar takes
   that lower tenth back from the bottom, so the panel's foot leaves the
   visible screen once the toolbar stands taller than a tenth of the collapsed
   viewport — and the toolbar IS showing on the one load this panel is ever
   opened from, a tap on a banner pinned to the bottom of a page nobody has
   scrolled. Driven at 390 x 745 (the collapsed viewport `vh` answers with)
   and read against the 664 that is on screen while the toolbar shows, both
   engines agreeing to the tenth:

     cap            panel box        below the visible screen
     min(80vh, …)   74.5 – 670.5     6.5 px
     min(80dvh, …)  106.9 – 638.1    none, 25.9 px to spare

   6.5 px is the panel's bottom border and the air under the actions row, not
   a control: "Auswahl speichern" ends at 637.8 and clears the toolbar on this
   pair. It is a border because the toolbar here is 81 of 745 — a hair over
   the tenth this cap leaves. The margin is the difference between two numbers
   that are both the screen's, so it closes wherever the toolbar takes a
   larger share of it, which is the small phones; and the fix is free either
   way. dvh costs nothing where it is missing — a browser without it keeps the
   vh line above — and nothing at all on a pointer machine, where the two
   units are the same number. */
.cf-consent__dialog {
  width: min(38rem, calc(100vw - 2 * var(--space-4)));
  max-height: min(80vh, 46rem);
  max-height: min(80dvh, 46rem);
  padding: 0;
  background: var(--surface-inverse);
  color: var(--text-inverse);
  border: var(--stroke-1) solid var(--border-strong);
  border-radius: var(--radius-none);
}
.cf-consent__dialog::backdrop { background: rgba(0, 0, 0, 0.56); }
.cf-consent__dialog-inner { padding: var(--space-8); }
.cf-consent__dialog-title {
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
}
/* The dialog's title owns the air beneath it, like every other header in the
   system. All five pages that ship the dialog were setting this one gap
   inline instead, and the markup documented on components/consent.html never
   carried it — so the documented snippet rendered the title and its opening
   sentence with no space at all between them. */
.cf-consent__dialog-title + .cf-consent__body { margin-top: var(--space-4); }
.cf-consent__group {
  display: grid;
  gap: var(--space-4);
  margin-block: var(--space-8);
  border-top: var(--stroke-1) solid var(--border-default);
}
.cf-consent__row {
  padding-block: var(--space-4);
  border-bottom: var(--stroke-1) solid var(--border-default);
}
.cf-consent__meta {
  margin-top: var(--space-2);
  padding-left: calc(var(--check-box) + var(--space-3));  /* box + gap */
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--text-secondary);
}
.cf-consent__dialog .cf-check__label { color: var(--text-inverse); }
.cf-consent__dialog .cf-check__box { border-color: var(--border-strong); }

@media (max-width: 56.25rem) {
  .cf-consent__inner { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
  /* The accept/reject pair shares one row wherever the row can hold both at
     their natural width, and stacks only where it cannot. max-content as the
     basis is what decides that: the fold moves with the labels' real rendered
     width, so no font swap can leave a button mid-wrap — a fixed basis would
     pin the fold to today's fallback metrics, the trap the breakpoint
     register's logo-wall note warns about. Wherever the pair stacks, each
     button takes the full measure; a 2+1 ragged row cannot occur because the
     two hypothetical sizes only fit together or not at all. The ghost is not
     part of the pair and never joins the row — reject must not be the button
     that got squeezed to make room for something quieter. The short-screen
     block below is the one exception, and it is the same argument rather than
     a break with it: there the ghost joins a row the pair still fills to the
     measure, so nothing is squeezed to make room for it. */
  .cf-consent__actions .cf-btn { flex: 1 1 max-content; }
  .cf-consent__actions .cf-btn--ghost { flex: 1 1 100%; justify-content: flex-start; }
}
/* A tall banner on a short screen must not eat the page it is asking about.
   THAT SENTENCE WAS TRUE OF THE INTENT AND FALSE OF THE RULE. 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
   wide-independently from 375 to 900, so across the whole band this threshold
   governs the cap is above it and never engages; measured on the landing page,
   first load, nothing dismissed:

     viewport    banner   of screen   nav band   CLEAR between the two fixed layers
     667 x 375    253       68 %        84         38
     740 x 375    253       68 %        84         38
     812 x 375    253       68 %        90         32
     844 x 390    253       65 %        90         47
     900 x 400    253       63 %        90         57

   The nav is fixed at the top and the banner is fixed at the bottom, so that
   CLEAR figure is a constant — it does not move with scroll. Nothing in the
   hero is 32 px tall: the headline is 190 and the one call to action is 48,
   the .cf-btn floor. So on a landscape phone, on the one visit the notice is
   shown, there is NO scroll offset at which the page's opening line can be
   read whole or its button pressed whole. The reservation .cf-hero pays out of
   --cf-consent-height cannot save it: that cap is a term of a min-height, and
   a min-height cannot shrink a box whose content is taller than it — the hero
   renders 467 px against a 122 px cap, so the link measures as applied and
   the button is buried exactly as it was before it existed.

   SO THE BLOCK SHORTENS THE BANNER INSTEAD OF CAPPING IT, in the two places
   the narrow form spends height that a short screen has not got:

     - the ghost rejoins the pair's row. It is a full-width row of its own only
       because the screen is NARROW, and a screen under 35rem tall is landscape
       — the width is the axis with room. This is the taller of the two: 52 px.
     - the block padding drops one rung, 24 to 16. 16 px.

   Measured after, same viewports: 191 px and 94-119 px of CLEAR from 667 up,
   and 219 / 72 at 600, where the row cannot hold three, the ghost still wraps
   and the cap on .cf-consent is what finishes the job.
   The pair is never squeezed to make room: both shrink by the same 58 px and
   floor at the 13rem min-width, 225 px against a 208 px floor at the narrowest
   width the three share a row. Above 900 the banner is already two columns and
   only the padding applies — 144 to 128 at 1024 x 400.

   AND THE CAP IS STATED AGAINST WHAT IT LEAVES. 70vh was a share of the
   screen and said nothing about the nav, which is the other fixed layer and is
   there at every height: at 568 x 320 the cap engaged at 224 and left 12 px.
   Its terms are the two things that must survive it — the nav band and one
   control, .cf-btn's own 3rem min-height, plus a rung of air — so the backstop
   leaves 72 px there instead of 12. It is still a backstop and still rarely
   reached: at 375 tall it sits at 219 against a banner of 191.

   AND THE CAP HAS LEFT THIS BLOCK, which is the correction rather than a
   retreat from any of the above. Everything this block still does is about
   the SHAPE the narrow form takes on a screen with no height to spend, and
   both remaining declarations are landscape's answer to landscape: the ghost
   rejoins a row the width has room for, the padding drops a rung. The cap was
   never that. It is the backstop for a banner that has outgrown the screen at
   all, and the commonest way that happens is not a phone turned sideways — it
   is a reader who has turned their type up, which this query cannot see.

   rem IN A MEDIA QUERY RESOLVES AGAINST THE ROOT'S INITIAL FONT SIZE, never
   against the reader's; 35rem is 560 px at every text size there has ever
   been. The banner's own height is rem all the way down, so it grows and the
   threshold does not: measured at 320 x 812 and a 24 px default, 644 px of
   banner and 41.6 px of clear band against a 72 px call to action, with this
   query not matching at all. The cap now stands on .cf-consent itself, with
   the same value and the measured table beside it.
   → the base .cf-consent rule; scripts/check-consent-clearance.py link 5c */
@media (max-height: 35rem) {
  .cf-consent__inner { padding-block: var(--space-4); }
  .cf-consent__actions .cf-btn--ghost { flex: 0 0 auto; justify-content: center; }
}

/* --------------------------------------------------------------------------
   SCROLL HINT
   The mono "( klick um scroll zu simulieren )" marker from the mockups.
   Use it for any inline, parenthetical instruction.
   -------------------------------------------------------------------------- */
.cf-hint {
  display: block;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.cf-hint::before { content: "( "; }
.cf-hint::after  { content: " )"; }

/* --------------------------------------------------------------------------
   ISOMETRIC ILLUSTRATION
   The four process objects, and anything else drawn in the 2:1 system.
   Geometry lives in the markup; this file owns the contour weight and the
   one piece of motion the objects are allowed.

   Part roles, used by both the drawing and the motion:
     .cf-iso__scene   the whole object — the only thing that travels
     .cf-iso__form    solid contoured body
     .cf-iso__ghost   dashed reference geometry, the object's future states
     .cf-iso__orbit   a ghost that circulates — turns, then settles
     .cf-iso__light   the single lime-gradient element. One per object.
     .cf-iso__node    construction points

   .cf-iso__trace — the signal reaching the object — was the fifth layer and is
   RETIRED: foundations/illustration.html sets out four, the drawing checklist
   no longer mentions one, and the generators in scripts/ cannot emit one. Every
   trace rule below is kept for the drawings that already carry one — the landing
   page's statement figure and process cards 01 to 04 — and is not a licence to
   draw another.
   -------------------------------------------------------------------------- */
.cf-iso { display: block; }

/* "1 px contour at every size" is a device pixel, not a viewBox unit. A 640-unit
   drawing rendered at 352 px would otherwise put its contours on screen at
   0.55 px — grey, not black. non-scaling-stroke makes the rule literally true,
   and pins the dash patterns to the px values in --dash-*, which is what keeps a
   ghost's 1-4 dash a 1-4 dash at any size. The trace is excluded — see below. */
.cf-iso :is(path, line, circle, ellipse, rect, polygon, polyline):not(.cf-iso__trace) {
  vector-effect: non-scaling-stroke;
}

/* The trace is the one exception, and it has to be.
   Under non-scaling-stroke the dash pattern is measured in SCREEN pixels, but
   pathLength normalises against the path's USER-space length. The two disagree
   by exactly the render scale, so a pathLength="1" draw is stretched by 1/scale:
   at the shipped 352 px size (scale 0.55) the line is fully drawn once
   stroke-dashoffset falls to 0.45, and the last 45 % of its range does nothing.
   Measured at 0.75 / 0.50 / 0.25 offset: 45 % / 91 % / 100 % drawn, against the
   25 / 50 / 75 % those offsets are supposed to mean.

   So the trace is stroked in user units, where pathLength works, and carries
   stroke-width 2 to land back on the contour weight: 1.1 CSS px where the object
   renders at 352, and 0.58 at a 375 px viewport, where the height cap above puts
   it at 184. Under a CSS pixel at the small end, which is the cost of a draw
   that is linear at every size — and it is the small end that has the device
   pixels to spend: no 375 px viewport is DPR 1, so 0.58 CSS px is 1.15 device
   pixels at DPR 2 and 1.7 at DPR 3. Verified 25/50/75 % exact.

   SO THE WEIGHT IS A PER-OBJECT NUMBER, AND IT USED TO BE A CONSTANT. A user-
   unit stroke lands on screen at `weight x render scale`, and scale is a
   property of the drawing's own frame — viewBox width over the pixels it is
   rendered into. One literal therefore cannot serve two objects at two scales,
   and it was serving five families at four:

     .cf-process__figure   scale 0.55            2 -> 1.10 CSS px
     .ex-step__figure      scale 0.78 - 0.86     2 -> 1.55 - 1.71 CSS px
     .ill-frame            scale 0.19 - 0.25     2 -> 0.38 - 0.50 CSS px

   Measured across 375 / 768 / 1280 / 1920, the shipped trace ran from 0.38 to
   1.71 CSS px — a 4.5x spread on the one line in an illustration that is meant
   to read as the same hairline as the contours beside it. At the heavy end the
   signal was 71 % thicker than the object it arrives at; at the light end it
   was a grey line at 2.5:1 against CF-Grau, under the 3:1 the system holds its
   contours to.

   --trace-weight is that number, declared where the object's SIZE is declared
   so the two cannot drift apart — the same reason --iso-travel is written next
   to the viewBox it is derived from. Its value is viewBox width divided by the
   width the frame actually renders at, and it is 2 by default, which is what
   every object got before and is still right for nothing in particular. */
.cf-iso { --trace-weight: 2; }
.cf-iso__trace { stroke-width: var(--trace-weight); }

/* The frame is a crop, not a bounding box, and a trace is a signal from
   off-stage — so its path routinely runs past the edge. pathLength normalises
   against the DRAWN length, which means every unit outside the crop spends
   scroll range on a line nobody can see. Measured on the landing page, at the
   settled object: card 02's outgoing trace is 54 % inside the crop and its
   incoming one 49 %, so under a plain 1 → 0 draw the first finished in the
   first fifth of its range and the second had not appeared at all by the
   halfway mark.

   So the two ends of the draw are authored rather than assumed:
     --trace-from  stroke-dashoffset at the start. 1 = nothing drawn.
     --trace-to    stroke-dashoffset at the end.   0 = drawn to the last unit.
   A trace that leaves the crop stops at the edge (--trace-to = the fraction
   still to run); one that enters from outside starts at the edge
   (--trace-from = 1 − the fraction already behind it). Defaults draw the whole
   path, so a trace that fits its frame needs neither.

   These are the animated values themselves, NOT an expression over them, and
   that is load-bearing: a keyframe whose value is a calc() containing var()
   is not interpolated — Chromium steps straight from `from` to `to`. Measured
   across the range at cover 18/20/22/24/27/30/35/45 %: `calc(1 - var(--lead))`
   gave 0 0 0 then the end value, five samples of nothing. Registering the
   property with @property does not rescue it; the calc is what fails. Keep
   these two properties whole values.

   Measure them once against the SETTLED object: while the scene is still
   arriving it carries the 26.57° offset, which moves the crop boundary by up
   to --iso-travel. Only the endpoints count — a path that leaves the crop and
   re-enters wastes no range at either end, so a gap in the middle is left
   alone. It reads as the signal passing behind the crop, which is what it is.

   THE CROP IS NOT THE ONLY THING THAT EATS THE RANGE. A dash pattern RESTARTS
   at every subpath, while pathLength normalises the path as a WHOLE — so on a
   path made of several strokes, each one draws its own first (1 − offset) of
   the TOTAL, all at the same time, and the draw is over as soon as the LONGEST
   subpath is finished. Card 03's incoming arrow is five strokes: shaft 112.50
   units, two chevrons 77.37 each, two barbs 38.68 each, 344.61 in total. The
   longest is 0.3265 of that, so the arrow was finished at dashoffset 0.6735 —
   cover 26.8 % of a range running to 45 %, with 18.19 of its 27 points spent
   on a picture that had stopped changing. It also drew all five strokes at
   once, growing outward from five different points, which reads as a sketch
   scribbling itself rather than as a signal arriving.

   A trace is ONE stroke. Where the signal is drawn as several, each is its own
   trace and they are led along the direction of travel:

     --trace-lead  how far into the window this stroke starts drawing.
     --trace-span  how much of the window it spends drawing.

   Both are percentage points of `cover`, both default to the whole window, so
   an unslotted trace runs cover 18 % → 45 % exactly as before. Keep
   lead + span ≤ 27 % and the last stroke still lands on the light. */
.cf-iso__trace { --trace-from: 1; --trace-to: 0; --trace-lead: 0%; --trace-span: 27%; }

/* Scroll-driven assembly.
   Progressive enhancement in the honest direction: without support for
   scroll-driven animations, under reduced motion, or on paper, the object is
   simply drawn in its finished state. Nothing is ever hidden behind a script.
   Only transform and opacity animate, so nothing reflows.

   `screen and` is doing real work. A paged medium has no scroll, so a view
   timeline never advances: printing put whichever illustration happened to be at
   the current scroll position on the page and left the other three blank at
   opacity 0. Scoping the whole assembly to `screen` is the fix — print then sees
   no animation at all and draws the finished object, which is the same thing an
   unsupporting browser gets. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-iso {
      view-timeline-name: --cf-iso;
      view-timeline-axis: block;
    }
    .cf-iso__scene,
    .cf-iso__ghost,
    .cf-iso__light,
    .cf-iso__trace,
    .cf-iso__node,
    .cf-iso--build .cf-iso__form {
      animation-timeline: --cf-iso;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
    }

    /* The object arrives along 26.57° — 2 across, 1 down — never straight up. */
    .cf-iso__scene {
      animation-name: cf-iso-arrive;
      animation-range: cover 5% cover 30%;
    }
    /* The dashed geometry resolves after the body it belongs to. */
    .cf-iso__ghost {
      animation-name: cf-iso-fade;
      animation-range: cover 20% cover 40%;
    }
    /* The incoming signal draws itself. pathLength="1" in the markup makes the
       dash maths independent of how long the path actually is.
       The window is authored per stroke — see --trace-lead / --trace-span above.
       The defaults resolve to `cover 18% cover 45%`, which is what every trace
       that is a single stroke still gets. */
    .cf-iso__trace {
      stroke-dasharray: 1;
      animation-name: cf-iso-draw;
      animation-range:
        cover calc(18% + var(--trace-lead))
        cover calc(18% + var(--trace-lead) + var(--trace-span));
    }
    /* Lime is light: it is the last thing to arrive, and it arrives as a fill
       coming up, not as an object moving. */
    .cf-iso__light {
      animation-name: cf-iso-light;
      animation-range: cover 30% cover 48%;
    }
    .cf-iso__node {
      animation-name: cf-iso-fade;
      animation-range: cover 35% cover 50%;
    }

    /* AN ORBIT TURNS. It is the one piece of geometry in the system that is
       drawn as motion, and until now it was the only thing on the page that
       said "this is circulating" and then held perfectly still.

       It carries .cf-iso__ghost as well, so it fades up with the rest of the
       dashed geometry and then keeps turning for another ten per cent of the
       range, settling exactly where the nodes do. That is the brand's rule for
       anything spatial — turn slowly, then settle — and it is why the second
       range runs long rather than starting late: an orbit that begins to move
       after everything else has stopped reads as a separate event.

       Two animations on one element, which is the shape .cf-nav::after already
       uses: names, timelines and ranges are all comma lists, and the timeline
       has to be repeated because a single value would not pair with two names.
       --ease-out does the settling — most of the turn is spent in the first
       third of the range, and the last dash creeps into place. */
    .cf-iso__orbit {
      animation-name: cf-iso-fade, cf-iso-orbit;
      animation-timeline: --cf-iso, --cf-iso;
      animation-range: cover 20% cover 40%, cover 20% cover 50%;
    }

    /* ----------------------------------------------------------------------
       .cf-iso--build — THE OBJECT IS ASSEMBLED, NOT DELIVERED.

       The default assembly moves the whole scene: the finished object slides
       in along 26.57° and its layers resolve on top of it. That is right for a
       figure that illustrates a sentence, and wrong for the four process
       objects, which ARE the sentence. A telescope that arrives already
       extended has not shown you a telescope; it has shown you a picture of
       one. So on these, the scene holds still and the PARTS arrive.

       Two rules keep that from becoming decoration:

       1. A PART TRAVELS ONLY ALONG AN AXIS THE OBJECT ALREADY CONTAINS, and
          only as far as the drawing says. A telescoping section moves on the
          shaft axis, by the distance it is extended. A plate on a rack moves
          on the rack axis, by one slot pitch. A radial tick moves out along
          its own radius. Nothing is given a direction that is not already
          drawn in the geometry, which is why --build-dx / --build-dy are
          authored per part in viewBox units rather than derived from one
          token: 98 and 238 on card 01 are not a motion decision, they are the
          two extensions measured off the source vector.

          Both default to 0, so a part with nowhere to come from resolves in
          place. That is the common case and it should be: most parts of most
          objects are not going anywhere.

       2. STAGE ORDER IS CONSTRUCTION ORDER, NOT PAINT ORDER. --stage is an
          index, and it is deliberately independent of where the element sits
          in the markup — the sphere's meridian is drawn second and built
          first. Nothing is reordered to animate it, so occlusion, paint
          servers and the diff against assets/source/illustrations/ are all
          exactly what they were.

       Ranges overlap by ten points of the seventeen each stage runs, which is
       what makes this read as one assembly rather than four events. Four
       stages fit before the light comes up at 30 %.

       The nodes are NOT staged. Construction points are added to a finished
       object, at its finished position, which is what they mean — and it
       keeps them out of the per-part bookkeeping entirely.

       VERIFY THIS UNDER REDUCED MOTION, NOT IN THE SETTLED ANIMATED STATE.
       Animating an element at all changes how Chromium rasterises its
       contours by a sub-pixel, so NO animated object lands on its own still
       render to the pixel — the four cards diff 24,925 / 34,982 / 25,826 /
       25,270 of 495,616 against themselves. That is not the build's cost: the
       version that moved the whole scene paid 25,379 / 36,058 / 34,896 /
       25,483 for the same thing, more on every card. Under
       prefers-reduced-motion the four are identical to the pixel before and
       after, which is the check that actually means something.
       ---------------------------------------------------------------------- */
    .cf-iso--build .cf-iso__form {
      animation-name: cf-iso-build;
      animation-range: cover calc(5%  + var(--stage, 0) * 7%)
                       cover calc(22% + var(--stage, 0) * 7%);
    }

    /* The light is a part like any other while the object is being built, so
       it takes a stage and travels with the section it belongs to — card 01's
       lime plate IS the top of the inner shaft and cannot stay behind while
       the shaft rises. Two animations, one for the arrival and one for the
       light.

       THE FILL OPENS WHERE THE ARRIVAL CLOSES, AND IT USED TO OPEN AT 30 FLAT.
       This paragraph said "its fill still comes up last, on the range it
       always had", and on two of the four objects that was not true. A light
       that carries a stage arrives over `5 + 7n` to `22 + 7n`, and the fill's
       literal 30 is only later than that end while n is 0 or 1. Cards 01 and
       04 both put their lime plate on stage 2 — card 01's IS the top of the
       telescoping shaft and card 04's is the sphere's equator, so neither
       stage is free to move — and both arrive until cover 36, so the fill had
       six points of the range to itself before the plate carrying it landed.
       Measured on the stacked landing page at 375 x 812, on the one sample
       inside that overlap: card 01's crown at opacity 0.93 with 17 of its 238
       units of travel still to run, and its lime already filling at 0.03 —
       the shaft was lit before it had finished extending. The rule this is
       written against is the brand's own: lime is light, and light is the
       LAST thing to arrive.

       So the fill's start is the later of the two rather than a literal: the
       constant where the constant is still later, and the part's own arrival
       end where the stage has pushed past it. Stage 0 and 1 resolve to 30 %
       exactly, so cards 02 and 03 were byte-identical under it and cards 01
       and 04 opened at 36. (All four move under the correction below; that
       sentence is the state this one left, not the state that ships.) Nothing
       here is a new number — 22 and 7 are read off the arrival window
       immediately above, which is what keeps the two from drifting apart.
       scripts/check-iso-motion.py holds the inequality on this timeline and on
       the pinned one.

       AND THE INEQUALITY IS ABOUT THE OBJECT, WHICH IS NOT WHAT --stage NAMES.
       The paragraph above argues from the brand — "an object lit while it is
       still travelling is lit before it is there" — and then reads the arrival
       of ONE part: the plate the lime is painted on. Those are the same
       sentence only on an object whose lime plate happens to sit on its last
       stage, which is card 01 and nothing else. Everywhere else the fill still
       opened while other parts were arriving, and by more than the defect it
       was written to fix. Measured on components/process-card.html at
       390 x 812, sweeping card 03's own view timeline:

         cover 32 %   lime at fill-opacity 0.325   4 ticks still 16.5 units out
         cover 36 %   lime at 0.683                4 ticks still  5.3 units out
         cover 40 %   lime at 0.875                4 ticks still  0.9 units out
         cover 43 %   lime at 0.956                the object lands

       The lime reaches 96 % of its strength before card 03 has finished
       arriving. Card 01 reads 0.000 at cover 32 and 0.018 at 36, which is the
       correction above working exactly as it says.

       So the object's last stage is a fact about the object, and the object is
       what declares it: --build-stages, on the <svg>, beside the viewBox — the
       same place --iso-travel is declared and for the same reason. Nothing here
       is a new number: 30 and 48 are the window that shipped, 22 and 7 are the
       arrival's own end and step read off the rule above, and 40 is 48 − 18, so
       the fill keeps the 18 points of range it has always had rather than being
       squeezed against a fixed end.

       THE END MOVES NOW, AND THAT IS THE OTHER HALF OF THE SAME FINDING. Held
       at 48 while the start moved, the correction above silently cut cards 01
       and 04 from an 18-point fill to a 12-point one, and holding it for a
       stage-3 object would leave 5. A light that comes up in five points on one
       card and eighteen on another is one element behaving two ways, on the one
       moment per screen the brand gives to lime. Both ends travel with the
       object; the nodes and the orbit's settle follow the light, so the object
       still finishes on its construction points.

       An object that declares nothing resolves to 0 and every max() collapses
       to the literal beside it, so every figure outside the four process cards
       — is byte-identical, and so is every object that stops at stage 1.
       → foundations/motion.html#light-object */
    .cf-iso--build .cf-iso__light {
      animation-name: cf-iso-light, cf-iso-build;
      animation-timeline: --cf-iso, --cf-iso;
      animation-range: cover calc(max(30%, 22% + var(--build-stages, 0) * 7%))
                       cover calc(max(48%, 40% + var(--build-stages, 0) * 7%)),
                       cover calc(5%  + var(--stage, 0) * 7%)
                       cover calc(22% + var(--stage, 0) * 7%);
    }

    /* THE CONSTRUCTION POINTS ARE ADDED TO A FINISHED OBJECT, and on the two
       cards that carry a stage 3 they were not. The nodes' own comment above
       says what they mean — "added to a finished object, at its finished
       position" — and their window is 35–50, a literal measured when the
       deepest object went to stage 1. A stage-3 object arrives until 43, so its
       points began landing 8 points before the last of its geometry did.

       Same shape as the light's, one beat behind it: the nodes open 5 points
       after the fill and close 2 after it, which is the offset the two literals
       already state (35 − 30, 50 − 48). Stages 0 and 1 resolve to 35 and 50
       exactly. */
    .cf-iso--build .cf-iso__node {
      animation-range: cover calc(max(35%, 27% + var(--build-stages, 0) * 7%))
                       cover calc(max(50%, 42% + var(--build-stages, 0) * 7%));
    }

    /* THE PLAN COMES FIRST. A ghost is the object's future state, and on a
       built object the parts arrive out of exactly where the ghosts are — so
       a ghost that fades up after the part it predicts is the drawing told
       backwards. Under --build the dashed geometry is the first thing on
       screen and the solids come along it.

       The :not() is a mechanical exclusion, not a timing decision: an orbit
       runs two animations and this rule states ONE range, so without it the
       orbit's turn would be clobbered by the plan's window. The orbit's own
       --build range is the rule below, and it says the same thing this one
       does. */
    .cf-iso--build .cf-iso__ghost:not(.cf-iso__orbit) {
      animation-range: cover 2% cover 14%;
    }

    /* AND IT COMES FIRST FOR AN ORBIT TOO, WHICH FOR ONE RELEASE IT DID NOT.
       The exclusion above was written to protect the turn and it also handed
       the fade back to the base rule's `cover 20% cover 40%` — a window that
       was measured against an object arriving whole, not one being built. On
       the one built object that has orbits, card 04, they are the ONLY dashed
       geometry it carries, so the plan's window ran empty and the object's
       three rings appeared after the sphere inside them was finished. Swept at
       375 x 812 on patterns/landing-page.html, card 04's own view timeline,
       every 2.5 points of cover:

         cover  2 %   the plan's window opens          nothing on screen
         cover  5 %   the first solid arrives          orbit opacity 0.000
         cover 20 %   first orbit ink                  eleven forms at mean 0.444
         cover 40 %   orbits at 1.000                  forms at 0.997

       Twenty points of an object's arrival with nothing drawn for the solids
       to arrive into, and then the reference geometry fading up over the
       finished body. That is the drawing told backwards, on the object the
       paragraph above is written for.

       IT IS ALSO WHAT THE PINNED TRACK ALREADY DOES. acts.css re-times this
       same object onto the process track and gives the orbits `+0.5 %` — the
       ghosts' own head, one point ahead of the forms — so the two paths told
       the same drawing in opposite orders depending on the viewport. This is
       the stacked path agreeing with the designed one.

       THE FADE IS THE PLAN'S WINDOW, VERBATIM, and it is written as the same
       two literals rather than inherited, because a comma list cannot take one
       of its entries from another rule. THE TURN OPENS WITH IT AND ENDS WHERE
       THE NODES SETTLE — 50 %, which is what the base rule's "settling exactly
       where the nodes do" already says and is the one end that does not move.
       So the turn now runs the whole assembly instead of its second half: the
       ring is turning while the sphere builds inside it, which is the sentence
       the object is drawing.

       The travel does not change — 60 px, twelve dashes of --dash-1-4, both
       ends on the phase the source vector draws — so the settled ring is the
       same pixels it was and check-orbit-turn.py's share ceiling is untouched.
       What changes is the rate: the same twelve dashes over 48 points of cover
       instead of 30, which is slower, and "turn slowly, then settle" is the
       rule it is being slowed towards.

       THE SETTLE IS THE NODE WINDOW'S END, WRITTEN AS THE NODE WINDOW'S END.
       "Settling exactly where the nodes do" was true while that end was the
       literal 50; it stopped being true the moment the nodes learned to follow
       the object. Card 04 is the only built object with orbits and it is a
       stage-3 object, so its points now settle at 63 — a ring left at 50 would
       stop thirteen points before the object it circles is finished, which is
       the separate event this whole note is written against.
       → foundations/motion.html#build */
    .cf-iso--build .cf-iso__orbit {
      animation-range: cover 2% cover 14%,
                       cover 2% cover calc(max(50%, 42% + var(--build-stages, 0) * 7%));
    }

    /* An object that stages its own parts does not also travel as a whole:
       every child starts at opacity 0, so the scene has nothing left to fade
       and nowhere to arrive from. */
    .cf-iso--build .cf-iso__scene { animation-name: none; }
  }
}

/* The run is twice the rise, so the object arrives along 26.57°. See --iso-travel. */
@keyframes cf-iso-arrive {
  from {
    transform: translate(calc(var(--iso-travel) * -2px), calc(var(--iso-travel) * -1px));
    opacity: 0;
  }
  to   { transform: translate(0, 0); opacity: 1; }
}
/* A part fades up to the opacity it RESTS at, which is 1 for all but two of
   them. The literal that used to stand here silently outranked every authored
   opacity on every element this keyframe touches — see the statement lattice's
   note above --iso-rest. The fallback keeps the six parts that rest at full
   strength saying nothing at all. */
@keyframes cf-iso-fade {
  from { opacity: 0; }
  to   { opacity: var(--iso-rest, 1); }
}

/* A PART MOVES WITH `translate`, NOT WITH `transform`, AND THAT IS NOT A STYLE
   CHOICE. Half the parts that build carry a transform ATTRIBUTE — card 03's
   three ellipses and card 04's sphere are all rotate(-90 …), which on the
   ellipses is the shape and on the circle is the paint server's axis. An SVG
   transform attribute is a presentation attribute for the `transform`
   PROPERTY, so a keyframe that sets `transform` does not compose with it, it
   replaces it: card 03's apertures would snap from a tall ellipse to a wide
   one for the length of the build, and card 04's orbit fade would swing a
   quarter turn.

   The individual `translate` property is applied before `transform` and leaves
   it alone, so the rotation the markup asked for survives the animation. The
   scene's own cf-iso-arrive can keep using `transform` because it moves a bare
   <g> that carries no attribute of its own.

   Units are viewBox units — 1px on an SVG element is one user unit — which is
   the same basis --iso-travel uses and the same basis the numbers were
   measured in. */
@keyframes cf-iso-build {
  from {
    translate: calc(var(--build-dx, 0) * 1px) calc(var(--build-dy, 0) * 1px);
    opacity: 0;
  }
  to { translate: 0 0; opacity: 1; }
}
@keyframes cf-iso-light {
  from { fill-opacity: 0; }
  to   { fill-opacity: 1; }
}
/* The ends are per-trace, so the range that is spent is the range that is seen. */
@keyframes cf-iso-draw {
  from { stroke-dashoffset: var(--trace-from); }
  to   { stroke-dashoffset: var(--trace-to); }
}

/* THE ORBIT TURNS BY MOVING ITS DASHES ALONG ITSELF, NOT BY ROTATING.
   A rotation is the obvious way to turn a ring and it is the wrong one here,
   for a reason this object has already cost the system once: all three orbits
   are stroked with userSpaceOnUse gradients, and a paint server is resolved in
   the user space in force where it is referenced — which includes the element's
   own transform. Rotating the ring rotates its fade with it, so the designer's
   "solid on the left, dissolving to the right" would sweep round the object and
   spend its solid half across the top of the lime disc. That is the exact
   failure components/process-card.html documents and measures.

   stroke-dashoffset touches no paint server at all. The fade axis stays where
   the source vector put it and only the dashes move, which is what an orbit
   looks like anyway: the ring does not swing, the things on it go round.

   It ends at 0 and starts at a whole number of dash periods, so BOTH ends of
   the animation are on the phase the source draws — including the `both` fill
   held before the range opens. See --iso-orbit-travel.

   THE DEFAULT IS CARD 04'S, AND A SMALLER RING OVERRIDES IT. The travel is an
   arc, and what a reader sees is that arc as a SHARE of the ring it is on. One
   arc for every ring is deliberate — it makes an object's inner ring turn
   through more of itself than its outer one — but it holds only while the
   rings are of a size. Card 04's are 1 171 to 1 521 units and take one part in
   19.5 to 25.3; the six on Expertise are 103 to 324 and were taking up to 0.58
   of themselves, which is past a lap. So an orbit whose share would exceed
   card 04's tightest declares its own --iso-orbit-travel inline, still a whole
   number of dashes. → foundations/motion.html#orbit-share

   This is a paint-only animation, not a composited one. That is a real cost
   and it is the same one the system already pays on every card: .cf-iso__trace
   has animated stroke-dashoffset since the assembly was written. Three dashed
   hairlines on one object, running only while that object crosses the
   viewport, is the same order of work again — not a new class of expense. */
@keyframes cf-iso-orbit {
  from { stroke-dashoffset: var(--iso-orbit-travel); }
  to   { stroke-dashoffset: 0; }
}

/* FORCED COLOURS, AND THE OBJECT COMES BACK AS WHAT THIS BRAND SAYS IT IS.
   Geometry lives in the markup here, so the palette does too: every face is a
   `fill="#CFCFCF"` and every contour a `stroke="#000"`, and a presentation
   attribute cannot resolve a token — check-paint-register.py exists because of
   exactly that. The mode does not force SVG `fill` and `stroke` either, so an
   object arrives in this mode wearing the literal five greys and the literal
   black it was drawn in, whatever palette the reader asked for.

   In the light palette that passes for correct. In the dark one it is a solid:
   measured on components/process-card.html, the faces came through at their
   own greys on Canvas black and every contour, every ghost and every
   construction point was drawn black on black. The object was a light
   silhouette — the one drawing in the system whose own first rule is "the
   contour is the drawing", rendered as the fill alone.

   ATTRIBUTE SELECTORS, BECAUSE THE none/NOT-none SPLIT IS THE WHOLE RULE. A
   blanket `fill: Canvas` would fill the ghosts and the contour paths, which
   are unfilled on purpose — the <svg> carries `fill="none"` and they inherit
   it — and an opaque ghost is a reference plate over the body it references.
   The drawing already writes the distinction down as an attribute, so it is
   read as one: a shape that was filled becomes Canvas and goes on occluding
   exactly what it occluded before, a shape that was not stays unfilled, and
   the far edges keep disappearing behind the near faces. Painter's algorithm
   intact, in two colours.

   THE ROOT IS IN THE SELECTOR LIST AND IT IS NOT DECORATION. Half these
   drawings put the contour on the <svg> itself — `<svg class="cf-iso"
   fill="none" stroke="#000">` — and let the whole tree inherit it, so a
   descendant rule alone repaints the elements that name their own stroke and
   leaves every element that inherits one black. Measured on
   components/article.html with the descendant rule alone: .cf-iso__scene,
   .cf-iso__form and .cf-iso__ghost all still computed rgb(0, 0, 0), each with
   a null stroke attribute and the root carrying #000. Naming .cf-iso covers
   the inherited case and the attribute rule covers the declared one; between
   them there is no third way a contour in these drawings gets its colour.

   The nodes are the exception and they are named after: a construction point
   is a filled mark with no stroke, so the fill IS the mark and Canvas would
   erase it. It goes to CanvasText, which is what it always was — and it is
   written `.cf-iso .cf-iso__node[fill]` rather than `.cf-iso__node[fill]`
   because `:not()` takes its argument's specificity, which puts the blanket
   fill rule at (0,3,0) and would otherwise outrank it.

   THE LIGHT FALLS AWAY WITH THE REST OF THE FILLS, and it is `[fill]` that
   takes it — .cf-iso__light is a paint server, `fill="url(#cf-01-light)"`, so
   it matches the same selector and lands on Canvas. That is the answer
   .cf-gantt__mark--lit and .cf-pie__band already give: a lime ramp inside a
   palette the reader chose is what this mode exists to stop, and the lit face
   keeps its contour, which is the one thing every other face keeps too. */
@media (forced-colors: active) {
  .cf-iso,
  .cf-iso [stroke]:not([stroke="none"]) { stroke: CanvasText; }
  .cf-iso [fill]:not([fill="none"])     { fill: Canvas; }
  .cf-iso .cf-iso__node[fill]           { fill: CanvasText; }
}

/* --------------------------------------------------------------------------
   HERO — the still switch, and reduced motion
   The loop is the one infinite animation in the system, so it is also the one
   that has to be switched off by hand. There are two doors to the same room,
   and they are not interchangeable:

     prefers-reduced-motion   the OS preference. A default, applied silently.
     .cf-hero__still-toggle   the reader's own switch, on the page.

   The second is not a nicety on top of the first. WCAG 2.2.2 (level A) asks
   for a MECHANISM over content that moves automatically for more than five
   seconds alongside other content; this loop is 12 s and repeats forever, and
   prefers-reduced-motion is named nowhere in that criterion — it is sufficient
   for 2.3.3, which is a different question. So the switch is the conformance
   and the media query is the courtesy.

   Both doors do the identical thing: hide the loop, show the still. "Hide" is
   the third of the criterion's own three verbs, and it is the only one CSS can
   reach — display: none removes the video from the presentation but does not
   pause the element, so decoding continues in the background. That is an honest
   cost and it is worth naming: what the reader gets back is that nothing moves,
   without a line of script, and with JS off. A scripted video.pause() would also
   stop the decode; it would also make the mechanism disappear exactly for the
   readers most likely to have scripting turned off.

   THE DOWNLOAD WAS THE LARGER HALF OF THAT COST, and only one of the two doors
   can close it. `display: none` is a rendering instruction and the resource
   selection algorithm has never read one, so the file arrived in full either
   way: measured on the landing page at 1280 x 800, 1,890,957 bytes total with
   the preference set and unset alike, 995,398 of them the mp4 — 53 % of the
   page, for a reader who is shown a frame of none of it. Re-measured 2026-08-03
   on the artwork that shipped that day, uncompressed off a server that does not
   gzip: 4,798,744 bytes over 20 requests, 3,328,598 of them the mp4 — 69 %. The
   share the gate withholds went UP with the better loop, which is the argument
   for the gate and not against it. The markup answers
   that door now: the source is a <source> gated on (prefers-reduced-motion:
   no-preference), which is the selection algorithm rather than a hint to it,
   so under `reduce` nothing is requested and nothing is decoded. `preload`
   was not the fix and looks like it should be — autoplay overrides it,
   measured. → patterns/landing-page.html, scripts/check-hero-video.py

   The reader's own switch still pays both costs, and that is correct rather
   than an oversight: it exists for readers who did NOT set the preference, so
   the loop has to be there to be switched off, and by the time a checkbox is
   ticked selection happened long ago. The media gate answers the door that was
   opened before the page was laid out. The other one has a person standing in
   it.

   The switch reads FIRST in the markup so these sibling selectors can reach
   both media elements. Keep it there.

   AND IT SITS BESIDE .cf-hero__media RATHER THAN INSIDE IT, which is the one
   thing about its placement that is load-bearing rather than convenient. The
   media box is `position: absolute; z-index: 0` — a STACKING CONTEXT, and
   deliberately so: the note over .cf-hero__media::after banks on it, "below
   .cf-hero__body's z-index 1 by being in the media's box at all, so no
   stacking order has to be stated for the type". That seal is right for the
   artwork and the scrim. It is wrong for a control, and it does not ask which
   it is holding down. Inside the media box the plate's own `z-index` ranks it
   only against the video and the scrim; against anything outside it the whole
   subtree is one layer at 0, under the type at 1, and no number written here
   can lift it.

   The type has no background, so nothing looked wrong. What was wrong was the
   hit test. .cf-hero__body is `width: fit-content` sized to the LONGEST line
   the headline sets, and the h1's box therefore runs the full width of that
   column on every line — including the trailing emptiness after the short
   ones. Wherever that box reaches the plate, the plate is under it and the h1
   takes the click. Measured on the landing page, elementFromPoint at the
   plate's centre and at both of its diagonal corners:

     375 x 812   h1.cf-hero__title   h1.cf-hero__title   h1.cf-hero__title
     414 x 896   label.cf-hero__still  (own)  ...  and clear at every width above

   375 is where it binds because that is where the hero runs out of height:
   min(92vh, 56rem, 100vh - banner) leaves 478.5 px for ~473 px of content, the
   flex column stops being bottom-slack, and the headline climbs to y 84 —
   through the plate's band at 96–136. Above 414 the body starts at 179 and the
   two boxes never meet. So the one viewport where this control is unreachable
   by pointer is the phone, and this control is WCAG 2.2.2 (level A): the
   mechanism over a 12 s loop that repeats forever. The checkbox kept working —
   focus is not hit-testing — so a keyboard reader could always pause the loop
   and a touch reader never could.

   Out here the plate and its input are children of .cf-hero, which is
   `position: relative` with `z-index: auto` and so establishes no context of
   its own (`overflow: clip` does not make one). Control and type therefore
   rank against each other directly, at 2 against 1, and the artwork keeps its
   seal at 0 with nothing in it that has to escape. The geometry does not move:
   absolute positioning resolves against the padding box either way, and
   .cf-hero__media is `inset: 0` on that same box — measured identical, 96 px
   from the top at 375 and 106 at 1440, before and after.

   The sibling selectors pay one hop for it: the media elements are no longer
   the switch's own siblings, so the two rules below reach through
   .cf-hero__media to them. Everything else about the mechanism is unchanged —
   still no script, still working with JS off.
   → scripts/check-pointer-reach.py
   -------------------------------------------------------------------------- */
.cf-hero__media img { display: none; }
.cf-hero__still-toggle:checked ~ .cf-hero__media video { display: none; }
.cf-hero__still-toggle:checked ~ .cf-hero__media img   { display: block; }

.cf-hero { --hero-still-top: calc(var(--nav-height) + var(--space-4)); }

/* The input is not `.visually-hidden`, and that is a fix rather than a style.
   The hero is pulled up by the nav's height, so its media box starts ABOVE the
   top of the document — and a 1 px clipped input pinned to that box's origin is
   off-screen. Chromium scrolls a focused element into view, so tabbing to the
   switch yanked the page upward before the plate the reader is aiming at had
   been drawn. The input therefore occupies exactly the plate's box, transparent
   and not hit-testable: focus lands where the control is, the label takes every
   click, and the accessible checkbox is still a real checkbox. */
.cf-hero__still-toggle {
  position: absolute;
  /* 2, not 1, and against .cf-hero__body's 1 — see the block header. The input
     is not hit-testable, so this rank is only what keeps it in the same layer
     as the plate it has to stay under. */
  z-index: 2;
  top: var(--hero-still-top);
  right: var(--column-inset);
  width: 2rem;
  height: 2rem;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}
/* An opaque plate, not glass. The token file allows exactly two blurred layers
   on a page and both are already spent here — the navigation band and the hero
   button — so this control takes the opaque material layer instead. Black also
   holds its own contrast against whatever frame of the artwork is underneath it,
   which a translucent plate over a moving image cannot promise.

   IT IS THE PAGE'S QUIETEST BLACK PLATE AND IT USED TO BE ITS LOUDEST. The
   contour was --border-strong, which under data-theme="inverse" resolves to
   rgba(255,255,255,0.72) — every other black plate on this page carries
   --border-inverse at 0.18, and that is not a near miss, it is four times the
   weight. .cf-nav__list and .cf-nav__toggle both stand on this same hero, over
   this same loop, in this same top band, and both are drawn at 0.18. The one
   optional control on the page was the only thing outlined at 0.72, so a
   secondary switch was the hardest-edged object in the picture and the
   MENÜ button 12 px above it — the primary control — was softer than the
   switch beside it.

   What made 0.72 look necessary is that the plate has to be findable over
   artwork it does not own. It already is, and by its fill rather than by its
   edge. Measured off the shipping render, the 24 px band immediately outside
   the plate: mean rgb(162,162,162) at 1440 x 900 and rgb(176,176,175) at
   375 x 812, which puts the black plate at 8.23:1 and 9.68:1 against its own
   surroundings before the contour is drawn at all. The darkest single pixel
   in that band at 1440 still leaves 4.69:1. A contour on top of eight-to-one
   is not doing the separating; it is only stating the edge, which is the job
   0.18 is the token for.

   Size follows the same argument. The plate was 2.5rem against the nav
   furniture's 44 px — the same rank, near enough, for a control that is
   subordinate to it. At 2rem it reads as subordinate and still clears
   WCAG 2.5.8's 24 px target floor with 8 px to spare.
   → the Notion row asked for "classy und dezent an rand" */
.cf-hero__still {
  position: absolute;
  /* Above .cf-hero__body's 1, which is the whole point of the move out of the
     media box — the plate has to take the click where the headline's box
     overlaps it. */
  z-index: 2;
  top: var(--hero-still-top);
  right: var(--column-inset);
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: var(--stroke-1) solid var(--border-inverse);
  border-radius: var(--radius-sm);
  /* Both resolve against data-theme="inverse" on the label itself: black plate,
     white contour, white marks, lime focus ring — the same flip the footer and
     the dark button already take. --border-inverse is declared once, at :root,
     and is already white — it does not flip, which is why the nav's plates and
     this one can share it across the two themes. */
  background: var(--surface-base);
  color: var(--text-primary);
  cursor: pointer;
  /* The control acknowledging a pointer, which is --duration-fast by doctrine.
     Nothing here is light crossing a surface. */
  transition: background var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard);
}
/* The plate is already black, so hover lifts rather than deepens — there is
   nothing under black to go to. Same move as .cf-btn--solid. */
/* The input carries the focus, the label carries both the ring and the lift —
   the ring says where the reader is, the lift says the plate is a control. */
.cf-hero__still:hover,
.cf-hero__still-toggle:focus-visible ~ .cf-hero__still { background: var(--surface-raised); }
/* --focus-ring RESOLVES TO LIME HERE, because the label carries
   data-theme="inverse" — and the label stands on the hero. Measured against
   the poster, worst of eight edges sampled: 1.18:1 at 375, 1.15 at 768,
   1.00 at 1280, 1.01 at 1440. The 1.00 is not a rounding: at 1280 the plate's
   top-right corner sits over the artwork's lime passage and the ring is
   rgb(225,255,0) against rgb(225,254,99). The one control WCAG 2.2.2 asks
   this page for, and on a wide screen a keyboard reader cannot see where they
   are on it.

   The plate is black (--surface-base under inverse, lifted to
   --surface-raised on focus), so the black inner ring is invisible against
   the plate and that is the point: what it buys is the black/lime boundary at
   2 px, 18.51:1, internal to the indicator and out of the video's reach. Same
   two-tone construction as .cf-btn--glass and .cf-nav, and for the same
   reason — a single tone over an artwork has nothing it can guarantee.
   → scripts/check-focus-ring-edge.py */
.cf-hero__still-toggle:focus-visible ~ .cf-hero__still {
  outline: var(--stroke-2) solid var(--focus-ring);
  outline-offset: 2px;
  box-shadow: 0 0 0 var(--stroke-2) var(--cf-schwarz);
}
.cf-hero__still-mark {
  /* Half the plate, as it was at 2.5rem/1.25rem — the mark keeps its share of
     the box rather than staying 20 px inside a box that shrank around it. */
  width: 1rem;
  height: 1rem;
  stroke: currentColor;
  stroke-width: var(--stroke-1);
  /* Contour before fill: both marks are drawn, not filled — the same rule the
     icon set and the illustrations hold to. */
  fill: none;
}
/* Each mark names the NEXT action, so the pair swaps with the state: bars while
   the loop runs, the triangle once it has stopped. */
.cf-hero__still-mark--play { display: none; }
.cf-hero__still-toggle:checked ~ .cf-hero__still .cf-hero__still-mark--pause { display: none; }
.cf-hero__still-toggle:checked ~ .cf-hero__still .cf-hero__still-mark--play  { display: block; }

/* LAST, and that is the whole reason it is down here rather than up with the
   two ~ rules it belongs to. `display: none` on .cf-hero__still has to beat the
   `display: grid` above it, and both selectors weigh (0,1,0) — a media query
   adds nothing to specificity, so above the plate's own rule this block lost
   silently and the control stayed on screen with nothing left for it to switch.
   Caught in the browser, not read off the cascade.

   AND PRINT TAKES THE SAME ARM, because print is the other state where nothing
   is moving. Paper cannot loop twelve seconds of artwork, so what a printed
   hero was getting was whichever frame the decoder happened to be holding when
   the sheet was composed — a different picture every time the same page is
   printed — under a 40 x 40 black plate carrying a pause glyph for a loop that
   is not running. The still is the authored frame and the one this system
   already publishes contrast figures against; the switch is a control, and a
   control on paper is ink that does nothing. Both answers are already written
   here, so print joins the query rather than restating it.

   AND (scripting: none) IS THE THIRD ARM, FOR A REASON NEITHER OF THE OTHER
   TWO WOULD HAVE FOUND. Nothing on this page carries a `controls` attribute,
   so nobody expects a media UI — but the HTML standard says the user agent
   should expose one anyway "if scripting is disabled for the media element",
   and every engine does. Photographed at 1440 x 900 with JavaScript off, on
   the shipped page: a scrub track across the full width of the hero at y 781,
   a 0:00 readout at x 60 sitting on the kicker's own baseline, the button
   cluster at x 1320-1420, and a loading arc over the middle of the artwork at
   735, 400. With scripting on, that strip is clean. It is the front door, in
   the one tier a developer never browses in, under a video that is
   aria-hidden, muted, looping and decorative — and beside a pause control this
   page already draws itself, in the system's own language, out of a checkbox
   and two ~ selectors that need no script at all.

   The UA chrome cannot be styled away without !important — Chromium's own
   sheet carries one on ::-webkit-media-controls, and there are none in this
   system — and a vendor pseudo-element would answer one engine. So the answer
   is the one already written three lines down: no video box, no controls. A
   reader with scripting off gets the authored still, which is exactly what the
   reduced-motion reader and the printed sheet get, and the switch goes with it
   for the reason stated above — nothing is moving, so there is nothing to
   switch. The <source> on patterns/landing-page.html carries the matching
   `and (scripting: enabled)`, so the 3,328,598 bytes are not fetched either:
   the same two-part answer, the media gate for the bytes and this query for
   the paint, that the note over .cf-hero__media spells out for reduce.
   → scripts/check-media-controls.py */
@media print, (prefers-reduced-motion: reduce), (scripting: none) {
  .cf-hero__media video { display: none; }
  .cf-hero__media img   { display: block; }
  /* Nothing is moving, so there is nothing to switch off. The control is not
     merely hidden — it is removed, because a switch whose state cannot change
     what the reader sees is worse than no switch at all. */
  .cf-hero__still-toggle,
  .cf-hero__still { display: none; }
}

@media (max-width: 48.75rem) {
  /* The navigation is full-bleed at this width and the headline climbs, so the
     control tucks a rung closer under the bar. One custom property moves both
     the plate and the input that has to stay under it — declared on .cf-hero
     rather than .cf-hero__media since the pair left the media box; both are
     still inside it, so both still inherit. */
  .cf-hero { --hero-still-top: calc(var(--nav-height) + var(--space-3)); }
}

/* --------------------------------------------------------------------------
   PINNED TRACK — the stage the big pages scrub their builds on
   A track of (steps × 1.6 + 1) viewports with one sticky stage: an index of
   marks at the top, a progress hairline at the bottom, and each step owning
   a quarter of the range — the object crossfading over a three-point overlap
   at each end, the copy holding its own quarter and snapping out at the end
   of it while the eye is on the object's dissolve.

   Expertise and the landing page each carried a private copy of this
   scaffolding, byte for byte the same, and each stated in place that nothing
   moved here until a second page wanted this exact stage. The census found
   the second page: ten identical rules and five identical keyframe sets
   under two names. So the scaffolding moved, on the line both preambles
   drew. What stays on the page is the page's — what the stage holds, how
   its copy arrives, which windows its build reads — because there the two
   genuinely differ, and folding those here would be widening a component to
   cover a one-off.

   The quarter in every range below is 100% over the default four steps,
   written as the literal both pages agreed on. A page that changes
   --pin-steps owns its own ranges.

   Everything is a progressive enhancement over the stacked column
   .cf-pin__steps lays out below: no view timelines, reduced motion, print,
   or a viewport under 64rem × 45rem and none of this applies — the chrome
   stays hidden and nothing pins. The gate is in the register, with the
   derivation on the landing page that measured it. → foundations/layout.html
   -------------------------------------------------------------------------- */

/* ---- the readable state: no chrome, nothing pinned ---- */
.cf-pin__index,
.cf-pin__bar { display: none; }
/* THE STACKED COLUMN IS THE MECHANISM'S TOO. When the fold above moved the
   scaffolding here, the fallback stack stayed behind "for the pages to lay
   out for themselves" — and all three pin adopters laid it out with this
   identical rule: the landing page in acts.css AND a second, verbatim copy
   in its own <style> block, expertise under .ex-steps. A mechanism whose
   readable state every adopter must re-type is a mechanism shipped
   incomplete, so the rule moved on the same line the scaffolding did.
   Steps overlap on the stage only inside the gate, where a page's own
   enhanced rules (acts.css, expertise's block) re-take the grid; out here
   they are a column with the section rhythm between them. */
.cf-pin__steps {
  display: grid;
  /* THE COLUMN IS DECLARED, AND IT WAS THE IMPLICIT ONE scripts/check-grid-tracks.py
     counts in its census: `display: grid` with no track list leaves one `auto`
     column, and `auto` as a minimum is the widest thing inside it. Every card in
     this stack is a grid ITEM, so the reset's break-word net could not reach the
     words that mattered — a net breaks a word at the box's edge, and the track
     had already grown to the word. Measured on patterns/landing-page.html, act
     4's four cards, the document's scroll width against the viewport:

                        root 16   root 20   root 24   root 32
       320 x 800          320       332       368       459
       375 x 800          375       375       375       463

     The width was one compound in one card's copy: "Wartungsintervalle" in
     card 03's title is 309 px of unbreakable word at a 32 px root, inside a
     stack whose whole width at 320 is 280, and the track took the word. The
     cell census in check-text-zoom.py carried these as "act 4's cards" since
     the act landed.

     minmax(0, 1fr) is the floor the same script asks of every explicit track:
     one column, as wide as the stack and no wider, so a card takes the width
     it is given and its copy wraps inside it — which is what the net was
     always for. Same four cells after, with the card's own two implicit
     columns declared in acts.css beside it:

                        root 20   root 24   root 32
       320 x 800          326       343       379
       375 x 800          375       375       406

     What was left past the edge in every one of those was .cf-pie__label at
     the ring's 3 o'clock — a 5rem cap beside a px radius, the component's own
     tier question and not this stack's — and, at 320 x 32, the folded nav's
     22 px that every page carries. The pie has since taken its half: the cap
     is min()'d against the room the label's own angle leaves it, and the same
     four cells now read

                        root 20   root 24   root 32
       320 x 800          320       320       377
       375 x 800          375       375       381

     — zero at the two rows WCAG 1.4.4 asks for, and what remains at 32 is the
     act's rem insets and the nav's 22 px, neither of which is a track. The pinned tiers above the gate place every card
     at grid-area 1 / 1 and are unmoved by naming the column they were already
     in; measured identical to the pixel on all four adopters at 375, 1024 and
     1440. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-12);
}

@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) and (min-width: 64rem) and (min-height: 45rem) {
    /* 1.6 SCREENS OF SCROLL PER STEP, not one. The track's contain range is
       its own height minus one viewport, so at (steps + 1) each step had a
       single screen to arrive, hold and leave in — every part of the build
       was over in a flick of the wheel and the handovers read as cuts. */
    .cf-pin {
      --pin-steps: 4;
      height: calc((var(--pin-steps) * 1.6 + 1) * 100vh);
      view-timeline-name: --cf-pin;
      view-timeline-axis: block;
    }
    .cf-pin__stage {
      position: sticky;
      top: 0;
      height: 100vh;
      /* dvh on top of vh, the pair .cf-nav__list already pins above for the
         same reason: Safari's dynamic toolbar makes 100vh the chrome-
         collapsed viewport, so a stage sized off it alone can stand taller
         than what the reader can see until the bar retracts, and the pinned
         build this stage holds is clipped, not scrolled, so the surplus is
         cut rather than reachable. dvh tracks the chrome as it shows and
         hides; a browser with no dvh keeps the vh line above. */
      height: 100dvh;
      /* clip, not hidden — a crop must not become a scrollport. */
      overflow: hidden;
      overflow: clip;
    }
    .cf-pin__inner {
      height: 100%;
      display: grid;
      /* The floor the pages' own copies never had: page-local blocks are
         outside check-grid-tracks.py's scope, so the bare 1fr shipped twice
         unchallenged. Folding it here is what surfaced it. */
      grid-template-rows: auto minmax(0, 1fr) auto;
      /* The nav floats over the page, so a stage pinned to the viewport top
         has to start below it or the index reads through the bar. */
      padding-block: calc(var(--nav-height) + var(--space-4)) var(--space-8);
    }
    /* ONE MEASURE FOR THE WHOLE STAGE, and it is the CARD's. The index names
       the card and the bar runs under it: the three are one column, and the
       index's 01 has to stand over the card's left edge or it is annotating
       nothing. The stage's three rows are therefore sized together, off
       --pin-measure, which a page sets when its card is narrower than the
       container — and inherits down to all three, so there is no way to
       narrow one without narrowing the others.

       IT WAS POSSIBLE, AND THE LANDING PAGE DID IT. Its card is bound to the
       viewport HEIGHT (max-inline-size: calc((100vh - 13rem) * 2), derived in
       place), and that cap was written on the card alone. So the card
       narrowed and centred while the index and bar stayed the full container:

         viewport     container   card    chrome wider by
         1024 x 900        911     911      0
         1280 x 900       1139    1139      0
         1280 x 720       1139    1024    115
         1366 x 768       1216    1120     96
         1512 x 782       1280    1148    132
         1440 x 720       1280    1024    256

       Every zero is a viewport where the height cap does not bind — which is
       every size anyone checks at, because 900 and 1080 are tall. Every
       non-zero is a laptop. `none` is the default, so a stage that does not
       set --pin-measure is exactly what it was.

       CENTRED BY justify-self AND NOT BY AUTO MARGINS, which is not a taste.
       `margin-inline: auto` here is (0,1,0) and base.css writes
       `ol[role="list"] { margin: 0 }` at (0,1,1) — so the two cards centred,
       the bar centred, and the index alone stayed flush to the container,
       which is the same bug one element narrower. justify-self is nobody
       else's property. */
    .cf-pin__inner > * {
      width: 100%;
      max-inline-size: var(--pin-measure, none);
      justify-self: center;
    }
    .cf-pin__index { display: flex; }
    .cf-pin__bar { display: block; }

    .cf-pin__step {
      opacity: 0;
      animation: cf-pin-step linear both;
      animation-timeline: --cf-pin;
      animation-range: contain calc(var(--i) * 25% - 3%) contain calc((var(--i) + 1) * 25% + 3%);
    }
    .cf-pin__copy {
      animation: cf-pin-copy-hold linear both;
      animation-timeline: --cf-pin;
      animation-range: contain calc(var(--i) * 25%) contain calc((var(--i) + 1) * 25%);
    }
    /* THE NUMERAL IS LIT WHILE ITS CARD IS ON THE STAGE, and for as long as
       this stage has existed it was lit for rather less than that.

       The mark ran the QUARTER — `contain 25i% -> 25(i+1)%` — with
       cf-pin-mark reaching --text-primary at 14 % of it and leaving at 86 %.
       The card runs a range three points WIDER at each end and reaches full
       opacity at 11 % of that, which is 0.41 of a point into its quarter. Two
       different ranges, two different literals, and the arithmetic falls out:

         card i fully opaque   contain 25i + 0.41  ..  25i + 24.59
         mark i at primary     contain 25i + 3.50  ..  25i + 21.50

       So the numeral arrived 3.09 points after the card it names and left 3.09
       points before it, and between one mark going dark and the next lighting
       there was a seven-point band with NOTHING at --text-primary — centred
       exactly on the quarter boundary, where all four numerals are --text-muted
       to the integer while a card stands fully opaque behind them. The index is
       the one element on this stage whose whole job is to say which of the four
       you are looking at, and it said none of them. Swept at 20 px, counting a
       mark as lit within a quarter of the primary-to-muted range:

         landing    1440 x 900   1020 px of 5760   17.7 %   160 / 280 / 280 / 300
         landing    1280 x 800    900 px of 5120   17.6 %   140 / 240 / 260 / 260
         expertise  1440 x 900   1040 px of 5760   18.1 %   160 / 300 / 280 / 300
         expertise  1280 x 800    920 px of 5120   18.0 %   140 / 280 / 260 / 240

       The first band of each row is the worst of the four, because it is not a
       handover: it is the stage pinning. The reader arrives, the page stops,
       card 01 is complete — and the index over it names nothing for 160 px.

       THE FIX IS TO STOP WRITING IT TWICE. The mark takes the step's range and
       the step's two literals, so "lit" and "on the stage" are the same
       interval by construction rather than by two numbers agreeing. The cards
       cross over at the quarter boundary with both at 0.88; the numerals now
       cross there too, both half-way between the two inks over 0.82 of a point
       — 47 px, the same gesture, instead of 280 px of nothing.
       scripts/check-pin-name.py holds the two bands to each other. */
    .cf-pin__mark {
      animation: cf-pin-mark linear both;
      animation-timeline: --cf-pin;
      animation-range: contain calc(var(--i) * 25% - 3%) contain calc((var(--i) + 1) * 25% + 3%);
    }
    .cf-pin__bar::after {
      animation: cf-pin-bar linear both;
      animation-timeline: --cf-pin;
      animation-range: contain 0% contain 100%;
    }

    /* THE TRACK ENDS WITH THE LAST STEP STILL STANDING, and it did not.

       THIS RULE IS NOT NEW HERE — it is the one part of the first pinned track
       that did not come with the scaffolding when it moved. Unsere Werte has
       carried it since it was written, in three lines a few hundred down this
       file: cf-values-item ends at opacity 0, .cf-values__item:nth-child(6)
       runs cf-values-item-last instead, and the comment over it says "the last
       one holds: there is no seventh stage to hand over to". When Expertise and
       the landing page's identical copies were folded into .cf-pin, the
       quarters, the crossfade, the 62 % hold and the chrome all came across.
       The sixth-stage exception did not, because there was nothing in either
       page's copy to fold — neither had ever had it.

       foundations/motion.html#hold states it a second time, about the glass
       pane: "The last cycle has no exit, because the track ends with the pane
       still standing." The pane obeys it too, because the pane is one element
       with its cycle written out four times and somebody had to type the
       fourth. The three animations above never did, because all three are
       HANDOVER animations: each is written to come back to nothing at 100 % so
       the next quarter can take the stage from it, and the fourth quarter has
       no next quarter.

       So the track dismantled itself and then made the reader scroll the
       wreckage. Measured on the landing page at 1440 x 900, contain 0-100 %
       spanning y 1664-7424:

         copy 04    opacity 1 -> 0   contain 96.5 -> 99.3   (y 7224 - 7384)
         mark 04    ink -> muted     contain 96.5 -> 100    (y 7224 - 7424)
         step 04    opacity 1 -> 0   contain 100  -> 103.5  (y 7424 - 7624)

       The copy goes 200 px before the stage has moved at all, so the last card
       stands there wordless while its own index number greys out under it; the
       drawing follows once the stage unpins; and from y 7624 the reader has
       700 px — 600 of them with the frame still on screen — of an empty
       lectern, four dead index numbers and a full progress bar to scroll past
       before the section ends at 8324. Nothing in that stretch changes at all:
       it is the longest dead zone on the page by a factor of two. Same shape at
       every size the gate admits (1024 x 720: 560 px; 1280 x 800: 640 px) and
       the same on Expertise, which shares these rules.

       The last quarter therefore runs the same animations WITHOUT their exits:
       the step still fades in, the mark still lights, and both hold; the copy,
       which only ever left, has nothing to do at all. Binding is `:last-child`
       rather than `:last-of-type` — on the landing page the lectern plate is
       an <article> sibling of the four cards, so `:last-of-type` is right there
       and wrong for the copy, whose panel is the last <div> in EVERY card.
       scripts/check-pin-handover.py holds both the keyframe pairs and the
       markup shape `:last-child` depends on. */
    .cf-pin__step:last-child { animation-name: cf-pin-step-last; }
    .cf-pin__step:last-child .cf-pin__copy { animation-name: none; }
    .cf-pin__mark:last-child { animation-name: cf-pin-mark-last; }

    /* THE BUILD, RE-TIMED ONTO THE TRACK. A sticky object never travels
       through the viewport, so its own view() freezes at one value and the
       object simply stands there, finished. On the pinned stage every part
       reads the track's timeline instead. The scene stops travelling — it
       would add its translate to each part's and the object would come in
       twice. WHICH keyframes the parts run, and in which windows, stays on
       the page — except the trace and the nodes, whose windows both pages
       agreed on to the half point: the trace draws 5 to 13 of the quarter,
       the nodes land 13 to 15.5, the reveal's 62 % hold.
       → foundations/motion.html#hold */
    .cf-pin .cf-iso__scene { animation-name: none; }
    .cf-pin .cf-iso__form,
    .cf-pin .cf-iso__ghost,
    .cf-pin .cf-iso__orbit,
    .cf-pin .cf-iso__trace,
    .cf-pin .cf-iso__light,
    .cf-pin .cf-iso__node {
      animation-timeline: --cf-pin;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
    }
    /* THE LEAD SURVIVES THE RE-TIMING, AND IT DID NOT.
       --trace-lead and --trace-span exist because a signal drawn as several
       strokes has to arrive along its own direction of travel: the base
       assembly's note above measures what happens without them — card 03's
       arrow "drew all five strokes at once, growing outward from five
       different points, which reads as a sketch scribbling itself rather than
       as a signal arriving". The four cards carry the answer in their markup,
       leads of 0, 3, 6, 6 and 6 points.

       This rule threw all five away. It named ONE window for every stroke on
       the track, so the two properties resolved to nothing at all on the one
       page that ships them. Measured on patterns/landing-page.html at
       1440 x 900, inside card 03's quarter: all five strokes at progress
       0.061 and stroke-dashoffset 0.79497 — the same number five times, from
       five paths whose leads differ by six points. The failure the base
       assembly is written against, shipped on a designed page, under an
       authored lead that could not reach it.

       SO THE WINDOW IS MAPPED RATHER THAN REPLACED. The base window is 27
       points of `cover` — 18 to 45, which is what the default --trace-span
       names — and the quarter's is the 8 points between 5 and 13. A stroke
       occupies (lead / 27) to (lead + span) / 27 of the first, so it takes
       the same fraction of the second: `* 8 / 27` is that change of basis and
       nothing else.

       BOTH ENDS OF THE DEFAULT ARE PRESERVED EXACTLY. With the shipped
       defaults — lead 0, span 27 — this resolves to 5 % and to 5 % + 27 x 8 /
       27 = 13 %, the two literals it replaces, so every trace that authors no
       lead draws in the window it drew in before. Card 02's two traces are
       byte-identical. Card 03's five become 5 to 11.22, 5.89 to 12.11 and
       6.78 to 13.00 — led along the arrow's own travel, the last of them
       still landing on 13, which is where .cf-pin .cf-iso__node opens and
       where this rule handed over before.

       The 27 is load-bearing at both ends now rather than one, so it is
       checked: scripts/check-iso-motion.py fails a trace whose lead and span
       sum past it, which on this timeline would push the last stroke into the
       nodes' window instead of onto it. → foundations/motion.html#trace */
    .cf-pin .cf-iso__trace {
      stroke-dasharray: 1;
      animation-name: cf-iso-draw;
      animation-range:
        contain calc(var(--i) * 25% + 5% + var(--trace-lead) * 8 / 27)
        contain calc(var(--i) * 25% + 5%
                     + (var(--trace-lead) + var(--trace-span)) * 8 / 27);
    }
    .cf-pin .cf-iso__node {
      animation-name: cf-iso-fade;
      animation-range: contain calc(var(--i) * 25% + 13%) contain calc(var(--i) * 25% + 15.5%);
    }
  }
}

/* The chrome's own type and hairline, unconditional — outside the gate both
   elements are display: none above, so these cost the stacked state nothing. */
.cf-pin__index {
  gap: var(--space-4);
  padding: 0;
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
}
.cf-pin__bar {
  position: relative;
  height: var(--stroke-1);
  background: var(--border-default);
}
.cf-pin__bar::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 100%;
  background: var(--border-strong);
  transform-origin: left;
  transform: scaleX(0);
}

/* In over the first tenth or so, out over the last — between them, the still
   that is the point of the hold. */
@keyframes cf-pin-step {
  0%       { opacity: 0; }
  11%, 89% { opacity: 1; }
  100%     { opacity: 0; }
}
/* The copy holds its quarter and snaps out at the end of it. Its ARRIVAL is
   the page's — staggered, typed, flown — so this one only ever leaves. */
@keyframes cf-pin-copy-hold {
  0%, 88%   { opacity: 1; }
  96%, 100% { opacity: 0; }
}
/* One line arriving from a rung down; the pages stagger the windows. */
@keyframes cf-pin-copy {
  from { opacity: 0; translate: 0 var(--space-4); }
  to   { opacity: 1; translate: 0 0; }
}
/* The two literals are cf-pin-step's, on cf-pin-step's range, which is the
   whole of the fix recorded over .cf-pin__mark: the numeral is at --text-primary
   over exactly the interval its card is at opacity 1. */
@keyframes cf-pin-mark {
  0%, 100% { color: var(--text-muted); }
  11%, 89% { color: var(--text-primary); }
}
@keyframes cf-pin-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* The last quarter's pair, written to the shape cf-values-item-last already
   uses: the same arrivals, with the handover cut off the end. Read them against
   the two above — the arrival halves are identical literals (11 % in both now,
   see .cf-pin__mark) so the last step arrives exactly like the other three, and
   only the return to nothing is gone. There is no cf-pin-copy-hold-last, because a keyframe set
   that holds one value for a whole range is an animation that does nothing: the
   last copy runs `animation-name: none` and is simply present, the same move
   .cf-iso--build .cf-iso__scene makes.
   → foundations/motion.html#hold */
@keyframes cf-pin-step-last {
  0%        { opacity: 0; }
  11%, 100% { opacity: 1; }
}
@keyframes cf-pin-mark-last {
  0%        { color: var(--text-muted); }
  11%, 100% { color: var(--text-primary); }
}

/* --------------------------------------------------------------------------
   VALUES — the mark builds itself out of its own subdivision grid
   The six values, one per stage of a pinned scroll. The mark is assembled in
   the order the manual constructs it: contour first, then the isometric
   subdivision at 1 / .5 / .25 / .125, then the solid form. Six construction
   steps, six values — the count is not a coincidence and neither is the order.

   NO SCRIPT. The whole thing is a view timeline over the track, which is what
   foundations/motion.html already says every scroll-driven animation in this
   system is. That buys the fallbacks rather than costing them:

     no support         the finished mark, the six values stacked and readable
     reduced motion     the same
     print              the same, because the block is scoped to `screen`
     no JavaScript      the same, because the copy is real markup

   That last one is the point. The prototype this comes from built its copy
   char by char out of a JS array, so with the script off there was nothing to
   read and nothing for a screen reader to announce, and under reduced motion
   it still gated six screens of copy behind six screens of scroll. Here the
   values are six list items in the document; scrolling only chooses which one
   is lit. Read the fallback below as the real state and the enhancement as
   the decoration, because that is the order they are declared in.

   The grid lines carry pathLength="1" and are deliberately NOT under .cf-iso:
   non-scaling-stroke measures dashes in screen pixels while pathLength
   normalises to user units, and the two disagree by the render scale — the
   bug foundations/motion.html documents. These draw linearly instead.
   -------------------------------------------------------------------------- */

/* ---- the readable state, which is also the un-enhanced one ---- */
.cf-values__track { position: relative; }
.cf-values__frame {
  display: grid;
  /* Written out rather than left implicit, and the minmax(0, …) is the point.
     Every explicit track in this file already carries it, because a grid track
     sized `auto` or `1fr` will not go below its content's min-content width. An
     IMPLICIT single-column grid gets exactly that auto track, so this frame was
     the one grid in the system quietly exempt from the rule the rest of it
     follows. → foundations/layout.html */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-8);
  align-items: center;
}
/* The track is only half of it. A grid ITEM has `min-width: auto`, which floors
   it at its own min-content independently of how shrinkable its track is — so
   the longest German compound in the list ("Technologie-Kompetenz") set the
   width of the whole column. Measured at a 24 px browser default and a 320 px
   viewport, that pushed the list to 303 px inside a 260 px column and scrolled
   the page sideways. `min-width: 0` is the item-side counterpart to minmax(0, …)
   and is already the idiom here — .cf-consent__actions .cf-btn carries it for
   the same reason. */
.cf-values__list { min-width: 0; }
/* 11.25rem is the prototype's 180px cap, and it is the mark's real size rather
   than a limit it rarely reaches. The middle column is deliberately wider than
   the mark: the prototype gives it minmax(240px, 30vw) and then draws a 180px
   sign inside it, so the sign sits in air rather than filling its column. Drop
   the cap and the mark takes the whole column — 317px at 1440, near twice the
   drawn size — which is what made this read as the wrong scale. */
.cf-values__mark { display: block; width: 100%; max-width: 11.25rem; margin-inline: auto; }
.cf-values__mark line,
.cf-values__contour { stroke: var(--cf-schwarz); }
.cf-values__solid   { fill: var(--cf-schwarz); }
/* Named ink, so named again for the mode that does not force it. --cf-schwarz
   is the literal here rather than a border token, which changes nothing: the
   spec exempts SVG `fill` and `stroke` from forced colours whatever the author
   wrote, and this sign is the whole of the figure on Über uns. Same declaration
   the plot, the pie and the line take, and it is a fill AND a stroke because
   this drawing has both — .cf-values__solid is the one filled mark in it. */
@media (forced-colors: active) {
  .cf-values__mark line,
  .cf-values__contour { stroke: CanvasText; }
  .cf-values__solid   { fill: CanvasText; }
}

.cf-values__list { list-style: none; margin: 0; padding: 0; }
.cf-values__item + .cf-values__item {
  margin-top: var(--space-8);
  padding-top: var(--space-8);
  border-top: var(--stroke-1) solid var(--border-subtle);
}
.cf-values__title {
  font-size: var(--text-h4);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}
.cf-values__body {
  margin-top: var(--space-3);
  max-width: 46ch;
  color: var(--text-secondary);
}
/* The live index belongs to the pinned state only — in the stacked state every
   value is on screen at once, so counting to one of them says nothing. */
.cf-values__index,
.cf-values__progress { display: none; }

/* ---- pinned and scrubbed, where the browser can and the reader wants it ----
   Gated at 51.25rem — 820 px at a 16 px default — on purpose. The track is
   seven viewports of scroll; on a phone that is a long hijack for copy that
   reads perfectly well stacked, and the three-column composition has nowhere to
   go at that width anyway.

   THE SIXTH VIEWPORT THRESHOLD, AND THE REGISTER DID NOT HAVE IT. It was
   `min-width: 820px` and lived in a shipping stylesheet while tokens.css listed
   three viewport thresholds and named 820 only as a figure belonging to
   prototypes/werte-scroll.html — which is out of scope and is a different query
   that happens to share the number. That is the fourth time the register has
   been caught short, and it is the first time the missing entry was hiding
   behind a correct statement about a different file.

   In rem with the rest of them: whether there is room for a pinned
   three-column stage is a question about how big the type is, so at a 24 px
   default this now waits for 1230 px rather than engaging at 820 with the
   composition already too tight for it. → foundations/layout.html

   WIDTH WAS NEVER THE ONLY QUESTION, AND THIS GATE ASKED IT ALONE. The other
   two consumers of this same mechanism family — .cf-pin (Expertise and the
   landing page's process, both in cf-stream.js's own accounting) — pair their
   min-width with `min-height: 45rem`, the register's own note on that
   threshold: "a stage that must also clear the nav band cannot hold a card
   worth reading under 45rem of viewport." This track never asked. A width of
   51.25rem says nothing about a phone turned sideways: iPhone 14 Pro Max
   landscape (932 x 430), iPhone 14 Pro landscape (852 x 393), Pixel 7
   landscape (915 x 412) and Galaxy S23 Ultra landscape (915 x 412) all clear
   820 px wide while standing at less than half of 45rem tall — ordinary
   phones, held the ordinary way people hold a phone to watch a video or read
   a wide table, not an edge case. Measured on all four, both Chromium and
   WebKit: `view-timeline-name` computed to `--cf-values` and `.cf-values__stage`
   computed `position: sticky` — the seven-viewport scroll hijack this
   component's own doc-page swears never happens on a phone, running on one,
   on the exact mechanism the sibling gate was already built to keep it away
   from. The card the width term protects has nowhere to go at 430 px of
   height either; the fix is the same term the other two consumers already
   carry, not a new one. → foundations/mobile.html#order */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) and (min-width: 51.25rem) and (min-height: 45rem) {
    .cf-values__track {
      --values-stages: 6;
      height: calc((var(--values-stages) + 1) * 100vh);
      view-timeline-name: --cf-values;
      view-timeline-axis: block;
    }
    .cf-values__stage {
      /* sticky is also `position`, so this is the containing block the index
         and the progress bar resolve against. Without it they escape to the
         page and land at its top corners. */
      position: sticky;
      top: 0;
      height: 100vh;
      /* vh first, dvh on top — see the note on .cf-pin__stage, above. */
      height: 100dvh;
      /* clip, not hidden — a crop must not become a scrollport. */
      overflow: hidden;
      overflow: clip;
      display: grid;
      align-content: center;
    }
    /* The composition is 4 + 4 + 4 on the page's own 12 columns — title, mark,
       body — not a bespoke three-track ruler. At 1440 that puts each band at
       410px against the prototype's 432px middle column, so the proportions
       come out where the prototype had them while every edge still lands on a
       column line the rest of the page uses.

       The items were previously `position: absolute; inset: 0` against this
       frame. .cf-values__frame is also a .container, and an absolutely
       positioned child resolves against the PADDING box, so each item's three
       tracks were measured across 1438px while the frame's own were measured
       across the 1280px content box: 528.8 / 316.8 / 528.8 against 449.6 /
       316.8 / 449.6. The title and body were hanging one full gutter — 79px a
       side at 1440 — outside the grid everything above them is aligned to.
       Stacking them as grid items in a shared cell keeps the overlay without
       leaving the content box. */
    .cf-values__frame {
      position: relative;
      grid-template-columns: var(--grid-tracks);
      gap: var(--grid-gap);
      min-height: 60vh;
    }
    .cf-values__mark { grid-column: 5 / 9; grid-row: 1; }

    /* every value occupies the same place; the timeline chooses which is lit.

       1 / -1 rather than 1 / 1 on the item below, because the two spellings
       stop being the same the moment the list has more than one track — which
       is exactly what the @supports block at the foot of this composition
       gives it. Written this way in both paths so the fallback and the subgrid
       form differ in ONE declaration rather than in the placement too. */
    .cf-values__list { grid-column: 1 / -1; grid-row: 1; display: grid; }
    /* The stacked state's separator has to be undone at its own specificity.
       `border: 0` on .cf-values__item below loses to
       .cf-values__item + .cf-values__item, so all five separators survived into
       the pinned state — and because the items stack in one grid cell they land
       on the same line, reading as one hairline straight through the mark. */
    .cf-values__item + .cf-values__item {
      margin-top: 0;
      padding-top: 0;
      border-top: 0;
    }
    .cf-values__item {
      grid-row: 1;
      grid-column: 1 / -1;
      display: grid;
      grid-template-columns: var(--grid-tracks);
      gap: var(--grid-gap);
      align-items: center;
      margin: 0;
      padding: 0;
      border: 0;
      opacity: 0;
      animation-name: cf-values-item;
      animation-timeline: --cf-values;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: linear;
    }
    /* min-height on both is what keeps the row still while the copy types in:
       without it the line count changes as characters land and the whole
       composition shifts under the reader. Reserved from the longest value in
       the set at the pinned breakpoint. */
    .cf-values__title { grid-column: 1 / 5;  text-align: right; min-height: 2.5em; }
    .cf-values__body  { grid-column: 9 / 13; margin-top: 0;     min-height: 7.5em; }

    .cf-values__item:nth-child(1) { animation-range: contain 0%      contain 16.667%; }
    .cf-values__item:nth-child(2) { animation-range: contain 16.667% contain 33.333%; }
    .cf-values__item:nth-child(3) { animation-range: contain 33.333% contain 50%; }
    .cf-values__item:nth-child(4) { animation-range: contain 50%     contain 66.667%; }
    .cf-values__item:nth-child(5) { animation-range: contain 66.667% contain 83.333%; }
    /* the last one holds: there is no seventh stage to hand over to */
    .cf-values__item:nth-child(6) {
      animation-name: cf-values-item-last;
      animation-range: contain 83.333% contain 100%;
    }

    /* --- the mark, one construction step per stage ---
       Each step draws over the first 62 % of its stage and then holds, so a
       stage at rest always shows its own step finished rather than mid-draw. */
    .cf-values__contour,
    .cf-values__grid-layer,
    .cf-values__solid {
      animation-timeline: --cf-values;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
    }
    .cf-values__contour,
    .cf-values__grid-layer line {
      stroke-dasharray: 1;
    }
    .cf-values__contour {
      animation-name: cf-values-draw;
      animation-range: contain 0% contain 10.33%;
    }
    .cf-values__grid-layer {
      animation-name: cf-values-fade;
    }
    .cf-values__grid-layer line {
      animation-name: cf-values-draw;
      animation-timeline: --cf-values;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: var(--ease-out);
    }
    /* THE INDEX IS THE MARKUP'S --layer, NOT THE ELEMENT'S POSITION. The four
       layers each carried `style="--layer:N"` and nothing in the system read it:
       the ranges below were four hand-written pairs selected by
       :nth-of-type(N+1) instead, so the same index was written twice and only
       one of the two copies did anything. The dead half is the dangerous half —
       reorder the layers, or wrap them in a <g> for a clip or a transform, and
       :nth-of-type follows the new positions while --layer keeps saying what the
       drawing meant, with nothing to reconcile them. `--layer` is now the one
       that runs, and it counts <g> elements by tag rather than by class, which
       is the other half of the same fragility.

       The arithmetic is the six stages the whole track is quoted in: a layer
       opens on stage --layer + 1 of 6 and draws over the first 62 % of it, the
       same 62 % .cf-values__contour and .cf-values__solid take, so a stage at
       rest always shows its own step finished rather than mid-draw. Written out
       that is 16.667/27, 33.333/43.667, 50/60.333 and 66.667/77 — the four pairs
       this replaces, to three decimal places. */
    .cf-values__grid-layer,
    .cf-values__grid-layer line {
      animation-range:
        contain calc((var(--layer) + 1) / 6 * 100%)
        contain calc((var(--layer) + 1) / 6 * 100% + 10.333%);
    }
    .cf-values__solid {
      animation-name: cf-values-fade;
      animation-range: contain 83.333% contain 93.667%;
    }

    /* --- the chrome: which stage, and how far through ---
       Both ends take --column-inset rather than --gutter. They are the two ends
       of the section header's own rule restated at the bottom of the pinned
       track — the label's counter, unpinned — so they have to stand on the same
       two verticals the rule does. On --gutter they stood on the window instead
       and slid out from under the rule by 160 px at 1920 and 480 at 2560, in
       opposite directions, which read as the track being wider than the page. */
    .cf-values__index,
    .cf-values__progress { display: block; }
    .cf-values__index {
      position: absolute;
      left: var(--column-inset);
      bottom: var(--space-12);
      font-family: var(--font-mono);
      font-size: var(--text-xs);
      letter-spacing: var(--tracking-label);
      color: var(--text-secondary);
      font-variant-numeric: tabular-nums;
    }
    .cf-values__index-n {
      position: absolute;
      left: 0;
      opacity: 0;
      color: var(--text-primary);
      font-weight: var(--weight-medium);
      animation-name: cf-values-item;
      animation-timeline: --cf-values;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: linear;
    }
    .cf-values__index-n:nth-child(1) { animation-range: contain 0%      contain 16.667%; }
    .cf-values__index-n:nth-child(2) { animation-range: contain 16.667% contain 33.333%; }
    .cf-values__index-n:nth-child(3) { animation-range: contain 33.333% contain 50%; }
    .cf-values__index-n:nth-child(4) { animation-range: contain 50%     contain 66.667%; }
    .cf-values__index-n:nth-child(5) { animation-range: contain 66.667% contain 83.333%; }
    .cf-values__index-n:nth-child(6) {
      animation-name: cf-values-item-last;
      animation-range: contain 83.333% contain 100%;
    }
    .cf-values__index-total { padding-left: 2.4em; }

    .cf-values__progress {
      position: absolute;
      right: var(--column-inset);
      bottom: calc(var(--space-12) + 0.35em);
      width: 7.5rem;
      height: var(--stroke-1);
      background: var(--border-strong);
    }
    .cf-values__progress::after {
      content: "";
      display: block;
      height: 100%;
      background: var(--cf-schwarz);
      transform-origin: left;
      transform: scaleX(0);
      animation-name: cf-values-progress;
      animation-timeline: --cf-values;
      animation-range: contain 0% contain 100%;
      animation-duration: auto;
      animation-fill-mode: both;
      animation-timing-function: linear;
    }

    /* --- THE 4 + 4 + 4 IS THE FRAME'S OWN LINES, NOT A SECOND SOLUTION OF THE
       SAME ARITHMETIC.

       The composition is three bands on the page's twelve columns: the title
       at 1 / 5, the mark at 5 / 9, the body at 9 / 13. The mark is a child of
       .cf-values__frame and stands on the frame's lines. The title and the
       body are two levels further in — .cf-values__frame > .cf-values__list >
       .cf-values__item — and .cf-values__item ran repeat(12, minmax(0, 1fr))
       of its own over the width the list handed it.

       Both grids read --grid-tracks, so they cannot disagree about the RECIPE.
       That is what the token fixed and it is not the same guarantee as landing
       on the same LINES, because a track set is a recipe plus a gutter and
       --grid-tracks carries only the first half. The item restated the second
       half separately, as `gap: var(--grid-gap)`. Two declarations, in two
       rules, that have to keep resolving to the same number forever for the
       title's right edge to meet the mark's left one — and the day the frame
       takes a gutter of its own is the day the three bands come apart with
       both rules still reading the token they were told to read. It is the
       same argument .subdivide__row is built on, one component along.

       subgrid ends it: the item adopts the frame's resolved lines, gutter
       included, so there is one solution of the grid on this page and the
       other two take it. column-gap: normal is load-bearing rather than
       tidying — on a subgrid axis `normal` means "the parent's gutter", and
       without it the item's own gap declaration would go on overriding the
       thing this block exists to inherit.

       Positive @supports, the form .cf-team-grid__item already uses, because
       more than one declaration moves. Without subgrid every rule above is
       untouched and the composition draws exactly as it did — the fallback is
       the old arithmetic, which was right, just solved twice.

       Measured before and after at 1024, 1280, 1440 and 1920: every vertical
       is identical to the hundredth of a pixel but one — the body's left edge
       at 1920, which moves 0.02 px, ONTO the frame's own line. That is the
       size of the thing this closes today and the whole of the argument for
       closing it: the two solutions agree to a rounding error while nothing
       has moved, and there is nothing in either rule that keeps them agreeing
       once something does. → foundations/layout.html#columns,
       base.css .subdivide__row */
    @supports (grid-template-columns: subgrid) {
      .cf-values__list { grid-template-columns: subgrid; }
      .cf-values__item {
        grid-template-columns: subgrid;
        column-gap: normal;
      }
    }
  }
}

/* The caret, and the box the streaming copy holds open.
   min-height on both title and body is what keeps the composition still while
   the text arrives: without it the line count changes as characters land and
   the whole three-column row jumps. Reserved from the longest value in the
   set, measured at the pinned breakpoint. */
.cf-stream__caret {
  display: inline-block;
  width: 0.45em;
  height: 0.95em;
  margin-left: 0.1em;
  background: var(--text-primary);
  vertical-align: text-bottom;
  animation: cf-stream-caret 0.9s steps(1) infinite;
}
.cf-stream__caret.is-resting { opacity: 0; animation: none; }
/* The caret is the other half of the same bill acts.css itemises over
   .sp-stream: an `infinite` animation on the document clock, running for the
   life of the tab whether its track is on screen or not. Two of them here
   against 126 there, and the count is not what costs — measured, ONE animation
   is worth 1150 ms of style recalculation in five seconds of a still landing
   page against 1375 for all 128, because what the page pays for is producing
   frames at all. So the last two go the same way as the other 126.
   → assets/js/cf-idle.js, scripts/check-idle-motion.py */
[data-idle] .cf-stream__caret { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .cf-stream__caret { animation: none; }
}
@keyframes cf-stream-caret {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0; }
}

/* THE STREAM NEVER RUNS ON PAPER, AND THE FALLBACK IT ALREADY BUILT WAS NEVER
   POINTED AT PRINT. cf-stream.js's own prepare() (see its header) exists to
   type copy from a scroll position: `run()` reads getBoundingClientRect()
   against a `scroll` listener, and build() empties every line's full sentence
   into .cf-stream__text up front, character count at zero. A real print
   render (page.pdf(), not emulateMedia — emulateMedia only swaps which CSS
   matches and stays on the screen compositor, so it never reproduces this)
   never scrolls and never fires `scroll`, so paint() never runs past zero and
   every streamed line prints as the empty span build() left behind.

   prepare() already writes the fix's other half for accessibility: each line
   is split into .cf-stream__text (streamed, aria-hidden — the one print
   catches empty) followed immediately by a plain .visually-hidden sibling
   carrying the line's whole sentence. Print wants that same sentence, so this
   hides the first and restores the second. The adjacent-sibling combinator is
   exact because cf-stream.js appends nothing else between the two spans, on
   any of its four consumers: acts.css's .sp-track/.sp4-track/.map-track
   (landing page, prototypes/statement-to-process.html), .cf-values__track
   (ueber-uns.html) and .cf-pin (expertise.html).

   Measured with page.pdf() against the shipped landing page: act 4's two
   team plates print "01 / Geschäftsführung" / "02 / Das Team" with nothing
   under either label — headline, body and the reader's own name entirely
   missing — before this rule, and the full sentence after. The same print
   caught the other two consumers just as empty: expertise.html's four field
   cards (every .cf-info-card__title, __sub, __body, __ask) and ueber-uns.html's
   six values printed illustration, tag chips and rules only, with a blank gap
   where the copy belongs — 0 characters on the last printed page of each,
   207 and 399 after. */
@media print {
  .cf-stream__text { display: none; }
  .cf-stream__text + .visually-hidden {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
  }
  .cf-stream__caret { display: none; }
}

@keyframes cf-values-item {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes cf-values-item-last {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes cf-values-draw {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}
@keyframes cf-values-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes cf-values-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* --------------------------------------------------------------------------
   PLOT — the isometric column diagram
   The system could draw an object in space, and it could set a number in mono.
   It could not put a number IN space, which is the one thing a brand about
   turning sensor data into answers has to be able to do. This is that
   vocabulary, and every part of it is borrowed rather than invented:

     the cell      one 2:1 rhombus — the lattice tile the whole brand is built
                   on — drawn 1-4 dashed, because the ground a column stands on
                   is construction geometry and not something measured
     the shaft     three verticals on the 90 deg axis. Contour, never fill
     the cap       the same rhombus, lifted by the value. The cube from the
                   icon set, extruded to an arbitrary height
     the light     exactly one cap may be filled lime
     the numbers   Geist Mono, tabular — the treatment .cf-stat already gives
                   every number in the system

   THE ROW IS LEVEL, AND THAT IS AN ISOMETRIC DECISION RATHER THAN A RETREAT
   FROM ONE.
   Every direction in the isometric ground plane slopes on screen: the two axes
   run at +26.57 and -26.57 deg. Marching columns along either one costs a unit
   of drawn height per column, and the first build of this component did exactly
   that. On five columns rising 31 -> 100 the recession ate almost the entire
   climb: the last column, three and a half times the first, was drawn a hair
   LOWER on screen. A chart that reads flat when its data triples is not a
   stylistic preference, it is a broken instrument.

   The fix is still a lattice vector. One step forward on the first axis plus
   one step back on the second is (2u, +u) + (2u, -u) = (4u, 0): exactly
   horizontal on screen, and a point of the lattice like any other. So the
   columns sit 4u apart on one level line, every one of them on a real cell,
   and cap height is once again proportional to the value alone. It is also the
   only spacing that works — lattice points sharing a screen y are 4u apart and
   nothing closer qualifies, so the 2u of air between two columns is derived
   rather than chosen.

   WHY EACH COLUMN IS ITS OWN SVG.
   Contour weight. A 1 px hairline drawn as a CSS border inside a scaled box is
   scaled with it; SVG with vector-effect: non-scaling-stroke is not, which is
   the answer this system already reached for the process illustrations. Mixing
   the two would put visibly different line weights side by side in a brand
   whose first rule is the contour.
   -------------------------------------------------------------------------- */

/* Registered, so the value is a NUMBER rather than a token that happens to
   look like one. Two things follow, and both matter here:

     type safety   --v is substituted into scaleY() and into a length
                   calculation. Unregistered, "--v: 42%" or a stray unit makes
                   both declarations invalid at computed-value time, and a
                   column with no transform is drawn at FULL height — a chart
                   that is confidently wrong. Registered, a bad value is simply
                   the initial one and the column is drawn at zero.
     animation     an unregistered custom property cannot be interpolated. With
                   --plot-grow registered, one interpolated number drives the
                   shaft, the cap and the number that rides it, instead of
                   three animations that have to agree with each other.

   @property reached Baseline in July 2024. It is the one modern CSS capability
   this system had no use for until there was something to interpolate. */
@property --v {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
@property --plot-grow {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

.cf-plot {
  /* u is the half-width of a cell: the rhombus is 2u x u, the unit cube's
     vertical edge is u, and every other measure here is a multiple of it.
     Fluid rather than queried — this is a crossover, not a threshold, so it is
     deliberately absent from the breakpoint register. Five columns and the
     four gaps between them come to 18u: 288 px at the floor, which fits
     inside a 375 px viewport's 20 px gutters with 47 px to spare, and 576 px
     at the cap. Five is therefore the practical ceiling for a figure that has
     to survive a phone; a wider frame takes more.

     THE FLOOR WAS 1rem, AND rem IS THE READER'S SETTING HERE, NOT THE
     SYSTEM'S. Every other length this figure is measured against — the
     viewport, --gutter — is px or vw and does not move when a reader sets
     their browser's default text size above 16 px, but 1rem does: at a 20 px
     default the floor became 20 px (18u = 360, against a 280 px content
     column at 320 px wide) and at 24 px it became 24 px (18u = 432). Measured
     on blog-artikel at 320 px wide, root 20/24, both engines: the document
     scrolled sideways 65 px and 142 px, identically on Chromium and WebKit,
     because the fault is arithmetic rather than a rendering difference. Type
     is meant to answer the reader's own setting; a fixed-count geometric
     drawing sized in the same unit is not, so the floor and cap are px, the
     same convention --gutter and --section-gap already use, and the 288/576
     px figures above hold at every root size rather than only at 16. */
  --plot-u: clamp(16px, 3.4vw, 32px);
  --plot-rise: calc(var(--plot-u) * 6);
  margin: 0;
  /* Room for the tallest number to hang above its cap. */
  padding-top: var(--space-8);
}

.cf-plot__set {
  display: flex;
  /* 4u pitch = 2u of column and 2u of air. See the header. */
  gap: calc(var(--plot-u) * 2);
  /* The keys hang below the row on absolute position, so the row's layout box
     ends at the cells and anything after it prints across the labels.

     THE RESERVATION IS TWO LABEL LINES, NOT ONE, and it used to be one. A key
     takes `hyphens: manual` (see .cf-plot__key), so it breaks only where its
     author put a &shy; — and one authored break is a second line. This margin
     read --space-8: 32 px, being the 8 px drop, one 11 px label at
     --leading-normal (14.3 px) and 9.7 px of air. Two lines are 28.6 px, so
     the second one had 4.6 px more label than there was room for.

     Measured on patterns/blog-artikel.html, the only consumer standing on the
     component's own default — landing-page.html, prototypes/evidence-scroll
     and this component page's second demo all override the margin — with the
     key's bottom read against the caption's hairline:

                          320      375      768     1024     1440
       lines               2        2        2        1        1
       key bottom       +4.59    +4.59    +4.59    -9.70    -9.70

     A positive number is the label ending below the rule: ZEIT/ZONE and
     AB/TASTUNG were drawn with the caption's hairline through the bottom of
     their second line at every width the phone and tablet tiers cover. From
     1024 px the column is 64 px, every key fits on one line, and the same
     figure clears by the 9.7 px this margin was designed around.

     acts.css had already diagnosed exactly this and fixed it one level too
     low: `.lp-ev-card .cf-plot__set` said "the reservation goes one rung up
     the scale" and bought the second line for that one card, which left the
     component — and so every published article carrying a plot — on one line.
     The rung belongs here. 48 px is the 8 px drop, two 14.3 px labels and
     11.4 px of air, so a wrapped key now clears by slightly more than an
     unwrapped one used to, and the two landing consumers measure exactly what
     they measured before. Both lengths are rem, so the reservation and the
     label it reserves for grow together when a reader raises their default
     text size.

     The acts.css declaration stays, and for a different reason than it was
     written for: that card's plot is a .cf-plot--ground, whose row reserves
     --space-16 for the value labels, and --space-12 there is what takes it
     back down. Removing it moved the landing page's caption 16 px, which is
     how that was established.
     → scripts/check-key-line-reserve.py */
  margin: 0 0 var(--space-12);
  padding: 0;
  list-style: none;
}

.cf-plot__col {
  position: relative;
  flex: 0 0 auto;
  width: calc(var(--plot-u) * 2);
  height: calc(var(--plot-u) * 7);
  /* The drawn height. clamp() is not decoration: --v is registered as a
     number, which rejects a unit but not a 4 or a -1, and either would send a
     column off the top of the figure or through the floor. */
  --plot-h: clamp(0, calc(var(--v) * var(--plot-grow)), 1);
}

/* 40 x 140 user units = 2u x 7u, so one user unit is u/20 at every size. The
   cell occupies the bottom 20; the remaining 120 are the rise. Six units of
   rise on a one-unit footprint — the column is six of the icon set's cubes
   stacked, which is as tall as an object gets before it stops reading as a
   body in space and starts reading as a line. */
.cf-plot__draw {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* :not(.cf-plot__cap) is load-bearing and cost an hour once. Author CSS beats a
   presentation attribute, so `fill: none` here silently overrode the
   fill="url(#…)" the lit cap carries in its own markup — the cap kept its glow,
   lost its ramp, and rendered as an empty rhombus that looked almost right.
   Unlit caps are still unfilled: they inherit fill="none" from the <svg>. */
.cf-plot__draw path:not(.cf-plot__cap) {
  vector-effect: non-scaling-stroke;
  stroke-width: 1;
  fill: none;
}
.cf-plot__cap {
  vector-effect: non-scaling-stroke;
  stroke-width: 1;
}

/* The ground. Construction geometry, so the ghost line type and the ghost
   weight: it says where the column stands, not what it measures. */
.cf-plot__cell {
  stroke: var(--border-default);
  stroke-dasharray: var(--dash-1-4);
}

/* The three visible verticals, each scaled about the point it stands on. Two
   origins rather than one, because the near edge starts half a cell lower than
   the side edges do. That half cell is the rhombus; scaling all three about
   one line would drag the near edge off it and shear the column. */
.cf-plot__rise {
  stroke: var(--border-strong);
  transform-box: view-box;
  transform-origin: 0 130px;
  transform: scaleY(var(--plot-h));
}
.cf-plot__rise--near { transform-origin: 0 140px; }

/* The cap only travels. Scaling it would squash the rhombus out of 2:1 and
   thin its contour on one axis while leaving it on the other. */
.cf-plot__cap {
  stroke: var(--border-strong);
  transform-box: view-box;
  transform: translateY(calc(120px * (1 - var(--plot-h))));
}

/* One lit column, and only ever one. Lime is the light layer, so it lights the
   cap — the face light would actually land on — and nothing else. The contour
   stays black: contour before fill, including here.

   THE FILL IS NOT DECLARED HERE, and that is the change this rule carries.
   It used to be `fill: var(--cf-lime)`, a flat lime plane, which is the one
   thing the palette does not do: light in this brand is a source and a
   falloff, and a face painted lime edge to edge has neither. See
   foundations/colors.html, "Lime is never flat".

   A CSS gradient cannot be an SVG fill, so the ramp is a paint server and it
   travels with the lit column's own markup — the same way every lit face in an
   illustration carries its own <linearGradient>, waypoint and all. This rule
   keeps the glow, which is the part that IS a filter. */
.cf-plot__col--lit .cf-plot__cap {
  --glow-r: 12px;
}

/* A PLOT THAT STANDS ON DRAWN GROUND, and one column in it that fell.

   THE HUE WAS FIRST SPENT ON THE CONTOUR AND THAT WAS THE WRONG INSTRUMENT. A
   column whose quantity fell was drawn in Violett 700 — the one step in the
   palette that clears both the wash and the black contour — and at a 1 px
   hairline, in a figure whose other lines are black, it read as "slightly
   faded" rather than as "other direction". A hairline gives the eye almost no
   area to judge a hue on; that is the same sentence .cf-line__trace--hue
   writes to explain why one tinted trace is its ceiling there.

   SO THE COLOUR MOVED TO THE FACE, where there is area to see it, and the
   figure gained a ground for the objects to stand on. The direction itself
   stays where this component has always kept its data: in the number, which
   every column prints, and which opens with a − or a +.

   AND EVERY COLUMN STANDS. A drop was drawn hanging under the plane for a
   while — geometry says direction better than any colour can — but a hanging
   solid is read against a ground it is behind rather than on, the two families
   no longer share a baseline the eye can run along, and the four magnitudes
   stop being comparable at a glance, which is the one thing a column plot is
   for. Rises and falls are both improvements here in any case; what differs is
   which way the underlying quantity moved, and that is a label on the column,
   not a direction for it to point. So --fell is a face and a sign, and the
   plot keeps one baseline.

   --v is unchanged by any of it: the magnitude as a fraction of the largest in
   the set, sign removed. */
.cf-plot--ground {
  /* The ground band reaches half a unit past the footprint, so the box below
     the columns is that much taller than a plain plot's. */
  padding-bottom: var(--space-2);
}
.cf-plot--ground .cf-plot__set {
  position: relative;
  /* THE PLANE IS THIS BOX, SO THIS BOX HAS TO BE THE ROW AND NOT THE FIGURE.

     The ground below is a ::before at `left: 0; right: 0` on the set, and the
     set is a block: stretched to the figure, the plane ran the whole measure
     while the columns stood on its leading 14u only. Measured on the evidence
     lab at 1440, four columns in a 545 px row: 392 px of objects, 153 px of
     empty ground after them and none before. The reading that produces is not
     "a wide field" — nothing in the drawing says where a field ends — it is a
     slab with its objects shoved against the left edge, which is a composition
     the figure never meant to make.

     fit-content is the row's own width, and auto inline margins put it in the
     middle. The plane then ends where the outer footprints do, the air left
     and right of it is equal, and the object sits in the figure the way
     .cf-pie's ring already sits in its own.

     THE LATTICE PHASE IS UNTOUCHED, and that is why the row moves rather than
     the columns inside it. background-position anchors at this box's starting
     corner, so keeping the box's left edge on the first column's left edge
     keeps all four footprint vertices on crossings — the arithmetic
     check-plane-phase.py re-solves, unchanged. Centring the columns inside a
     full-width plane would have thrown every vertex half a cell out unless the
     phase were re-derived from the free space, and the free space is not a
     number this stylesheet can name.

     fit-content also clamps to what is available, so where the row is wider
     than the figure — the narrow tiers, where 14u already fills it — this
     computes to exactly what it computed before: no free space, no margins,
     the same overflow behaviour. */
  width: fit-content;
  margin-inline: auto;
  /* The keys clear the deepest value label rather than the box: a column at
     --v: 1 ends at 14u and its number runs a line and a half past that. */
  margin-bottom: var(--space-16);
}
/* 7u of column, plus the half unit the ground band reaches below the
   footprint. The drawing keeps its own 7u so the viewBox is not restretched. */
.cf-plot--ground .cf-plot__col { height: calc(var(--plot-u) * 7.5); }
.cf-plot--ground .cf-plot__draw {
  inset: 0 0 auto 0;
  height: calc(var(--plot-u) * 7);
}

/* THE ZERO PLANE, AND IT IS A PLANE RATHER THAN A LINE ON PURPOSE.

   It was a 1 px black rule, and the figure read as four objects floating
   against a wire: the columns had nothing to stand on, the dashed footprints
   scattered along the rule looked like debris rather than like ground, and a
   pit had no surface to be sunk into. A flat rule is also the one thing this
   projection cannot mean — every plane in an isometric drawing is a rhombus,
   and a horizontal screen line is a plane seen from an angle it is never seen
   from here.

   SO THE GROUND IS THE SYSTEM'S OWN GROUND. This is .cf-ground's material,
   unchanged — the two rakes, the same stop list, --field-ink at --field-line —
   re-scaled so that one lattice diamond IS one column's footprint: 2u wide,
   1u tall. → foundations/field.html

   THE SCALE IS DERIVED, NOT PICKED. --field-unit is the horizontal spacing
   between parallel lines of one family, and --field-step is that distance
   measured perpendicular to them: unit / sqrt(5) for a 2:1 rake. Setting the
   unit to 2u therefore sets the step to 2u / 2.2360679775, and the diamond
   that falls out is 2u x 1u. Both are restated here rather than inherited
   because a custom property is substituted where it is DECLARED: --field-step
   at :root has already resolved against :root's --field-unit, so redefining
   the unit alone would change nothing.

   A COLUMN RISES FROM A CROSSING, NOT FROM THE MIDDLE OF A CELL, and the
   half unit that makes that true is the whole of --plane-phase.

   THE PERIOD IS ANCHORED AT THE STARTING CORNER, not at the centre. That was
   assumed the other way round once and the figure showed it: every vertical
   started in the middle of a diamond, so the columns read as floating over
   the lattice instead of standing in it. Worked out properly, with the band
   H = 2u tall and the ink half a step into each period, the two families land
   at

     A (start corner bottom-left)   y =  0.5x + (1.5u - k u)
     B (start corner top-left)      y = -0.5x + (0.5u + m u)

   which cross at x = n u, y = -0.5 n u + 0.5u + m u. A column's footprint has
   its vertices at (4i)u,1u · (4i+1)u,1.5u · (4i+2)u,1u · (4i+1)u,0.5u — the
   zero line being y = 1u in this box — and not one of those four solves the
   crossing equation for integer m. All four sit exactly half a cell out,
   which is the reading the eye gets: a lattice, and columns standing between
   its lines.

   Shift the ground down half a unit and all four solve it at once. The lattice
   is periodic in y with period u and the band is 2u, so the wrap is seamless
   and the shift costs nothing at the edges. After it, a footprint IS a
   diamond of the plane: the three verticals leave the ground at crossings and
   the cell they enclose is a cell of the ground.
   → scripts/check-plane-phase.py holds the arithmetic

   THE RULE IS GONE WITH THE LINE. A division is legal here — geometry.html
   admits one, and .cf-gantt's `heute` is one — but the plane states the same
   level with a surface instead of a stroke, and the footprints sitting in it
   are what locate zero for each column. Two marks for one fact is one too
   many. */
.cf-plot--ground .cf-plot__set::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* Centred on the footprint, which is at 6.5u of the drawing's 7u — so the
     band runs 5.5u to 7.5u and the cell's own centre is 1u into it, which is
     the coordinate check-plane-phase.py solves the crossings in. */
  top: calc(var(--plot-u) * 5.5);
  height: calc(var(--plot-u) * 2);
  --field-unit: calc(var(--plot-u) * 2);
  --field-step: calc(var(--field-unit) / 2.2360679775);
  --field-half: calc(var(--field-step) / 2);
  --field-stops: transparent 0,
                 transparent calc(var(--field-half) - var(--field-edge)),
                 var(--field-ink) var(--field-half),
                 var(--field-ink) calc(var(--field-half) + var(--field-line)),
                 transparent calc(var(--field-half) + var(--field-line) + var(--field-edge)),
                 transparent var(--field-step);
  background-image: repeating-linear-gradient(var(--field-rake),        var(--field-stops)),
                    repeating-linear-gradient(var(--field-rake-mirror), var(--field-stops));
  background-position: 0 calc(var(--plot-u) * 0.5);
}

/* AN OBJECT ON A GROUND HAS TO OCCLUDE IT, and as wireframes these did not.
   The lattice ran straight through every body, so the ground was drawn across
   the front of each solid and nothing in the picture said the columns were
   standing on it rather than floating over it.

   A FACE AT THE PAGE'S OWN COLOUR IS HOW THIS SYSTEM DRAWS A SOLID. Every
   isometric object on the landing page is built that way — a fill on the form,
   the contour carrying the shape — and foundations/illustration.html states
   the consequence in its own words: an unlit face sits at exactly the page's
   colour and the object would disappear entirely if the contour were not
   carrying it. --surface-page is that colour as a token, so it inverts with
   the theme instead of pinning the grey.

   THE SILHOUETTE IS THREE STATIC SHAPES, not one variable path, and that is
   forced by the same half cell that gives the rises two origins: a hexagon
   whose top V travels while its bottom V stays is not an affine transform of
   anything. Decomposed, it is the cap, which already translates; a rectangle
   between the cap's and the footprint's side vertices, which scales exactly
   like a rise; and the footprint, which is static. Their union is the hexagon
   exactly — the cap's lower half falls inside the rectangle, and the
   footprint's near point is the only thing below it. So the only new mark is
   the rectangle.

   NO z-index IS NEEDED, and that is what one baseline buys. The ground is a
   ::before of the set, so it paints before every column; with nothing below
   the plane there is no family that has to be painted under it. The variant
   that hung its falls needed three layers and a stacking context to keep the
   numbers above the ground. This one needs paint order and nothing else. */
.cf-plot--ground .cf-plot__draw .cf-plot__body {
  fill: var(--surface-page);
  transform-box: view-box;
  transform-origin: 0 130px;
  transform: scaleY(var(--plot-h));
}

/* The footprint stops being a ghost and becomes the face it always was. The
   phase above put it exactly on a lattice diamond, so the dash was a second
   drawing of a line the ground already draws; the fill is the solid's bottom
   face and the last piece of the silhouette. */
.cf-plot--ground .cf-plot__draw .cf-plot__cell {
  fill: var(--surface-page);
  stroke: none;
}

/* An unlit cap is a face too, and it is the one place a `fill: none` still let
   the ground through the top of a box. Scoped off the two caps that carry a
   paint server, because author CSS beats a presentation attribute — the
   mistake this component's own note records further up. */
.cf-plot--ground .cf-plot__col:not(.cf-plot__col--lit):not(.cf-plot__col--fell) .cf-plot__cap {
  fill: var(--surface-page);
}

/* THE HUE MOVES TO THE FACE, where a colour has room to be one. The ramp is
   the lit cap's, with the warm source swapped for the cool one and every
   other number in it unchanged: the brand's own violett, the waypoint at 19 %
   of the violett leg measured from violett, Glas at 0.32, CF-Grau at the rim.
   #8A94E3 is that waypoint computed in oklab exactly as #DBFC60 was computed
   on the lime leg — same construction, same proportions, other end of the
   spectrum. It is a paint server the column carries in its own <defs>, for
   the reason the lit cap's is: a CSS gradient cannot be an SVG fill.
   → foundations/colors.html#rakes-and-blooms

   AND IT IS NOT A SECOND LIGHT. Lime is the source in this brand and stays
   the one thing --lit spends. This face is not lit; it is the cool end of the
   same ramp, which is what the palette already does with every surface the
   light does not reach. */
.cf-plot__col--fell .cf-plot__cap { --glow-r: 0; }

/* FORCED COLOURS TAKES THE FILLS AND KEEPS THE DRAWING — the pie's rule, for
   the pie's reason, and the plot needed it before any of this arrived.

   A CUSTOM PROPERTY ARRIVING AT AN SVG `stroke` IS NOT FORCED. Every stroke
   here is var(--border-strong), which is --cf-schwarz in the light palette, so
   under a dark forced palette the columns computed black on a black Canvas:
   the whole drawing absent rather than faint. That is .cf-pie__contour's
   finding verbatim, and the plot carried the same fault unfixed because it had
   no block of its own.

   AND A PAINT SERVER IS NOT FORCED AT ALL, so both ramps would go on painting
   their own lime and violett under a reader's chosen ink. They come off. What
   survives is the drawing, the zero line, and every column's own number with
   its sign — and because the direction is in the geometry here, a falling
   column still falls in a palette with no colour in it at all. */
@media (forced-colors: active) {
  .cf-plot__rise,
  .cf-plot__cap,
  .cf-plot__cell { stroke: CanvasText; }
  /* AND THE FOOTPRINT TAKES ITS EDGE BACK WITH THE GROUND, at the specificity
     that reaches it. The line above is (0,1,0) and
     `.cf-plot--ground .cf-plot__draw .cf-plot__cell { stroke: none }` is
     (0,3,1), so on the one variant that has a plane the stroke never landed —
     and the note over that rule says why it was taken off in the first place:
     "the dash was a second drawing of a line the ground already draws". The
     ground is what the rule below removes here, so the reason goes with it and
     the face has to close its own bottom. Without this the columns lost their
     footprints the moment the slab stopped hiding that they had none: measured
     on components/plot.html in forced colours, four solids whose two verticals
     simply stopped in mid air with no line across their feet and no level in
     the figure at all. */
  .cf-plot--ground .cf-plot__draw .cf-plot__cell { stroke: CanvasText; }
  /* THE GROUND IS A LATTICE AND `background` IS A SHORTHAND, which is the
     whole of this finding. The plane is two repeating gradients — hairlines
     at a half step, transparent everywhere between them — and forced colours
     discards background-image outright. `background: CanvasText` therefore
     did not re-ink the lattice; it dropped the image and painted the band's
     whole box, 392 x 56 px at 1440, in the reader's ink. Measured on
     patterns/landing-page.html's act 4, card 03, forced colours on a white
     Canvas: a solid black slab from the set's left edge to its right, 56 px
     tall, centred on the zero line — the four columns standing in a trench,
     the shortest of them (+9 %, the one whose point is that it rose) more
     than half swallowed, and every footprint invisible against it.

     .cf-ground ALREADY ANSWERED THIS, for this exact material. The plane is
     that utility's field at the plot's scale — the note over the base rule
     says so — and base.css takes it out under both degraded palettes with
     the sentence this rule should have read: "a forced-colours palette has
     no value to give it that is not either invisible or a second border."
     CanvasText is that second border, filled rather than stroked.

     AND THE LEVEL DOES NOT GO WITH IT. The plane's job here is stated one
     comment up — "the footprints sitting in it are what locate zero for each
     column" — and the footprints are .cf-plot__cell, which this block draws
     as Canvas and which the rule directly above gives back its CanvasText
     edge. Four outlined diamonds on one line state the zero this figure
     needs; the slab was hiding them, and hiding that they had no edge to be
     seen by. Two marks for one fact is one too many, in this palette as in
     the other. */
  .cf-plot--ground .cf-plot__set::before { display: none; }
  .cf-plot__col--lit .cf-plot__cap,
  .cf-plot__col--fell .cf-plot__cap { fill: none; }
  /* The occluders keep their job and lose their colour. A fill written as a
     custom property is not forced, so --surface-page would paint CF-Grau over
     a reader's chosen Canvas; named Canvas, the body still hides the ground
     behind it and still does so in their palette. Depth survives a mode with
     one ink, because it was never carried by a colour. */
  .cf-plot--ground .cf-plot__draw .cf-plot__body,
  .cf-plot--ground .cf-plot__draw .cf-plot__cell,
  .cf-plot--ground .cf-plot__col:not(.cf-plot__col--lit):not(.cf-plot__col--fell) .cf-plot__cap {
    fill: Canvas;
  }
}

/* The number rides its own column: top is the cap's top vertex, 6u of rise
   times the part of it not yet climbed, and the element hangs above that line
   rather than starting at it.

   Every column carries its own value, and that is not decoration. Direct
   labelling is what lets a reader take a number off this figure at all, and it
   is the reason the component needs no legend and no axis. */
.cf-plot__val {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--plot-rise) * (1 - var(--plot-h)));
  transform: translateY(-100%);
  padding-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  /* Stated, not inherited. .cf-prose sets its running text in Light, and a
     number that quietly changes weight with the block it was dropped into is
     not a number the rest of the system can match. */
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-normal);
  text-align: center;
  color: var(--text-primary);
  /* THE NUMBER AND ITS UNIT ARE ONE TOKEN, and this rule is the third of three
     bounded values in the system to say so. .cf-line__val and .cf-block__val
     are the same shape — position: absolute, a value label bound to a data
     point — and both carry this declaration; this one shipped without it.

     The pitch is what makes the omission visible. The column is
     `--plot-u * 2`, which is the fluid unit floored: 30 px on this page and
     32 px on components/plot.html at every width from 320 to 414, 52 px at
     768. "−12 %" does not fit in 30 px, so the space between the number and
     the sign is taken as the break opportunity it looks like, and the reader
     gets "−12" with a "%" alone on a second line under the bar. Measured over
     the four consumers, values breaking onto two lines:

                                          320 / 375     768 and up
       patterns/landing-page.html          3 of 4          0 of 4
       components/plot.html                3 of 9          0 of 9
       prototypes/evidence-scroll.html     3 of 4          0 of 4
       patterns/blog-artikel.html          0 of 5          0 of 5

     blog-artikel is the control: its values are two digits and a sign inside
     the same 32 px, so they clear it and nothing about them changes here.

     The rule above says direct labelling "is what lets a reader take a number
     off this figure at all, and it is the reason the component needs no legend
     and no axis" — a number set as a stack of fragments is not one a reader
     can take off anything, and on a phone that is the only way three of these
     four were ever shown.

     `nowrap` AND NOT `overflow-wrap: normal`, which is what the two __key
     rules take. A key is a compound German noun with authored &shy; breaks in
     it and wants the net off so it hyphenates where the author said; a value
     has no interior break point that is ever right. The label is
     `left: 0; right: 0; text-align: center` over an absolutely positioned box,
     so a token wider than the pitch overhangs it symmetrically and costs the
     layout nothing: 6 px each side on this page's 30 px column, 4 px on the
     specimen's 32, zero adjacent-label overlap and zero horizontal scroll on
     <body> at 320, 375, 768, 1024 and 1440 across all four consumers.
     → scripts/check-bound-key-wrap.py */
  white-space: nowrap;
}
.cf-plot__col--lit .cf-plot__val { font-weight: var(--weight-medium); }

/* The key sits under the cell's near vertex, which is the bottom of the box.
   It is the same bounded-key shape .cf-block__key is, and needs the same two
   opt-outs for the same reason — see the long comment above that rule. This
   one shipped without them: at 375 px the column is 32 px (--plot-u*2, the
   fluid unit floored), and base.css's break-word net cut "Abtastung" to
   ABTA/STUN/G, a three-line key with an orphaned final letter overlapping the
   caption below it on blog-artikel.html. The second demo on this page had
   already been given &shy; in "Verfüg&shy;barkeit" and "Lebens&shy;dauer" in
   anticipation of this fix; without the opt-out those soft hyphens were
   inert and the words still broke wherever the net chose. */
.cf-plot__key {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  text-align: center;
  color: var(--text-secondary);
  overflow-wrap: normal;
  hyphens: manual;
}

/* The caption carries the message in words, so it is the figure's accessible
   summary as much as its label. Required — see the page. Its own hairline is
   --border-default rather than the contour black, so the caption rule does not
   read as one more edge of the drawing. */
.cf-plot__caption {
  /* Matches what .cf-prose gives its own figcaptions, so a plot sits the same
     distance off its caption inside an article and outside one. The air under
     the keys is the row's bottom margin, not this. */
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* The columns grow out of the plane as the figure arrives, on the reader's
   scroll rather than on a timer — the same posture, and the same three
   fallbacks, as .cf-iso. No support for view timelines, reduced motion, and
   paper all leave --plot-grow at its registered 1, which is the finished
   chart. A chart that stays empty until a timeline says otherwise is a chart
   some readers never see. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-plot {
      view-timeline-name: --cf-plot;
      view-timeline-axis: block;
    }
    .cf-plot__set {
      animation: cf-plot-grow linear both;
      animation-timeline: --cf-plot;
      /* Ends early, on purpose and for the same reason .cf-iso's does: a
         figure near the bottom of a short document may never reach the far
         end of its own cover range, and a growth that finishes at 90 % is a
         chart that quietly understates every column. Finishing inside the
         first third means the figure is complete long before the reader
         could scroll past it. */
      animation-range: cover 5% cover 35%;
    }
  }
}
@keyframes cf-plot-grow {
  from { --plot-grow: 0; }
  to   { --plot-grow: 1; }
}

/* THE DRAWING IS HANDED TO THE PALETTE BY HAND, because nothing hands it over
   on its own. Forced colours forces `color`, `background-color`, `border-color`
   and the rest of that list; it does NOT force SVG `fill` and `stroke`, which
   the spec leaves to the author. So every stroke here stays the literal
   CF-Schwarz it resolves to, and under a DARK forced palette — Canvas black,
   CanvasText white — the five columns are drawn black on black. Measured in
   Chromium on components/plot.html with the dark palette emulated: the keys,
   the values and the caption came through, and 99 % of the drawing's own box
   was Canvas. The figure kept everything except the figure.

   IT DID NOT LOOK BROKEN ON A SHIPPING PAGE, and that is the part worth
   keeping. An article page is .page-wash, which until base.css's own rule
   above was carrying its light CF-Grau ground straight through this mode — so
   the black columns stood on a light ground and read, while the text beside
   them was forced white on Canvas backplates. Naming the ink and dropping that
   ground are one change: CanvasText on the surviving wash would have been
   white on light grey, which is the same fault pointing the other way.

   The cell keeps the register it has, by keeping its dash. --border-default
   and --border-strong are one ink here — the mode has one — so the ground and
   the column arrive at the same weight, and the 1-4 that says "construction,
   not measurement" is a stroke-dasharray, which is geometry and survives
   untouched. Same substitution .cf-line__frame makes, for the same reason.

   AND THE LIT COLUMN GIVES UP ITS LIGHT, which is the answer .cf-gantt__mark
   --lit already gives one rung up: a lime spill around a mark whose palette
   the reader has chosen is the thing this mode exists to stop. The ramp is an
   SVG paint server rather than a background-image, so unlike the gantt's it
   does not fall away on its own and has to be named — author CSS beats the
   `fill="url(#cf-plot-light)"` presentation attribute, the same precedence
   .cf-plot__draw's `:not(.cf-plot__cap)` is written around.

   What that costs is nothing the figure needs, because "lit" is not carried by
   the light alone: .cf-plot__col--lit .cf-plot__val is --weight-medium, a
   typographic distinction the mode does not touch. This is the gantt's case
   and not .cf-annot--lit's — there, the rung was the ONLY thing telling two
   states apart and had to be bought back as Highlight; here the distinction
   survives and the emphasis is what the mode is allowed to drop.

   The cap's SPILL is dropped at the foot of this file rather than here, beside
   the composition that builds it — a `filter: none` written in this block is
   outranked by that composition on source order, which is a trap the gantt's
   own block fell into first and had never noticed. */
@media (forced-colors: active) {
  .cf-plot__cell,
  .cf-plot__rise,
  .cf-plot__cap { stroke: CanvasText; }
  .cf-plot__col--lit .cf-plot__cap { fill: none; }
}

/* --------------------------------------------------------------------------
   LINE — a quantity over a domain. The second half of the plot's argument.

   .cf-plot draws a short series as OBJECTS: five extruded lattice cells
   standing on the isometric ground plane, each labelled with its own number.
   It is the right figure for three to five values a reader should remember,
   and its own page names the ceiling — "five columns is the ceiling at which
   the figure still fits a 375 px screen, and it is also the ceiling at which
   a picture is still worth more than a table". Past that ceiling the system
   had nothing, and a series of eleven values is not a rare shape for a company
   whose subject is metering.

   WHY THIS ONE IS NOT DRAWN IN SPACE, which is the first decision and the one
   every other follows from. Everything the manual draws in isometry is a BODY
   — a cube, a stack, a solid of revolution, a sphere. A body has faces, one of
   which the light lands on, and it has volume, which is what its height is
   measuring. A line has none of that: it encodes a POSITION at each point of a
   domain and nothing about extent. Drawing it in isometry would claim a body
   where there is none, and it would cost what the plot already measured it
   costs — both ground axes slope 26.57 deg on screen, so a series marching
   along either one subtracts a unit of drawn height per step, and on the
   eleven points below that is more than the whole climb.

   So the line is a SECTION, not a plan: orthographic, 90 deg, the fourth of
   the four brand angles and the one geometry.html calls "the default for
   layout edges". The lattice is still there — every point the figure is read
   at stands on a cell, the same 2:1 rhombus --iso-transform cuts for
   .cf-annot::before — but the trace between those cells is drawn flat,
   because that is what it is.

   THE ANGLE OF THE TRACE IS THE ONE ANGLE IN THIS SYSTEM NOBODY CHOSE.
   foundations/geometry.html says every diagonal on the site is one of four
   values. A trace cannot be: its slope is the quotient of two numbers that
   arrived from somewhere else, and quantising it to 26.57 / 45 / 63.43 would
   be falsifying data to fit a brand — the same class of failure as the
   receding row the plot was rebuilt to remove, and worse, because that one
   only flattened the picture while this one would change the numbers. The
   frame is the brand's; the line inside it is the data's.

   WHICH LEAVES THE FRAME'S RATIO CARRYING THE STEEPNESS, and it is fixed at
   2:1 — the lattice tile's own ratio, the same 200 x 100 bounding box
   geometry.html derives from a 200 px square. The dataviz literature would
   tune it per dataset (bank the median segment to 45 deg); this system does
   not, and the reason is that steepness is the thing a reader takes off a
   line. Two line charts in one document with two aspect ratios cannot be
   compared and nothing tells the reader they differ. --line-ratio exists so a
   document can choose once, from the three ratios the brand owns (2, 1, 0.5 —
   the frame diagonals at 26.57, 45 and 63.43 deg), and never per figure.

   THE FLOOR IS NOT ASSUMED TO BE ZERO, and this is the other half of the
   body/trace distinction. A column is a body standing on the ground, so its
   floor must be zero — cutting it lies about its volume, which is why the plot
   has no baseline other than zero and says so. A trace stands on nothing, so
   its frame may start anywhere, and the moment that is true the frame has to
   SAY where it starts: .cf-line__bounds is required, and it is the only
   number in the figure that is not a datum.

   ONE COLOUR RULE FOR THE WHOLE FIGURE: the frame is grey, the data is black,
   and exactly one trace may take the one hue the page has room for. The frame's
   floor, ceiling and steps are --border-default hairlines for exactly the
   reason .cf-plot__caption's rule is — so they do not read as one more edge of
   the drawing. A second series is one rung down the presence ladder and not one
   step down the grey ramp, and that is measured rather than asserted. Grey
   fails at both ends at once, against CF-Grau, the wash's worst end:

     --border-default  #9D9D9D composited   1.74:1 on the wash     invisible
     grey-500          #9B9B9B              1.78:1 on the wash     invisible
     grey-400          #919191              2.02:1 on the wash     invisible
     grey-700          #484848              5.87:1 on the wash,
                                            2.30:1 from black      not a second

   Every grey light enough to READ as a second series is under the 3:1 this
   system holds a contour to — a line the reader is invited to compare and
   cannot see. The one grey that clears the wash is 2.30:1 from the trace it is
   supposed to differ from, and it is --text-secondary, the label ink, so a
   trace in it reads as annotation rather than as data. The accents are measured
   beside the rungs below, and that measurement is what says a figure may carry
   one hue and never two. Presence is the line type because it is the only cue
   that survives a 1 px stroke in forced colours, in grayscale and on paper.

   NO VERTICAL LINES AT ALL, which is the table's argument reused: a rule
   between two points would be neither an edge, nor a division, nor a label
   rule, and geometry.html calls a line that is none of the three decoration.
   So there is no domain axis and no tick, and the domain is named at the points
   it is read at — the same direct labelling the plot uses instead of a legend.

   THE VALUE SCALE IS THE EXCEPTION, AND IT IS ONE BECAUSE IT PASSES THE SAME
   TEST. A step is a number with a hairline running out of it to the frame's far
   edge, which is geometry.html's third kind exactly — a label rule, the mark
   .cf-section-header already is. It is optional, it subdivides and never
   restates the ends, and it is the alternative to printing values rather than
   an addition to them. See .cf-line__scale.
   -------------------------------------------------------------------------- */

/* The domain coordinate, and the twin of --v. Registered for the same two
   reasons --v is: a unit or a percentage typed here resolves to the initial 0
   instead of making `left` invalid at computed-value time and stacking every
   point on the frame's left edge, and a registered number is a number rather
   than a token that looks like one. --v is registered above, by the plot, and
   is reused verbatim — one name, one meaning, in both figures. */
@property --t {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
/* 1 is the finished chart, and it is what everything that cannot animate
   gets — no view timelines, reduced motion, print. Same posture, same
   registered default, as --plot-grow and --annot-draw. */
@property --line-draw {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

.cf-line {
  /* THE PLOT CARRIES ITS OWN SIZE AND THE LINE TAKES THE MEASURE IT IS GIVEN,
     and that difference is the two figures being two kinds of thing. A plot is
     an object: five columns four units apart is a width, and --plot-u is how
     you resize it. A line is a frame with a domain stretched across it, so it
     is as wide as the column it is set in and no wider than 640 px — which is
     20 x --plot-u at that unit's 32 px ceiling, so a plot and a line in the
     same article are the same width on a desktop rather than nearly so. */
  --line-max: 640px;
  /* 2:1, the tile. See the header — this is a document-level decision, not a
     per-figure one, and the only other legal values are 1 and 0.5. */
  --line-ratio: 2;
  /* The mark a number is anchored to. Same 8 px as --annot-node, because it is
     the same mark: a lattice cell, not a dot. */
  --line-node: 0.5rem;
  margin: 0;
  /* THE CAP IS ON THE FIGURE AND NOT ON THE FRAME, which .cf-plot has no
     reason to care about and this one does. The frame draws two full-width
     hairlines of its own; a caption rule running past them reads as a third
     edge of the drawing, longer than the drawing. One max-width here puts the
     bounds, the two frame edges and the caption rule on one right margin, so
     the figure is a single column rather than a chart with a wider footer. */
  max-width: var(--line-max);
  /* Room for the tallest value to hang above its node without touching the
     bounds line above the frame. */
  padding-top: var(--space-1);
}

/* The frame declares its own extent, in one mark, above its top edge. Right
   aligned so it lands on the corner the ceiling and the last value share, and
   in the label face rather than the value face: this is the instrument
   speaking, not a datum. Required — see the header. */
.cf-line__bounds {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  text-align: right;
  color: var(--text-secondary);
}

/* The frame. Two hairlines and nothing else: the floor and the ceiling are
   edges — they close the drawing — and they are the grey the caption rule uses
   so that neither is mistaken for the trace. The left and right sides are not
   drawn, because the domain does not end where the frame does; it is simply
   the part of it this figure shows.

   The bottom margin is the keys' room, and it is the plot's number for the
   plot's reason: 8 px of drop plus an 11 px label at --leading-normal, plus
   air. The keys hang below the frame on absolute position, so the frame's own
   layout box ends at the ceiling-to-floor box and anything after it would
   print across them. */
.cf-line__frame {
  position: relative;
  width: 100%;
  aspect-ratio: var(--line-ratio);
  margin-bottom: var(--space-8);
  border-block: var(--stroke-1) solid var(--border-default);
}

/* THE SCALE — the frame subdivided, and the one axis this figure has.

   IT IS A LABEL RULE, WHICH IS WHY IT IS ALLOWED TO EXIST. geometry.html
   #where-lines-go admits exactly three kinds of line and calls everything else
   decoration, and its third kind is "a mono label with the hairline running out
   of it to the far edge -- the line and the word are one mark". A value step is
   that construction and nothing else: the number, a gap, then a hairline to the
   frame's right edge. It is the same mark .cf-section-header is, turned into
   the frame. The vertical rules stay forbidden for the reason they always were
   -- a rule standing between two points carries no label at its end and closes
   nothing -- so there is no domain axis here, and there does not need to be:
   the domain is named at the points it is read at.

   THE ENDS ARE NOT STEPS, and that is what keeps this from being a second copy
   of anything. The floor and the ceiling are already drawn (the frame's own two
   hairlines) and already named (.cf-line__bounds, which stays required and
   still carries the unit). So the scale prints the SUBDIVISIONS only, bare, and
   a figure never says a number twice. A step at --v: 0 or --v: 1 would also put
   half its numerals outside the frame it is measuring.

   SAME GREY AND SAME WEIGHT AS THE FRAME'S OWN EDGES, deliberately. A
   subdivision is the same instrument at the same volume as the edges it lies
   between, and --border-default is already the quietest line the figure draws
   at 1.74:1 on the wash's worst end -- a rung below it is not a fainter rule,
   it is no rule. What separates a subdivision from an edge here is not tint: an
   edge runs the full width and carries no number, a subdivision stops at one.
   That is the label-rule distinction drawn rather than asserted.

   A SCALE AND A PRINTED VALUE ARE TWO ANSWERS TO THE SAME QUESTION and a figure
   picks one. Print three numbers when three numbers are the message; draw a
   scale when the shape is the message and the reader wants to estimate anywhere
   along it, and let the set go --quiet. The one hard case is the left edge: the
   step's number and a --first point's value are both flush left, and at 320 px
   the frame is 136 px, so a step at .25 (34 px) and an opening value at .12
   (28-46 px with its gap) overlap. Measured, not feared. A figure with a scale
   does not print its opening value -- it has a scale, which is what the reader
   would have read it off.

   IT IS aria-hidden, for .cf-line__draw's reason. The scale is the instrument,
   not the data: every value is already in the tree as text, and the extent is
   already in the tree as .cf-line__bounds. Exposed, "25 50 75" would read as a
   fourth series between the real ones. */
.cf-line__scale {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* translateY(50%) and not a bare `bottom`, because the box has a height and the
   value does not. bottom alone would sit the whole line-height above --v and
   put the rule half a line off the number it names; centring the box on --v
   lands the rule ON the value and the numerals beside it, at the same height,
   over clear ground rather than over the rule. */
.cf-line__step {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(clamp(0, var(--v), 1) * 100%);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  transform: translateY(50%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  /* Tabular, and this is the whole of the alignment mechanism: steps with the
     same digit count set to the same width, so the rules all start at one x
     without a wrapper element to align them in. A sensible step already has
     one -- 25/50/75, 20/40/60/80, 99,2/99,4/99,6 -- and a step series that
     does not is a step series to reconsider. */
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-label);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  white-space: nowrap;
}
.cf-line__step::after {
  content: "";
  flex: 1 1 auto;
  border-top: var(--stroke-1) solid var(--border-default);
}

/* The drawing is a normalised square that is stretched to whatever frame it is
   given: viewBox 0 0 100 100 with preserveAspectRatio="none", so x IS --t x 100
   and y IS (1 - --v) x 100 and there is no conversion anywhere between the two
   halves of the component. Distorting the box is correct here rather than
   tolerated — the aspect ratio is the frame's, not the data's, and stretching
   the data space is precisely what changing --line-ratio means. It costs
   nothing the drawing owns: the only geometry inside is polylines, whose stroke
   is non-scaling and therefore 1 px on screen at every ratio and every width,
   and every round mark in the figure is HTML sitting on top.

   THE DRAW IS A WIPE AND NOT A DASH, and that is the one place this component
   parts company with .cf-iso__trace. That trace draws with pathLength="1" and
   stroke-dashoffset, and pays for it by giving up non-scaling-stroke — the two
   measure in different spaces, so the draw stalls at the render scale — which
   leaves its weight a per-object number that ran from 0.38 to 1.71 CSS px
   across the tree. A chart cannot pay that: the trace IS the datum, and a grey
   0.5 px line at 2.5:1 on a phone is a datum the reader does not have. So the
   stroke keeps non-scaling-stroke and the draw is a clip travelling along the
   domain.

   It is also the better reading of the motion, which is the part worth keeping
   even if the arithmetic had gone the other way. A dash draw advances at
   constant ARC LENGTH, so a steep year takes longer to draw than a flat one; a
   wipe advances at constant DOMAIN, so every year takes the same scroll. The
   line arrives left to right at the rate time passed. */
.cf-line__draw {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  clip-path: inset(0 calc((1 - clamp(0, var(--line-draw), 1)) * 100%) 0 0);
}

/* The one black line in the box. stroke-width is a plain 1 under
   non-scaling-stroke, which is the system's contour made literally true at
   every width — the same declaration .cf-plot__draw's paths carry.

   Round joins rather than the drafting default: at a sharp reversal a mitre on
   a 1 px stroke grows to the mitre limit before it bevels, which is a 4 px
   spike on a hairline. Invisible either way at every join that is not a
   reversal. */
.cf-line__trace {
  vector-effect: non-scaling-stroke;
  fill: none;
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* THE OTHER THREE RUNGS, AND THERE IS NO FOURTH ENCODING BEHIND THEM.
   The four line types are the whole vocabulary a second series can be drawn
   in, and they are named here by RUNG rather than by ratio — --presence-near
   is 2-1 and --presence-absent is 1-4 — because what a reader takes off a
   broken line is how present the thing is, not what its duty cycle is.

   TWO READINGS, AND WHICH ONE APPLIES IS THE NUMBER OF TRACES.

     two     Presence. The solid line is the subject and the broken one is the
             reference it is being read against: the plan, the year before,
             the target. Use --ghost and nothing else, because the widest gap
             on the ladder is the one that reads fastest.
     three   Identity, and the ladder is still ordinal — so the three series
             have to BE ordinal, and the ordering that fits it is epistemic:
             measured, planned, forecast. Present, one rung down, not here yet.
             Three unordered peers is a figure this component cannot draw
             honestly; see below.

   THERE IS ONE COLOUR HERE AND THERE CANNOT BE TWO, WHICH IS ARITHMETIC AND
   NOT DOCTRINE. The brand has three accents besides lime, each with a full
   ramp, so a categorical set is not obviously unavailable — it was measured
   rather than assumed, and the measurement is what the ladder rests on.

   A TRACE HAS TO CLEAR TWO BARS AT ONCE, and that is the part the obvious
   version of this arithmetic misses. It owes 3:1 against the PAGE, whose worst
   end is CF-Grau at relative luminance 0.624; and it owes 3:1 against the BLACK
   TRACE it exists to differ from, which is Schwarz at 0. Solve both and the
   colour's own luminance is pinned into a band:

     3:1 off the wash's worst end   Y <= (0.624 + 0.05) / 3 - 0.05 = 0.175
     3:1 off the black trace        Y >= 3 x 0.05 - 0.05          = 0.100

   The widest contrast available anywhere INSIDE that band is
   (0.175 + 0.05) / (0.100 + 0.05) = 1.50:1. So a figure on this page can carry
   at most ONE such colour, whatever palette it is handed — a second would be
   under 1.5:1 from the first, which on a 1 px stroke is no distinction at all.
   That is a property of a black-ink figure on a CF-Grau ground and not of the
   brand: growing the brand a categorical ramp would not buy a second hue, it
   would only produce more colours that fail each other. This is the whole
   reason presence is a line type here.

   OF THE 28 STEPS IN THE FOUR RAMPS, EXACTLY ONE LANDS IN THE BAND. Against
   CF-Grau, the wash's worst end, and against the black it must differ from:

     Glas 500     #C5EBE2   1.21:1 wash                the brand hue    FAILS
     Sky 500      #72B0E2   1.49:1 wash                the brand hue    FAILS
     Violett 500  #7E7FE1   2.24:1 wash                the brand hue    FAILS
     Glas 700     #94B0A9   1.49:1 wash                                 FAILS
     Sky 700      #5684A9   2.56:1 wash                                 FAILS
     Glas 800     #45524F   5.24:1 wash, 2.57:1 black  a near-black     FAILS
     Sky 800      #283E4F   7.12:1 wash, 1.89:1 black  a near-black     FAILS
     Violett 800  #2C2C4F   8.53:1 wash, 1.58:1 black  a near-black     FAILS
     Violett 700  #5F5FA9   3.65:1 wash, 3.70:1 black, 5.68:1 white     PASSES

   Everything light enough to carry a hue fails the page; everything dark enough
   to clear the page is a near-black that fails the trace. Violett 700 sits in
   the gap, and it is the only step that does — Y 0.1348, near the middle of a
   band 0.075 wide. It is not --text-secondary (1.61:1 from grey 700), so it
   does not read as label ink the way a grey trace would.

   SO IT IS .cf-line__trace--hue, IT RIDES WITH A RUNG AND NEVER INSTEAD OF ONE.
   Nothing here may be encoded in colour alone, and this component has two
   independent reasons beyond the rule: the forced-colours block below hands
   every trace to CanvasText, so the hue is gone there and the dash is what
   survives; and check-line-trace.py's RUNGS rule stands — two traces in one
   figure may not share a rung, hue or no hue. That hand-off has to be written
   out, because forced colours does NOT force SVG stroke and a tinted trace left
   alone sails through the mode intact — which looks like the hue working and is
   the hue escaping. What the hue buys is the thing a dash on a hairline is worst
   at: telling two crossing lines apart at a glance, without stopping to inspect
   a duty cycle. It is redundancy, which is what a second channel is supposed to
   be.

   Past three series the answer is still not a fourth line type, and now not a
   second hue either. It is four frames: see the small multiples on the
   component's page.

   → foundations/geometry.html, the presence ladder; tokens.css section 8a */
.cf-line__trace--near  { stroke-dasharray: var(--presence-near); }
.cf-line__trace--faint { stroke-dasharray: var(--presence-faint); }
.cf-line__trace--ghost { stroke-dasharray: var(--presence-absent); }

/* The one hue, on the one trace that may carry it. Violett 700 and not a
   --line-hue custom property, deliberately: a knob here would read as a choice
   and there is no choice to make — it is the only step in the palette that
   clears both bars at once, and the band it sits in has room for nothing else.
   See the arithmetic above.

   The legend sample inherits it for free, because the sample IS the trace. The
   nodes and the numbers stay black: one hue in a figure means one, and a value
   is read off the text, not off its tint. */
.cf-line__trace--hue { stroke: var(--violett-700); }

/* THE HEAD: the key on the left, the frame's own extent on the right, one
   line above the ceiling. It costs no vertical space that .cf-line__bounds
   was not already spending, which is the reason the legend lives here rather
   than under the caption where a legend usually ends up — a reader who has to
   look below the figure to find out which line is which has left the figure.

   Optional. One trace needs no key at all, and two are usually better served
   by naming each at its own last point, which is direct labelling and beats a
   legend every time. */
.cf-line__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-6);
  margin-bottom: var(--space-2);
}
/* margin-inline-start: auto and not only space-between. When the key is wide
   enough to take the whole line — measured at 375 px, where three items do —
   the bounds becomes the only item on the second line, and space-between puts
   a lone item at flex-start. Its own text-align cannot save it: a flex item is
   shrink-to-fit, so the box IS the text and there is nothing to align inside.
   The auto margin pushes the box itself, which is what keeps the frame's
   extent over the frame's right edge at every width. */
.cf-line__head .cf-line__bounds { margin: 0 0 0 auto; }

.cf-line__legend {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
  /* THE LEGEND IS ITSELF A FLEX ITEM, of .cf-line__head, and a flex item's
     automatic minimum is its min-content — which for this list is its longest
     entry, not its longest word. So the list could wrap its items and never
     did: the head handed it the whole entry's width first. Measured on
     patterns/landing-page.html at a 32 px root, "MIT ENERGIEMANAGEMENT" made
     the legend 292 px wide inside a 204 px figure, the frame took the legend's
     width, and its end node stood 17 px past a 375 px viewport. min-width: 0
     is what lets the head decide the width and the wrap above do its job;
     base.css calls the same declaration on .tiles "load-bearing, not hygiene". */
  min-width: 0;
}
.cf-line__legend-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  /* And the entry is a flex item of the legend, with the same automatic
     minimum one level down: without this the list above wraps its entries and
     an entry never wraps its words, so "MIT ENERGIEMANAGEMENT" stays one line
     wider than the frame at a 32 px root. With it the entry takes the legend's
     width and the reset's net wraps the text. */
  min-width: 0;
}

/* THE SAMPLE IS THE TRACE, not a picture of it. It carries .cf-line__trace and
   the same rung modifier the polyline does, so the dash, the weight, the
   colour and the non-scaling stroke all arrive from the one rule above — there
   is no second declaration of what a rung looks like, and a rung added later
   reaches the key without anybody remembering to update it.
   scripts/check-line-trace.py holds the other half: the key's rungs must be
   the figure's rungs, in order. */
.cf-line__sample {
  display: block;
  flex: 0 0 auto;
  width: var(--space-6);
  height: var(--space-2);
  overflow: visible;
}

/* The data, as text, once. Each <li> carries the two numbers that place it and
   prints them where the reader can take them off the figure; nothing is
   duplicated into a hidden table that could drift out of step with the
   picture. The set is an <ol> because a series has an order and the order
   carries the meaning. */
.cf-line__set {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One point is a full-height column of the frame standing at its own place in
   the domain, so that the node and the value can sit at --v while the key sits
   under the frame — three marks, one position, no third number.

   --line-h IS --v, CLAMPED AND MADE INHERITABLE, and both halves of that are
   load-bearing. Registered custom properties do not inherit unless they say
   so, and --v says `inherits: false`, so a child of this <li> cannot read it;
   .cf-plot has the identical shape and solves it the identical way. The clamp
   is the other half: a registered <number> rejects a unit but not a 4 or a -1,
   and either would put a node outside the frame it is meant to be read in. */
.cf-line__point {
  position: absolute;
  left: calc(clamp(0, var(--t), 1) * 100%);
  top: 0;
  bottom: 0;
  width: 0;
  --line-h: clamp(0, var(--v), 1);
}

/* A POINT IS DATA BY DEFAULT AND A MARK ONLY WHEN IT IS ASKED TO BE, which is
   the rule that separates this figure from the plot. The plot prints every
   value because it has at most five; a line exists precisely when there are
   more values than a reader can be handed, and eleven printed numbers along a
   trace is a figure nobody reads. So the default is the quiet one and the
   painted point is the opt-in.

   The recipe is .visually-hidden's, restated rather than reused, because that
   class lives in base.css and this component's own positioning would outrank
   it on source order at equal specificity. clip-path rather than the clip
   rect, and no negative margin: the point stays in the accessibility tree, in
   series order, with its key and its value as real text. */
/* A SERIES READ FOR ITS SHAPE AND NOT FOR ITS NUMBERS, which is what happens
   the moment a figure carries more than two traces. A value needs about 28 px
   of clear space above its node — a fixed height against a frame that is 320 px
   on a desktop and 136 px at 320 px wide — and in a multi-series figure the
   space above a point usually belongs to another trace. Measured on the
   three-series figure at 375 px: the plan ran through the subject's own
   opening value, because 13 points of frame is 21 px there and the label is 28.
   So the numbers go in the caption, which has room for them, and the figure
   keeps its keys: the domain is still named at both ends and every value is
   still in the accessibility tree. */
/* A SECOND READING AT A PLACE THE AXIS HAS ALREADY NAMED, which is the other
   half of the same idea and the only way two series may both be read at one
   --t. The component's own doctrine forbids the collision and not the pair:
   "print keys from two series at the same --t" is a don't because both keys
   are laid against the same side of the frame and land on top of each other,
   and the escape is to print one of them. So this modifier keeps the point's
   value, its node and its place in the tree, and takes only the key — the
   label that would have been the duplicate.

   IT IS THE END OF A COMPARISON SERIES AND ALMOST NOTHING ELSE. Read at an
   interior --t a ghost needs no such thing: it has the axis to itself there,
   which is why components/line.html's own paired figure reads its plan at
   --t: .5 and carries its key. The case this exists for is the figure whose
   comparison is FLAT — a do-nothing baseline — where the interior is the one
   place the reading must not go, because a flat ghost says the same number at
   every t and the only t that means anything is the last one. Measured on the
   evidence line at 320 px, where the frame is 214 px: the two keys are
   "OHNE · JAHR 5" and "MIT · JAHR 5", both against the frame's right side,
   overlapping by the whole of the shorter. The two VALUES at that same --t do
   not collide — they ride their own points at --v .785 and .367, 23.2 px
   apart at the tightest width in the system — and the legend above the frame
   is what names them, which is what a figure that carries a legend has it for.

   NOT A --quiet SET AND NOT ITS INVERSE. --quiet takes the numbers off a
   whole series and keeps its keys; this takes one key off one point and keeps
   its number. The two are used together on the same figure in principle and
   have never needed to be. */
.cf-line__set--quiet .cf-line__val,
.cf-line__point--unkeyed .cf-line__key,
.cf-line__point:not(.cf-line__point--read) {
  width: 1px;
  height: 1px;
  /* clip, not hidden — a crop must not become a scrollport, and this one would
     be a scrollport per unread point. Both declarations: a browser too old for
     `clip` drops the second and still crops. */
  overflow: hidden;
  overflow: clip;
  clip-path: inset(50%);
  white-space: nowrap;
}
.cf-line__point:not(.cf-line__point--read) {
  left: 0;
  top: 0;
  bottom: auto;
}

/* The anchor: a lattice cell, not a dot — the same 2:1 rhombus at 8 px that
   .cf-annot::before cuts and that the ground is tiled with at 120. Drawn as a
   border, which is the paint forced-colours keeps. It exists to tie a number
   to the trace; a point with no number needs no anchor, which is why the
   quiet points above carry none.

   At --t: 0 and --t: 1 the cell straddles the frame's side by half its width,
   and that is correct rather than tolerated: the point IS at the side, and the
   only way to tuck the mark inside would be to inset the domain, which moves
   every value in the figure to flatter 5.6 px of hairline. It overhangs into
   the measure's own margin and never into the gutter. */
.cf-line__node {
  position: absolute;
  left: 0;
  bottom: calc(var(--line-h) * 100%);
  width: var(--line-node);
  height: var(--line-node);
  border: var(--stroke-1) solid var(--border-strong);
  transform: translate(-50%, 50%) var(--iso-transform);
}

/* The number, riding its own point. Stated weight rather than inherited, for
   the reason .cf-plot__val states its own: .cf-prose sets running text in
   Light and a figure that changes weight with the block it was dropped into is
   not a figure the rest of the system can match. */
.cf-line__val {
  position: absolute;
  left: 0;
  /* THE LABEL IS A FIXED HEIGHT AND THE FRAME IS NOT, so a value near the
     ceiling climbs out through it. The label needs its own line plus the gap
     — 0.75rem x 1.3 of type over --space-3, about 27.6 px at a 16 px root —
     and that is the same 27.6 px whether the frame is 320 px tall on a desktop
     or 136 px on a 320 px viewport. As a fraction of the frame it is 8.6 %
     there and 20.3 % here, so a value that clears the top edge on one screen
     crosses it on another. Measured before this min(): at 320 px wide the
     84 % on the figure above sat 5.1 px over its own ceiling, with the frame's
     top hairline running through the numerals; at 375 px, 0.8 px over.

     The clamp parks such a label flush under the ceiling instead of through
     it, and leaves its node where the data says. Nothing moves on a frame that
     has the room — at 320 px tall the second term is 292 px and no value in
     this system's own examples reaches it — so this is a floor and not a
     layout. The alternatives were worse: flipping the label below its node
     puts it on top of a rising trace at exactly the point where a series
     usually peaks, and reserving the headroom in the frame would mean either
     a min-height that breaks the 2:1 or a ceiling chosen to suit the labels
     rather than the data. */
  bottom: min(
    calc(var(--line-h) * 100%),
    calc(100% - var(--space-3) - var(--text-sm) * var(--leading-normal))
  );
  margin-bottom: var(--space-3);
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-normal);
  color: var(--text-primary);
  white-space: nowrap;
}

/* What the point is, under the frame. --text-secondary at 5.9:1 on CF-Grau,
   never --text-muted, which is 2.0:1 there. */
.cf-line__key {
  position: absolute;
  left: 0;
  top: 100%;
  margin-top: var(--space-2);
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* The two ends of the domain are read against the frame's own sides rather
   than centred on their point, because half of a centred label at --t: 0 is
   off the figure. Everything between them is centred. */
.cf-line__point--first :is(.cf-line__val, .cf-line__key) { transform: none; }
.cf-line__point--last  :is(.cf-line__val, .cf-line__key) { transform: translateX(-100%); }

/* The comparison series speaks at label strength, so that the solid line is
   unambiguously the subject. Its keys already carry its name — "Plan 2020" —
   which is what keeps the two series apart in the accessibility tree without
   an aria-label, a legend, or a second copy of anything. */
.cf-line__set--ghost .cf-line__val { color: var(--text-secondary); }

/* The caption carries the message in words and is the figure's accessible
   summary as much as its label. Required, for the reason the plot's is.
   Identical construction, so a line and a plot sit the same distance off their
   captions inside an article and outside one. */
.cf-line__caption {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* The trace arrives along its own domain as the figure does, scrubbed from
   scroll position rather than played on a timer — the same posture and the
   same three fallbacks as .cf-plot and .cf-iso. No support for view timelines,
   reduced motion and paper all leave --line-draw at its registered 1, which is
   the finished chart.

   ONLY THE TRACE MOVES. The frame, the bounds, the nodes and every printed
   number are drawn at once and stay drawn: they are the instrument, and a
   figure whose readable content waits for a timeline is a figure some readers
   never read. The measurement is the only thing that arrives.

   The range ends early for the plot's reason — a figure near the bottom of a
   short document may never reach the far end of its own cover range, and a
   draw that finishes at 90 % is a chart that quietly stops short of its last
   two years. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-line {
      view-timeline-name: --cf-line;
      view-timeline-axis: block;
    }
    .cf-line__draw {
      animation: cf-line-draw linear both;
      animation-timeline: --cf-line;
      animation-range: cover 5% cover 35%;
    }
  }
}
@keyframes cf-line-draw {
  from { --line-draw: 0; }
  to   { --line-draw: 1; }
}

/* Forced colours maps every author colour onto one ink, so --border-default
   and --border-strong arrive at the same value and the frame's two hairlines
   come up at the trace's own weight — the one distinction this figure spends
   its whole colour budget on, gone. Redrawn as a dash, which is the
   substitution .rule already makes in this mode and for this reason: the exact
   ratio is lost because border-style cannot express one, and the register —
   instrument against datum — is kept. The ghost trace keeps its own 1-4,
   because a stroke-dasharray is geometry and this mode does not touch it.

   The scale's steps take the same substitution for the same reason — they are
   the frame subdivided, so they are instrument too, and a solid step rule in
   this mode would be a full-width hairline across the figure at the trace's own
   weight. The step still reads as a step and not as an edge, because it stops
   at its own number: that distinction is geometry and survives here intact.

   AND THE TRACE IS HANDED TO THE PALETTE BY HAND, because nothing hands it over
   on its own. Forced colours forces `color`, `background-color`, `border-color`
   and the rest of that list — it does NOT force SVG `fill` and `stroke`, which
   the spec leaves to the author. So `stroke: var(--border-strong)` stays the
   literal Schwarz it resolves to, and under a DARK forced palette — Canvas
   black, CanvasText white — the subject trace is drawn black on black and is
   simply not there. Measured in Chromium with the dark palette emulated: the
   frame, the nodes, the keys and every printed value came through, and the one
   thing missing from the figure was the measurement. That is the same failure
   the site's search matches had, and the same fix: name the system colour.

   It also makes the hue's rule true rather than merely intended. CanvasText is
   one ink, so every trace arrives on it and the tint is gone, which is what
   `.cf-line__trace--hue` is only ever allowed to be worth — the dash is what
   survives, and what a reader gets in this mode is the figure exactly as it was
   before the colour existed. Being wrong about this is easy: emulate the mode
   and a coloured stroke comes through untouched, which looks like the hue
   working and is the hue escaping. */
@media (forced-colors: active) {
  .cf-line__frame { border-block-style: dashed; }
  .cf-line__step::after { border-top-style: dashed; }
  .cf-line__trace { stroke: CanvasText; }
}


/* --------------------------------------------------------------------------
   BLOCK — several series compared, category by category, in section

   THE GAP THIS FILLS IS THE ONE .cf-plot NAMES IN ITS OWN "STILL OPEN": "One
   series only. Two series in one plot would need a second lattice depth — a
   row of columns stepped one cell back along an axis — which is the obvious
   extension and also the one most likely to reintroduce the recession problem
   this component was rebuilt to solve." That is the right refusal, and it
   leaves a figure the system could not draw at all: three quantities measured
   the same way at five places, which is the commonest chart in any technical
   report there is.

   SO THE BLOCK IS THE PLOT IN SECTION, and every consequence below follows
   from that one move. .cf-line already made it and stated why: everything the
   manual draws in isometry is an OBJECT with faces and volume, and a figure
   that needs to be read rather than inhabited is drawn orthographically at
   90 deg — the fourth of the four angles. A column in space can carry one
   series because its ground plane is spent on the projection; a block on a
   flat frame has both screen axes free, so one of them can carry the series
   and the other the categories.

   WHAT IT BUYS, AND WHAT IT COSTS. It buys the second and third series, the
   scale, and a legend — none of which the plot can have. It costs the body:
   these are faces, not solids, and the figure says nothing about volume. That
   is the correct trade for a comparison and the wrong one for a single number
   the reader is meant to remember, which is what .cf-plot stays for.

   EVERY PART IS BORROWED, as it was there:

     the frame     .cf-line__frame — two hairlines, floor and ceiling, at the
                   tile's 2:1. Same construction, same grey, same ratio, so a
                   line and a block in one article are the same instrument
     the scale     .cf-line__scale — the label rule, which is the only kind of
                   rule geometry.html admits inside a drawing
     the pitch     .cf-plot's 4u — 2u of column and 2u of air. Written here as
                   a FRACTION of the category's pitch rather than a length,
                   because the categories share the domain, and 0.5 is that
                   same 1:1
     the contour   1 px, --border-strong, and adjacent blocks SHARE it the way
                   .cf-pie's neighbouring shares share a cut
     the faces     one ramp, four sources — see below
     the numbers   Geist Mono, tabular, and cropped by default: this figure has
                   a scale, and .cf-line already settled that a scale and a
                   printed value are two answers to one question

   THE BLOCK IS A CSS BOX AND NOT AN SVG, which reverses .cf-plot's decision on
   the same question and for the same reason. That component draws in SVG
   because its columns are SCALED — scaleY(--plot-h) on a shaft — and a 1 px
   CSS border inside a scaled box is scaled with it, while an SVG stroke under
   vector-effect: non-scaling-stroke is not. Nothing here is scaled: a block's
   height is a percentage of a frame, resolved at layout, so its border is 1 px
   at every value, every width and every zoom without asking for it. The
   section is the projection where the cheap medium is also the correct one.

   And it is what makes the ramps legal. A CSS gradient can be written
   `in oklab`; an SVG <linearGradient> cannot, which is why every lit face in
   this system carries a hand-computed waypoint at 19 % of its lime leg. The
   faces below take the family's own path directly.
   -------------------------------------------------------------------------- */

/* Registered for .cf-plot's two reasons, and the second one is the load-
   bearing one here. Unregistered, --block-grow could not be interpolated at
   all, so the growth would be a height animation per block instead of one
   number every block reads. Registered, a figure of fifteen blocks grows as
   one thing. --v is already registered above, by the plot, as <number> with an
   initial 0 — so a typo'd value here draws a block at nothing rather than at
   full height, which is the same failure mode and the same defence. */
@property --block-grow {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

.cf-block {
  /* The line's width and the line's reason: 640 px is 20 x --plot-u at that
     unit's 32 px ceiling, so a plot, a line and a block in one article are the
     same width on a desktop rather than nearly so. */
  --block-max: 640px;
  /* 2:1, the tile. A document-level decision and not a per-figure one, for
     .cf-line's reason: steepness is what a reader takes off a frame, and two
     frames at two ratios in one document cannot be compared. */
  --block-ratio: 2;
  /* THE PLOT'S 4u PITCH, WRITTEN AS A FRACTION. There the pitch is a length —
     2u of column and 2u of air — because a plot is an object and five columns
     four units apart is a width. Here the categories divide the frame between
     them, so the same 1:1 has to survive a changing category count, and the
     only form that does is a share of whatever pitch there turns out to be.
     0.5 is 2u of blocks and 2u of air at five categories, and it is still 1:1
     at eleven. */
  --block-fill: 0.5;
  /* THE SCALE'S NUMERALS NEED GROUND OF THEIR OWN, and this is the one measure
     the line does not have to carry. .cf-line__scale sets its numbers inside
     the frame at the left and nothing else is ever there, because a trace is a
     hairline: it crosses a numeral as one more hairline and both stay legible.
     A block is a surface, and the first category stands exactly where the
     numbers are. Measured on the nine-category figure at a 700 px column
     before this: the first block covered BOTH digits of all three steps, so
     the figure had a scale a reader could not read.

     So the set starts one gutter in. The numbers keep the line's position —
     inside the frame, flush left, on the rule they name — and the rule runs
     the frame's whole width from behind them, past the blocks, to the far
     edge. That is the label rule drawn slightly more literally than the line
     draws it, not a different mark.

     32 px carries two or three digits: "100" sets 20 px in 11 px Geist Mono
     and the step's own --space-2 gap takes 8 of the rest. A step series with
     a decimal in it — 99,6 — needs this raised, and raising it is the whole
     interface. */
  --block-gutter: var(--space-8);
  margin: 0;
  /* On the figure and not on the frame, for .cf-line's reason: the frame draws
     two full-width hairlines, and a caption rule running past them reads as a
     third edge of the drawing, longer than the drawing. */
  max-width: var(--block-max);
}

/* The head. The legend on the left, the frame's extent on the right, one line
   above the ceiling — .cf-line__head's box exactly, because it is doing
   exactly that job. */
.cf-block__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-6);
  margin-bottom: var(--space-2);
}

/* THE CEILING AND ITS UNIT, AND HERE IT IS NOT NEGOTIABLE. .cf-line__bounds
   states both ends because a trace stands on nothing and a tight frame is a
   legitimate choice there. A block stands ON the floor: its LENGTH is the
   quantity, so a floor anywhere but zero multiplies every comparison in the
   figure by a number the reader cannot see. The floor is therefore not stated
   because it cannot be anything else, and what this mark carries is the
   ceiling and the unit. Required. */
.cf-block__bounds {
  margin: 0 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  text-align: right;
  color: var(--text-secondary);
}

/* THE LEGEND IS AN <ol> AND THAT IS THE WHOLE ACCESSIBILITY ARGUMENT OF THIS
   COMPONENT. Every other figure in this system refuses a legend, and each
   states the same reason: a legend is a distinction carried by a colour a
   third of readers cannot draw. The refusal is right and the reason is about
   COLOUR, not about legends — so the block keeps the legend and takes the
   colour out of it.

   What identifies a series here is its POSITION IN THE GROUP. The series run
   left to right inside every category in the order the legend lists them, and
   that order is geometry: it survives a monochrome screen, every colour vision
   deficiency, a forced palette, and a printer that drops background images.
   The hue is redundant reinforcement of a fact the figure has already stated
   twice — by position, and by the series name on every block in the tree.

   That is also what lets a block be lit. A lime block in position two is still
   position two, so .cf-plot's one-light-per-figure emphasis costs the legend
   nothing, which it would if hue were what the legend was made of. */
.cf-block__legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}
.cf-block__legend-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* THE SAMPLE IS THE FACE, not a picture of it — .cf-line__sample's rule, kept
   by putting the sample in the same selector list as the block it names, so
   there is exactly one declaration of what a series looks like. A rung added
   later reaches the key without anybody remembering to update it. */
.cf-block__sample {
  flex: 0 0 auto;
  width: var(--space-3);
  height: var(--space-3);
  border: var(--stroke-1) solid var(--border-strong);
}

/* The frame. .cf-line__frame verbatim — two hairlines and no sides, because
   the value axis is closed at both ends and the category axis is not an axis.

   THE TOP MARGIN IS HEADROOM AND NOT AIR. A block that prints its number puts
   that number above its own head, .cf-plot__val's construction, and a block at
   --v: 1 puts it above the ceiling. --space-8 is the plot's own reservation —
   8 px of drop plus a 12 px number at --leading-normal, plus air — so the
   figure never prints a numeral through its own top hairline.

   THE BOTTOM MARGIN IS NOT THE SAME MEASURE, and the line above used to say it
   was. The mirror job is bigger than the job: a value is one token and cannot
   wrap (.cf-block__val takes white-space: nowrap), while a key is a compound
   German noun that breaks where its author put a &shy; (.cf-block__key takes
   hyphens: manual), so headroom reserves one line and the ground under the
   keys has to reserve two. The two consumers that carry a wrapping key here —
   patterns/landing-page.html and prototypes/evidence-scroll.html, whose keys
   are SICHER-/HEIT and TECHNO-/LOGIE at the phone tier — were each buying the
   second line back with `.lp-ev-card .cf-block__frame { margin-bottom:
   var(--space-12) }` in acts.css, which is the rung being paid for in the
   consumer instead of stated in the component. --space-12 is 8 px of drop, two
   11 px labels at --leading-normal (14.3 px each) and 11.4 px of air; the
   landing card measures exactly what it measured before, and the three figures
   on components/block.html gain the line they would have needed the first time
   one of their categories was named in German.
   → scripts/check-key-line-reserve.py */
.cf-block__frame {
  position: relative;
  width: 100%;
  aspect-ratio: var(--block-ratio);
  margin-block: var(--space-8) var(--space-12);
  border-block: var(--stroke-1) solid var(--border-default);
}

/* THE SCALE — .cf-line__scale, and it is the same instrument for the same
   reason. A label rule is the third of the three kinds of line
   geometry.html#where-lines-go admits: the number, a gap, the hairline out to
   the frame's far edge, and the line and the word are one mark. The ends are
   not steps — the floor and the ceiling are drawn already, as the frame's own
   two hairlines, and named already, by .cf-block__bounds and by the zero this
   figure cannot move.

   THE RULES RUN HORIZONTALLY, AND THAT IS WHY THIS CHART STANDS UP. The
   example a block chart is usually copied from lies on its side, with the
   categories as rows and the value axis along the bottom, and it is the one
   orientation this system cannot draw: the subdivisions of a horizontal value
   axis are VERTICAL rules, and a vertical rule standing between two points
   carries no label at its end and closes nothing, which is the line
   geometry.html calls decoration. Lying the chart down would cost it its
   scale. So the value axis is the vertical one, the blocks stand on the floor,
   and the categories are named under the frame where a key belongs. It is the
   same finding .cf-pie made when it refused the 2:1 ground plane, one figure
   along.

   aria-hidden for .cf-line__scale's reason: the scale is the instrument, every
   value is already in the tree as text, and exposed it would read as one more
   series between the real ones. */
.cf-block__scale {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
.cf-block__step {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(clamp(0, var(--v), 1) * 100%);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  transform: translateY(50%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-label);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  white-space: nowrap;
}
.cf-block__step::after {
  content: "";
  flex: 1 1 auto;
  border-top: var(--stroke-1) solid var(--border-default);
}

/* The categories, sharing the domain. An <ol>: the order of a category axis
   carries meaning often enough — time, stage, size — that the exceptions are
   better served by an ordered list read out of order than by a figure that
   cannot say the order matters.

   It sits UNDER the scale in paint order, which is the opposite of
   .cf-line__set's arrangement and is what a solid face requires: a trace is a
   hairline and the rules may pass beneath it, a block is a surface and a rule
   drawn across its front is a rule inside the object. The blocks occlude the
   scale where they stand and the scale runs on either side of them, which is
   what the drawing means. */
.cf-block__set {
  position: absolute;
  inset: 0 0 0 var(--block-gutter);
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
}
.cf-block__group {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
}

/* What the category is, centred under its own pitch. --text-secondary at
   5.9:1 on CF-Grau, never --text-muted, which is 2.0:1 there and is the
   mistake foundations/colors.html records.

   THE KEY IS BOUND BY ITS PITCH, WHICH IS THE ONE THING .cf-line__key CANNOT
   BE. A point on a line has no width of its own — the domain is continuous and
   the label is centred on a coordinate — so that key is `white-space: nowrap`
   and overhangs into whatever is beside it. A category HAS a width: its own
   share of the frame. So this key takes exactly that share and no more, and a
   name too long for it wraps inside its own ground instead of into its
   neighbour's.

   THE TWO WRAPPING PROPERTIES ARE THE WHOLE RULE, and both are opt-OUTS.
   base.css puts `overflow-wrap: break-word` on every element that holds copy,
   <span> included, as a net against a document that scrolls sideways — and its
   own comment calls it "a NET, not typography", inert until a line genuinely
   cannot be set. Under a key bounded to 31 px on a phone it is not inert at
   all: measured on prototypes/evidence-scroll.html at 375 px, five German
   factor names came out KULT/UR, DATE/N-/SICH/ERHE/IT and TECH/NO-/LOGI/E.
   A word cut at the box edge is worse than a word that overhangs it, and this
   key cannot take the document sideways with it — it is absolutely positioned,
   so it contributes nothing to any track's min-content. So the net comes off
   here and `hyphens: manual` says the same thing from the other side: this
   label breaks where the AUTHOR put a soft hyphen and nowhere else.

   Which makes &shy; the interface. Daten&shy;sicherheit sets as DATEN- /
   SICHERHEIT; without one, a long name simply overhangs its pitch the way the
   plot's keys do, and the fix is a shorter name or fewer categories. */
.cf-block__key {
  position: absolute;
  left: 50%;
  top: 100%;
  inline-size: 100%;
  margin-top: var(--space-2);
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  text-align: center;
  color: var(--text-secondary);
  overflow-wrap: normal;
  hyphens: manual;
}

/* The series inside one category, standing on the floor. --block-fill of the
   pitch, centred in it, which is where the plot's 2u of air comes from. */
.cf-block__bars {
  display: flex;
  align-items: flex-end;
  width: calc(var(--block-fill) * 100%);
  height: 100%;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

/* ONE BLOCK, ONE VALUE. The height is resolved at layout rather than by a
   transform, which is the whole reason this figure is CSS: scaleY on a bordered
   box scales the border with it, and a chart whose contour thickens with its
   own value is a chart that says something it does not mean.

   clamp() is not decoration. --v is registered as a number, which rejects a
   unit but not a 4 and not a -1, and either would send a block through the
   ceiling or below the floor of a frame that is supposed to state its own
   extent. */
.cf-block__bar {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  --block-h: clamp(0, calc(var(--v) * var(--block-grow)), 1);
  /* The same number with zero taken out of it, because it is a DIVISOR down at
     .cf-block__val. A division by zero is invalid at computed-value time, which
     would drop that whole declaration rather than merely misplace it. 0.02 is
     under half a pixel of block at the 320 px frame, so nothing that is drawn
     is affected by the floor. */
  --block-hz: max(var(--block-h), 0.02);
  height: calc(var(--block-h) * 100%);
  border: var(--stroke-1) solid var(--border-strong);
  /* The face is opaque so the block occludes the scale it stands in front of,
     the job .cf-plot__body does for a column on drawn ground. Declared as a
     colour as well as an image because a mode that drops the image still needs
     the occlusion — see the forced-colours block. */
  background-color: var(--surface-page);
}

/* ADJACENT BLOCKS SHARE THEIR CONTOUR, which is .cf-pie's rule for the cuts
   between neighbouring shares and .cf-blog-grid's for the rules between its
   cells: one hairline everywhere rather than a doubled seam at every join.
   Only where both blocks reach — above the shorter one the taller one's edge
   stands alone, which is what the drawing means. */
.cf-block__bar + .cf-block__bar {
  margin-left: calc(var(--stroke-1) * -1);
}

/* ---- ONE RAMP, FOUR SOURCES ----------------------------------------------

   foundations/colors.html states the family in one line: "There is only one
   gradient in this brand: a source falling away to CF-Grau, with Glas where
   the two meet." A categorical figure needs more than one face and may not
   invent a second family, so it does what .cf-plot--fell already did for its
   one cool face — keeps the ramp and swaps the SOURCE. Lime is the light and
   stays the one thing --lit spends; the other three are surfaces the light has
   not reached, which is what the palette already says they are.

   THAT LINE READ "lime falling away to CF-Grau" WHEN THIS BLOCK WAS WRITTEN,
   and the three faces below are half the reason it no longer does. The page
   now counts four sources rather than one, and check-gradient-family.py
   derives them off the leg instead of holding a list of two.
   → foundations/colors.html#sources

   The order is the spectrum's own. --spectrum-stops runs lime, Weiss, Glas,
   Sky, Violett, so Glas -> Sky -> Violett is the cool half read in the order
   the brand already reads it in, and a series set drawn in that order is
   drawn ordinally. THREE IS THE CEILING because the palette has three
   chromatic colours that are not the light, and a fourth series is four
   frames — .tiles with one .cf-block per cell, all on one stated scale — which
   is the answer .cf-line reached at its own ceiling for the same reason.

   THE GLAS RAMP IS THE PIE'S GROUND CODING, transposed. #cf-pie-ground is
   CF-Grau -> Glas, "the bloom's own ramp without its lime leg, renormalised
   over the band", and its job is to say THIS IS A SURFACE. That is exactly
   what an uncoded block is, so the default face is that ramp and the first
   series needs no modifier at all.

   NO GLAS STOP IN THE MIDDLE OF THE OTHER TWO, AND THAT IS MEASURED. The
   obvious construction is the near rake with its source swapped — <hue> 0 %,
   Glas 32 %, CF-Grau 100 % — which is what the plot's violett cap draws. On
   one face that is right. On a SET of faces it is fatal: the leg from Violett
   to Glas turns 102 deg of hue, and the arc waypoint it owes under
   foundations/colors.html#the-arc is #74C1E6, which is Sky's own hue at
   Sky's own chroma. The Violett series would pass through the Sky series on
   its way down, in a figure whose entire premise is that the two are
   different things. So a series ramp is a FALLOFF and not a turn: source to
   CF-Grau, straight, never leaving its own hue. It owes no arc waypoint for
   the reason check-gradient-family.py gives — CF-Grau is achromatic, so there
   is no chord to fall short of and oklab's straight line is already the path.

   THE AXIS POINTS DOWN because the light comes from above: the source sits on
   the block's head, where a cap would be if this were a column, and falls away
   to CF-Grau at the floor. 180 deg is --angle-square turned a quarter, which
   is the same square angle the whole system is drawn on.

   EACH RAMP IS WRITTEN OUT RATHER THAN COMPOSED THROUGH ONE CUSTOM PROPERTY,
   and that is about being checked rather than about style. check-gradient-
   family.py resolves custom properties last-wins across the three stylesheets:
   composed as one --block-ramp restated per modifier, all four gradients would
   expand to whichever ramp was declared last and three of the four would go
   unread by the gate. Four literal declarations are four checked ones. */
.cf-block__bar,
.cf-block__sample {
  background-image: linear-gradient(180deg, var(--cf-glas) 0%, var(--cf-grau) 100%);
}
.cf-block__bar--sky,
.cf-block__sample--sky {
  background-image: linear-gradient(180deg, var(--cf-sky) 0%, var(--cf-grau) 100%);
}
.cf-block__bar--violett,
.cf-block__sample--violett {
  background-image: linear-gradient(180deg, var(--cf-violett) 0%, var(--cf-grau) 100%);
}

/* THE ONE LIT BLOCK, and it is the family's own ramp with nothing swapped:
   lime at the head, Glas at the near rake, CF-Grau at the foot. .material-rake
   at 180 deg, written here rather than composed onto the markup because a
   block is a component's own face and not a surface an author dresses.

   At most one per figure, and it is emphasis and never a category — the rule
   .cf-plot and .cf-pie both state. The caption has to name the block the light
   is on, because a reader who cannot see the lime must still be told which one
   the figure is about.

   NO GLOW. .cf-pie measured what a 12 px glow does to a 1 px contour on a
   small mark: it lays light on both sides of the hairline and the contour goes
   invisible. A block is a smaller mark than a pie's band. Contour before
   fill, here as everywhere. */
.cf-block__bar--lit,
.cf-block__sample--lit {
  background-image: linear-gradient(180deg,
                      var(--cf-lime) 0%,
                      var(--cf-glas) var(--rake-near),
                      var(--cf-grau) 100%);
}

/* THE FAMILY'S OWN PATH, and in CSS it costs one block rather than a hand-
   computed waypoint per drawing. tokens.css leaves a leg in sRGB when the two
   paths agree and names the ones that do not; measured across these four, the
   worst divergence between the sRGB chord and the oklab line is

     lime    -> Glas      0.04430     the leg the family exists to correct
     Violett -> CF-Grau   0.00473
     Sky     -> CF-Grau   0.00316
     Glas    -> CF-Grau   0.00073

   THESE FOUR WERE EACH ABOUT 0.001 HIGH and the sentence under them read the
   old numbers wrongly on top of that. They were 0.04512 / 0.00612 / 0.00399 /
   0.00180, and the first disagreed with tokens.css about a leg the two files
   share: it publishes 0.04430 for lime -> Glas and 0.00073 for Glas ->
   CF-Grau, both of which a straight recomputation reproduces to five decimals.
   A consistent positive offset of that size is what sampling RENDERED 8-bit
   pixels adds to the arithmetic, so the old row was most likely measured off a
   screenshot and never labelled as such. The gate recomputes from the
   arithmetic, so the arithmetic is what this table states.

   TWO OF THE FOUR ARE OVER THE LINE, not one, and that was true of the old
   figures too. tokens.css leaves Glas -> Sky at 0.00148 in sRGB deliberately
   and that is the mark: Glas -> CF-Grau at 0.00073 is under it, and the two
   into CF-Grau from Sky and Violett are two and three times over. Only the
   lime leg would need a waypoint in SVG -- that rule is about lime and not
   about this threshold -- but the reason all four are restated here is the one
   that was always the real one: one @supports block is cheaper than a rule
   about which faces are on the path and which are not. */
@supports (background: linear-gradient(in oklab, red, blue)) {
  .cf-block__bar,
  .cf-block__sample {
    background-image: linear-gradient(180deg in oklab, var(--cf-glas) 0%, var(--cf-grau) 100%);
  }
  .cf-block__bar--sky,
  .cf-block__sample--sky {
    background-image: linear-gradient(180deg in oklab, var(--cf-sky) 0%, var(--cf-grau) 100%);
  }
  .cf-block__bar--violett,
  .cf-block__sample--violett {
    background-image: linear-gradient(180deg in oklab, var(--cf-violett) 0%, var(--cf-grau) 100%);
  }
  .cf-block__bar--lit,
  .cf-block__sample--lit {
    background-image: linear-gradient(180deg in oklab,
                        var(--cf-lime) 0%,
                        var(--cf-glas) var(--rake-near),
                        var(--cf-grau) 100%);
  }
}

/* ---- PARTS OF A WHOLE, PER CATEGORY --------------------------------------

   .cf-pie answers what ONE thing is made of. Nothing answered whether five
   things are made of the same thing, and five pies in a row is the figure that
   asks a reader to compare angles across five rotations — the comparison
   .cf-pie's own header shows an isometric pie cannot survive even once.

   Stacked, the series run bottom to top in legend order: the first item is the
   foot of the stack, which is where a base belongs and where a reader looks
   for it. The shared contour turns with them.

   --v CHANGES MEANING HERE and the caption has to say so. In a grouped figure
   --v is the value against the ceiling; in a stack it is that SEGMENT's share
   of the ceiling, and the segments of one category sum to its total. A stack
   whose segments are authored as running totals draws every segment too tall
   and looks entirely plausible. */
.cf-block--stacked .cf-block__bars {
  flex-direction: column-reverse;
  /* column-reverse packs at the main START, which is the bottom. The stack
     stands on the floor without anything else being said. */
  justify-content: flex-start;
  align-items: stretch;
}
/* FLEX-GROW HAS TO COME OFF, and this is the one place turning the axis costs
   something rather than nothing. `flex: 1 1 0` governs the MAIN axis, so in the
   grouped row it distributes width and leaves the height to --v — which is the
   whole mechanism. Turn the container to a column and the same declaration
   distributes HEIGHT: measured before this rule, Konstanz's three segments
   came out 106.7 px each, an exact third of the frame apiece, on values of 52,
   24 and 12 %. A stacked chart drawn in equal thirds is not a faint fault; it
   is three numbers replaced by one, and it looks entirely like a chart. */
.cf-block--stacked .cf-block__bar {
  flex: 0 0 auto;
  width: 100%;
}
.cf-block--stacked .cf-block__bar + .cf-block__bar {
  margin-left: 0;
  margin-bottom: calc(var(--stroke-1) * -1);
}

/* The number, riding its own block — .cf-plot__val's construction, at
   .cf-plot__val's size and weight. Stated weight rather than inherited,
   because .cf-prose sets running text in Light and a figure that changes
   weight with the block it was dropped into is not a figure the rest of the
   system can match.

   CROPPED UNLESS ASKED FOR, which is .cf-line__set--quiet's rule and not a
   new one: a figure with a scale does not print its values, because the scale
   is what the reader would read them off, and fifteen numerals over five
   categories is not a chart. The text stays in the accessibility tree at full
   strength — crop, not display: none — so what a screen reader is handed is
   every value the figure draws. --read prints one, for the block the caption
   is about.

   overflow: clip and not hidden, for .cf-line's reason: a crop must not become
   a scrollport, and this one would be a scrollport per block. Both
   declarations, so an engine too old for `clip` drops the second and still
   crops. */
.cf-block__val {
  position: absolute;
  left: 50%;
  /* THE LABEL IS A FIXED HEIGHT AND THE FRAME IS NOT, which is .cf-line__val's
     finding and arrives here with the same arithmetic and a different
     denominator. The number needs its own line plus the gap — 0.75rem x 1.3
     over --space-2, about 23.6 px — and that is the same 23.6 px whether the
     frame is 320 px tall on a desktop or 163 px at a 375 px viewport, where it
     is a seventh of the whole range. Measured before this min(): the 88 % on
     the second figure crossed its own ceiling hairline by 4 px at 375 px wide,
     with the frame's top rule running through the numerals.

     THE FRAME'S HEIGHT IS AVAILABLE HERE, IN THE BLOCK'S OWN UNITS. A
     percentage on this element resolves against the block it sits on, which is
     --block-h of the frame — so `100% / var(--block-hz)` is one frame height
     expressed as a percentage of the block, exactly as `100%` is on
     .cf-line__val, and the clamp is the line's clamp with that substitution.
     No new token, no measured length, and it follows the frame at every width.

     max(0px, ...) is the growth. At --block-grow: 0 the block has no height, so
     every percentage on it is zero and the second term is a bare negative label
     height; without the floor the number would sit 24 px UNDER the floor for
     the first frame of every arrival. It rides the block up from the floor
     instead.

     IN A STACK THE CLAMP IS A FLOOR RATHER THAN A CEILING, and that is stated
     rather than fixed. A segment's own --v is its share, not the stack's total,
     so `100% / --block-hz` is still one frame height but the segment's top is
     not --block-h of the way up — it is however far the segments under it
     reached. Nothing here can know that without the total being authored a
     second time, which is the one thing --v's own note forbids. Keep --read for
     the grouped form. */
  bottom: max(0px, min(100%, calc(100% / var(--block-hz)
                                  - var(--space-2)
                                  - var(--text-sm) * var(--leading-normal))));
  width: 1px;
  height: 1px;
  margin-bottom: var(--space-2);
  overflow: hidden;
  overflow: clip;
  clip-path: inset(50%);
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-normal);
  color: var(--text-primary);
  white-space: nowrap;
}
.cf-block__bar--read .cf-block__val {
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;
}

/* THE SERIES NAME, ON EVERY BLOCK, NEVER PAINTED. This is the one piece of
   text in the system that exists only for the accessibility tree, and the
   reason is the reason the legend exists: what tells a sighted reader which
   series a block belongs to is where it stands in its group, and position is
   not something a screen reader can be handed. So each block carries its own
   series name as text, and the tree reads "Rohdaten 62 %" rather than three
   unattributed numbers under one category.

   It is not a second copy of the picture — the crop that .cf-plot forbids is
   a hidden TABLE restating values the drawing already prints. This is the
   label the drawing does not print, and it is the thing that makes the hue
   redundant rather than load-bearing. Never used for anything the figure says
   in visible text. */
.cf-block__series {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  overflow: clip;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The caption carries the message in words and is the figure's accessible
   summary as much as its label. Required, for .cf-plot's and .cf-line's
   reason, and identical to both so the three figures sit the same distance
   off their captions inside an article and outside one. */
.cf-block__caption {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* The blocks rise out of the floor as the figure arrives, scrubbed from scroll
   position rather than played on a timer — the same posture and the same three
   fallbacks as .cf-plot, .cf-line and .cf-iso. No support for view timelines,
   reduced motion and paper all leave --block-grow at its registered 1, which
   is the finished chart. A chart that stays empty until a timeline says
   otherwise is a chart some readers never see.

   ONLY THE BLOCKS MOVE. The frame, the scale, the legend, the keys and the
   bounds are drawn at once and stay drawn: they are the instrument, and the
   measurement is the only thing that arrives. The range ends inside the first
   third of the cover range for .cf-plot's reason — a figure near the foot of a
   short document may never reach the far end of its own range, and a growth
   that finishes at 90 % is a chart that quietly understates every block. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-block {
      view-timeline-name: --cf-block;
      view-timeline-axis: block;
    }
    .cf-block__set {
      animation: cf-block-grow linear both;
      animation-timeline: --cf-block;
      animation-range: cover 5% cover 35%;
    }
  }
}
@keyframes cf-block-grow {
  from { --block-grow: 0; }
  to   { --block-grow: 1; }
}

/* FORCED COLOURS TAKES THE FACES AND KEEPS THE FIGURE, and this component is
   the one place in the system where that is a design requirement rather than
   an accommodation. The mode discards every background image and does not
   repaint a background-colour on a generic box — .cf-gantt's finding — so all
   four ramps come off at once and every block in the figure is painted the
   same. In any chart that coded its series with hue, that is the legend
   destroyed and the figure with it.

   Here nothing is lost, because nothing was carried by the hue. The series
   are still in their order inside every group, the legend still lists that
   order, the contours are borders and are forced to the reader's own ink, the
   scale still subdivides, and every value and every series name is still in
   the tree as text. What the reader loses is the colour, which is what this
   mode is for.

   The occlusion has to be named, though. A background-colour written as a
   custom property is not forced, so --surface-page would go on painting
   CF-Grau over a reader's chosen Canvas; named Canvas, the block still hides
   the scale rules behind it and does so in their palette. Same substitution
   .cf-plot makes for its bodies, same reason.

   The frame and the steps go dashed for .cf-line's reason: one ink means the
   frame's hairlines arrive at the contour's own weight, and the distinction
   this figure spends its whole colour budget on — instrument against datum —
   is redrawn as the one thing border-style can still express. */
@media (forced-colors: active) {
  .cf-block__frame { border-block-style: dashed; }
  .cf-block__step::after { border-top-style: dashed; }
  .cf-block__bar,
  .cf-block__sample {
    background-image: none;
    background-color: Canvas;
  }
}

/* Paper is the same figure and the same reasoning. A print path drops
   background images unless the reader ticks "Background graphics", which is off
   by default in every browser's print dialog, and this component does not ask
   for them back with print-color-adjust: the series are in their order, the
   contours are borders and print, and a chart that demands ink to be readable
   is a chart that is not readable. What is printed is the drawing, the scale
   and the keys. The growth is already frozen at the finished chart there —
   the @supports block above is @media screen — which is checked rather than
   assumed: emulated as print, --block-grow computes 1 and the set's
   animation-name is none. */

/* --------------------------------------------------------------------------
   PIE — parts of one whole, drawn on the one curve this system has
   The plot answers "how do these compare"; nothing answered "what is this made
   of". A share is not a value on a scale, it is a piece of something, and the
   thing a reader has to be able to see is the WHOLE it is a piece of.

   Everything here is a measure the plot already carries:

     the ring       6u across — the same 6u a column at --v:1 stands, so a pie
                    and a plot in one article are drawn at one scale
     the band       1u deep, the unit cube's vertical edge. Outer radius 3u,
                    inner 2u
     the hole       4u across, the plot's column pitch. It is not empty: the
                    whole goes in it, with its unit, which is the one number a
                    pie has always failed to state
     the cuts       one 1 px hairline per share, on the radius. Adjacent
                    shares SHARE their cut, the way .cf-blog-grid's cells share
                    a border, so the finished figure carries one hairline
                    everywhere instead of a doubled seam
     the light      exactly one share may be lit
     the numbers    Geist Mono, tabular — the same treatment as .cf-plot__val

   THE FIGURE IS FACE-ON, AND THAT IS AN ISOMETRIC DECISION RATHER THAN A
   RETREAT FROM ONE. Laying the disc on the 2:1 ground plane is the obvious
   move in this brand and it is the one thing a chart of angles cannot survive.
   The projection is scaleY(0.5), so a ray at t from the horizontal is drawn at
   atan(0.5 * tan t): area ratios survive an affine squash, but the ANGLE a
   reader actually judges a pie by does not. Two true 30 deg shares — the same
   8.3 % of the whole — come out at

     0 -> 30 deg      drawn 16.1 deg      4.5 % of a turn
     60 -> 90 deg     drawn 49.1 deg     13.6 % of a turn

   a factor of 3.05 between two equal numbers, decided by nothing but where on
   the ring they happened to land. Extrude that disc into a cylinder and the
   near shares gain a visible side wall the far ones do not have, which is the
   same lie again with a second mechanism behind it. So the pie stands up: a
   true circle, --radius-full, the single curve foundations/geometry.html
   allows. It is the same finding .cf-plot made about the level row, one
   dimension along.

   NO LEADERS, AND THAT IS THE "WHERE A LINE MAY GO" RULE DOING ITS JOB.
   .cf-annot draws a leader from an anchor to a note, always on the lattice —
   26.57 deg or 63.43 deg, one of the four angles. A share's mid-angle is a
   measurement, so a leader off it would land on none of the four, and a line
   that is neither edge nor division nor label rule is decoration. Each label
   therefore stands against its own arc and nothing else: proximity IS the
   link, which is what direct labelling means. The keys stay one word for the
   same reason .cf-plot's do.

   WHY THE SWEEP IS NOT A DASH. pathLength="1" + stroke-dasharray: 1 is the
   system's draw mechanism (.cf-iso__trace), and it is measured in USER UNITS,
   while vector-effect: non-scaling-stroke measures a dash in SCREEN pixels.
   Put the two on one path and the drawn fraction is off by the viewBox scale:
   measured here at --pie-u 32, a full-circle dash on a non-scaling contour
   drew 0.625 of the ring — 120/192, the scale factor exactly. So the two never
   meet. The contours and the cuts are 1 px hairlines and keep
   non-scaling-stroke and are therefore never dashed; the band is 1u deep and
   scales with the drawing, so it is the one path here that may be. Which is
   also the whole of the motion: the drawing is drawn, and the light arrives.
   -------------------------------------------------------------------------- */

/* The turn a share starts on, as a fraction of the circle. Registered for the
   same two reasons --v is: an unregistered property cannot be interpolated,
   and a typo has to fail toward nothing rather than toward a confident wrong
   answer. --turn: 42% or a stray unit resolves to the initial 0 and the cut
   goes to 12 o'clock, where a reader can see it is wrong. --v is the plot's
   own registration, reused unchanged: in both components it is the value as a
   fraction, and there is no second name for that. */
@property --turn {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
@property --pie-grow {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* THE LIGHT HAS TWO SOURCES AND THEY ARE TWO PROPERTIES, because they answer
   two different questions about the same one lime moment. --pie-home is how
   much of the AUTHORED light a share is holding: 1 on .cf-pie__seg--lit, 0
   everywhere else, and 0 on the lit share itself for exactly as long as the
   pointer is on another one. --pie-point is the light the POINTER has brought,
   which any share may hold and only one ever does.

   Per share, so neither inherits. Registered, so both interpolate — the whole
   of the hover is the two of them crossing, and an unregistered property is a
   token substitution rather than a number and cannot be transitioned at all.

   THEY MEET IN A max(), WHICH IS THE ONE-LIGHT RULE WRITTEN AS ARITHMETIC
   rather than as a note somebody has to keep. Two sources on one share cannot
   add up to more lit arc than the share has, a share the pointer is on cannot
   dip while its authored light hands over, and the figure cannot be made to
   hold two lit arcs by any combination of scroll position and pointer. A sum
   would fail all three. */
@property --pie-home {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
@property --pie-point {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

.cf-pie {
  /* u is the plot's unit at the plot's crossover, so the two figures grow
     together and land at one scale in one article. The FLOOR is the pie's own,
     and it is higher: a plot has to fit five columns and four gaps (18u), a
     pie is 6u of ring plus a label hanging off each side however many shares
     it has. 6u + 2 * (0.5u + --pie-label-w) at u = 24 is 328 px against the
     335 px a 375 px viewport leaves inside its gutters — at a 16 px browser
     default, which is the qualification the sum needs and did not carry: the
     second term is rem and the first two are px, so the figure this floor
     sizes is 368 px at a 20 px default and 408 at 24. The label's own cap
     answers that; the floor here is the 16 px row and stays exact. px rather
     than rem for the same reason the plot's are — see --plot-u. */
  --pie-u: clamp(24px, 3.4vw, 32px);
  /* The ceiling a label's TYPE may occupy: one mono word over one number fits
     inside it at every size, and a key that does not is a key, not a sentence.
     It is not the room beside the ring, which is a second and separate ceiling
     — this one is in rem and moves with the reader, the ring is in px and does
     not, so the room has to be measured against the screen rather than
     declared here. The tier that rides the ring takes the smaller of the two;
     see the note over .cf-pie__label's max-width. */
  --pie-label-w: 5rem;
  /* THE WIDTH AT WHICH THIS FIGURE CHANGES TIER, as a value rather than only
     as a prelude. The query below cannot read it — var() is not allowed in a
     @container prelude, which is the whole reason tokens.css keeps a register
     of literals — so the number is typed there and named here, and the list
     tier's own measure reads the name. Two places, one of them able to say
     what it is. → tokens.css, 21rem. */
  --pie-tier: 21rem;
  /* Where a label's near edge sits: half a cell clear of the rim. The same
     half cell the plot's near edge starts on. */
  --pie-label-r: calc(var(--pie-u) * 3.5);
  /* Declared as well as registered, and this one is not belt and braces. The
     registration below is what makes --pie-grow interpolable; the declaration
     is what makes it RESOLVE where @property is not supported. Without it,
     var(--pie-grow) is invalid at computed-value time, --pie-arc goes with it,
     and `stroke-dashoffset: calc(1 - var(--pie-arc))` falls back to the initial
     0 — which is the whole circle lit. The same shape of failure the plot
     documents on --v, at the other end of the ramp: not a chart that is missing
     something, a chart that is confidently wrong. */
  --pie-grow: 1;
  margin: 0;
  /* THE FIGURE ASKS ITSELF WHETHER THE RING TIER FITS, and the sentence three
     comments up is the arithmetic it asks with: 6u of ring plus half a cell
     and a label cap on each side is 7u + 2 * --pie-label-w, which at the u
     floor and a 16 px root is 328 px. The figure is the container because the
     room a label needs is a fact about the DRAWING's box and never about the
     screen — the same rule .cf-annot-fig states one component over, for the
     same decision. Named, so the tier below cannot be answered by whatever
     container happens to be nearest on a caller's page. → tokens.css, the
     breakpoint register, 21rem.

     AND THE MEASURE IS DECLARED, BECAUSE A CONTAINER HAS TO HAVE ONE. Inline-
     size containment is the other half of `container-type: inline-size`: the
     box stops contributing its contents to its own intrinsic width. This
     figure had been getting its width from its caption's max-content and being
     clamped to the track — which reads like a width and is not one — so under
     `justify-items: center` (acts.css, .lp-ev-card__figure) containment took
     it to 0 px at every viewport and every label on the landing page landed on
     one point. .cf-line and .cf-block already take this declaration from that
     same rule, with the same sentence attached: a frame takes the measure it
     is given or it collapses to min-content. The pie is a frame now too, for
     the stronger reason — a container that asks how much room it has must
     first be given some. Percentage of a grid item resolves against its grid
     AREA, which is definite, so this is the track's own width and the same
     number the caption was borrowing: 214 px at 320, 446 at 1024, 587 at
     1440, unchanged to the pixel. */
  width: 100%;
  container-type: inline-size;
  container-name: cf-pie;
}

/* The drawing's own box, 6u square. Every layer inside it is measured against
   this, so a label's angle and a cut's angle come off one centre. The block
   margins are the room the 12 and 6 o'clock labels hang into.

   THE HEIGHT IS A PSEUDO-ELEMENT AND NOT A HEIGHT, which is what lets the two
   tiers below share one box. Where the labels ride the ring they are out of
   flow and the ring is 6u tall; where they cannot, they flow underneath and the
   ring is 6u of drawing plus however many rows that takes. One declaration
   answers both, and neither tier has to take the other's back. */
.cf-pie__ring {
  position: relative;
  width: calc(var(--pie-u) * 6);
  margin: calc(var(--pie-u) / 2 + var(--space-8)) auto;
  /* THE FIGURE'S PAINT ORDER IS A STACK OF THREE LAYERS AND THIS IS WHAT KEEPS
     THEM INSIDE THE FIGURE. Every z-index below is a number about this ring and
     nothing else; without a stacking context of its own they would be numbers
     about the page, and a pie in a section that had its own layer would sort
     against it. isolation rather than a z-index because the ring is not asking
     for a position in anybody's stack — it is asking not to be in one. */
  isolation: isolate;
}
.cf-pie__ring::before {
  content: "";
  display: block;
  aspect-ratio: 1;
}

/* A <ol>: the shares are a series and their order round the ring carries
   meaning. The top margin is the base tier's — where the labels flow under the
   drawing they need a step of air off it, and where they ride the ring the list
   has no height of its own and that margin would push the whole drawing's
   centre down by 16 px. Taken back below, in the tier that positions them. */
.cf-pie__set {
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
  /* THE LIST IS AS WIDE AS THE FIGURE AND NOT AS WIDE AS THE DRAWING, and
     until the tier below became something a reader sees this was never
     tested. The list is a child of .cf-pie__ring, whose width is the drawing's
     6u — 144 px at the u floor — so each row had 144 px for a key and a
     reading side by side. "STILLSTÄNDE €0,90 Mio." does not fit in that, and
     what a flex row does when it does not fit is wrap: the landing page's two
     longest shares set "Mio." alone on a second line under "€0,90", which is
     the fault 34446b2 fixed on the bounded values — a number parted from its
     unit — reappearing one component over.

     The drawing cannot widen with it (the three paint layers are `width: 100%`
     of this same box with `aspect-ratio: 1`, so a wider ring is a bigger
     circle), so the list steps out of the ring instead: the figure's own
     inline size, pulled back by half the difference on each side. It lands on
     the same measure .cf-pie__caption already takes, which is what the rules
     under each row now line up with.

     CAPPED AT THE TIER WIDTH, because this tier has two entrances. Below
     --pie-tier the cap never binds — the figure is narrower than it by
     definition. It binds in the other entrance, a browser with no CSS trig,
     where the figure may be 1 072 px wide and a legend stretched that far is
     a worse answer than the 192 px one this replaces. Same number as the
     query, by name. */
  inline-size: min(100cqi, var(--pie-tier));
  margin-inline: calc((var(--pie-u) * 6 - min(100cqi, var(--pie-tier))) / 2);
}
.cf-pie__seg:last-child .cf-pie__label { border-bottom: 0; }

/* Top-left and square, rather than inset: 0 — the ring's box is taller than
   the drawing in the tier where the labels flow under it, and a four-sided
   inset would stretch the circle into an ellipse exactly there. */
.cf-pie__draw,
.cf-pie__ink,
.cf-pie__frame {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  /* The outer contour is r = 3u in a 6u box, so it lands ON the edge of the
     viewBox and its hairline would be cropped in half by it. */
  overflow: visible;
}

/* CONTOUR BEFORE FILL, FOR THE WHOLE FIGURE AT ONCE. The brand's first rule is
   the contour, and a ring of banded shares is the one drawing here where honouring
   it cannot be a fact about markup order. Each share's band runs from its own cut
   to the next share's, so a band always ENDS on a hairline belonging to somebody
   else — and a 20-user-unit stroke laid over a 1 px contour wins by 30 to 1. Which
   band wins which seam is then decided by which <li> came later, and the wrap-round
   seam at 12 o'clock is decided the wrong way by construction: the last share's
   band is above the first share's cut, always, in every figure.

   The version this replaces answered the same question with `z-index: 0` on the lit
   share, which worked while exactly one share could ever be filled AND that share
   was authored. Neither is true now: every share carries a body, and the light
   moves to whichever share the reader is pointing at. A per-share number cannot
   express "under all of the ink" when the share is chosen at pointer time.

   So the figure is three layers instead, and each is a kind of mark rather than a
   share: the FILLS, then the INK, then the TYPE. A band cannot reach a hairline
   from a layer below it, whichever <li> it belongs to and whichever share is lit. */
.cf-pie__draw { z-index: 0; }
.cf-pie__ink,
.cf-pie__frame { z-index: 1; }

/* THE INK TAKES NO POINTER. Both of these layers cover the whole drawing, and
   both sit ABOVE the bands — which is the point of them — so an <svg> box that
   hit-tests would seal every share underneath it. Nothing in either layer is
   something to point at: they are the contour and the cuts, and what a reader
   points at is a share. */
.cf-pie__ink,
.cf-pie__frame { pointer-events: none; }

/* 120 x 120 user units = 6u x 6u, so one user unit is u/20 at every size and
   the numbers in the markup are the plot's numbers. The centre is 60,60; the
   outer contour is 60, the inner 40, and the band rides the 50 between them. */
.cf-pie__contour,
.cf-pie__cut {
  vector-effect: non-scaling-stroke;
  stroke-width: var(--stroke-1);
  stroke: var(--border-strong);
  fill: none;
}

/* One share, one <li>. The two numbers that place its cut and its label are
   written once, here, and both are clamped: --v and --turn are registered
   <number>, which rejects a unit but not a 4 or a -1, and either would send a
   cut round the ring more than once. */
.cf-pie__seg {
  --pie-from: clamp(0, var(--turn), 1);
  --pie-share: clamp(0, var(--v), 1);
  /* Where the label stands: the middle of the share's own arc. Derived, so it
     needs no third number in the markup. */
  --pie-a: calc(var(--pie-from) + var(--pie-share) / 2);
  /* Declared as well as registered, and for the reason --pie-grow is: the
     registration is what makes them interpolable, the declaration is what makes
     them RESOLVE where @property is not supported. Without it --pie-arc is
     invalid at computed-value time and the dashoffset falls back to the initial
     0, which on a dash of 1 is the whole ring lit — a chart that is confidently
     wrong rather than one that is missing something. */
  --pie-home: 0;
  --pie-point: 0;
  /* How much of this share's band is lit: the authored light as far as it has
     arrived, or the pointer's, whichever is further round the arc. */
  --pie-arc: clamp(0, calc(var(--pie-share)
                           * max(var(--pie-home) * var(--pie-grow), var(--pie-point))), 1);
  /* THE SHARE IS NOT THE HIT TARGET; THE MARKS INSIDE IT ARE. In the tier that
     rides the ring the <li> is inset: 0 — it covers the whole drawing — so left
     alone the LAST share in the markup would take every pointer event in the
     figure and the other four would be unreachable at every angle. Turned off
     here and turned back on, once, on the band and the label.

     :hover still reaches this element from a descendant that is a target, in all
     three engines: pointer-events decides what is HIT, not which ancestors the
     hover chain runs through. The states below are written against that. */
  pointer-events: none;
}
/* Where the light lives when nobody is pointing at anything. */
.cf-pie__seg--lit { --pie-home: 1; }

/* The cut and both bands start at 12 o'clock in the markup and are turned to
   the share they belong to. Rotation rather than a redrawn path: nothing in the
   drawing is per-value, which is the same promise .cf-plot__draw makes — five
   paths, one viewBox, and the data in two custom properties. */
.cf-pie__cut,
.cf-pie__band,
.cf-pie__light {
  transform-box: view-box;
  transform-origin: 60px 60px;
  transform: rotate(calc(var(--pie-from) * 1turn));
}

/* A BAND IS THE ANNULUS SECTOR — a 1u stroke on the 2.5u circle covers exactly
   the ground between the two contours, so the one filled shape in this component
   still needs no path of its own. It is the only kind of path here that scales
   with the drawing, which is what lets it use the pathLength draw.

   TWO OF THEM PER SHARE, AND THAT IS THE WHOLE OF THE COLOUR CODING.
   .cf-pie__band is the share's BODY and every share carries one; .cf-pie__light
   is the lit face and one share carries it at a time. Neither declares a colour,
   for the reason .cf-plot__cap does not: lime is never a flat fill and a CSS
   gradient cannot paint an SVG, so both are stroked with a paint server the
   figure carries in its own <defs>.

   THE TWO SERVERS ARE ONE RAMP CUT AT TWO PLACES, which is what keeps this a
   coding rather than a palette. Both are .material-bloom — mirrored, centred on
   the ring, the member of the family foundations/colors.html gives to "a body of
   revolution", and the only one that survives being turned: a bloom on the ring's
   own centre looks identical at every --turn, where a linear rake would swing its
   axis off the four angles by whatever the data happened to say.

     #cf-pie-light    CF-Grau -> Glas -> lime. The whole ramp, at the mid rake,
                      with the oklab waypoint the lime leg owes.
     #cf-pie-ground   CF-Grau -> Glas. The SAME RAMP WITHOUT ITS LIME LEG,
                      renormalised over the band — the construction
                      --gradient-foil already makes for .text-foil, and the one
                      leg of the family that needs no waypoint, because
                      Glas -> CF-Grau is dEok 0.00073 and sRGB and oklab agree
                      on it to three decimal places.

   So the difference between a share and the LIT share is the presence of lime,
   not a hue. Every unlit share is painted identically — there is no per-share
   colour to look up, nothing a legend could be made of, and nothing encoded in a
   distinction a third of readers cannot draw. The comparison is still carried by
   the contour and the printed number; what the ground adds is that the ring has
   a body for the light to land on. */
.cf-pie__band,
.cf-pie__light {
  fill: none;
  stroke-width: 20;
  stroke-dasharray: 1;
}

/* MULTIPLIED INTO A LENGTH, AND GECKO IS WHY — both of them, below.
   stroke-dashoffset takes a <length-percentage> or a <number>, and Firefox 153
   accepts the number only as a literal: every unitless calc() on this property —
   calc(1 - var(--x)), and calc(1 - 0.27) with no custom property in it at all —
   is invalid at computed-value time and drops to the initial 0. Which on a dash
   of 1 over pathLength="1" is the WHOLE RING: not a share drawn short, a share
   drawn as every share there is. Measured on the same markup, all three engines:
   unitless computes 0.73px in Chromium and WebKit and 0px in Firefox; * 1px
   computes 0.73px in all three. pathLength normalises against the drawn length
   either way, so the unit costs nothing and buys the engine that would otherwise
   draw the figure confidently wrong. */

/* The body runs the share's whole arc and never moves. It is the share, drawn. */
.cf-pie__band {
  stroke-dashoffset: calc((1 - var(--pie-share)) * 1px);
  /* THE ONE HIT TARGET IN THE DRAWING, AND IT IS GEOMETRY THE FIGURE ALREADY
     HAD. The band is dashed to exactly its own share, and all three engines
     hit-test a dashed stroke on the DRAWN dash and let the gap fall through to
     whatever lies under it — measured on Chromium 151, Firefox 154 and
     WebKit 26.5, at four points on one ring. So pointing at an arc points at the
     share that arc belongs to, with no second shape to keep in step with the
     first and nothing per-value to author.

     `stroke` and not `visibleStroke`, deliberately: `stroke` ignores paint and
     visibility, so the target survives .cf-pie--hollow taking the paint away. */
  pointer-events: stroke;
}

/* The lit face, and the only thing in this figure that moves. */
.cf-pie__light { stroke-dashoffset: calc((1 - var(--pie-arc)) * 1px); }

/* AND IT DOES NOT GLOW, which is not an omission. The spill is a small mark's
   light: --glow-r is 6 px on an icon, 8 px on a relay head, 12 px on a plot
   cap, and .cf-iso__light — the lit FACE of every object in the system — has
   never carried it at all. This band is the largest lit surface in the system,
   and the plot cap's 12 px on it is not a spill but a wash: rendered at
   --pie-u 52 it laid bloom on both sides of the outer contour and the 1 px
   hairline over the lit share went from black-on-lime to invisible, on the one
   arc where the reader most needs to see where the share ends. The brand's
   first rule is the contour, so the light gives way to it. A lit face, like
   every other lit face here. */

/* THE RING WITHOUT ITS BODY. A ground band is a fill, and a fill is the first
   thing this system gives up: on a dark section, over artwork, or beside a
   drawing already carrying the page's material, a pale annulus is one surface
   too many. --hollow takes the paint off the body and changes nothing else —
   the cuts, the contours, the numbers, the lit share and the pointer all behave
   exactly as they do above, and the figure is the one this component shipped
   before it had a body. The path itself stays in the drawing because it is the
   hit target, and pointer-events: stroke does not ask whether the stroke is
   painted. */
.cf-pie--hollow .cf-pie__band { stroke: none; }

/* THE LIGHT FOLLOWS THE POINTER, AND THERE IS STILL ONLY ONE OF IT. Point at a
   share — at its arc, or at its label — and the light moves there; take the
   pointer away and it goes home. The authored share gives its light up for
   exactly as long as the pointer is on a different one, so no reading of this
   figure ever holds two lime moments. :not() is on the second rule because the
   lit share pointed at is BOTH sources at once, and without it --pie-home would
   fall while --pie-point rose and max() would dip to 0.5 halfway through — a
   share that darkens when you point at it.

   IT REVEALS NOTHING, WHICH IS WHY IT SHIPS NO TAB STOP. Every key and every
   number is printed at every moment; what the pointer moves is emphasis, not
   information. That is Mobile's "nothing is hover-only" in its strongest form —
   remove hover entirely and the figure is exactly the figure this component
   shipped — and it is the reason five spans that go nowhere do not get a
   tabindex each. :focus-within is named beside :hover because every state pair
   in this file names both, and here it means a share that a page has wrapped in
   a link lights from the keyboard the same way it lights from the pointer.

   NOT GATED ON (hover: hover). On touch the first tap lights the share and the
   next one moves the light, which is a fair reading of the same gesture and
   loses nothing when it sticks — the state it sticks in is a legal state of the
   figure. */
.cf-pie__seg:is(:hover, :focus-within) { --pie-point: 1; }
.cf-pie__set:has(.cf-pie__seg:is(:hover, :focus-within))
  .cf-pie__seg--lit:not(:hover, :focus-within) { --pie-home: 0; }

/* Motion is opted into here, the way .cf-subdiv opts into its own, rather than
   switched off under a reduce query. --duration-slow is the token's own
   definition — "a reveal, or light crossing a surface" — and light crossing a
   surface is the entire content of this transition. Both properties are
   registered <number>, so what interpolates is a number and what redraws is one
   dashoffset per frame; nothing here reflows, and nothing but the light moves. */
@media screen and (prefers-reduced-motion: no-preference) {
  .cf-pie__seg {
    transition: --pie-home var(--duration-slow) var(--ease-standard),
                --pie-point var(--duration-slow) var(--ease-standard);
  }
}

/* THE BASE TIER: A LEGEND, WHICH IS THE THING THIS COMPONENT EXISTS TO AVOID.
   Placing a label against its own arc is trigonometry and there is no literal
   that stands in for it — .cf-annot can fall back on 26.565deg because a leader
   has two angles, and a share's mid-angle is a measurement. So a browser
   without CSS trig gets the labels as a ruled list under the drawing, in ring
   order, which is a worse figure and a readable one.

   The alternative was to author the ring placement here and let the gated tier
   refine it, and that is precisely the mistake acts.css shipped and
   check-fallback-tier.py now holds: a declaration authored in the base tier is
   a declaration the base tier SHIPS. Authored here, `position: absolute` with a
   transform nobody can compute puts four labels and the total on one point. */
.cf-pie__label {
  display: flex;
  /* AND IT WRAPS AS A ROW, NOT AS A SENTENCE. A key and a reading side by side
     stop fitting eventually — at a 32 px root the landing page's figure is
     150 px wide and "STILLSTÄNDE" alone is most of it — and the two ways of
     not fitting are not equal. Without this the flex line holds and the
     READING breaks inside itself: "€0,90" on one line and "Mio." alone on the
     next, which is the fault 34446b2 fixed on the bounded values, in a
     component that had never been looked at below 21rem because nothing shipped
     it there. With it the value moves WHOLE to a second line under its key —
     the arrangement .cf-pie__whole already uses in the hole — and the pair is
     still one row with one rule under it. Measured on landing-page.html at a
     32 px root: 4 of 4 readings split before, 0 after. */
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: space-between;
  align-items: baseline;
  padding: var(--space-2) 0;
  border-bottom: var(--stroke-1) solid var(--border-default);
}
/* The other half of that: the row may wrap, the reading may not. A figure's
   number and its unit are one token, and this is the FOURTH bounded value in
   the system to say so — .cf-line__val and .cf-block__val have carried the
   declaration all along and .cf-plot__val was given it on 2026-09-01, with the
   census that made the omission visible. Same shape, same argument, and this
   one was invisible for the same reason .cf-plot's was: the tier where the
   value has to share a line with its key did not ship until the query above.
   NOT on .cf-pie__whole-val, which shares the rule below rather than this one:
   that reading stands in a 4u hole it cannot leave, and wrapping is the right
   answer inside it. */
.cf-pie__val { white-space: nowrap; }

/* THE TIER THAT RIDES THE RING. The label stands against its own arc, half a
   cell clear of the rim, and the second half of the transform is what keeps it
   there at every angle: the box is pushed out of its own centre by half its own
   size along the same radius, so its NEAR EDGE lands on the anchor whichever
   way the anchor points. No per-quadrant class, no leader, nothing to author.

   CSS trig has been Baseline since March 2023 — the same capability, and the
   same @supports posture, .cf-annot already takes for the isometric angle.

   AND TRIG IS NOT THE ONLY THING IT NEEDS. It needs the ROOM, and on a phone
   this figure has never had it. The label is `width: max-content` capped at
   --pie-label-w and it hangs off a ring measured in px, so the label grows
   with the reader's text size and the ring does not: at 320 px the landing
   page gives .cf-pie a 214 px box, the 3 o'clock label's far edge lands at
   centre + 3.5u + its own width, and the whole figure paints outside the card
   it stands in — 48 px past on the left and 41 px past on the right at a 16 px
   root, and past the VIEWPORT at 20 px (+6), 24 px (+23) and 32 px (+31 at
   375). A document wider than its screen because the reader enlarged the type
   is the fault scripts/check-text-zoom.py is named after, and its census had
   all four of those cells filed here. Two of them were: 320 x 20 and 320 x 24
   are clean now. The other two, at 200 %, were act 4's line chart standing
   behind the pie — a cell names its widest offender and this label was wider —
   and they are entered under their own cause there.

   A NARROWER CAP CANNOT FIX IT — "Stillstände" is the widest key this figure
   ships and a cap that fits it inside 214 px breaks it mid-word — so the tier
   itself is what does not fit, and the component already owns the answer: the
   base tier above, the ruled list under the drawing, which was written for
   browsers without trig and is just as much the right figure for a box without
   room. So the ring tier asks for both, and where either is missing the same
   legend is what ships. Below 21rem of container the labels flow under the
   ring in ring order; above it they ride the arcs. Measured after, on
   .cf-pie's own box: landing-page 214 / 198 / 182 px at 320 and 268 / 202 at
   375 all take the list, 446 px at 1024 and 587 px at 1440 are unmoved, and
   the census cells are gone.

   THE SAME SHAPE AS .cf-annot's 28rem AND DELIBERATELY NOT THE SAME NUMBER.
   That threshold turns callouts into a legend on the FIGURE's width too, and
   the temptation is to reuse it rather than mint a rung. It is measured on a
   different drawing: 28rem is 448 px, and this component rides the ring
   correctly in the 446 px box the landing page gives it at 1024 — reusing it
   would drop a working desktop figure to the legend to save a register row.
   21rem is 336 px, eight of them clear of the 328 px the arithmetic above
   asks for at a 16 px root, and it scales with the reader because a container
   query's rem — unlike a media query's — resolves against the root's ACTUAL
   font size. Verified in Chromium: a 400 px container answers a 20rem query
   yes at a 16 px root and no at 24 px. */
@supports (top: calc(1px * sin(45deg))) {
 @container cf-pie (min-width: 21rem) {
  /* Both of the base tier's list declarations come off together: the margin
     for the reason below, and the measure because the rows have no measure to
     take — every label is out of flow and placed against its own arc. */
  .cf-pie__set { margin: 0; inline-size: auto; }
  /* NO z-index HERE, AND THE ABSENCE IS THE MECHANISM. A share is a box that
     covers the whole drawing and carries three marks belonging to three
     different layers of it — a fill, a hairline and a label. Give the <li> a
     z-index and it becomes a stacking context, the three marks are sealed into
     it, and the figure sorts by share instead of by kind: the last share's band
     over the first share's cut, at 12 o'clock, in every figure.

     Left at auto the <li> is transparent to the stack, the three layers above
     sort against each other across all five shares, and contour still comes
     before fill — which is the rule this was always trying to keep. It replaces
     `.cf-pie__seg--lit { z-index: 0 }`, a number that could only ever name one
     share and could not name the share a reader is pointing at. */
  .cf-pie__seg {
    position: absolute;
    inset: 0;
  }
  /* AND THE CEILING IS TWO TERMS, BECAUSE ONE OF THEM IS IN THE READER'S UNITS
     AND THE RING IS NOT. --pie-label-w is 5rem and grows with the browser's
     default font size; --pie-u is px and floors at 24, so the ring the label
     hangs off does not grow with it. A cap that moves with the type it is
     capping is not a cap: at a 20 px default the widest key measured 92 px
     against a 100 px ceiling, at 24 px it measured 111 against 120, and the
     ceiling never once bound. What bound instead was the screen. Measured on
     the shipped landing page at 320 px, document scrollWidth against the
     viewport, and the two side labels' own edges:

       browser default   scrollWidth   "Wartung" left   "Stillstände" right
       16 px              320            5 px             308 px
       20 px              326           -13 px            326 px
       24 px              343           -31 px            343 px
       32 px              379           -67 px            379 px

     The right-hand label took the document sideways; the left-hand one fell
     off the left edge, where a negative left costs no scrollWidth at all and
     no overflow gate can see it — at a 24 px default a third of "WARTUNG /
     €0,50 Mio." was simply not on the screen.

     THE SECOND TERM IS THE ROOM, AND IT IS ANGLE-DEPENDENT because the room
     is. A label's near edge stands --pie-label-r from the ring's centre along
     its own radius, so what it spends horizontally is that radius times
     sin(a): everything at 3 and 9 o'clock, nothing at 12 and 6. 50vw is the
     distance from the centre of a centred figure to the screen edge, so
     50vw - --pie-label-r * |sin(a)| is exactly the room the label has left,
     and min() takes whichever ceiling is lower.

     |sin| AS max(x, -1 * x) AND NOT abs(). The tier is gated on trig alone,
     and abs() is four years younger than the sin() this @supports tests — a
     cap that fails to parse is a cap that is not there, in the one tier that
     needs it. max() of a number and its negation is the same value out of the
     capability this rule already has.

     WHY NOT A TIGHTER --pie-label-w. Because the token is the TYPE's ceiling,
     one mono word over one number, and every width it holds at a 16 px default
     is a width the figure was drawn at. A px cap tight enough to fit 320 px at
     a 24 px default is 76 px, which at a 16 px default wraps "DURCHSATZ" at 6
     o'clock — where the room was never the problem — into a third line, out
     through the ring's own bottom margin and over the caption below it.
     Measured after, same page, same widths, and the 16 px row is unchanged to
     the pixel on all four labels at all sixteen widths swept:

       browser default   scrollWidth   "Wartung" left   "Stillstände" right
       16 px              320            5 px             308 px
       20 px              320            0 px             317 px
       24 px              320            0 px             317 px
       32 px              377            0 px             317 px

     The 57 px left at a 32 px default is the act's own — four dense figures
     and their rem insets in a 280 px card — and acts.css records it there.
     → acts.css, .lp-ev-card's note on card 01; scripts/check-text-zoom.py */
  .cf-pie__label {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 2;
    display: block;
    width: max-content;
    max-width: min(var(--pie-label-w),
                   calc(50vw - var(--pie-label-r)
                        * max(sin(var(--pie-a) * 1turn),
                              -1 * sin(var(--pie-a) * 1turn))));
    padding: 0;
    border: 0;
    text-align: center;
    transform:
      translate(calc(var(--pie-label-r) * sin(var(--pie-a) * 1turn)
                     - 50% + 50% * sin(var(--pie-a) * 1turn)),
                calc(var(--pie-label-r) * -1 * cos(var(--pie-a) * 1turn)
                     - 50% - 50% * cos(var(--pie-a) * 1turn)));
  }
 }
}

/* THE SECOND HIT TARGET, AND IT IS THE ONE THE BASE TIER HAS. Direct labelling
   means the name of a share stands beside its arc, so a reader whose pointer is
   already on the word is pointing at the share — and where CSS trig is missing
   and the labels are a ruled list under the drawing, the row IS the only target
   there is. One declaration gives the ring's labels and the list's rows the same
   behaviour, in both tiers, without either knowing about the other. */
.cf-pie__label { pointer-events: auto; }

/* The key and the value, and the treatment is the plot's — a figure's numbers
   look the same wherever the figure is. Stated rather than inherited, because
   .cf-prose sets its running text in Light. */
.cf-pie__key,
.cf-pie__whole-key {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}
.cf-pie__val,
.cf-pie__whole-val {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-normal);
  color: var(--text-primary);
}
.cf-pie__seg--lit .cf-pie__val { font-weight: var(--weight-medium); }

/* The whole, in the hole. This is the ring's entire justification over a
   filled disc: a pie tells a reader what the shares are OF each other and has
   never once told them what the whole is, and 4u of cleared ground in the
   middle of the figure is exactly where that sentence goes. The width is the
   hole less a step of air on each side, so a number that will not fit wraps
   inside the drawing rather than crossing the inner contour. */
.cf-pie__whole {
  position: absolute;
  left: 50%;
  /* 3u, not 50 %. The drawing's centre and the ring box's centre are the same
     point only while the labels are out of flow; where they flow underneath,
     50 % is halfway down the drawing AND the list, and the whole ends up
     sitting on the inner contour. 3u is the centre of the drawing in both
     tiers, because the drawing is 6u and starts at the box's top edge. */
  top: calc(var(--pie-u) * 3);
  z-index: 2;
  width: calc(var(--pie-u) * 4 - var(--space-4));
  margin: 0;
  transform: translate(-50%, -50%);
  text-align: center;
  /* The hole is 4u across and this box is 4u wide, so its CORNERS stand at
     2.24u from the centre and the band starts at 2u: a box that clears the
     inner contour on all four sides still has four points inside the ring. They
     are empty points on an empty box, and left alone they would take the
     pointer off the two shares nearest 12 and 6 o'clock. */
  pointer-events: none;
}
.cf-pie__whole-val { font-weight: var(--weight-medium); }

/* Same rule, same distance and same reason as .cf-plot__caption: --border-default
   rather than the contour black, so the caption's hairline does not read as one
   more edge of the drawing. */
.cf-pie__caption {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* THE DRAWING IS DRAWN, AND WHAT ARRIVES IS THE LIGHT. The ring, every cut and
   every label are at their true values in the first frame and never move; the
   lit band alone fills clockwise along its own share as the reader comes to it.
   That is the rule .cf-iso__light already states in as many words — "lime is
   light: it is the last thing to arrive, and it arrives as a fill coming up,
   not as an object moving" — and it is the only thing here that may move.

   The version this replaces turned the cuts out of 12 o'clock, so the ring
   divided as it arrived. It read well and it put four labels on top of each
   other: a label rides its own share, so at --pie-grow 0 all four stack at 12
   o'clock and they only untangle at about half the range. A chart whose labels
   collide is not an intermediate state, it is a wrong picture — and the
   alternative, holding the labels still while the cuts travel, is worse again:
   every label then names the arc beside it, and for half a second the arc
   beside it is somebody else's.

   Scrubbed from scroll position, with the plot's three fallbacks — no support
   for view timelines, reduced motion and paper all leave --pie-grow at its
   registered 1. Note what the floor is here: at --pie-grow 0 the figure is
   still the finished ring with every share at its real value, and only the
   light is missing. There is no frame of this animation in which the chart
   says something untrue. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-pie {
      view-timeline-name: --cf-pie;
      view-timeline-axis: block;
    }
    .cf-pie__set {
      animation: cf-pie-light linear both;
      animation-timeline: --cf-pie;
      /* The same window .cf-iso__light takes, and for the same reason: the
         light is the last thing to arrive, so it arrives when the figure is
         already up rather than on the way in. */
      animation-range: cover 30% cover 48%;
    }
  }
}
@keyframes cf-pie-light {
  from { --pie-grow: 0; }
  to   { --pie-grow: 1; }
}

/* FORCED COLOURS TAKES THE FILLS AND KEEPS THE DRAWING, which is the answer
   .rule and .text-foil already give one altitude up: CanvasText is what ink
   means in this palette. Two things are true here at once, and only both
   together leave a figure.

   THE INK WAS NEVER IN THE PALETTE. --border-strong is a custom property, and a
   custom property arriving at an SVG `stroke` is not forced. Measured in
   Chromium 151, forced-colors active, prefers-color-scheme dark, this page over
   http: .cf-pie__contour computes rgb(0, 0, 0) against a Canvas of rgb(0, 0, 0).
   Black on black — both contours and every cut, absent rather than faint, in the
   palette where a drawing has nothing else to be read by. It is the finding the
   found state's five rungs carried, in a component that draws instead of marks,
   and it takes the same answer.

   AND A GRADIENT IS NOT IN THE PALETTE EITHER. A paint server is not forced at
   all — `stroke` computes url("#cf-pie-ground") in both palettes — so the bands
   would go on painting their own Glas and lime under a reader's chosen black.
   That is not merely off-palette: it is where the ink would become unreadable
   again the moment it was fixed, a CanvasText hairline laid on a pale annulus.
   So the fills come off, and what is left is exactly .cf-pie--hollow — the
   contours, the cuts, the labels and the whole, in the reader's own ink.

   The lit share loses its light and nothing goes with it. It is the share the
   caption names in words, which is this component's own rule that nothing here
   is encoded in colour alone, and its number keeps the one weight step
   .cf-pie__seg--lit gives it. The pointer still moves a light that is not being
   painted, which costs nothing: it was emphasis, and emphasis is the one thing
   this mode is entitled to take. */
@media (forced-colors: active) {
  .cf-pie__contour,
  .cf-pie__cut { stroke: CanvasText; }
  .cf-pie__band,
  .cf-pie__light { stroke: none; }
}

/* --------------------------------------------------------------------------
   GANTT — a plan on one axis

   The system could draw a VALUE in space (.cf-plot) and it could draw ONE
   thing's way along a line (.cf-progress). A plan is the second of those put
   on a shared axis and stacked: several runs, all measured against the same
   span of time, with one of them where the work has actually got to.

   NOTHING HERE IS A NEW MARK. The run is .cf-progress__rail — solid behind
   the head, the rung's own dash ahead of it, the head itself the lattice cell
   filled with light. The milestone is that cell at rest. The ground under all
   of it is .cf-ground's field with one of its two families of lines left out.
   The layer that names a point on it is .cf-annot, whole and unmodified. What
   the component adds is the axis the marks are measured against, and the one
   division on it that says where today is.

   WHY THE PLAN IS NOT DRAWN ON THE ISOMETRIC PLANE. The arithmetic .cf-plot's
   header sets out, one axis over — and the third figure in this section to
   reach it, after .cf-line declining to draw a trace as a body and .cf-pie
   standing its disc up rather than laying it on the ground plane. Every direction in the ground plane slopes
   on screen at ±26.57°, so a run marched along one of them trades drawn
   length for drawn height, and two spans of equal duration drawn at different
   depths come out different lengths. A chart whose measure depends on where
   it stands is not a chart. Time runs level here; the isometry stays in the
   marks that stand on it.

   THE ONE VERTICAL THAT IS A LINE IN THE LAYOUT IS `heute`.
   foundations/geometry.html#where-lines-go allows a line to be an edge, a
   division or a label rule and calls everything else decoration. The now line
   is a DIVISION in the strictest reading of that page — done on one side of
   it, not done on the other, content on both — and it carries its own mono
   label, so it is a label rule as well. It is therefore black, at full
   weight, once.

   THE TIME GRID IS NOT A LINE AT ALL, IT IS THE FIELD. It draws no edge and
   divides nothing, which is exactly the case foundations/field.html makes for
   the lattice: "a texture on the ground, under everything, at a tenth of an
   ink." So the grid is --field-ink at --field-line, it is the same material
   .cf-ground is, and it inverts with the theme because that token does.

   AND THE 10 % CAP COMES WITH IT, which is why the grid is --field-ink and
   not --border-subtle. tokens.css §8b derives that ceiling from the worst
   thing that has to survive crossing the field — --text-secondary at 14 px on
   CF-Grau under a line: 4.71:1 at 10 %, 4.51:1 at 12 %, 4.15:1 at 16 %.
   --border-subtle is that 12 % row, with no margin left in it, and it is a
   BORDER token besides: reaching for it here would be claiming the grid is a
   line in the layout after arguing that it is not.

   This figure then spends none of the cap. Every word in it is kept off the
   ground on purpose — the keys stand left of the field, the ticks above it,
   `heute` below the last row — so the only things crossing the grid are the
   runs and the leaders, which are ink and not type. The ceiling is inherited
   whole rather than measured again, and it is not being leaned on.

   WHAT --field-edge IS FOR, AND WHY THIS FAMILY DECLINES IT. The half pixel
   of ramp either side of a field line is antialiasing for a DIAGONAL: "a hard
   stop on a diagonal is a hard diagonal edge … the hairline stair-steps."
   These lines are vertical. A vertical has no stair to step, so the stops are
   hard and the line is 1 px wherever the division lands it.

   THE ROW PITCH IS THE LATTICE CELL'S OWN HEIGHT, and that is what puts an
   annotation on the lattice. --field-unit is 96 and the cell is 96 × 48, so a
   row of --space-12 is half a cell: a shallow (2, 1) leader out of a point on
   a run travels one whole cell across and half a row up, and every note in
   the figure lands on the same lattice the runs stand on.

   WHAT IS DELIBERATELY NOT DRAWN IS THE DEPENDENCY. It was built and taken
   out again, and the reason is arithmetic rather than taste. A link between
   two adjacent rows has to drop exactly one pitch, p; the brand sanctions two
   obliques and no others, so the horizontal run is either 2p (96 px, the
   shallow step) or p/2 (24 px, the steep one) — while a finish-to-start
   dependency, which is most of them, has a horizontal run of ZERO. Every
   version of it therefore departs from inside the run above or arrives past
   the mark below, and both of those are a line crossing a body that is not
   its own. The sequence is already in the drawing — the runs' positions and
   the order of the rows — and exactly in the text, in the dates.

   AND THE MARK IT WAS BORROWING BELONGS TO SOMETHING ELSE. A leader in this
   system carries a WORD: .cf-annot's own rule is that the line and the label
   are one mark. An unlabelled leader running between two runs is a leader
   with nothing to say, which is the decoration geometry.html bans by name. So
   the step is spent where it has something to carry — on a note. → .cf-gantt__notes

   .cf-pie ARRIVES AT THE SAME PLACE FROM THE OTHER SIDE, and the pair is worth
   reading together because the two figures fail the leader test for opposite
   reasons. A pie has no leaders because its anchors are MEASUREMENTS — a
   share's mid-angle lands on none of the four sanctioned slopes, so the line
   could not be drawn on the lattice even with a word on it, and proximity is
   made to do the linking instead. A plan's anchors are on the lattice by
   construction — every point of the axis is, at half a row — so here the line
   CAN be drawn and the only question was whether it had anything to say. Both
   components refuse the same thing: a leader that is not carrying a label.

   WHY THIS FIGURE'S AXIS IS REQUIRED AND .cf-line'S IS OPTIONAL, which is the
   question a reader lands on with two charts one section apart. The axis at
   issue here is the DOMAIN, and .cf-line has none at all — it is right for the
   reason it gives: a trace is read AT its points, every one of them is
   labelled where it stands, and a rule standing between two of them would be
   neither an edge, nor a division, nor a label rule. A plan is read the other
   way round. Its subject is EXTENT — where a run starts and where it stops —
   and a length has to be read against something or it is not a length. So the
   two figures answer the same test with opposite hands: the line labels its
   points and needs no frame between them; the plan labels its phases and needs
   the frame, which is why the ground here is the field rather than a set of
   rules, and why the one line drawn in ink is the one that is genuinely a
   division.

   .cf-line__scale is the OTHER axis and disturbs none of that. It measures
   value rather than domain, it is a label rule — a number with a hairline
   running out of it, which is geometry.html's third kind — rather than a rule
   between two points, and it is the alternative to printing values rather than
   an addition to them. A plan has no second quantity to scale: a run is a span
   and not a height. So this figure carries one axis where that one may carry
   two, and the one it carries is the one it cannot do without.

   AND THE OTHER THING THAT ARRIVED WITH IT IS THE LADDER. .cf-line settles by
   measurement that a second series is one rung down the presence ladder and
   not one step down the grey ramp — every grey light enough to read as a
   second series is under the 3:1 a contour owes. That is the same conclusion
   this figure reaches from the other end, and it is why four states of a plan
   are four line types rather than four inks.

   TWO COMPONENTS DRAW TODAY, AND ONLY ONE OF THEM MAY BE LIT.
   .cf-calendar's today is [aria-current="date"] — the found state's lit
   numeral — and its header claims it as "the one lime moment on any page
   carrying it", discharged by arithmetic because a month has exactly one
   today. The claim is sound and this component does not contest it: on a page
   carrying both, the calendar's day keeps the light and the plan's head is
   simply not written. Nothing is lost by that, because the head is emphasis
   and not information — the run's own solid-to-ghost boundary is already
   drawn at --span-done and still says where the work has got to.

   The two drawings differ because the word means two things. In a month today
   is a SLOT, a numeral standing on the ground line, and the light is what
   picks it out of thirty others. In a plan today is a BOUNDARY — done on one
   side, not done on the other — which is a division, and a division is drawn
   as a rule.

   ALL FOUR RUNGS OF THE PRESENCE LADDER ARE IN USE HERE, AND THIS IS THE
   FIRST FIGURE IN THE SYSTEM WHERE THAT IS TRUE. tokens.css §8a says the
   ladder is "only ever legible when the SAME object is drawn more than once,
   which is why no static page has needed it": a plan is one object — a span
   of work — drawn once per phase, at four different degrees of being real, on
   one sheet. It is the case the ladder was written for.
   -------------------------------------------------------------------------- */

/* Registered for the two reasons .cf-plot's are.

   TYPE SAFETY. These numbers are substituted into a left, a width, a
   background-size and a clip inset. Unregistered, `--span-to: 40%` or a
   stray unit makes every one of those declarations invalid at computed-value
   time, and a run with no width declaration is drawn at the full width of its
   track — a plan that is confidently wrong. Registered, a bad value is the
   initial one and nothing is drawn.

   INTERPOLATION. --gantt-draw is one number that the run's clip, the head's
   scale and the leader's scale all read, so the three cannot fall out of step
   with each other.

   THEY INHERIT, and that is the whole authoring story — the one .cf-progress
   already tells about --progress. The dates are written ONCE, on the row,
   beside the key that names them in words, and the marks two levels down read
   them. Non-inheriting, which is the safer-looking default, every mark would
   silently draw at the initial value under a key stating a date. */
@property --span-from { syntax: "<number>";  inherits: true; initial-value: 0; }
@property --span-to   { syntax: "<number>";  inherits: true; initial-value: 0; }
@property --span-done { syntax: "<number>";  inherits: true; initial-value: 0; }
@property --gantt-now { syntax: "<number>";  inherits: true; initial-value: 0; }
@property --gantt-cols { syntax: "<integer>"; inherits: true; initial-value: 6; }
@property --gantt-draw { syntax: "<number>";  inherits: true; initial-value: 1; }

.cf-gantt {
  /* The key column: the phase's name on one line and its dates under it in
     mono. rem, and that is the OPPOSITE of the choice --plot-u makes, for the
     opposite reason — the plot's unit sizes a fixed-count drawing and must not
     grow with a reader's text setting, while this measure is set by two lines
     of TYPE and must. */
  --gantt-key: 12rem;
  --gantt-gap: var(--space-4);

  /* Half the lattice cell, and the pitch the leader is built on. See the
     header. */
  --gantt-row: var(--space-12);

  /* The narrowest a time column may be drawn: five characters of Geist Mono
     at --text-xs, plus air. It is the figure's own floor, and it is here for
     the reason .cf-table's column heads are its one — a figure with no floor
     does not overflow, it COMPRESSES. The axis would simply keep dividing a
     narrower and narrower track, every run would shorten with it, and the
     scroll box that makes the plan readable on a phone would never engage. */
  --gantt-col-min: 3rem;

  /* The mark: the lattice cell at UI scale, one size up from .cf-progress's
     8 × 4 head because it stands in a 48 px row rather than on an 8 px rail. */
  --gantt-node: 0.75rem;

  margin: 0;
}

/* A plan is the one figure that legitimately outgrows its column — the axis
   is data, not layout — so it takes the scroll container .cf-table takes, and
   the tabindex with it: scrollable content with no focusable child is
   unreachable by anything but a mouse. WCAG 2.1.1. It is named by the
   caption, which is the figure's own summary in words. The focus ring comes
   from base.css, which already rings :focus-visible on anything carrying
   [tabindex]. */
.cf-gantt__scroll { overflow-x: auto; }

.cf-gantt__frame {
  /* Derived, not chosen: the key column, the gap, and every time column at
     its own floor. */
  min-width: calc(var(--gantt-key) + var(--gantt-gap)
                  + var(--gantt-cols) * var(--gantt-col-min));
}

/* The axis. Mono labels standing ON their own grid lines with the strong
   hairline running out from under them — the label rule, the third of the
   three uses geometry.html sanctions, and the same mark .cf-section-header is
   made of.

   A label is placed at the line its column BEGINS at, not centred in the
   column. Centred, it names an interval whose two edges the reader then has
   to find; standing on the line, it names the line. */
.cf-gantt__scale {
  display: grid;
  grid-template-columns: repeat(var(--gantt-cols), minmax(0, 1fr));
  margin: 0;
  margin-inline-start: calc(var(--gantt-key) + var(--gantt-gap));
  padding: 0 0 var(--space-2);
  list-style: none;
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
.cf-gantt__tick {
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  /* A label broken over two lines is read as two labels, which is why
     nothing else in this system wraps one either. */
  white-space: nowrap;
}

/* The body is the positioning context for the now line, so that the line
   spans the rows and stops at the axis rule rather than crossing the labels
   over it.

   The padding is the room the now label hangs in, and it is scoped with
   :has() because it is that label's room and nobody else's — a plan with no
   `heute` on it ends at its own last row, and 24 px of reserved air under a
   figure with nothing to put in it reads as a misaligned caption. */
.cf-gantt__body { position: relative; }
.cf-gantt__body:has(.cf-gantt__now) { padding-bottom: var(--space-6); }

.cf-gantt__set {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* THE ROWS ARE NOT RULED, AND THAT IS THE EXACT INVERSE OF .cf-table BY THE
   SAME TEST. A table rules its rows and draws nothing vertical, because there
   the row is the unit of reading and the rule between two of them is a
   division in geometry.html's sense: one thing ends, another begins, content
   on both sides.

   In a plan it is not. Each phase already has a drawn body of its own — the
   run — and what is on both sides of a line between two rows is the same
   plan, at the same date, saying nothing. That is the definition of the
   decoration the geometry chapter bans, and drawn against the field's
   verticals it makes the one thing .cf-table's header names as the failure to
   avoid: a grid of boxes, which says spreadsheet. So this figure draws its
   verticals and rules nothing, and the table rules its rows and draws no
   vertical, and both follow from the same three-way test.

   THE HEIGHT IS FIXED AND NOT A MINIMUM. The pitch is the leader's drop, so a
   row that grows takes the next row's centre line with it and the step stops
   landing on it. Keys are short here for the reason a table's column heads do
   not wrap. */
.cf-gantt__row {
  display: grid;
  grid-template-columns: var(--gantt-key) minmax(0, 1fr);
  column-gap: var(--gantt-gap);
  align-items: center;
  height: var(--gantt-row);
}

/* The key is the row's subject — content, in the body face at body size, the
   same call .cf-table makes for a th[scope="row"]. Uppercasing it would shout
   a phase name and spend the mono face on the one thing in the row that is
   prose. */
.cf-gantt__key {
  min-width: 0;
  font-size: var(--text-md);
  line-height: var(--leading-normal);
  color: var(--text-primary);
}

/* THE DATES ARE TEXT, AND THEY ARE THE ONLY COPY OF THEMSELVES. The drawing
   is aria-hidden and adds nothing this line does not already say, so the
   figure reaches the accessibility tree as an ordered list of phases with
   their dates — no visually hidden table, nothing that can drift out of step
   with the picture. It is also where the ladder is stated IN WORDS, so that
   `geplant` and `läuft` are not carried by a dash pattern alone. */
.cf-gantt__when {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-label);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

.cf-gantt__track {
  position: relative;
  min-width: 0;
  height: 100%;
}

/* The ground: one family of the field's two, at the field's ink and the
   field's weight, its period the time column. Painted by a ::before that
   comes before every mark in tree order, so the marks paint over it without
   a z-index — the same reason .cf-plot__draw needs none.

   The second layer is the horizon. Every column carries a line at its start,
   which leaves the far end of the axis — the one edge of the plan that is not
   the start of anything — as the only division on it nothing draws. */
.cf-gantt__track::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(90deg, var(--field-ink) 0 var(--field-line), transparent var(--field-line)),
    linear-gradient(90deg, var(--field-ink) 0 var(--field-line), transparent var(--field-line));
  background-size:
    calc(100% / var(--gantt-cols)) 100%,
    var(--field-line) 100%;
  background-position: left top, right top;
  background-repeat: repeat-x, no-repeat;
}

/* THE RUN. Two layers and no overlay, which is .cf-progress__rail's exact
   construction: the part of the phase that is done is one solid tile sized to
   --span-done and pinned left, the part that is not is the rung's own dash
   sized to the remainder and pinned right. --gantt-rung is what a modifier
   changes, so the ladder is one declaration and not a fork per state.

   clamp() because --span-done is a registered number, which rejects a unit
   but not a 2 or a -1, and either would draw a head off the end of its own
   run.

   2 px, and it is the only mark in the figure at that weight. The run is what
   the figure is about and it has to be read against a field of hairlines;
   base.css already has .rule--2 for exactly this — a heavier rule for a
   heavier statement. Everything else here is 1 px.

   IT DRAWS OUT OF ITS OWN START ON A CLIP AND NOT ON A scaleX. Scaling a box
   scales the dash period inside it, so a 2-1 rung would arrive through every
   ratio except its own — the same fault check-grow-origin.py describes for a
   dashed stroke under a scale, in the one medium where it is not fixable by
   a vector-effect. */
.cf-gantt__bar {
  --run: clamp(0, var(--span-done), 1);
  --gantt-rung: linear-gradient(90deg, var(--border-strong) 0 100%);
  position: absolute;
  top: 50%;
  left: calc(var(--span-from) * 100%);
  width: calc((var(--span-to) - var(--span-from)) * 100%);
  height: var(--stroke-2);
  transform: translateY(-50%);
  background-image:
    linear-gradient(90deg, var(--border-strong) 0 100%),
    var(--gantt-rung);
  background-size:
    calc(var(--run) * 100%) 100%,
    calc((1 - var(--run)) * 100%) 100%;
  background-position: left center, right center;
  background-repeat: no-repeat;
  clip-path: inset(0 calc((1 - var(--gantt-draw)) * 100%) 0 0);
}

/* The ladder, along one run. The three dashed periods are the ones base.css
   already paints as .rule--dashed-*: 4/2, 2/4 and 1/4, on periods of 6, 6 and
   5 px. They are written out again rather than reached for as a utility class
   because .rule owns `background` outright, and this run also carries a
   second layer, a head and a clip.

     (none)      solid   the phase as committed — dates fixed
     --planned   2-1     agreed, not fixed
     --proposed  1-2     under discussion
     --live      1-4     ahead of the head only: the part that has not
                         happened yet, which is --presence-absent doing
                         exactly the job it does on .cf-progress__rail */
.cf-gantt__row--planned .cf-gantt__bar {
  --gantt-rung: repeating-linear-gradient(90deg, var(--border-strong) 0 4px, transparent 4px 6px);
}
.cf-gantt__row--proposed .cf-gantt__bar {
  --gantt-rung: repeating-linear-gradient(90deg, var(--border-strong) 0 2px, transparent 2px 6px);
}
.cf-gantt__row--live .cf-gantt__bar {
  --gantt-rung: repeating-linear-gradient(90deg, var(--border-strong) 0 1px, transparent 1px 5px);
}

/* THE MARK: the lattice cell, drawn as a BORDER under --iso-transform rather
   than as a clip-path polygon. That is .cf-annot::before's call and it is
   made for the same reason — forced colours discards a background-color and
   maps a border colour into its own palette, so a rhombus that is a border
   survives that mode and a rhombus that is a clip vanishes from it.

   It scales in with the figure rather than drawing out: a point has no
   length to draw along, and a cell coming up from nothing IS the presence
   ladder's own figure, "solid and small at the left". */
.cf-gantt__head,
.cf-gantt__mark {
  position: absolute;
  top: 50%;
  width: var(--gantt-node);
  height: var(--gantt-node);
  border: var(--stroke-1) solid var(--border-strong);
  transform: translate(-50%, -50%) var(--iso-transform) scale(var(--gantt-draw));
}

/* A milestone is a phase of no duration, so it is the mark alone on its own
   row, and it stands at --span-to. There is deliberately no --mark-at beside
   it: a milestone's date would then be written under a second name, a leader
   drawn out of that row would still read --span-to, and the two would be free
   to disagree about when the same thing happens. */
.cf-gantt__mark { left: calc(var(--span-to) * 100%); }

/* The head stands where the work has actually got to, which is not the same
   fact as where today is — a phase can be behind, and the whole reason this
   mark and the now line are two marks is that the distance between them is
   the thing worth reading.

   AND IT IS THE FIGURE'S ONE LIME MOMENT. Lime is light, so the cell carries
   the near rake and not a flat fill — foundations/colors.html#lime-is-never-
   flat — the same ramp .cf-annot--lit's anchor takes, for the same reason: a
   lattice cell is a flat face. */
.cf-gantt__head {
  left: calc((var(--span-from)
              + (var(--span-to) - var(--span-from)) * clamp(0, var(--span-done), 1)) * 100%);
}

/* The lit rung, for the head and for a milestone that is the figure's light
   instead of it. One or the other and never both — the lime budget is one
   mark per screen, and this figure is competing with everything else on the
   page for it. */
.cf-gantt__head,
.cf-gantt__mark--lit {
  background-image: var(--gradient-light);
  --glow-r: 8px;
}

/* THE ANNOTATION LAYER, AND IT IS NOT A NEW COMPONENT. A plan is a drawing,
   and the system already owns the layer that names a part of one: .cf-annot,
   an anchor on the lattice, a leader that is one lattice step, and a word
   standing on its own rule. All this element does is hand that layer the
   plan's coordinates — it covers the plot area exactly, so --annot-x is a
   fraction of the axis and --annot-y is a length down the rows,
   calc(--gantt-row * (row + 0.5)).

   THE UNIT IS THE PLAN'S OWN, AND THIS IS WHAT THE ROW PITCH IS FOR.
   --annot-u is half a row. The row pitch is half the lattice cell, so a
   shallow (2, 1) step out of an anchor travels one whole cell across and half
   a row up and comes down on the lattice the runs are already standing on.
   The anchor cell takes --gantt-node, the rhombus the head and the milestones
   are already drawn at, so an annotation's mark and the plan's marks are one
   size rather than two.

   THE TIMELINE IS KILLED AND THE VALUE IS TAKEN FROM THE PLAN'S OWN DRAW.
   .cf-annot-set animates --annot-draw on `animation-timeline: view()`, which
   resolves against the nearest scrollport — and inside this component that is
   .cf-gantt__scroll, whose block axis never scrolls (overflow-x: auto leaves
   overflow-y computing to auto, which is a scroll container in both axes).
   A view timeline against a box that does not scroll is precisely the .cf-hero
   defect check-overflow-clip.py exists for: it resolves, it reports a
   progress, and the progress does not move. So the animation is cancelled —
   which this file has already established is the only way to get the property
   back, since a custom property cannot override an animation — and the value
   is derived from --gantt-draw instead. 3x - 2 puts the labels in the last
   third of the plan's own draw, which keeps the order .cf-annot states in
   prose: the drawing is finished before it is labelled.

   THE CONTAINER IS DELIBERATELY NOT NAMED cf-annot-fig. That name switches
   the layer to its legend fallback under 28rem, and the plot area is under
   28rem exactly when the plan is scrolling — which is when the callouts are
   correct and a static legend inside an absolutely positioned layer is not.
   A plan answers a narrow screen by scrolling. It does not fold. */
.cf-gantt__notes {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--gantt-key) + var(--gantt-gap));
  right: 0;
}
.cf-gantt__notes .cf-annot-set {
  --annot-u: calc(var(--gantt-row) / 2);
  --annot-node: var(--gantt-node);
  --annot-gap: calc(var(--gantt-node) / 2);
  --annot-draw: clamp(0, calc(var(--gantt-draw) * 3 - 2), 1);
  animation: none;
}

/* `heute`. See the header for why this is the one vertical here that is a
   line in the layout, and therefore the one drawn in ink rather than in
   material.

   THE LABEL HANGS BELOW THE LAST ROW RATHER THAN ABOVE THE AXIS. The scale's
   labels are placed by the axis and this one is placed by the data, so above
   the axis the two collide at whatever date the plan happens to have reached
   — and a figure whose top edge is legible only in some months is not one. */
.cf-gantt__now {
  position: absolute;
  top: 0;
  bottom: var(--space-6);
  left: calc(var(--gantt-key) + var(--gantt-gap));
  right: 0;
  pointer-events: none;
}
.cf-gantt__now::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--gantt-now) * 100%);
  width: var(--stroke-1);
  background: var(--border-strong);
}
.cf-gantt__now-label {
  position: absolute;
  top: 100%;
  left: calc(var(--gantt-now) * 100%);
  transform: translateX(-50%);
  padding-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-normal);
  white-space: nowrap;
  color: var(--text-primary);
}

/* The caption carries the message in words, so it is the figure's accessible
   name and its title at once — which is why it is a figcaption named by the
   scroll region above it and not a heading floating near the block. Same
   treatment as .cf-plot__caption, so a plan and a plot sit the same distance
   off their captions. */
.cf-gantt__caption {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--stroke-1) solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* The plan draws itself onto a ground that is already there: the field, the
   axis and the now line do not arrive, because they are not the data. Same
   posture and the same three fallbacks as .cf-plot and .cf-iso — no support
   for view timelines, reduced motion, and paper all leave --gantt-draw at its
   registered 1, which is the finished plan. A chart that stays empty until a
   timeline says otherwise is a chart some readers never see. */
@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  @media screen and (prefers-reduced-motion: no-preference) {
    .cf-gantt {
      view-timeline-name: --cf-gantt;
      view-timeline-axis: block;
    }
    /* On the BODY and not on the row list, because the annotation layer is
       the row list's sibling and has to read the same number. */
    .cf-gantt__body {
      animation: cf-gantt-draw linear both;
      animation-timeline: --cf-gantt;
      /* Ends early, for .cf-plot's reason: a figure near the foot of a short
         document may never reach the far end of its own cover range, and a
         plan that finishes drawing at 90 % is a plan that understates every
         phase in it. */
      animation-range: cover 5% cover 35%;
    }
  }
}
@keyframes cf-gantt-draw {
  from { --gantt-draw: 0; }
  to   { --gantt-draw: 1; }
}

/* Paper keeps the plan and loses the ground, the same call .cf-ground makes:
   a printer renders the field as a grey mesh over the copy. */
@media print {
  .cf-gantt__track::before { display: none; }
}

/* Forced colours discards every background image and does not repaint a
   background-colour on a generic box, so the field, the run and the now line
   all disappear and the two marks — which are borders — do not. Each is
   redrawn as the one paint the mode keeps, exactly as .rule is.

   The dash RATIOS are lost, because border-style cannot express them, and the
   ladder survives as the three-way distinction .rule's own fallback keeps:
   solid, some dash, finer dash. The live run goes back to solid rather than
   to a dash — the part of it behind the head is real work, and its head is
   still drawn. */
@media (forced-colors: active) {
  /* The draw is frozen at the finished plan, for both halves of this figure
     and for the reason .cf-annot's own forced-colours block gives: a mode that
     has asked for certainty gets the finished drawing rather than a frame of
     one. It is the stronger claim here than there — a half-clipped run is not
     a faint run, it is a plan stating dates it does not have. */
  .cf-gantt__body,
  .cf-gantt__notes .cf-annot-set {
    animation: none;
    --gantt-draw: 1;
    --annot-draw: 1;
  }
  .cf-gantt__track::before { display: none; }
  .cf-gantt__bar {
    background: none;
    height: 0;
    border-block-start: var(--stroke-2) solid CanvasText;
  }
  .cf-gantt__row--planned  .cf-gantt__bar { border-block-start-style: dashed; }
  .cf-gantt__row--proposed .cf-gantt__bar { border-block-start-style: dotted; }
  /* The lit rung has nothing to give this mode: the ramp is a background
     image it discards, and a lime spill around a mark whose palette the
     reader has chosen is the one thing the mode exists to stop. Both fall
     away and the contour is left, which is what the mark was already.

     THE SPILL IS NOT TURNED OFF HERE, and the `filter: none` that used to
     stand in this rule never was either: --glow-* is composed at the foot of
     this file, at the same specificity and 900 lines later, so source order
     decided and the lime went on spilling under a forced palette for as long
     as this rule has existed. It is switched off beside the composition now,
     with every other member of it. */
  .cf-gantt__head,
  .cf-gantt__mark--lit { background-image: none; }
  .cf-gantt__now::before {
    width: 0;
    background: none;
    border-inline-start: var(--stroke-1) solid CanvasText;
  }
}

/* --------------------------------------------------------------------------
   SUBDIVISION FIELD — "Raster > Teilungsraster", made interactive
   The manual's own claim for this system is the one part of it the web
   implementation never built: "kann als Konzept für interaktive UI-Elemente
   oder Menüstrukturen genutzt werden." Until now the subdivision was a
   layout — .subdivide in base.css, the blog grid on top of it — and the
   hierarchy in it was authored once and then fixed. Which article is
   largest is a fact about the archive.

   A field is the same series with the hierarchy handed to the reader. The
   halving series never changes: 1/2, 1/4, 1/8 … with the last two equal, so
   it sums to exactly one. What changes is who stands at its head. Point at
   a cell, or tab to it, and it takes the half; every other cell keeps its
   place in the row and continues the series behind it.

   Three properties fall out of that rule, and all three are the reason it is
   this rule and not a rotation of the series:

   1. THE CELLS NEVER REORDER. Reading order is markup order, always.
   2. THE HEAD ONLY EVER GROWS AROUND THE POINTER. Cell k at the head takes
      s1 while cells 1..k-1 take s2..sk; since the series is non-increasing,
      sum(s2..sk) <= sum(s1..sk-1), so the head's leading edge moves left and
      never right. Its box strictly contains the box it had. A layout that
      moves under the pointer that summoned it is a trap; this one cannot.
   3. THE RULES AHEAD OF YOU DO NOT MOVE. Cells after the head keep their
      resting weights exactly, so every rule from the head rightwards stands
      where it stood. Only the rules behind your attention slide.

   Nothing is authored per cell. Depth comes from the DOM via
   :has(> .cf-subdiv__cell:nth-child(n):last-child), so --subdiv-n cannot
   drift from the number of cells the way a hand-written count does; index
   comes from :nth-child; the head is a single inherited number.
   Supported depth is 3 to 6, the same range .subdivide covers.
   -------------------------------------------------------------------------- */
.cf-subdiv {
  /* The resting head is the first cell — which makes the resting field the
     manual's pure plate, drawn exactly: 1/2, 1/4, 1/8, 1/8. */
  --subdiv-h: 0;
  --subdiv-n: 5;
  --subdiv-height: 14rem;
  display: flex;
  min-height: var(--subdiv-height);
  container-type: inline-size;
  /* Three edges here, the leading rule of every cell on the cell — the same
     construction as .cf-blog-grid and .cf-team-grid, and for the same reason:
     the fourth edge would lay a second hairline over the first cell's rule
     and the field would read 2 px on one side and 1 px on the other three. */
  border-top: var(--stroke-1) solid var(--border-strong);
  border-right: var(--stroke-1) solid var(--border-strong);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}

.cf-subdiv__cell {
  --subdiv-i: 0;
  /* --subdiv-r is the cell's rank in the series: 0 at the head, then 1, 2, …
     in markup order, skipping the head. Written without a conditional
     because CSS has none — clamp(0, x, 1) is the sign of x for integers, so
     --after is 1 when this cell precedes the head and --eq is 1 only when
     this cell IS the head, which zeroes the rank. */
  --subdiv-after:  clamp(0, var(--subdiv-h) - var(--subdiv-i), 1);
  --subdiv-before: clamp(0, var(--subdiv-i) - var(--subdiv-h), 1);
  --subdiv-eq:     calc(1 - var(--subdiv-after) - var(--subdiv-before));
  --subdiv-r:      calc((var(--subdiv-i) + var(--subdiv-after)) * (1 - var(--subdiv-eq)));

  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: stretch;
  gap: var(--space-3);
  /* The cell itself carries no padding, and that is arithmetic rather than
     taste. Padding on a flex item is added OUTSIDE its flex-basis, so 12 px
     of it on five cells takes 60 px off the top before the series divides
     anything: measured at a 911 px field the ranks came out
     437.5 / 224.8 / 118.4 / 65.2 / 65.2, where 437.5 is not twice 224.8 and
     the rules do not fall on the halves the plate draws. The padding lives on
     the tick and the body instead, inside the box, where the series cannot
     see it. */
  padding: 0;
  /* The fallback, and it is the manual's other form rather than a
     degradation: equal columns, hierarchy carried by the copy alone. Any
     browser without :has() or without pow() lands here. */
  flex: 1 1 0;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* The contour. Every cell draws its own leading rule, so the first cell's
   rule is the field's left edge. */
.cf-subdiv__cell::before,
.cf-subdiv__cell::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: var(--stroke-1);
}
.cf-subdiv__cell::before { background-color: var(--border-strong); }

/* The light. The head's contour is the foil rather than ink — the light
   layer entering the contour layer instead of sitting on top of it, which
   is the one place in the six-layer stack they are allowed to meet. It is
   the foil's shadow half because the field sits on the page wash, where the
   lit half measures 1.1–1.5:1; the ink half is the same three hues moved
   into the 800 band and clears AA. Never lime: #E1FF00 on CF-Grau is 1.4:1
   and a 1 px contour has to clear 3:1 on its own.

   Opacity rather than a selector, so the light travels with the head and
   arrives by transition instead of by repaint. */
.cf-subdiv__cell::after {
  background-image: var(--gradient-foil-ink);
  opacity: calc(1 - clamp(0, var(--subdiv-r), 1));
}

/* The tick names its cell at every width — it is the one thing a 1/16 cell
   still has room for, and it is how the application plate labels a column
   too. justify-self: start for the reason written on .cf-blog-axis__tick:
   under vertical-rl a stretched box starts its text at its own right edge,
   which would set the label a whole cell away from the rule it belongs to. */
.cf-subdiv__tick {
  writing-mode: vertical-rl;
  justify-self: start;
  align-self: start;
  padding: var(--space-3) 0 var(--space-3) var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* Title at the head of the cell, meta at its foot — which is how the
   application plate sets a column: headline against the top rule, author and
   date against the bottom one. */
.cf-subdiv__body {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-4);
  min-width: 0;
  overflow: hidden;
  overflow: clip;
  padding: var(--space-3) var(--space-3) var(--space-3) 0;
}
/* Copy arrives in the same series the widths do. The head carries all of it;
   rank 1 keeps its title; from rank 2 the tick is the whole cell. Opacity,
   not display — the copy stays in the accessibility tree at every rank, so
   the link's name is the headline whether or not the headline is drawn. */
.cf-subdiv__title {
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  color: var(--text-primary);
  opacity: calc(1 - clamp(0, var(--subdiv-r) - 1, 1));
}
.cf-subdiv__meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--text-secondary);
  opacity: calc(1 - clamp(0, var(--subdiv-r), 1));
}

/* The system, gated on both capabilities it needs. Without either, every
   rule above still applies and the field is the even form. */
@supports selector(:has(*)) and (flex-grow: pow(2, 3)) {
  /* Depth, read off the DOM rather than authored. */
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(3):last-child) { --subdiv-n: 3; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(4):last-child) { --subdiv-n: 4; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(5):last-child) { --subdiv-n: 5; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(6):last-child) { --subdiv-n: 6; }

  .cf-subdiv__cell:nth-child(1) { --subdiv-i: 0; }
  .cf-subdiv__cell:nth-child(2) { --subdiv-i: 1; }
  .cf-subdiv__cell:nth-child(3) { --subdiv-i: 2; }
  .cf-subdiv__cell:nth-child(4) { --subdiv-i: 3; }
  .cf-subdiv__cell:nth-child(5) { --subdiv-i: 4; }
  .cf-subdiv__cell:nth-child(6) { --subdiv-i: 5; }

  /* The head. Pointer and keyboard reach it by the same rule, so tabbing
     through the field opens each cell exactly as pointing at it does. */
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(1):is(:hover, :focus-visible)) { --subdiv-h: 0; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(2):is(:hover, :focus-visible)) { --subdiv-h: 1; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(3):is(:hover, :focus-visible)) { --subdiv-h: 2; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(4):is(:hover, :focus-visible)) { --subdiv-h: 3; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(5):is(:hover, :focus-visible)) { --subdiv-h: 4; }
  .cf-subdiv:has(> .cf-subdiv__cell:nth-child(6):is(:hover, :focus-visible)) { --subdiv-h: 5; }

  /* The series itself. 2^(n-1-r), with the last two ranks tied by min() —
     that tie is what makes the fractions sum to exactly one rather than to
     one minus the tail. */
  .cf-subdiv__cell {
    flex-grow: pow(2, calc(var(--subdiv-n) - 1 - min(var(--subdiv-r), var(--subdiv-n) - 2)));
  }
}

/* Motion is opted into, the way the rest of the system does it, rather than
   switched off under a reduce query. flex-grow is a number and interpolates;
   nothing here animates to or from an intrinsic size, which is deliberate —
   interpolate-size and calc-size() are still Chromium-only and a field whose
   proportions only work in one engine is not a system.

   Transitioning flex-grow reflows the field each frame. It is affordable
   because the field is one flex line of at most six items with no text
   reflow inside them: the copy is faded, not re-wrapped. This is not a
   licence to transition flex-grow on a page-level layout. */
@media screen and (prefers-reduced-motion: no-preference) {
  .cf-subdiv__cell { transition: flex-grow var(--duration-base) var(--ease-standard); }
  .cf-subdiv__cell::after,
  .cf-subdiv__title,
  .cf-subdiv__meta { transition: opacity var(--duration-base) var(--ease-standard); }
}

/* Below the width where a 1/16 cell stops being a cell, the field folds the
   way .subdivide does — and asks its own width, not the viewport's, because
   the same field is comfortable in 900 px of page and impossible in 900 px
   of viewport behind a 272 px sidebar. Stacked, every cell is equal, every
   cell is open, and the leading rule becomes the top rule, so the first
   cell's rule is still the field's own edge. */
@container (max-width: 44rem) {
  .cf-subdiv {
    flex-direction: column;
    min-height: 0;
    border-top: 0;
    border-left: var(--stroke-1) solid var(--border-strong);
  }
  .cf-subdiv__cell {
    flex: 0 0 auto;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
    padding: var(--space-4);
  }
  .cf-subdiv__body { padding: 0; gap: var(--space-2); }
  .cf-subdiv__cell::before,
  .cf-subdiv__cell::after {
    inset-inline: 0;
    inset-block: 0 auto;
    width: auto;
    height: var(--stroke-1);
  }
  .cf-subdiv__tick {
    writing-mode: horizontal-tb;
    padding: 0;
  }
  .cf-subdiv__title,
  .cf-subdiv__meta { opacity: 1; }
}

/* Paper and forced colours get the whole field: no weighting, no fading. */
@media print {
  .cf-subdiv { display: block; min-height: 0; }
  .cf-subdiv__title,
  .cf-subdiv__meta { opacity: 1; }
}
@media (forced-colors: active) {
  .cf-subdiv__cell { flex: 1 1 0; }
  .cf-subdiv__cell::after { display: none; }
  .cf-subdiv__title,
  .cf-subdiv__meta { opacity: 1; }
}

/* --------------------------------------------------------------------------
   ARRIVAL — the state a thing is in before it is here

   Every other state in this system is a fact about content: an error, an
   empty filter, a page that does not exist. This one is a fact about TIME,
   and the brand's first rule is already a statement about time — contour
   before fill is the order a drawing appears in, not merely the order it is
   painted in. A thing that has not arrived is therefore the thing itself,
   drawn and unfilled, one rung down the presence ladder. It is not a grey
   slab. A grey slab is an object that will never arrive.

   Three parts, and the third is the one that carries the brand:

     the ladder   the placeholder is the object's own contour at
                  --presence-absent (1-4). When the content lands it does
                  not fade in over the ghost — the ghost is replaced by the
                  object at --presence-present, which is the same drawing
                  one rung up. See tokens.css 8a.
     the light    one band crossing the ghost at --angle-b, above the
                  contour, because the light layer is layer 5 and the
                  contour is layer 4. This is the one place in the system a
                  continuous loop is honest: something genuinely is still
                  happening. Capped at --arrive-light-peak; the arithmetic
                  is in tokens.css.
     the word     a role="status" line beside it. The ghost is decorative
                  and hidden from assistive technology; a shape nobody can
                  see is not a loading state on its own.
   -------------------------------------------------------------------------- */

/* Registered so the band can travel. Unregistered it would jump between
   computed values or, more likely, invalidate the gradient outright — the
   same reasoning as --v on the plot. */
@property --arrive-sweep {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

.cf-arrive {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* The drawn half. Everything under here is aria-hidden in the markup, which
   is why the status line is its sibling and not its child. */
.cf-arrive__ghost {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* THE LIGHT. Above the contours it crosses (z-index 1), never under type —
   the status line sits outside this box for that reason. -30 % to 130 %
   keeps the band fully off the object at both ends of the loop, so the
   period reads as one pass and a rest rather than as a strobe. */
.cf-arrive__ghost::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: linear-gradient(var(--angle-b),
      transparent          calc(var(--arrive-sweep) - 22%),
      var(--arrive-glas)   calc(var(--arrive-sweep) - 8%),
      var(--arrive-weiss)  var(--arrive-sweep),
      var(--arrive-sky)    calc(var(--arrive-sweep) + 8%),
      transparent          calc(var(--arrive-sweep) + 22%));
  animation: cf-arrive-sweep var(--arrive-period) linear infinite;
}

/* The band's three colours are the foil's own three, at the measured cap.
   Composed here rather than in tokens.css because color-mix() resolves on the
   element that declares it and these have to sit next to the gradient that
   uses them. A browser that cannot mix loses the light and keeps the ghost,
   which is the correct thing to lose. */
.cf-arrive__ghost {
  --arrive-weiss: color-mix(in oklab, var(--cf-weiss)    calc(var(--arrive-light-peak) * 100%), transparent);
  --arrive-glas:  color-mix(in oklab, var(--cf-glas)     calc(var(--arrive-light-peak) * 100%), transparent);
  --arrive-sky:   color-mix(in oklab, var(--sky-300)     calc(var(--arrive-light-peak) * 100%), transparent);
}

@keyframes cf-arrive-sweep {
  from { --arrive-sweep: -30%; }
  to   { --arrive-sweep: 130%; }
}

/* A LINE OF TYPE THAT IS NOT HERE YET is an empty box the height of its own
   line, not a filled bar. Height in em so it tracks whatever type it stands
   in for; --arrive-w is the ragged right edge real copy has.

   Four gradients, one per edge, because CSS gives no control over the
   dash-to-gap ratio of border-style: dashed and the four types are ratios.
   Same construction as .rule--dashed-1-4 in base.css; 1 px on, 4 px off is
   --presence-absent written as a gradient, which is the only form a border
   can take. Change one and change the other. */
.cf-arrive__line,
.cf-arrive__plate {
  --ghost-x: repeating-linear-gradient(90deg, var(--border-strong) 0 1px, transparent 1px 5px);
  --ghost-y: repeating-linear-gradient(0deg,  var(--border-strong) 0 1px, transparent 1px 5px);
  display: block;
  background-image: var(--ghost-x), var(--ghost-x), var(--ghost-y), var(--ghost-y);
  background-size:
    100% var(--stroke-1), 100% var(--stroke-1),
    var(--stroke-1) 100%, var(--stroke-1) 100%;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: no-repeat;
}

.cf-arrive__line {
  width: calc(var(--arrive-w, 1) * 100%);
  height: calc(1em * var(--leading-relaxed));
}

/* A plate — a card, a photo, a figure. --arrive-h is the height the arriving
   thing will have; give it the real one, because a placeholder of the wrong
   size is a layout shift with extra steps. */
.cf-arrive__plate {
  width: 100%;
  height: var(--arrive-h, 12rem);
  border-radius: var(--radius-sm);
}

/* An isometric object. The cell is the lattice's own 96 x 48 rhombus, which
   is what every spatial object in the system is cut from — so the ghost of an
   illustration is the ground it will stand on. Stroked in SVG for the same
   reason the plot is: a transform on a bordered box scales its contour, and
   1 px means 1 px at every size. */
.cf-arrive__object {
  display: block;
  width: 100%;
  max-width: var(--field-unit);
  height: auto;
  overflow: visible;
}
.cf-arrive__object path {
  fill: none;
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-dasharray: var(--presence-absent);
  vector-effect: non-scaling-stroke;
}

/* The word. A mono label, and --text-secondary rather than --text-muted
   because it sits on the page wash. */
.cf-arrive__status {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   PROGRESS — the ladder, traversed

   A wait with a known end is drawn as one line changing type as it is
   crossed: solid behind the head, --presence-absent ahead of it, and the head
   itself the lattice cell filled with light. Nothing here is invented for the
   component — it is the plot's lit cap put on the rail the ladder already
   describes. The rail is a division in the sense of geometry.html: done on
   one side of the head, not done on the other.
   -------------------------------------------------------------------------- */
/* INHERITS, and that is the whole authoring story: the value is written once on
   the block, where the label and the number that quote it already live, and the
   rail two levels down reads it. Registered non-inheriting — the safer-looking
   default — the rail silently falls back to the initial value and draws an
   empty bar under a label that says 42 %. */
/* The travelling segment's position. Registered so it interpolates; without
   it the segment would jump between two computed values, and the left
   declaration falls back to the 36 % still. */
@property --run-x {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 36%;
}

@property --progress {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

/* minmax(0, 1fr), not 1fr. This was the one fr track in the whole system with
   no minimum of any kind — 32 of 33 already had one, 28 floored on the track
   and 4 guarded by min-width: 0 on their items — and it is the failure .tiles
   and .subdivide both warn about in prose, live in a shipping rule.
   1fr is minmax(auto, 1fr) and auto floors at min-content, so the label
   sets the track's width rather than the track setting the label's. Measured
   with a 61-character German compound in .cf-progress__label: the document
   went 320 -> 469 px at a 320 px viewport and 375 -> 469 at 375, and swapping
   in the floor put both back on the nose. The reset's overflow-wrap:
   break-word is already on the label and does not help — it breaks the word
   when the track is drawn, and intrinsic track sizing happens first.
   → foundations/layout.html#intrinsic-minimum */
.cf-progress {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
}

.cf-progress__label {
  margin: 0;
  font-size: var(--text-md);
  color: var(--text-primary);
}

.cf-progress__value {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  text-align: right;
}

/* The rail spans both columns. Its box is 8 px tall so the cap has room; the
   line itself is 1 px and sits at the middle of it.

   Two layers, no overlay: the traversed run is one solid tile sized to the
   value and pinned left, the untraversed run is a 1-4 tile sized to the
   remainder and pinned right. clamp() because --progress is a number, which
   rejects a unit but not a 2 or a -1, and either would draw a rail longer
   than the rail. */
.cf-progress__rail {
  --p: clamp(0, var(--progress), 1);
  position: relative;
  grid-column: 1 / -1;
  height: var(--space-2);
  background-image:
    linear-gradient(90deg, var(--border-strong) 0 100%),
    repeating-linear-gradient(90deg, var(--border-strong) 0 1px, transparent 1px 5px);
  background-size:
    calc(var(--p) * 100%) var(--stroke-1),
    calc((1 - var(--p)) * 100%) var(--stroke-1);
  background-position: left center, right center;
  background-repeat: no-repeat;
}

/* THE HEAD IS THE SCREEN'S LIME MOMENT while it runs, and there is only ever
   one — two progress bars on one screen is two lights, which is why the
   second one is a list and not a second bar. 8 x 4 is the lattice cell at UI
   scale: the same 2:1 rhombus, small enough to read as a mark on a line. */
.cf-progress__rail::after {
  content: "";
  position: absolute;
  top: 50%;
  left: calc(var(--p) * 100%);
  width: 8px;
  height: 4px;
  transform: translate(-50%, -50%);
  /* The ramp mirrored onto a level run, lime at the leading edge: the head is
     lit where it is going and falls off behind it. Flat lime is not available
     to this mark or to any other — foundations/colors.html, "Lime is never
     flat" — and on a head that travels the ramp is the better drawing anyway,
     because a mark that is brightest at its front reads as moving. */
  background: var(--gradient-light-90);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  --glow-r: 8px;
}

/* A WAIT WITH NO KNOWN END DOES NOT GET A HEAD. A cap that travels and resets
   is a claim about how far along the thing is, made by something that does not
   know — so the rail is 1-4 end to end and the head is replaced by a SEGMENT
   OF LIT LINE that passes along it. It is anchored at neither end: it enters
   off the left, leaves off the right, and never grows from zero, so there is
   no reading of it under which it is measuring anything.

   Drawn in --gradient-foil-ink, which is the one gradient in the family that
   is legible on this surface — the lit half is above OKLab L 0.82 and lands at
   1.1:1 on CF-Grau. The band that crosses .cf-arrive is wrong here and was
   tried first: spread at --angle-b over an 8 px box it puts almost no ink on
   the one pixel that is the line, and a wait indicator nobody can see is not
   one. A ghost line and a lit line is a difference in line TYPE, which is the
   difference this whole component is built on. */
.cf-progress--indeterminate .cf-progress__rail {
  --progress: 0;
  overflow: hidden;
  overflow: clip;
}
.cf-progress--indeterminate .cf-progress__rail::after {
  top: 50%;
  left: var(--run-x, 36%);
  width: 28%;
  height: var(--stroke-1);
  transform: translateY(-50%);
  clip-path: none;
  filter: none;
  background: var(--gradient-foil-ink);
  animation: cf-progress-run var(--arrive-period) var(--ease-in-out) infinite;
}
@keyframes cf-progress-run {
  from { --run-x: -28%; }
  to   { --run-x: 100%; }
}

/* The ghost's band on the oklab path, the same way every other gradient in the
   system takes it; browsers without it keep the sRGB band above. */
@supports (background: linear-gradient(in oklab, red, blue)) {
  .cf-arrive__ghost::after {
    background-image: linear-gradient(var(--angle-b) in oklab,
        transparent          calc(var(--arrive-sweep) - 22%),
        var(--arrive-glas)   calc(var(--arrive-sweep) - 8%),
        var(--arrive-weiss)  var(--arrive-sweep),
        var(--arrive-sky)    calc(var(--arrive-sweep) + 8%),
        transparent          calc(var(--arrive-sweep) + 22%));
  }
}

/* 50 % IS THE DESIGNED STILL — the band at the middle of the object, which is
   the same rest value the swinging foil and the field's pool land on. Every
   path that cannot run the loop arrives here rather than at an edge case. The
   ghost stays: nothing is moving, and the thing is still not here yet. */
@media (prefers-reduced-motion: reduce) {
  .cf-arrive__ghost::after,
  .cf-progress--indeterminate .cf-progress__rail::after {
    animation: none;
    --arrive-sweep: 50%;
  }
}

/* Paper has no waits in it. The ghost prints — it is where the thing will be —
   and the light does not. */
@media print {
  .cf-arrive__ghost::after,
  .cf-progress--indeterminate .cf-progress__rail::after { display: none; }
}

/* Forced colours drop every background image, ghost included, so the rail and
   the head are redrawn as borders — the only paint that survives.

   The object is the third thing here and it was missed, because it is not a
   background at all: an SVG whose `stroke` the mode leaves to the author, so
   it stayed literal CF-Schwarz while the two boxes around it went to
   CanvasText. On a dark palette the plate and the line were drawn and the
   thing they are waiting for was not — a wait indicator with nothing in it.
   The 1-4 is a stroke-dasharray and survives, so the object stays a ghost. */
@media (forced-colors: active) {
  .cf-arrive__line,
  .cf-arrive__plate {
    border: var(--stroke-1) dashed CanvasText;
  }
  .cf-arrive__object path { stroke: CanvasText; }
  .cf-progress__rail {
    border-bottom: var(--stroke-1) solid CanvasText;
    height: 0;
  }
  .cf-progress__rail::after,
  .cf-arrive__ghost::after { display: none; }
}

/* ==========================================================================
   ANNOTATION — naming a part of a drawing

   The system could mark a found WORD (foundations/found.html) and it could
   draw an OBJECT (foundations/illustration.html). It had nothing in between:
   no way to say which part of a drawing you are looking at. Fifteen isometric
   objects ship with construction nodes on them — .cf-iso__node, a black dot at
   a vertex — and not one of those dots says anything. This is the layer that
   gives them words.

   It is the found state one scale up. Two rungs, the same two: every
   annotation is drawn in CONTOUR, and the one the reader is on is LIT. A
   drawing with six annotations has one lit and five contoured, which is how
   the lime budget is satisfied by the drawing rather than waived for it.

   THE LEADER IS A LATTICE STEP, and that is the whole geometry. Technical
   drawing has required for a century that a leader run at neither horizontal
   nor vertical, and this brand sanctions exactly four angles — so the leader
   is declared as a step in lattice units, --annot-mx across by --annot-my
   down, and its angle is READ OFF that step rather than typed in. (2, 1) is
   26.57 deg and (1, 2) is 63.43 deg: the only two obliques the brand owns, and
   the only two a leader can be. A leader that is off the lattice is not
   expressible here.

   The elbow at the note end — the level shoulder every drafting standard asks
   for — is the label's OWN bottom rule. Line and word are one mark, which is
   the third sanctioned use of a line in this system: the label rule. There is
   no bar beside anything. → foundations/geometry.html#where-lines-go

   Anatomy, from the anchor outward:

     ::before   the anchor: the lattice cell itself, drawn with --iso-transform
                so it is the same 2:1 rhombus every object in the system is cut
                from. Contour, or filled with the light ramp when lit.
     ::after    the leader: one lattice step, gapped off the anchor so it
                starts at the cell's edge rather than through it.
     __label    the note, in the mono label face, standing on its own rule.
     __value    an optional measure after it, tabular.
   ========================================================================== */

/* 1 is the finished annotation, and it is what everything that cannot animate
   gets: no @property support, no view timeline, reduced motion, print. */
@property --annot-draw {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* The figure is the positioning context AND the container. It is queried
   rather than the viewport because what decides whether a callout layer fits
   is the width of the drawing, not the width of the screen — the same rule
   the rest of the system holds to. */
.cf-annot-fig {
  position: relative;
  /* margin-BLOCK only. The reset already zeroes a figure's margin, and writing
     the shorthand here would beat a caller's own `margin-inline: auto` — which
     is exactly how .cf-error__figure centres its drawing. */
  margin-block: 0;
  container-type: inline-size;
  container-name: cf-annot-fig;
}
.cf-annot-fig > svg {
  display: block;
  width: 100%;
  height: auto;
}
/* The drawing an annotation is annotating is still a drawing, so it takes the
   same handover every other one in this file takes: forced colours does not
   force SVG `stroke`, and the ghost rhombus these specimens point at is a
   `stroke="#000"` in the page's own markup. Left alone the leaders and their
   labels came through on a dark palette with nothing under them to name. */
@media (forced-colors: active) {
  .cf-annot-fig > svg [stroke]:not([stroke="none"]) { stroke: CanvasText; }
}

/* The layer sits exactly over the drawing's own box, so --annot-x / --annot-y
   are fractions of the viewBox and nothing has to be converted. That only
   holds while the SVG fills the figure — give the figure no padding and no
   aspect ratio of its own. */
.cf-annot-set {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  pointer-events: none;

  /* u — the lattice unit the leader is measured in. Reads the container, not
     the viewport: a drawing in a narrow column gets a shorter leader. */
  --annot-u: clamp(0.875rem, 3.2cqi, 1.75rem);
  --annot-node: 0.5rem;
  /* How far the leader stands off the anchor cell. Half the cell's own width,
     so the line starts at the edge of the mark rather than inside it. */
  --annot-gap: 0.25rem;
}

.cf-annot {
  position: absolute;
  left: var(--annot-x, 50%);
  top: var(--annot-y, 50%);
  width: 0;
  height: 0;

  /* The step, in lattice units. (2, 1) is the shallow isometric. */
  --annot-mx: 2;
  --annot-my: 1;
  /* Which way it runs: sx mirrors it left, sy turns it up. Both are signs,
     never magnitudes — the magnitude is the step above, so a direction cannot
     quietly become a different slope. */
  --annot-sx: 1;
  --annot-sy: -1;
  --annot-align: 0;

  /* sqrt(5) — the diagonal of both sanctioned steps, since (2,1) and (1,2)
     have the same length. Derived below wherever the browser can do the
     arithmetic itself. */
  --annot-len: calc(var(--annot-u) * 2.2360679775);
  --annot-turn: calc(var(--annot-sy) * 26.565deg);
}
.cf-annot--steep {
  --annot-mx: 1;
  --annot-my: 2;
  --annot-turn: calc(var(--annot-sy) * 63.435deg);
}

/* THE ANGLE IS READ OFF THE STEP. 26.565deg and 63.435deg above are the same
   two numbers to five places, and what this branch buys is that they cannot
   drift from the step they are the slope of: change --annot-mx / --annot-my
   and the leader follows. CSS trig has been Baseline since March 2023 and
   sqrt() since December 2023, which makes this the first place the system can
   state an isometric angle as the ratio it has always been rather than as a
   rounded degree. The literals stay as the fallback, and they are identical
   drawings — 26.57 against 26.565 is 0.005 deg. */
@supports (rotate: atan2(1, 2)) and (width: calc(1px * sqrt(5))) {
  .cf-annot,
  .cf-annot--steep {
    --annot-turn: calc(var(--annot-sy) * atan2(var(--annot-my), var(--annot-mx)));
  }
  .cf-annot {
    --annot-len: calc(var(--annot-u) *
      sqrt(var(--annot-mx) * var(--annot-mx) + var(--annot-my) * var(--annot-my)));
  }
}

.cf-annot--ne { --annot-sx:  1; --annot-sy: -1; --annot-align:  0; }
.cf-annot--se { --annot-sx:  1; --annot-sy:  1; --annot-align:  0; }
.cf-annot--nw { --annot-sx: -1; --annot-sy: -1; --annot-align: -1; }
.cf-annot--sw { --annot-sx: -1; --annot-sy:  1; --annot-align: -1; }

/* The anchor: a lattice cell, not a dot. --iso-transform is the token every
   spatial thing in the brand is built with, so this mark is the same rhombus
   at 8 px that the ground is tiled with at 120. Drawn as a BORDER because
   forced-colours mode discards background-color and maps border colours into
   its own palette — the same reason the radio's mark is a border. */
.cf-annot::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: var(--annot-node);
  height: var(--annot-node);
  border: var(--stroke-1) solid var(--border-strong);
  transform: translate(-50%, -50%) var(--iso-transform);
}

/* The lit rung. Lime is light and never a flat fill, so the cell carries the
   ramp — the near rake, because a lattice cell is a flat face.
   → foundations/colors.html#lime-is-never-flat */
.cf-annot--lit::before {
  background-image: var(--gradient-light);
}

/* The leader. A zero-height box with one border, turned onto its step:
   scaleX(sx) mirrors the whole construction rather than naming a second
   angle, so left and right cannot end up on different slopes. */
.cf-annot::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--annot-len) - var(--annot-gap));
  height: 0;
  border-block-start: var(--stroke-1) solid var(--border-strong);
  transform-origin: 0 0;
  transform: scaleX(var(--annot-sx))
             rotate(var(--annot-turn))
             translateX(var(--annot-gap))
             scaleX(var(--annot-draw));
}

/* The note stands on its own rule, and the leader runs into the end of that
   rule. The label is placed by its BOTTOM edge, which is what keeps the two
   marks one mark in all four directions — a leader arriving from below ends
   at the shoulder, never at the cap line. */
.cf-annot__label {
  position: absolute;
  left: calc(var(--annot-u) * var(--annot-mx) * var(--annot-sx));
  bottom: calc(var(--annot-u) * var(--annot-my) * var(--annot-sy) * -1);
  transform: translateX(calc(var(--annot-align) * 100%));
  padding-block-end: var(--space-1);
  border-block-end: var(--stroke-1) solid var(--border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: var(--leading-normal);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-primary);
  white-space: nowrap;
  opacity: var(--annot-draw);
  pointer-events: auto;
}
.cf-annot__value {
  margin-inline-start: var(--space-2);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;

  /* THE DRAW STOPS HERE, AND THIS LINE IS A RECALC BOUNDARY RATHER THAN A
     VALUE. --annot-draw is registered `inherits: true` because the two boxes
     that read it are not one box: the leader is .cf-annot::after and the note
     is .cf-annot__label, so the animation has to run on the SET and arrive by
     inheritance. Nothing below the note reads it — the value is the end of
     that chain — but an inherited registered property whose value CHANGES
     costs a style recalc on every descendant of the animated element, on
     every frame the range is scrubbing, whether or not the descendant reads
     it. Redeclaring it here gives this element a computed value that does not
     move, and Blink stops the walk at an element whose inherited values are
     unchanged, so the readouts under it are never visited.

     THE SUBTREE IS NOT THE FOUR WORDS THIS COMPONENT WAS MEASURED ON. On the
     landing page the set is .sp-annots: 21 annotations whose values are live
     readouts — .sp-stream, six <b> apiece — so 126 of the set's 189
     descendants sit under .cf-annot__value, and not one of them reads
     --annot-draw. Chromium 1194, 1440 x 900, scrubbing scrollY 2050 -> 2500
     in 40 steps (cf-annot-draw's own `cover 45% cover 62%`, act 1 lit),
     Performance.RecalcStyleDuration, median of three runs:

       as shipped                    1000 ms    25.0 ms / step
       with this line                 594 ms    14.9 ms / step
       cf-annot-draw force-paused     361 ms     9.0 ms / step

     The 233 ms this still leaves over the paused run is the 21 leaders and
     21 notes that do read the property, which is the work the animation is
     for. Cutting one level lower, on .sp-stream > b, buys nothing (1062 ms,
     within noise of shipped): by then the walk has already reached them, and
     what a boundary saves is the visit, not the comparison.

     1 is the finished annotation and the registered initial-value, the same
     number the legend fallback, the plan and forced colours each write when
     they hand --annot-draw back. */
  --annot-draw: 1;
}

/* The leader draws itself out of the anchor as the figure settles. The range
   opens at 45 % of the figure's cover — after .cf-iso has arrived (5-30 %) and
   after its own nodes have faded up (35-50 %) — so the drawing is finished
   before it is labelled, which is the order the two things happen in on paper.
   Scrubbed from scroll position, so scrolling back un-draws it. */
@keyframes cf-annot-draw {
  from { --annot-draw: 0; }
  to   { --annot-draw: 1; }
}
@media screen and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
    .cf-annot-set {
      animation: cf-annot-draw linear both;
      animation-timeline: view();
      animation-range: cover 45% cover 62%;
    }
  }
}

/* BELOW THE FOLD OF ITS OWN CONTAINER THE LAYER BECOMES A LEGEND. Callouts
   need clear ground to point into, and a 320 px drawing has none; overlapping
   two notes is worse than not drawing the leaders at all. So the set flows
   under the figure as a row of marks and words. The rule under each label goes
   with the leader, because a label rule with no line running into it is an
   underline, and an underline that is not a link is decoration. */
@container cf-annot-fig (max-width: 28rem) {
  .cf-annot-set {
    position: static;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-5);
    margin-block-start: var(--space-4);
    /* No leader to draw, so nothing to draw it with. Left running, the only
       thing the timeline would still animate is the label's own opacity — a
       word that is sometimes missing, timed against a 14 px strip.
       THIS BLOCK HAS TO SIT AFTER THE ANIMATION IT CANCELS. Both rules are
       .cf-annot-set, so the later one wins, and an animation cannot be
       overridden by a custom property at all — killing the animation is the
       only way to get --annot-draw back. Measured before the move: the label
       was in the flow, in ink, at opacity 0. */
    animation: none;
    --annot-draw: 1;
  }
  .cf-annot {
    position: static;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    width: auto;
    height: auto;
  }
  .cf-annot::after { display: none; }
  .cf-annot::before {
    position: static;
    flex: none;
    transform: var(--iso-transform);
  }
  .cf-annot__label {
    position: static;
    transform: none;
    padding-block-end: 0;
    border-block-end: 0;
  }
}

/* Forced colours exists to guarantee legibility, and an opacity ramp is the
   one thing it cannot guarantee — the label is ink at 79 % of itself halfway
   through the range. Measured, that is still 11:1 on white, so this is not a
   contrast failure being fixed; it is a mode that has asked for certainty
   being given the finished annotation instead of a frame of one. The lit rung
   loses its ramp here too, which is correct: it is emphasis, and emphasis is
   what this mode is allowed to drop.

   What is NOT allowed to drop is the rung itself. .cf-annot--lit::before
   carries "the one the reader is on" (this file's own words, above) entirely
   as a gradient background-image, and forced colours computes every gradient
   to `none` — measured on the landing page, the lit node's background-image
   comes back identical to a contoured node's: `none`. Same border, same
   colour, same box: the two rungs this component exists to tell apart become
   one. Highlight/HighlightText is the pair the mode reserves for exactly this
   — "the selected one of a set" — same as .cf-pagination__page[aria-current]
   above. A fill rather than a border swap because the anchor's border is
   already spoken for (--border-strong, shared by every rung on
   .cf-annot::before) and forced colours honours a system-colour fill even
   though it discards an author one. */
@media (forced-colors: active) {
  .cf-annot-set {
    animation: none;
    --annot-draw: 1;
  }
  .cf-annot--lit::before {
    background-color: Highlight;
    border-color: Highlight;
  }
}

/* ---- THE SPILL, COMPOSED WHERE IT IS APPLIED ----
   tokens.css publishes the reach, lime's share of it and the two colours; the
   filter is built here, in a real property, because that is the only place
   var(--glow-r) resolves against the element that set it. As a composed token
   on :root it baked in the :root default and all three of these rendered 12 px
   whatever they asked for — see the note beside --glow-r.

   One rule and its members rather than a copy each: the composition is the
   thing that must not drift, and each member keeps its own reach above. */
.cf-icon--light,
.cf-plot__col--lit .cf-plot__cap,
.cf-gantt__head,
.cf-gantt__mark--lit,
.cf-relay__head {
  filter:
    drop-shadow(0 0 calc(var(--glow-r) * var(--glow-core)) var(--glow-lime))
    drop-shadow(0 0 var(--glow-r) var(--glow-glas));
}

/* AND ONE MEMBER OF THE COMPOSITION IS AN SVG CHILD, WHICH SAFARI DOES NOT
   PAINT. The rule above has five members and they are not the same kind of
   element: .cf-gantt__head is a <span>, .cf-icon--light is an OUTER <svg>, and
   both of those take a CSS filter in every engine. .cf-plot__col--lit
   .cf-plot__cap is a <path> INSIDE an <svg>, and that is the one case WebKit
   drops — the finding acts.css sets out in full beside the flow drawing's
   leaves, under "SAFARI PAINTS NO CSS FILTER ON AN SVG CHILD": no filter
   FUNCTION reaches an SVG child there, drop-shadow or blur, while
   `filter: url()` on the same element paints, including from a <filter> in
   another <svg> in the document.

   So the lit cap has been standing in Safari as a rhombus with a ramp and no
   light in it. That is not a softened glow; it is the whole of what --lit
   spends. "One lit column, and only ever one" (this file, 250 lines up) makes
   the glow the entire signal that says which column the figure is about, and
   without it the lit cap is an unlit cap with a different fill — measured on
   components/plot.html at 1440, WebKit's cap and its halo are background to
   the last pixel outside the contour, while Chromium's ramp reads 194 of 255
   one pixel out. Re-checked with will-change, isolation, translateZ and
   transform-box on the path: every one of them changes nothing, and
   `filter: blur(3px)` on the same path renders nothing either, which is what
   rules out anything specific to shadows or to this component.

   THE TWO NUMBERS ARE THE SHORTHAND'S OWN, WRITTEN OUT: stdDeviation 12 and
   5.4, which is --glow-r and --glow-r x --glow-core with the unit dropped.
   Two facts put them there and both were established by measurement rather
   than by arithmetic. The first is the flow drawing's, and this only
   confirmed it: the deviation is the shorthand's RADIUS 1:1, not half of it.
   The second is this component's: the unit is the drawing's, and one user
   unit here is --plot-u/20 — 1.6 px at the clamp's 32 px ceiling, 0.8 px at
   its 16 px floor. So 3.75, which is what you get by converting 12 px at the
   ceiling and halving it, renders about a third of the light; it was the
   first thing tried and it is wrong on both counts at once.

   AND UNLIKE THE FLOW, NOTHING IS TRADED AWAY FOR IT. The flow's own note
   accepts that its Safari reach follows the drawing while the other engines
   hold a px figure. Sampling the halo out from the cap's edge every 4 px on
   both axes says that does not happen here: 12/5.4 lands within two levels of
   255 of Chromium's shipped CSS filter at every radius at 1440, where the
   unit is 1.6 px, AND at 390, where it is 0.8 — so Chromium is reading the
   shorthand's lengths in this drawing's user space too, and the two spellings
   scale together across the whole clamp rather than agreeing at one width.

   THE REGION IS BOUNDED ON PURPOSE, the same argument the flow's filters
   carry: WebKit rasters one buffer of this size per filtered element. The cap
   is 40 x 20 user units and the halo is gone by 18 of them — m=18 renders
   identically to m=26 and m=16 does not — so the <filter> in the markup is
   x="-18" y="-18" width="76" height="56".

   FLOOD COLOURS BELONG HERE, not in the markup, for the reason the flow's do:
   flood-color is a CSS property, so the two primitives take --glow-lime and
   --glow-glas from tokens.css the way the shorthand above does and a palette
   move still moves both spellings. Both tokens carry their own alpha, so
   flood-opacity stays at its default 1.

   AND IT IS DECLARED BEFORE THE FORCED-COLOURS BLOCK BELOW, which matters:
   that block names the same selector at the same (0,2,0) weight, so source
   order is what lets it win. A lower weight is not available — a bare
   .cf-plot__cap would hand the reference to unlit caps too, and an SVG
   element whose filter names an id it cannot resolve is not rendered at
   all. */
@supports (-webkit-hyphens: none) {
  .cf-plot__col--lit .cf-plot__cap { filter: url(#cf-plot-glow); }
  #cf-plot-glow feDropShadow:first-child { flood-color: var(--glow-lime); }
  #cf-plot-glow feDropShadow + feDropShadow { flood-color: var(--glow-glas); }
}

/* AND THE SPILL IS SWITCHED OFF WHERE IT IS COMPOSED, for the same reason it
   is composed here: a member that turns it off in its own section is a member
   whose rule loses. .cf-gantt's forced-colours block wrote `filter: none` on
   .cf-gantt__head and .cf-gantt__mark--lit and it has never once applied —
   equal specificity, and this rule is 900 lines further down, so source order
   decides and the spill wins. Measured on components/gantt.html with the dark
   palette active: `drop-shadow(rgba(225,255,0,.5) 0 0 3.6px) …`, the lime
   still spilling. Same trap took .cf-plot__col--lit .cf-plot__cap when its own
   block tried the same thing, which is how this was found.

   A `filter` is not on the list forced colours forces, so nothing drops it on
   its own; and a lime halo around a mark whose palette the reader has chosen
   is the one thing this mode exists to stop — .cf-gantt's block says exactly
   that, and it can now be true. Every member of the composition loses the
   spill together, which is the property the composition is here to keep.

   Filter and not `--glow-r: 0`: the reach is a per-member number and zeroing
   it would still run two drop-shadows at radius 0, which is a lime core
   painted hard against the mark's own edge rather than no light at all. */
@media (forced-colors: active) {
  .cf-icon--light,
  .cf-plot__col--lit .cf-plot__cap,
  .cf-gantt__head,
  .cf-gantt__mark--lit,
  .cf-relay__head { filter: none; }
}

/* ==========================================================================
   SIGNET — a mark for one thing, drawn from that thing's own name

   The generator is assets/js/cf-signet.js and its header carries the whole
   construction. This file owns only what the drawing is PAINTED in, and that
   split is deliberate: the geometry is a property of the system and is the
   same in every context, while the three greys are a property of the surface
   the mark stands on and have to answer to the theme.

   THE THREE FACE TONES ARE THE DESIGNER'S, off the isometric stack on
   foundations/geometry.html and off 01-discovery.svg under it: #DADADA on
   top, #CFCFCF on the left face, #C4C4C4 on the right. Only the middle one
   has a token (--cf-grau). The other two are written as literals here for
   the reason the ramp waypoints are written as literals in tokens.css —
   they are measured values off the source material, and a token that named
   them would be a token with one consumer and an invented name. They are the
   light coming from the upper left, which is where the stack puts it.

   IT IS INK BY DEFAULT AND LIT ON REQUEST. One lime moment per screen is a
   rule about a screen, and a wall of signets is the one place in this system
   where an object could break it twenty times over without any single mark
   being wrong. So .cf-signet--ink is what the generator emits unless asked,
   and it paints the peak in CF-Grau like every other top face. The light is
   opt-in, once, on the mark that is the screen's lime moment.
   → components/signet.html
   ========================================================================== */
.cf-signet {
  --signet-face-top:   #DADADA;
  --signet-face-left:  var(--cf-grau);
  --signet-face-right: #C4C4C4;
  --signet-contour:    var(--border-strong);

  display: block;
  width: var(--signet-size, 6rem);
  height: auto;
  /* The mark is square and its content is square; nothing about it wants to
     stretch, and inside a flex row it otherwise would. */
  flex: none;
}

.cf-signet--sm { --signet-size: 3rem; }
.cf-signet--lg { --signet-size: 12rem; }

/* AT 3 rem THE FAINT LINE STOPS BEING A LINE. The frame scales by 0.4, so the
   1-2 dash lands at roughly one device pixel on and two off — which does not
   read as an outline, it reads as dirt on the mark. The same arithmetic takes
   the construction dots to 0.6 px. So the small form keeps what survives at
   that size — the blocks, the contour and the light — and drops the two
   annotations that do not. → components/signet.html#sizes */
.cf-signet--sm .cf-signet__plot,
.cf-signet--sm .cf-signet__dots { display: none; }

/* Every face and every plot outline is the same 1 px contour at every size —
   vector-effect on the shapes keeps it there, and the weight is named once
   here rather than nine times in the generator. */
.cf-signet polygon,
.cf-signet circle {
  stroke: var(--signet-contour);
  stroke-width: var(--stroke-1);
}

.cf-signet__top   { fill: var(--signet-face-top); }
.cf-signet__left  { fill: var(--signet-face-left); }
.cf-signet__right { fill: var(--signet-face-right); }

/* AN EMPTY PLOT IS DRAWN AND NOT OMITTED. The field is nine plots whether or
   not something stands on them, and a plot with nothing on it is exactly the
   presence ladder's "faint" rung — the 1-2 line, which is the system's word
   for a thing that is there and is not the subject. Leaving it out would make
   the mark a silhouette; drawing it solid would make nine towers of which
   three happen to be flat. */
.cf-signet__plot {
  fill: none;
  stroke-dasharray: var(--presence-faint);
}

/* The lit face carries the gradient as a paint server, so its fill comes from
   the markup and this rule must not name one. */
.cf-signet__lit { stroke: var(--signet-contour); }

/* The construction dots off the manual's stack. They are the same black as
   the contour and they mark the lit face alone. */
.cf-signet__dots circle {
  fill: var(--signet-contour);
  stroke: none;
}

/* The ink form: the peak is a top face like any other, and the mark carries
   no light at all. Stated as a modifier rather than as the absence of one, so
   a page can read which form it is holding. */
.cf-signet--ink .cf-signet__lit { fill: var(--signet-face-top); }

/* On an inverse section the three tones cannot simply invert: black is the
   floor of the neutral scale, so the steps climb instead — the same move
   tokens.css makes for the inverse theme. The order of the three is kept
   (top lightest, right darkest) so the light still comes from the upper left.
   The lit face keeps the family's ramp unchanged: lime on black is the one
   place in this system where the light is at its loudest, and that is the
   point of it. */
[data-theme="inverse"] .cf-signet {
  --signet-face-top:   var(--grey-700);
  --signet-face-left:  var(--grey-800);
  --signet-face-right: #141414;
  --signet-contour:    var(--border-strong);
}

/* Forced colours does not force SVG paint — the same trap sight.html's plan
   view fell into. Nine faces in three near-identical greys would go out on a
   dark palette together, so the mark collapses to what it is under the paint:
   a contour drawing on the canvas, with the lit face left as the one filled
   shape so the moment survives a mode that has no lime in it. */
@media (forced-colors: active) {
  .cf-signet polygon,
  .cf-signet circle { stroke: CanvasText; }
  .cf-signet__top,
  .cf-signet__left,
  .cf-signet__right { fill: Canvas; }
  .cf-signet__lit   { fill: Highlight; }
  .cf-signet--ink .cf-signet__lit { fill: Canvas; }
  .cf-signet__dots circle { fill: CanvasText; }
}

/* --------------------------------------------------------------------------
   THE SHEET, PART TWO — the register of components

   base.css states the law and holds everything whose selector base.css owns:
   the page box, the container, the ground, the headings, the paragraph, the
   table's row and head. This is the other half, and it is here rather than
   there for one reason: this file is loaded last, and a `display: none` that
   loses to a `display: flex` at equal specificity is not a decision.

   Two registers. WHAT A SHEET DOES NOT CARRY, and WHAT MAY NOT BE CUT.
   → foundations/print.html
   -------------------------------------------------------------------------- */

@media print {
  /* WITHDRAWN. Each of these is an affordance rather than content — a thing
     whose whole purpose is to be operated — and on a sheet it cannot be
     operated. So it is not faint, it is absent: the standard this file already
     holds .cf-hero__still-toggle to, "a switch whose state cannot change what
     the reader sees is worse than no switch at all".

     .cf-nav is 84 px of black plate and seven links you cannot click, at the
     top of sheet 1 of every printout the site produces. .cf-scroll-cue asks the
     reader to scroll a sheet. .cf-pagination offers page 2 of an archive to
     somebody holding page 1 of a printout.

     .cf-section-header__action is the fourth and the one that reads as content
     until you ask what it does. It is a word in the same slot the counter uses,
     so on a sheet "189 BEITRÄGE" and "SUCHEN" are the same mark — except the
     first is the archive's size and the second is an offer to search a piece of
     paper. It goes, and the counter stays, which is the whole of the register's
     rule: ink is data or it is nothing.

     .cf-accordion__summary .cf-arrow is the fifth, and it is only withdrawn
     because of the clause below it. The arrow is the control that opens the
     panel, and it draws which way the panel stands: right for closed, rotated
     for open. On a sheet every panel is open and only the one the reader
     happened to leave open carries [open], so eleven rows would print an arrow
     stating the wrong state beside an answer that is already there. A control
     that cannot be operated is withdrawn; a control that cannot be operated AND
     contradicts the page is not a judgement call. */
  .cf-nav,
  .cf-scroll-cue,
  .cf-pagination,
  .cf-section-header__action,
  .cf-accordion__summary .cf-arrow { display: none; }

  /* AND THE THREE THINGS THE HERO DID FOR THE NAV, which have to be given back
     on the same line the nav is taken away, because none of them was ever a
     decision about paper — they are the nav's decisions, and the nav is not
     here. The hero is pulled up by exactly --nav-height so the artwork starts
     where the page starts; it pads the same distance back so its first line is
     not under the plate. With the plate gone the pull crops 84 px of artwork
     off the top of the sheet and the pad returns it as white.

     The min-height is the third and it is the largest single change on the
     sheet. min(92vh, 56rem, …) resolves against the PAGE BOX in a print
     context, so at A4 under base.css's margins it is min(~922px, 896px) — an
     896 px reservation for about 400 px of content. That is most of sheet 1 of
     every printed page in this site spent on one headline. A hero is a first
     screen; a sheet is not a screen. */
  .cf-hero {
    margin-top: 0;
    padding-block-start: 0;
    min-height: 0;
  }

  /* A section header is the top of its section, the same way an <h2> is the top
     of what follows it. base.css makes that rule for the headings it owns; this
     is the same rule for the two components that ARE a heading. */
  .cf-section-header,
  .cf-page-header { break-after: avoid; }

  /* WHAT MAY NOT BE CUT. Every entry is one object in the drawing: it has one
     contour around it, or it is one figure and its caption, or one record.
     Read the list as the answer to a single question asked of each component —
     if a sheet boundary landed in the middle of this, would the reader be
     looking at one thing in two places, or at two things?

     .lp-ev-card ANSWERED "ONE THING" AND WAS NOT ON THE LIST. Act 6's evidence
     card is a .lp-ev-card__figure and a .lp-ev-card__panel inside one border:
     the drawing, and under it the eyebrow, the headline and the source list
     that say where the drawing's numbers came from. The register already held
     the drawing — .cf-pie, .cf-plot, .cf-line and .cf-block are all on this
     list and all four of them ARE the figure inside one of these four cards —
     so the only boundary it left available was the one between a chart and the
     citation that makes it a claim rather than a picture.

     MEASURED, index.html printed to A4 against this file's own @page margins
     (16 mm head and foot, 265 mm = 1002 px of sheet), Chromium 1194. The four
     evidence cards are 691, 745, 677 and 754 px tall at the sheet's 182 mm
     measure, so every one of them fits whole with room to spare. Before this
     line the print ran to eleven sheets with an open contour — a top border,
     an empty card and no bottom edge — running off the foot of sheets 7, 8 and
     9, and each of those cards printed its sources overleaf from its chart.
     After it the three are gone, each card moves whole onto the next sheet,
     and the sheet count is unchanged at eleven.

     WHAT IT DOES NOT FIX, AND THE REASON IS NOT THIS REGISTER. Act 3's FIRST
     .cf-process card is still cut, at sheet 2, though .cf-process has been on
     this list since the chapter was written. It is the first in-flow child of
     .cf-pin__steps, and Blink will not push a box out of its container's first
     fragment, because that would leave the fragment with no content in it at
     all. Proved by giving .cf-pin__steps a 1 px ::before in print: the same
     card then moves whole and the printout has no open contour left anywhere.
     The three cards behind it obey `avoid` because by then the container has
     already put content on the earlier sheet. Nothing that can be written on
     this list reaches that one — the cures are a forced break before the act
     or a spacer inside it, and both are compositions rather than corrections.

     AND IT IS WRITTEN HERE RATHER THAN BESIDE .lp-ev-card IN acts.css. The
     register is one list: check-print-law.py reads it out of the three
     shipping stylesheets and holds it against foundations/print.html in both
     directions, so a rule filed where that gate does not look is a rule that
     drifts back out. acts.css loads after this file and declares no
     `break-inside` at all, so there is nothing here for the later file to lose
     to. */
  .cf-vacancy,
  .cf-blog-card,
  .cf-info-card,
  .cf-process,
  .lp-ev-card,
  .cf-team-grid__item,
  .cf-culture__row,
  .cf-accordion__item,
  .cf-contact__row,
  .cf-value-row,
  .cf-subdiv__cell,
  .cf-stat,
  .cf-quote,
  .cf-event,
  .cf-annot-fig,
  .cf-plot,
  .cf-pie,
  .cf-line,
  .cf-gantt,
  .cf-block,
  .cf-calendar { break-inside: avoid; }

  /* WHAT A SHEET HAS TO OPEN — the third clause, and the one the two registers
     above could not make. Both of them assume the object is on the page: one
     takes away what cannot be operated, the other keeps whole what is left.
     Neither asks whether the object brought its content with it, and there is
     exactly one component in this system whose resting state is to withhold it.

     <details> is that component, and the register above was already protecting
     .cf-accordion__item from the cut — so the sheet was carefully keeping whole
     an object carrying a question and no answer. Measured on the shipped
     Expertise page under emulated print media: eleven questions, eleven panels,
     checkVisibility() false on all eleven. Karriere prints four the same way.
     The FAQ is the one section of either page a reader prints in order to keep
     what it says.

     ::details-content is what makes this reachable from a stylesheet at all.
     The panel is a box the UA generates and hides with content-visibility, and
     until that pseudo-element was exposed there was no author selector for it
     — which is why a printed accordion on the web is normally either a
     beforeprint script or nothing. It is Baseline as of September 2025 (Chrome
     131, Firefox 143, Safari 18.4), and this component already stands on it for
     the opening animation, so the clause introduces no floor the accordion did
     not already have. Below that floor the panel prints as it printed before,
     which is the same fallback the animation takes.

     block-size and overflow-y come with it because the animation block sets
     them on a CLOSED panel — 0 and clip — at this rule's own weight. This file
     is loaded last and this block is at the foot of it, which is the whole of
     why the register lives here rather than in base.css.

     The summary is restated as one column because the arrow it kept a column
     for is withdrawn above. A grid gutter is drawn between tracks whether or
     not the second one has anything in it, so left alone the question would
     wrap 24 px short of the answer standing under it — on paper only, where
     nobody would go looking. */
  .cf-accordion__item::details-content {
    content-visibility: visible;
    block-size: auto;
    overflow-y: visible;
  }
  .cf-accordion__summary { grid-template-columns: minmax(0, 1fr); }

  /* THE ADDRESS OF A THING YOU CANNOT CLICK. An article's outbound links are
     its sources, and on a sheet a source with no address is an assertion. This
     is the one place in the system where CSS writes text that is not in the
     markup, and it is scoped to the prose of an article for exactly that
     reason: the address is printed where the reader is being asked to believe
     something, and nowhere else. Internal links are left alone — those are the
     site talking to itself, and a reader holding a sheet of it does not need
     the route. Set in the mono face, which is what this system uses for machine
     strings. */
  .cf-prose a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    word-break: break-all;
  }
}

/* --------------------------------------------------------------------------
   THE CONSTRUCTION LAYER — the working, kept

   Every plate in the brand manual shows how it was made. The Winkelraster is
   a star of rays with the isometric rhombus laid over it; the logo plate
   stands the mark inside a measured frame with the unit written on it; the
   illustration example carries the angles it was struck at. The manual is not
   a gallery of finished objects, it is a set of objects with their setting-out
   still visible — which for a company whose first line is "find the answers"
   is the argument itself, not decoration.

   The website threw all of it away. Every drawing here ships as the finished
   object and nothing else, and the one exception proves the rule rather than
   softening it: the demon core on Über uns has its shells and its axis drawn
   at --presence-absent and labelled "reference geometry" in its own source
   comment — construction geometry, baked into one figure, with no name, no
   material and no way for a second drawing to carry it.

   tokens.css had already named this. --presence-absent's own comment reads
   "the ghost line: CONSTRUCTION GEOMETRY, a footprint, a cell that says where
   a thing stands rather than what it measures" — and until this block the rung
   was spent entirely on the other half of that sentence, the not-here-yet
   register on .cf-arrive, .cf-progress and .cf-line__trace--ghost. The word
   was in the token and the drawing was nowhere.

   WHAT IT IS. A second drawing behind the first, in the first's own coordinate
   space, carrying the rays the object was set out on, the cells it was cut
   from and the points where those meet. It is dormant: at rest a reader sees
   the object the designer drew, which is the state every one of these figures
   already shipped in. A reader who asks gets the working.

   THE RANK IS THE LINE TYPE, NOT THE INK, and that is the whole reason this
   layer is cheap. The obvious way to hold construction back from an object is
   to draw it fainter, and every system that does it that way loses the
   distinction in the two modes that discard ink strength: forced colours maps
   authored colour into a palette of its own, and a printer renders a 20 %
   black as a grey mesh. The presence ladder is a ladder of DASH PATTERNS —
   "same box, same size, same place", one rung down — so the construction here
   is drawn in --border-strong, the same contour ink as the object, and told
   apart by --presence-absent alone. Nothing has to be re-argued under forced
   colours, nothing has to be measured for contrast that was not already
   measured for the contour, and the layer survives paper intact.

   THE FOUR ANGLES, AND THE TOKEN THAT HAD NEVER BEEN SPENT. The rays are the
   Winkelraster: the vertical, its horizontal datum, and the fan at ±26.57°,
   ±45° and ±63.43°. --angle-neutral, 45°, is a row in geometry.html's token
   table and a value in tokens.css, and it had EXACTLY ZERO consumers anywhere
   in the tree — no stylesheet, no page, no drawing named it.

   The angle was not absent; the token was. 45° is written as a literal four
   times in these stylesheets — inside --iso-transform itself, the checkbox's
   tick, the radio's rhombus and one act rule — and every one of them is a
   ROTATION APPLIED TO A MARK. Not one is a line struck at 45° across a
   drawing, which is the only thing the plate uses the angle for: 45° is a
   neutral construction ray in a 2:1 system, so a system that never draws
   construction has nowhere to put it. This is the token's first consumer, and
   the figures make the point by standing on it — an object's own edges lie on
   the 26.57° rays and on nothing else, and the neutral rays run out of the
   drawing carrying nothing.

   IT PAINTS UNDER THE OBJECT. __work comes first in the markup and __figure
   second, both positioned, so the object occludes the construction exactly as
   an opaque face occludes anything behind it. A ray therefore emerges from a
   corner already travelling along the edge it set out, which is the reading
   the plate gets for free by being drawn on paper and this had to be ordered
   for.

   THE SWITCH IS .cf-check, WHOLE. A real checkbox with a real label — the
   role, the state, the keyboard behaviour and the focus ring all come with it,
   nothing is reimplemented in ARIA, and a mode with no :has() support rests at
   the object as drawn, which is the state the figure shipped in for a year.
   The construction is aria-hidden: it is the same geometry the prose beside it
   states in words, and a screen reader that announced eight rays would be
   reading out a drawing rather than a document.
   -------------------------------------------------------------------------- */
/* The plate stretches and the switch does not, and the two have to be said
   separately: a `justify-items: start` on the grid would leave the plate's
   `width: 100%` resolving against a track sized by its own content. */
.cf-construct { display: grid; gap: var(--space-4); }

.cf-construct__control { justify-self: start; cursor: pointer; }

.cf-construct__plate { position: relative; width: 100%; }

/* Both layers carry the same viewBox, so inset: 0 over a figure sized by its
   own aspect ratio is registration: nothing is positioned twice and the two
   drawings cannot come apart at a width nobody tested. */
.cf-construct__figure { position: relative; display: block; width: 100%; }
.cf-construct__work {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--border-strong);
  stroke-width: var(--stroke-1);
  /* It is never the thing a pointer is after — the object is. */
  pointer-events: none;
  opacity: 0;
}

.cf-construct__ray,
.cf-construct__cell { stroke-dasharray: var(--presence-absent); }

/* A setting-out point is where an edge WOULD start, so it is the corner dot
   one rung down: the same mark the isometric objects fill, drawn as contour.
   Its stroke is solid because a 5-unit circle cannot carry a 1-4 dash — the
   ladder is about a line long enough to have a rhythm, and below that length
   the rung is carried by the fill being absent instead. */
.cf-construct__point { fill: none; stroke-dasharray: var(--dash-solid); }

/* A DIMENSION LINE IS THE ONE MARK HERE THAT IS SOLID, and geometry.html's
   "where a line may go" is what decides it. That page allows a line to be an
   edge, a division or a LABEL RULE and calls everything else decoration. A
   ray is none of the three, which is exactly why it is drawn at the absent
   rung — it is where an edge would be. A measure is the third: it is a rule
   carrying a word, it states a fact about the object that is already true,
   and it is no more provisional than the caption under a figure. So it takes
   full ink at full length, and the ticks at its ends are part of it. */
.cf-construct__measure { stroke-dasharray: var(--dash-solid); }

/* A LABEL IS LAYER 6, SO IT IS NOT IN THE DRAWING. Every measure here was an
   SVG <text> first, and on a phone that is what it costs: the plate is capped
   at a rem measure and shrinks to the column, so a 22-unit label in a 780-unit
   viewBox rendered at 7 px on a 375 frame — below anything this system sets
   type at, and shrinking further at every narrower width. SVG text is drawn IN
   the user coordinate system; there is no vector-effect for a font size, and
   no unit that survives the transform.

   So the labels are HTML over the plate, positioned in the drawing's own
   coordinates as percentages. The mapping is exact and needs no arithmetic at
   runtime: __figure is width: 100% with no height, so it takes the viewBox's
   aspect ratio, and the plate's box IS the viewBox's box. --x and --y are
   (coordinate − viewBox min) / viewBox extent, written on the element.

   What that buys beyond the size: the measure is real text. It takes
   --text-xs and the mono face like every other label in the system, it scales
   with the reader's own text size rather than with the drawing, it is
   selectable and searchable, and it needs nothing said about forced colours
   because a `color` is exactly what that mode does remap. */
.cf-construct__label {
  position: absolute;
  left: var(--x);
  top: var(--y);
  margin: 0;
  transform: translate(-50%, -50%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  line-height: 1;
  white-space: nowrap;
  color: var(--text-primary);
  pointer-events: none;
  opacity: 0;
}
/* A label at the edge of the frame is anchored by the side that would leave
   it, not centred and nudged back in by hand. */
.cf-construct__label--end   { transform: translate(-100%, -50%); }
.cf-construct__label--start { transform: translate(0, -50%); }

.cf-construct:has(.cf-construct__switch:checked)
  :is(.cf-construct__work, .cf-construct__label) { opacity: 1; }

/* Gated, like every other animation in these three stylesheets. The state
   still switches without it — a revealed construction is information, and it
   simply arrives rather than fading in. */
@media (prefers-reduced-motion: no-preference) {
  .cf-construct__work,
  .cf-construct__label { transition: opacity var(--duration-base) var(--ease-standard); }
}

/* FORCED COLOURS IS ONE DECLARATION, AND IT IS NOT THE ONE THIS BLOCK EXPECTED
   TO NEED. The mode does not force SVG paint — geometry.html's stack has said
   so over the same drawing for as long as it has had a forced-colours rule —
   so a stroke authored as --border-strong stays #000 on a dark system canvas
   and the entire working goes invisible. Caught by screenshotting the logo
   plate in the dark palette, where it rendered as a mark on nothing.

   What DOES come through for free is the rank, and that is the dividend of not
   having spent ink on it: --presence-absent is a dasharray, the mode does not
   touch dasharrays, so the construction is still exactly one rung behind the
   object the moment the ink is restored. A layer held back by opacity would
   have had to re-argue the whole distinction here. The labels need nothing
   either — they are HTML text and a `color` is precisely what the mode remaps.
   One line, and it is about visibility rather than about rank. */
@media (forced-colors: active) {
  .cf-construct__work { stroke: CanvasText; }
}

/* Paper keeps whatever is on screen and loses the control, for the reason the
   sheet chapter gives: a printed page has no state to change, so a checkbox on
   it is an instruction the reader cannot follow. The plate is not allowed to
   break across a sheet — a construction cut in half is two drawings. */
@media print {
  .cf-construct__control { display: none; }
  .cf-construct__plate { break-inside: avoid; }
}

/* --------------------------------------------------------------------------
   THE CUT
   The edge of a crop, and whether the drawing continues past it.
   → foundations/cut.html

   THE SYSTEM HAS FIVE SCROLL BOXES AND NONE OF THEM SAYS SO. A table, a
   calendar, a Gantt frame and the team strip all outgrow their column by
   design — components/table.html calls a table "the one block that
   legitimately outgrows its column" — and each of them answers that by
   cropping and letting the reader drag. Every one of those crops is drawn
   exactly like an edge that is the end of the drawing, which is the one thing
   it is not. The reader is told a column ended when a column was hidden.

   That is a contour making a false statement, and this system has a rule about
   contours making statements: foundations/geometry.html's whole argument is
   that a line here is an edge or a division and never decoration. A crop is a
   third thing, and it had no line.

   WHY THIS COULD NOT BE A BACKGROUND. The obvious construction is the scroll
   shadow every other system uses -- two background layers, one
   `background-attachment: local` covering the other. It works everywhere and
   it cannot draw THIS mark, for a reason worth writing down: a local layer is
   positioned against the scrollable area, so the pair can only ever occlude
   each other. Both states are therefore always drawn, and the resting state
   -- a box with nothing hidden past its edge -- gains a vertical rule it does
   not want. components/table.html bans exactly that rule by name: "a grid of
   boxes says spreadsheet". The mark has to be able to be ABSENT, and no
   background can be absent conditionally.

   `scroll-state(scrollable: …)` can, because it is a state rather than a
   picture: it asks whether the container can still be scrolled that way, and
   the answer is false both at the end of the travel and when there is no
   travel. So a box with nothing hidden draws nothing, which is what ships
   today, and that is the whole fallback: a browser without the query gets the
   page it already had. There is no half-drawn tier.

   A CONTAINER CANNOT QUERY ITSELF, and the mark belongs to the box rather
   than to anything inside it. Its own pseudo-elements can: they are child
   boxes of the container, so ::before and ::after answer the query the box
   asks. They are also the only children that can be added without touching
   five components' markup.

   WHICH COSTS ONE LAYOUT DECLARATION. A pseudo-element in normal flow is a
   block after the content with no height of its own, and the mark is the full
   depth of the crop. `display: grid` with one column gives it that depth for
   free -- one cell, `align-self: stretch` -- and gives the two marks somewhere
   to stand at the two ends of the CONTENT, which is what `position: sticky`
   then measures from. The single child is placed in the same cell explicitly,
   because grid auto-placement skips a cell an explicitly placed item already
   holds and would otherwise push the content into row 2.
   → scripts/check-cut-edge.py holds every box in this block to one element
     child, since a second one would land in a row the marks do not cover.

   The track is `minmax(min-content, 1fr)` and not `1fr`: a table that outgrows
   its column is the entire subject here, so the floor has to be the content's
   own minimum rather than zero. That is the deliberate non-zero floor
   check-grid-tracks.py credits, and it reproduces what `display: block` did --
   the child fills the port, or exceeds it and scrolls.

   THE MARK IS THE GHOST LINE TYPE. --presence-absent, 1-4, is the rung
   foundations/geometry.html reserves for a thing that is there and not drawn,
   which is precisely a column past the crop. It is written here as a gradient
   rather than as a dasharray because this is a box and not a stroke, and both
   numbers are the hairline: one unit of ink, four of gap, --stroke-1 each.
   -------------------------------------------------------------------------- */
@supports (container-type: scroll-state) {
  .cf-table__scroll,
  .cf-prose__table-scroll,
  .cf-calendar__scroll,
  .cf-gantt__scroll,
  .cf-team-strip {
    container-type: scroll-state;
    display: grid;
    grid-template-columns: minmax(min-content, 1fr);
  }
  .cf-table__scroll > *,
  .cf-prose__table-scroll > *,
  .cf-calendar__scroll > *,
  .cf-gantt__scroll > *,
  .cf-team-strip > * { grid-area: 1 / 1; }

  .cf-table__scroll::before,
  .cf-table__scroll::after,
  .cf-prose__table-scroll::before,
  .cf-prose__table-scroll::after,
  .cf-calendar__scroll::before,
  .cf-calendar__scroll::after,
  .cf-gantt__scroll::before,
  .cf-gantt__scroll::after,
  .cf-team-strip::before,
  .cf-team-strip::after {
    content: "";
    grid-area: 1 / 1;
    align-self: stretch;
    position: sticky;
    z-index: 1;
    inline-size: var(--stroke-1);
    background-image: repeating-linear-gradient(
      to bottom,
      var(--border-strong) 0 var(--stroke-1),
      transparent var(--stroke-1) calc(var(--stroke-1) * 5));
    opacity: 0;
    pointer-events: none;
  }
  .cf-table__scroll::before,
  .cf-prose__table-scroll::before,
  .cf-calendar__scroll::before,
  .cf-gantt__scroll::before,
  .cf-team-strip::before { justify-self: start; inset-inline-start: 0; }

  .cf-table__scroll::after,
  .cf-prose__table-scroll::after,
  .cf-calendar__scroll::after,
  .cf-gantt__scroll::after,
  .cf-team-strip::after { justify-self: end; inset-inline-end: 0; }

  /* The two queries are the two directions, and neither is a scroll position:
     `scrollable` is false at the end of the travel AND when there is no
     travel, which is the one behaviour a scroll listener gets wrong first. */
  @container scroll-state(scrollable: inline-start) {
    .cf-table__scroll::before,
    .cf-prose__table-scroll::before,
    .cf-calendar__scroll::before,
    .cf-gantt__scroll::before,
    .cf-team-strip::before { opacity: 1; }
  }
  @container scroll-state(scrollable: inline-end) {
    .cf-table__scroll::after,
    .cf-prose__table-scroll::after,
    .cf-calendar__scroll::after,
    .cf-gantt__scroll::after,
    .cf-team-strip::after { opacity: 1; }
  }

  /* A mark that snaps on and off at the two ends of the travel reads as a
     glitch rather than as a state, so it crosses on the shortest duration the
     motion chapter has. Nothing MOVES -- it is one opacity -- and it is still
     inside the no-preference gate, because a reader who asked for less has
     asked for the state to change and not to be animated into. */
  @media (prefers-reduced-motion: no-preference) {
    .cf-table__scroll::before,
    .cf-table__scroll::after,
    .cf-prose__table-scroll::before,
    .cf-prose__table-scroll::after,
    .cf-calendar__scroll::before,
    .cf-calendar__scroll::after,
    .cf-gantt__scroll::before,
    .cf-gantt__scroll::after,
    .cf-team-strip::before,
    .cf-team-strip::after { transition: opacity var(--duration-fast) var(--ease-standard); }
  }

  /* Forced colours does not remap a gradient -- it is a background IMAGE, and
     the mode leaves images alone -- so --border-strong would stay #000 on a
     dark system canvas and the mark would go invisible against it. This is the
     same finding the construction layer records one section up, read through a
     background instead of through a stroke: one declaration, about visibility
     rather than about rank. The 1-4 period survives untouched either way. */
  @media (forced-colors: active) {
    .cf-table__scroll::before,
    .cf-table__scroll::after,
    .cf-prose__table-scroll::before,
    .cf-prose__table-scroll::after,
    .cf-calendar__scroll::before,
    .cf-calendar__scroll::after,
    .cf-gantt__scroll::before,
    .cf-gantt__scroll::after,
    .cf-team-strip::before,
    .cf-team-strip::after {
      background-image: repeating-linear-gradient(
        to bottom,
        CanvasText 0 var(--stroke-1),
        transparent var(--stroke-1) calc(var(--stroke-1) * 5));
    }
  }

  /* Paper has no travel, so it has no cut. The print block above already
     releases the table's crop to `overflow: visible`, which makes the box stop
     being a scroll container and the query false -- but the calendar, the
     Gantt frame and the strip keep their crops, and on those the mark would
     print as a dashed rule beside a column the sheet simply does not have.
     A reader cannot drag a page. → foundations/print.html */
  @media print {
    .cf-table__scroll::before,
    .cf-table__scroll::after,
    .cf-prose__table-scroll::before,
    .cf-prose__table-scroll::after,
    .cf-calendar__scroll::before,
    .cf-calendar__scroll::after,
    .cf-gantt__scroll::before,
    .cf-gantt__scroll::after,
    .cf-team-strip::before,
    .cf-team-strip::after { content: none; }
  }
}
