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

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

     480 ms has four consumers, not three as this comment first claimed.
     Three are light crossing a surface — this specular, plus the accordion
     row and the blog cell sliding --sheen-panel by background-position. The
     fourth is .cf-accordion__item::details-content, 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. 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;
  background-size: 40% 100%;
  background-position: -70% 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-pass linear both;
      animation-timeline: view();
    }
  }
}
@keyframes cf-glass-rim-pass {
  from { background-position: -70% 0; }
  to   { background-position: 170% 0; }
}

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

/* Both of these hand the label back a real colour. Ink on paper has no plate
   to sit on, and forced-colors has already decided what a button looks like. */
@media print {
  .cf-btn--solid {
    background: none;
    color: var(--cf-schwarz);
    -webkit-text-fill-color: var(--cf-schwarz);
  }
  .cf-btn--solid .cf-arrow { fill: var(--cf-schwarz); }
}
@media (forced-colors: active) {
  .cf-btn--solid {
    background: none;
    color: ButtonText;
    -webkit-text-fill-color: ButtonText;
  }
  .cf-btn--solid .cf-arrow { fill: ButtonText; }
}

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

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

     -70 % is off-canvas and not a guess. A background layer positioned at P
     is offset by P x (container - image); at 40 % width that is 0.6W x P, so
     the band's left edge sits at -0.42W and its right edge at -0.02W. Nothing
     of it is on the bar. 170 % puts it fully off the other end the same way.
     Change the 40 % and both endpoints move with it. */
  background-image: var(--glass-rim-light), var(--glass-edge);
  background-repeat: no-repeat, no-repeat;
  background-size: 40% 100%, 100% 100%;
  background-position: -70% 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-nav-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; } }
/* background-position is a list property, so both layers have to be restated
   in each keyframe. The second pair is the designed rim and never moves. */
@keyframes cf-nav-rim {
  from { background-position: -70% 0, 0 0; }
  to   { background-position: 170% 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 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. */
.cf-nav__list {
  display: flex;
  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. That is the mockup showing a single page rather
   than a rule, and a five-item 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;
    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; }
.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". */
.cf-section-header__count {
  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);
}

/* --------------------------------------------------------------------------
   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. */
.cf-page-header__rule:has(.cf-page-header__figure) {
  background: linear-gradient(to right,
    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;
}
.cf-page-header__meta :last-child { color: var(--text-secondary); }

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

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

   "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 {
  display: grid;
  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 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. */
@media (forced-colors: active) {
  .cf-scroll-cue__stem {
    background: none;
    border-inline-start: var(--stroke-1) dotted;
  }
}

/* --------------------------------------------------------------------------
   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 3.33 % of its own width, a third further than 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.
   → 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, which is every width from the phone
   band up: 1.00 CSS px, on the contour weight exactly. */
.cf-process__figure .cf-iso { --trace-weight: 1.82; }
/* The band where the OTHER cap binds, and the one place a number cannot fix.
   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 320 px of viewport and 0.53 at 608. That is a 2.4x ramp
   against a fixed multiplier, so no single weight lands on 1 px across it: the
   value that fits one end misses the other by the same 2.4x. It keeps the
   constant every object carried before this property existed, which puts the
   band at 0.45 - 1.06 CSS px, and the ramp is stated here rather than papered
   over. A weight that tracked it would have to be read off the rendered box,
   and only the box knows. */
@container (max-width: 34.625rem) {
  .cf-process__figure .cf-iso { --trace-weight: 2; }
}
/* 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. 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, and that is an edge in the sense
   foundations/geometry.html means it. */
.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);
}
/* 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;
    border-bottom: 0;
    border-right: var(--stroke-1) solid var(--border-strong);
  }
  .cf-process__panel { padding: var(--space-12); }
}

/* --------------------------------------------------------------------------
   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. */
.cf-info-card--glass::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--stroke-1);
  background-image: var(--glass-rim-light), var(--glass-edge);
  background-repeat: no-repeat, no-repeat;
  background-size: 40% 100%, 100% 100%;
  background-position: -70% 0, 0 0;   /* parked off the left edge */
  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-info-card-rim linear both;
      animation-timeline: view();
    }
  }
}
/* background-position is a list property, so both layers restate in each
   keyframe. The second pair is the designed rim and never moves. */
@keyframes cf-info-card-rim {
  from { background-position: -70% 0, 0 0; }
  to   { background-position: 170% 0, 0 0; }
}
/* 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;
}
.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);
}

/* --------------------------------------------------------------------------
   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 34.625rem 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; }

/* --------------------------------------------------------------------------
   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;
  background-size: 200% 100%;
  background-position: 0 0;   /* parked in the gradient's transparent half */
  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: 100% 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; }
}

.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: 200% 100%;
  background-position: 0 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: 100% 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 not the pair used below.

   HighlightText DOES NOT PAINT. Measured in Chromium 141's forced-colors
   mode (the emulation this system is checked against, scripts/check-
   runtime.py): a bare `color: HighlightText` on plain text, with or without
   a `background: Highlight` beside it, produces zero visible glyphs — the
   box takes its background and border, the text takes its layout box, and
   no ink lands. SelectedItem/SelectedItemText, the newer pair for the same
   meaning, fails identically.

   MARK/MARKTEXT ALSO DOES NOT PAINT, AND ONLY UNDER THE DARK THEME — the gap
   the previous measurement here missed, because it only checked one. Same
   Chromium 141, same emulation, `color-scheme: dark` added: `background:
   Mark; color: MarkText` on `<span aria-current="page">1</span>` renders a
   solid opaque block the exact size of the glyph's layout box — zero ink,
   same failure shape as HighlightText, not merely low-contrast. Restating
   `-webkit-text-fill-color: MarkText` beside it, the fix the rest of this
   file uses for exactly this class of bug, does not help: the value itself
   is what fails to paint here, not a missing second declaration. `color-
   scheme: light` with the identical rule is fine, which is how this shipped
   unnoticed — the pair was verified once, in the theme that works.

   ButtonFace/ButtonText is the replacement, and it is verified in both:
   Chromium 141, forced-colors active, `color-scheme` light and dark, `<span
   aria-current="page">1</span>` and `<span aria-current="page">Telemetrie
   </span>` (.cf-article__tag below) — legible glyphs, both themes. It is
   also not a new keyword in this system: .cf-btn--solid already reaches for
   it under forced colours, in this same file, and restates -webkit-text-
   fill-color beside it for the reason base.css's ::selection comment gives.
   "Currently selected" reading as a button face is a smaller mismatch than
   the "currently matched" one Mark/MarkText was already accepted for below,
   and the page's own aria-current still carries the true meaning for
   assistive tech regardless of which system pair paints it. */
@media (forced-colors: active) {
  .cf-pagination__page[aria-current] {
    background: ButtonFace;
    border-color: ButtonFace;
    color: ButtonText;
    -webkit-text-fill-color: ButtonText;
  }
}

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

/* 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 TWO CONSUMERS, AND IT IS DECLARED ONCE.
   .cf-vacancy is the open-position list on patterns/karriere.html, and it is
   not a second drawing — it is this one. A register is a stack of type blocks
   standing on hairlines, and a job opening and a search hit are the same
   object at that level: a mono line, a linked title, a couple of sentences.
   So every rule below names both, 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 a result has no use for: a <dl> of
   facts (Standort, Anstellung, Umfang, Start). That is declared on its own,
   below, and nothing else is. → components/vacancy.html */
.cf-results,
.cf-vacancies {
  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 {
  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. */
  background-image: var(--sheen-panel);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  background-position: 0 0;
  transition: background-position var(--duration-slow) var(--ease-standard);
}
.cf-result:hover,
.cf-result:focus-within,
.cf-vacancy:hover,
.cf-vacancy:focus-within { background-position: 100% 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 {
  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 {
  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 {
  color: inherit;
  text-decoration: none;
}
.cf-result__link:hover,
.cf-result__link:focus-visible,
.cf-vacancy__link:hover,
.cf-vacancy__link:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: var(--stroke-1);
}

.cf-result__excerpt,
.cf-vacancy__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); }
/* ButtonFace/ButtonText, not Highlight/HighlightText and not Mark/MarkText —
   see the identical selected state on .cf-pagination__page[aria-current],
   which measures both failures: a bare `color: HighlightText` paints no
   glyphs in Chromium 141's forced-colors mode, on this element or in
   isolation, and `color: MarkText` paints none either once `color-scheme:
   dark` is added to the same mode — a solid block the size of the word's
   layout box, not merely low contrast. ButtonFace/ButtonText is the pair
   measured legible in both themes, and .cf-btn--solid already reaches for
   it under forced colours elsewhere in this file. */
@media (forced-colors: active) {
  .cf-article__tag[aria-current] {
    background: ButtonFace;
    color: ButtonText;
    -webkit-text-fill-color: ButtonText;
  }
}

/* --------------------------------------------------------------------------
   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);
}
.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-label);
  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. */
.cf-prose a {
  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:hover,
.cf-prose a: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-prose :is(ul, ol) { margin-left: 0; padding-left: 0; list-style: none; }
.cf-prose li:not(.cf-plot__col) { position: relative; padding-left: var(--space-8); }
.cf-prose li + li:not(.cf-plot__col) { margin-top: var(--space-3); }
.cf-prose ul > li::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)::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. */
.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);
}

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

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

/* --------------------------------------------------------------------------
   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. Nothing else in
   the system needs this, because nothing else in it is focusable AND flush
   with both edges of the screen — .cf-table__scroll, the other scroll box with
   the same three attributes, sits inside a .container. */
.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. */
.cf-team-grid {
  /* The cells are drawn by their own borders, so the tiles must touch. */
  --tile: 21rem;
  --flow: 0;
  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.

   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(100% - 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.

   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 {
  --inset: 15%;
  border-right: var(--stroke-1) solid var(--border-strong);
  border-bottom: var(--stroke-1) solid var(--border-strong);
}
.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. */
  width: calc(100% - 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. */
@supports (grid-template-rows: subgrid) {
  .cf-team-grid__item {
    display: grid;
    grid-row: span 4;
    grid-template-rows: subgrid;
    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. */
.cf-table__scroll,
.cf-prose__table-scroll { overflow-x: auto; }

.cf-table,
.cf-prose table {
  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 that carries the table's minimum width. Two reasons,
   and the second is the one that matters. A label broken over two lines is a
   label read as two words, which is why nothing else in this system wraps one
   either. And a table with no floor to its width does not overflow, it
   compresses: every cell wraps to three lines and the scroll box it is sitting
   in never engages, so the mechanism that makes a wide table readable on a
   phone is present and never used. The heads are the floor. Which is also why
   the do/don't asks for four or five columns and short labels — a long label
   here is a scroll bar everywhere. */
.cf-table thead th,
.cf-table th[scope="col"],
.cf-prose thead th,
.cf-prose th[scope="col"] {
  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 {
  margin-bottom: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  text-align: left;
  color: var(--text-secondary);
}

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

/* --------------------------------------------------------------------------
   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);
  }
  .cf-value-row__body { padding: var(--space-8); 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); }
/* 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);
}

/* 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);
}
.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-pill);
  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: border-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}
.cf-field__file::file-selector-button:hover {
  border-color: var(--text-primary);
}
.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 — see the note at the read-only rule for the one case
   that has no line left to thicken and therefore keeps the global ring. */
.cf-field__input:focus,
.cf-field__textarea:focus,
.cf-field__select:focus,
.cf-field__file: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. */
.cf-field__input[disabled],
.cf-field__textarea[disabled],
.cf-field__select[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. */
.cf-field__optional {
  margin-left: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-light);
  letter-spacing: 0;
  text-transform: none;
}

/* --------------------------------------------------------------------------
   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. The section header above it draws that hairline; giving the
   list one of its own is what --flush exists to avoid.
   -------------------------------------------------------------------------- */
.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. */
    .cf-footer {
      view-timeline-name: --cf-footer-arrival;
      view-timeline-axis: block;
    }
    .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);
}
.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);
  letter-spacing: var(--tracking-wide);
  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); }

.cf-consent__dialog {
  width: min(38rem, calc(100vw - 2 * var(--space-4)));
  max-height: min(80vh, 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 below 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 NOW 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.
   The terms below 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.

   VH FIRST AND DVH ON TOP, THE SAME PAIR .cf-nav__list[data-open="true"]
   ALREADY CARRIES FOR THE IDENTICAL SHAPE (max-height cap, overflow-y: auto).
   This box is bottom-anchored (`.cf-consent`'s own `inset: auto 0 0 0`) and
   clipped rather than scrolled, which the hero above is not — the hero's own
   note argues vh alone is right for it because the hero is meant to run past
   the fold and an oversized `vh` costs it nothing. A clipped, bottom-anchored
   box is the opposite case, the one the nav list and the two pinned stages
   already carry dvh for: iOS Safari's `vh` is the chrome-collapsed viewport,
   larger than what is on screen while the toolbar is still showing — which it
   is on the one load this banner ever matters for, before anything has been
   scrolled. A cap sized off the larger figure lets the banner grow taller
   than the true visible screen and, anchored to the bottom, pushes its top
   edge up into the 72 px this whole block exists to protect — the failure
   this backstop was written to prevent, reopened by the one unit that tracks
   the chrome it was already reasoning about. dvh costs nothing where it is
   missing: a browser with no dvh support keeps the vh line above it. */
@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; }
  .cf-consent {
    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;
  }
}

/* --------------------------------------------------------------------------
   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__trace   the signal reaching the object — draws itself
     .cf-iso__node    construction points
   -------------------------------------------------------------------------- */
.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. Its fill still comes up last, on the range it always
       had: two animations, one for the arrival and one for the light. */
    /* 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. Its fill still comes up last, on the range it always
       had: two animations, one for the arrival and one for the light. */
    .cf-iso--build .cf-iso__light {
      animation-name: cf-iso-light, cf-iso-build;
      animation-timeline: --cf-iso, --cf-iso;
      animation-range: cover 30% cover 48%,
                       cover calc(5%  + var(--stage, 0) * 7%)
                       cover calc(22% + var(--stage, 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 orbits are excluded rather than retimed: they are ghosts that turn,
       and their two-animation rule above is written against 20–50 %. Without
       the :not() this selector would win on specificity and take the turn
       away with it. */
    .cf-iso--build .cf-iso__ghost:not(.cf-iso__orbit) {
      animation-range: cover 2% cover 14%;
    }

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

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

/* --------------------------------------------------------------------------
   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. */
@media print, (prefers-reduced-motion: reduce) {
  .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; 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);
    }
    .cf-pin .cf-iso__trace {
      stroke-dasharray: 1;
      animation-name: cf-iso-draw;
      animation-range: contain calc(var(--i) * 25% + 5%) contain calc(var(--i) * 25% + 13%);
    }
    .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); }

.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 */
    .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-area: 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);
    }
    .cf-values__grid-layer:nth-of-type(1),
    .cf-values__grid-layer:nth-of-type(1) line { animation-range: contain 16.667% contain 27%; }
    .cf-values__grid-layer:nth-of-type(2),
    .cf-values__grid-layer:nth-of-type(2) line { animation-range: contain 33.333% contain 43.667%; }
    .cf-values__grid-layer:nth-of-type(3),
    .cf-values__grid-layer:nth-of-type(3) line { animation-range: contain 50%     contain 60.333%; }
    .cf-values__grid-layer:nth-of-type(4),
    .cf-values__grid-layer:nth-of-type(4) line { animation-range: contain 66.667% contain 77%; }
    .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 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. 32 px is
     the 8 px drop plus an 11 px label at --leading-normal, plus air. */
  margin: 0 0 var(--space-8);
  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;
}

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

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

/* --------------------------------------------------------------------------
   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. */
@media (forced-colors: active) {
  .cf-arrive__line,
  .cf-arrive__plate {
    border: var(--stroke-1) dashed 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 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 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 three members rather than three copies: 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-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));
}
