Foundations · 01

Colour

Seven core colours, four accent ramps and a nine-step neutral scale. The site lives in the neutral scale — the accents are events, not surfaces. The colour names stay in German: they are the brand's own names for them.

Core palette

Click to copy the hex value.

Seven, and no eighth

The dark surface of the site — footer, logo plate, consent banner, nav bar — is Schwarz, one of the seven. It used to be an eighth token, an anthracite #1B2022 sampled out of the mockups and declared as --cf-anthrazit because it could not be mixed from the palette. That was the reason to keep it and it was not a good enough one: anthracite and black differ by 1.2:1, which is a difference nobody can see except as a mistake when the two land next to each other — and they did, on the solid button, whose rest state was anthracite and whose hover was black.

So the eighth colour is gone. --surface-inverse resolves to --cf-schwarz, every dark plate in the system is the same black, and the palette is exactly the seven the manual names. Where a dark surface needs a second value — the solid button's hover, a raised panel inside a dark section — it comes from the neutral ramp, which is where every other surface in the system already comes from.

Depth on black is built upward, and it is the only theme that can be. Black is the floor of the neutral scale — there is no rung below it — so on an inverse surface the step moves away from the base upward: --surface-raised is grey-800 and --surface-sunken is a 6 % white veil, which renders #0F0F0F over Schwarz and keeps their old order and separation. The light theme used to climb toward white as well, and no longer does. It cannot: the wash it sits on ends at white, so a lighter step has nowhere left to go and dies at the bottom of every screen. See A step away from the page is a ratio.

A step away from the page is a ratio, not a grey

--surface-sunken was grey-100, #E7E7E7. That number is only meaningful against --surface-base, CF-Grau — and no page in this system is painted CF-Grau. Every page carries .page-wash, and the wash is background-attachment: fixed at background-size: cover. Fixed and cover together mean it spans the viewport rather than the document, so it does not scroll with the page: what is behind any element is a function of where that element currently sits on screen, and it runs the full CF-Grau-to-white every single time. The panel steps were calibrated against a colour that exists in the top row of pixels and nowhere else.

Measured on patterns/landing-page.html at 1280 × 900, walking the process card's note block down the viewport and sampling the wash beside it out of the same screenshot row:

Note atWash beside it#E7E7E7 reads6 % veil reads
14 % of viewport height210+21 — raised−13
44 %222+9−13
65 %2310 — invisible−14
86 %241−10 — sunken−14

An absolute grey inside the wash's own range is raised at the top of the screen, gone at two thirds of it, and sunken below that: one block that changes which way it steps as the reader scrolls past. Nothing renders wrong — the material simply is not one. A veil is the same declaration made relative. Six per cent black removes six per cent of whatever light is there, which is a constant 0.858 ratio in linear light at both ends of the wash, so the step is the same step everywhere.

Contrast moves with the surface and the floor is the CF-Grau end: --text-secondary measures 5.19:1 there, 6.48:1 in the middle and 8.03:1 at the white end; --text-primary never drops below 11.91:1. --text-muted still may not sit on it, for exactly the reason it may not sit on CF-Grau itself.

--surface-card is retired. It was grey-050 — the same value as --surface-raised, so wherever the two were stacked one of them painted nothing — and its only users were the process card and the stacked form of the Expertise step. Both are contour on the wash now, which is what Materials already claimed they were. A panel that needs a surface takes the veil; a panel that needs a boundary takes a contour. There is no third thing to reintroduce.

Ramps

Each accent is mixed with white (100–300) and with black (700–900). Step 500 is always the brand colour. UI states — hover, disabled, backgrounds — use these steps only, never opacity on the 500.

Lime
100 200 300 500 700 800 900
Glas
100 200 300 500 700 800 900
Sky
100 200 300 500 700 800 900
Violett
100 200 300 500 700 800 900

Neutral scale

The actual working palette. CF-Grau is step 300 — the anchor, not white.

Grey
000 050 075 100 200 300 400 500 700 900

--grey-400 (#919191) does not come from the manual but from the Figma redesign: it is the colour of every mono label and meta line. That is why it sits deliberately next to --grey-500 in the scale.

There is a misprint in the manual's shade plate, and this ramp follows the paint rather than the label. On the neutral column the 70 %-white row is painted #F1F1F1 but labelled #D9D9D9. The painted value is the arithmetically correct one — mixing CF-Grau with 70 % white gives 0.7·255 + 0.3·207 = 240.6, i.e. #F1. Sampling every one of the 35 swatches on that plate turns up exactly this one mismatch; all four accent columns agree with their labels. #D9D9D9 is kept in the scale as --grey-200 because the mockups do use it as a UI grey — it simply is not a step on the white ramp. The plate is on Source Material if you want to check it yourself.

Semantic tokens

Components use these only — never a ramp value directly.

TokenValueUsed for
--surface-basegrey-300the base surface of the page
--surface-sunkenblack 6 %inset blocks, note panels — a veil, not a grey, so the step is the same step wherever the block sits on the wash
--surface-raisedgrey-050figure plates that already carry a contour, and the hero still, which sits on video rather than on the wash
--surface-inverseblackfooter, logo plate, nav bar, dark sections
--surface-glasswhite 46 %frosted layer that carries text — the tint at which black type clears 4.5:1 over any backdrop
--surface-glass-veilwhite 4 %frosted layer that carries only opaque objects; the blur does the work
--surface-glass-solidgrey-100the opaque stand-in when the browser cannot blur, or the reader asked for less transparency
--text-primaryblackheadings, body copy
--text-secondarygrey-700descriptions, secondary text
--text-mutedgrey-400mono labels, counters, meta
--border-strongblackthe 1 px contour — the default frame
--accentlimeone lit element per object, and — where objects are sequential — one per screen. See below.
--gradient-lightlime → glas → grey, 132.36°isometric surfaces, primary button
"Exactly one element per screen" is not what this system does, and the wording is the thing that has never been tested. Counted against a scroll position rather than against a document — sweeping each page and counting .cf-iso__light elements that are both inside the viewport and above fill-opacity 0.15 — three pages carry four at once, by construction:
PageLightsMost on one screen
patterns/landing-page.html at 1440 × 90044 — the pinned stage assembles all four cards in place
the same page at 768 and 37541 — stacked, so they pass one at a time
patterns/expertise.html44 — four fields sharing a row
prototypes/expertise-scroll.html44
foundations/illustration.html32 — showing the vocabulary, not using it
patterns/404.html, blog-artikel.html11

Every one of those fours is a layout fact rather than a timing one: objects that share a row, or a pinned stage, enter and leave together and no animation range can separate them. So either three pages are wrong or the sentence is, and the practice the system actually follows — everywhere, without exception — is one light per object. The per-screen reading is a consequence of that wherever objects are sequential, and only there.

This was nearly fixed as a timing bug, and the measurement is why it was not. Before the pinned stage landed, the landing page put two lights on one screen at 1440 × 900 and one at the other two sizes, because every light came up on entry and then stayed up under animation-fill-mode: both — so a screen held every light it had ever passed. Dimming each light on the exit phase of the timeline that raised it took that 2 back to 1. Then .cf-pin arrived and made the four cards simultaneous instead of sequential, the 2 became a 4, and the same mechanism measured 4 before and 4 after — no change on any page in the tree. It was removed rather than shipped. A mechanism whose justification you cannot measure is one more thing for the next person to explain.

Gradients — one ramp, three rakes

There is only one gradient in this brand: lime falling away to CF-Grau, with Glas where the two meet. Lime is the light source, CF-Grau is the surface, and the single thing that varies is where Glas sits — how steeply the light rakes across. Three positions are sanctioned, in the same way four angles and four dash ratios are, and all three are measured off the designer's own files rather than chosen. Anything else is drift.

TokenGlas atMeasured from
--rake-near32 %Figma 132.36° linear · 01-discovery.svg (0.32)
--rake-mid51 %Figma 239.25° linear · 03-weniger-ausfaelle.svg (0.51)
--rake-far64 %Figma 45.72 %/95 % radial · 04-mehr-leistung.svg

The three swatches above are .material-rake, the shipping class, not a drawing of it — --mid and --far move the rake and --mirror reads the band from the other end, for a face turning away. It is the light layer with its one free parameter exposed; .material-light is the same ramp at the designer's 132.36° with his inset stops kept verbatim. → Materials

This row used to lie, and the lie was the colour space.

These swatches were three inline style attributes, and an inline style cannot be reached by the @supports (in oklab) block that carries the rest of the family. So the drawing that teaches the ramp was the one place in the system still painting it on the raw sRGB path: at the crossover of the lime leg, sRGB gives #D4F66A where oklab gives #D3F798 — a ΔEok of 0.045, the family's second-worst leg, and plainly visible at 7 rem square. Three more of the same literals sat on Illustration. Composing the image on a class puts all six back on the family's own path.

Why the Figma numbers look different.

Figma states its stops across the whole fill box, with flat colour either side of the ramp. Renormalise to the band and they land on the rakes exactly: (40.45 − 24.97) ÷ (73.05 − 24.97) = 32.2 %, and (43.85 − 4.82) ÷ (81.19 − 4.82) = 51.1 %. The two numbers that had looked like two unrelated gradients are the same ramp at two rakes.

The two halves — sweep and bloom

Four of the six gradients in the Figma dump are radial. The ramp is the same one; what changes is whether the light arrives as a sweep across a face or as a bloom out of a point. Both halves are classes for the same reason — the ramp is written once and the rake, the angle and the shape are parameters.

ClassShapeRakeThe light is
.material-rake--grazinglinear 239.25°any; the dump's own is --midgrazing a long surface
.material-bloomradial circle--mid, mirroredon the rim, surface at the centre
.material-bloom--isoradial 2:1 ellipse--mid, mirroredthe same, on an isometric plate
.material-bloom--topradial 45.72 % × 95 % from 50 % 0 %--far, inset 26.66 %entering a surface from its top edge
A bloom is clipped to an object. The swatches above are not, and that is why they read lime.

The two mirrored blooms put lime at the rim, so painted edge to edge on a rectangle the outer half of the box is lime and the thing spends the screen's one lime moment on a swatch. In use it is clipped — to a rhombus, a plate, a body of revolution — and what the reader sees is the lit edge of an object with CF-Grau in the middle of it. Both of the designer's own live inside a <path>. These four are drawn full-bleed because this is the page that shows the ramp, the same licence the three rakes above take. Do not copy the swatch; clip the bloom.

The bloom from above is the far rake, and that is arithmetic.

Its stops are lime 26.66 %, Glas 73.62 %, CF-Grau 100 %, so the band runs 26.66 → 100 and Glas sits at 26.66 + (100 − 26.66) × 0.64 = 73.60 % against the designer's 73.62. Two hundredths of a point — his exporter rounding 64.03 to the sanctioned 64. The inset is kept verbatim, because a lime core in the middle of a light source is part of the drawing; the rake under it is derived like every other.

The isometric bloom is 2:1 wide, and the source does not say so.

The retired --gradient-bloom-iso cited 02-datenfundament.svg, scale(120 59.88), as its authority for a 2:1 wide ellipse. The full transform is translate(…) rotate(90) scale(120 59.88), and the rotate swaps the axes: 59.88 wide by 120 tall. The designer's ellipse is 1:2 tall, and so is the Figma dump's (50 % 100 %).

It is tall because the thing it lights is. The rhombus under it in card 02 is 120 × 240 and the bloom is exactly half of it on both axes — a light fitted to an object, not a projection rule. A bloom fitted to its object needs no token, because it is drawn with the object in SVG, which is where both of the designer's live. What the system needs a token for is the other thing: the circle seen in 2:1 isometry, which is 2:1 wide — the shape --field-bloom's pool of light already is. So the shape is right and the citation was wrong, and the two were never in contact until someone read the transform.

One ramp, two colour spaces.

The CSS gradients interpolate in oklab where the browser supports it. SVG cannot: its color-interpolation attribute offers only sRGB and linearRGB, so an inline <linearGradient> is always sRGB. That split matters on the legs that leave lime, and on no others. Lime crosses a wide arc of hue on its way to anything, and on both of its legs the two paths pull apart far past the just-noticeable threshold for a smooth ramp, with sRGB taking the darker, more saturated route through chartreuse:

LegΔEokWhere
lime → Weiss0.04651the spectrum's first leg
lime → Glas0.04512every rake
Glas → Sky0.00148the glass edge
Glas → CF-Grau0.00073the tail of every rake
the page wash0.00071end to end
Weiss → Glas0.00045the panel sheen

Everything under a thousandth stays on the sRGB path untouched. The two lime legs do not, and the difference between them is worth stating plainly, because a comment in the dark wallpaper once granted lime → Weiss the exemption that belongs to Weiss → Glas and left the mark running through the dip on the worst leg in the family. They are different legs. Only the second is exempt.

Every leg that leaves lime in an inline SVG therefore carries one waypoint, #DBFC60 at 19 % of the leg, which is the colour the oklab path passes through there — exactly, not approximately. The path at t = 0.19 comes back as R 219.05, G 252.05, B 96.16, which is #DBFC60 at 8 bits, every channel well inside its rounding margin. The hex is re-derivable rather than hand-tuned: recompute the ramp and you get it back. One stop is enough: it takes the divergence to ΔE 0.0116, below the threshold. Evenly spaced stops do not work — the curve bends hardest near the lime end, so three even stops still leave ΔE 0.0119 while one placed at 19 % matches it. The waypoint is scaled into each gradient's own offset space: 0.061 on the near rake, 0.097 on the mid rake, 0.905 and 0.822 on the two radials that run Glas → lime.

The lime → Weiss leg takes the same treatment at the same 19 %: #E6FF66, at offset 0.0342 in the dark wallpaper's own space, which drops that leg from ΔE 0.0465 to 0.0125. 19 % is the family's convention rather than that leg's optimum — the best single waypoint for lime → Weiss sits at 21 % and buys 0.0013 — and one number across every drawing is worth more than a thousandth per drawing. A second waypoint reaches 0.0092 and no further on either leg, because what remains is the short segment before the first stop, which nothing placed after it can straighten. One stop, and no more.

Every ΔE above compares the two interpolation paths continuously, before the 8-bit quantization both are subject to. Rounding each path to 8 bits first adds a few thousandths of noise — invisible against the 0.044 on the lime leg, but several times larger than the real difference on the two near-neutral legs, where it makes 0.0007 read as 0.0037. Measure continuously, or the noise floor is what you measure. Sampled off rendered 8-bit pixels in Chromium the lime leg reads 0.045 and the waypointed leg 0.0124, which is the same few thousandths arriving as a floor.

The correction is not unconditional. The oklab gradients sit behind @supports; the base CSS declarations are sRGB. A browser that fails that query therefore takes the sRGB route in CSS while the SVGs carry the corrected path, which puts the two back out of step by ΔE 0.034 — in the opposite direction, where before they at least matched. Not the full 0.044: that is the gap between the sRGB path and the oklab one, and what these two browsers disagree about is only the part the waypoint corrected. That is the accepted cost: oklab gradient interpolation is supported across all mainline browsers, so the affected population is small and shrinking, and the alternative is holding the whole system to the worse curve for it.

The convention is now run rather than read. It held across twenty gradients and broke on the twenty-first: all four lights on Expertise were drawn on the near rake, Glas at 0.32, and carried the mid rake's waypoint offset of 0.097 — so the colour that belongs at 19 % of the leg was painted at 30.3 % of it. Nothing rendered wrong and no reference broke; the ramp simply held lime-yellow half again as long as it should and then hurried into Glas, at ΔE 0.0265 against the family's 0.0116, which is 60 % of the way back to carrying no waypoint at all. scripts/check-gradient-family.py reads every gradient the site ships and derives the answer instead of trusting the literal: it recomputes both the waypoint's position, from the leg's own two ends, and its colour, from the oklab path, so #DBFC60 and #E6FF66 are re-derived on every run rather than checked against a list. It also catches the other way this drifts — a stop that is almost a chromatic brand colour, which is what #E0FF02 is. It runs in CI on every pull request.

The arc — why the two foils carry a waypoint of their own.

The waypoint above puts an sRGB leg back on the oklab path. This is the same move one space further out, and it exists because oklab is not the right path for a leg that turns. The foil's whole claim is hue travel inside a narrow luminance band. Two stops of similar chroma and different hue sit at roughly the same distance from the neutral axis, so the straight line oklab draws between them is a chord, and the middle of a chord falls inside the circle. Chroma sags where no stop asked it to:

Legchordarcmidpointwaypoint
Glas → Sky0.03830.0452+17.9 %#B9E3EB
Sky → Violett0.05530.0588+6.3 %#B8CCF3
Glas 800 → Sky 8000.02540.0289+13.6 %#33494E
Sky 800 → Violett 8000.04840.0513+6.0 %#273650

A ramp that loses a sixth of its chroma halfway through its longest leg is greying out exactly where it is supposed to be turning, which reads as a fade with a colour cast at each end rather than as one material seen at an angle. This is not an argument for more saturation — that is the one thing the brand's definition of iridescence forbids, and this does not do it. Peak chroma on both ramps is unchanged: 0.0686 lit and 0.0620 ink, both still the endpoints' own values, and no point on either ramp is now more chromatic than the palette colour it is travelling between. The sag is removed; nothing is added.

The correct path is polar — and in oklch is not used. OKLCH interpolates L, C and h each linearly, so it holds chroma across the turn, and it was the obvious answer. It fails on a measurement rather than on a principle: Chromium discards the hue of a low-chroma stop. Rendered and sampled, linear-gradient(90deg in oklch shorter hue, #45524F, #283E4F) — Glas 800 to Sky 800, the ink foil's first leg — comes out with hue pinned flat at Sky's 242.5° for its entire length. Glas 800's own 179.95° never appears, at t = 0 or anywhere else. Sweeping the start stop's chroma in steps of 0.004 at fixed hue puts the threshold between 0.016 and 0.020: at or above about 0.018 the declared hue renders, below it the neighbour's is carried forward. Glas 800 is C 0.0171. The one ramp with the most to gain from a polar path is the one a polar path silently flattens — it would ship 39.7° of its 102.2° of hue travel. Writing the stops as oklch() literals renders correctly and was rejected for a different reason: it takes all three ramps off the palette tokens.

So the arc goes into the ramp, as one waypoint per chromatic leg at that leg's midpoint — exact where it sits, and engine-independent, which in oklch is not. Max deviation from the true polar arc over 2,001 samples falls from 0.00722 to 0.00212 on the lit foil's chromatic legs and from 0.00741 to 0.00257 on the ink foil.

Every measured figure on this page survives, by construction rather than by luck. Polar interpolation moves C and h and leaves L alone, so each waypoint's lightness is the mean of its leg's endpoints and lands on the linear L path that was already there — within 9.2 × 10−4, which is the 8-bit grid and not the arithmetic. Band depths (0.090, 0.115, 0.180 counting the specular), hue travel (104.5° / 102.2°) and every endpoint are untouched. Re-measured over the whole ramp anyway: 11.900:1 on black, 5.239:1 on CF-Grau, 8.162:1 on white — the same three numbers to three decimals.

No waypoint on a leg that ends in grey, and that is where this stops. --gradient-light, --gradient-light-90, the rakes, the blooms and the page wash all run into CF-Grau or out of Weiss, and the lit foil's own Weiss → Glas leg is the same case. An achromatic stop has no hue, so there is no arc for a chord to fall short of: oklab's straight line from the neutral axis to a chromatic stop already is the constant-hue radial. Only a leg chromatic at both ends turns, and only a leg that turns can cut a corner.

And no waypoint on a leg carrying lime, which is the second half of the same sentence and was left unwritten because it is invisible on the four legs above — every one of them sits inside the foil's narrow band. The chord argument has two premises, not one: the two stops must differ in hue and sit at roughly the same distance from the neutral axis. Lime is C 0.2201 and Glas is C 0.0414, so the move between them is radial rather than tangential — the polar path through it bows out to #A8FFB6, a green in no palette at C 0.1310, three times the chroma of the stop it is travelling to. That is the achromatic case again and it takes the same answer: oklab's straight line is already correct. It is exactly why the light family is corrected with in oklab and the SVG waypoint while the spectrum and the two foils are corrected with an arc. Lime is the light source and CF-Grau and Weiss are what it falls away to; a leg touching any of the three is a falloff, and only what happens in the band between them turns.

The four hexes are literals, like #DBFC60 — and they are now derived on every run rather than trusted. They carried the same trap the SVG waypoint did before it was checked: they do not follow the palette, so moving an endpoint left the ramp quietly back on the chord. scripts/check-gradient-family.py reads the shipping stylesheets as well as the drawings now. It resolves each var() down to a colour, recognises a waypoint by its being the polar midpoint of the two stops either side of it, and then compares that value exactly — so #B9E3EB, #B8CCF3, #33494E and #273650 are recomputed from OKLCh on every run and never checked against a list. A leg that turns and carries nothing is reported as a chord.

It checks one more thing, and that one had already slipped. A ramp carrying a lime leg has to appear somewhere on the oklab path, because lime → Glas is the single leg where the two paths actually part — ΔEok 0.0443, against 0.0015 and below for every other leg in the family, which is why --glass-edge and --sheen-panel are left in sRGB by name. Every lime ramp in the system was on that path except one: .cf-btn--glass drew Glas into lime with no @supports branch at all, measuring ΔEok 0.03866 composited over CF-Grau at its own alphas — seventy-nine times the divergence of the layer one step above it that the family deliberately leaves alone. Its stops are a custom property now and only the path changes, the way .material-rake and .text-foil already did it. Nothing about the drawing moved: the rim's 62 % window is measured off where this face's lime becomes visible and is unaffected by an interpolation path.

TokenShapeUsed for
--gradient-lightlinear 132.36°, nearthe signature — isometric surfaces, .material-light
--gradient-light-90linear 90°, near mirroredprimary button, horizontal edges
.material-rakelinear, any rake, --mirror, --grazing (239.25°)the linear half of the family, stated once — see The two halves
.material-bloomradial circle, mid, mirrored; --iso 2:1; --top far, from abovethe radial half, same ramp — same section
--gradient-spectrumlinear 132.36°, five stops plus two arc waypoints.material-foil — surfaces only
--spectrum-stopsthe spectrum's stops, without an anglethe same split the two foils make, so the oklab branch restates nothing
--gradient-foillinear --foil-rake, the spectrum minus lime, renormalised.text-foil — the lit half, inverse surfaces; and every .cf-btn--solid label
--gradient-foil-inklinear --foil-rake, the same three hues in the 800 band.text-foil--ink — the shadow half, light surfaces
--foil-rake116.57° = 90° + 26.57°the angle both foils are drawn at head-on
--foil-stopsthe lit half's four stops plus two arc waypoints, without an anglethe material, so the rake can change without restating it
--foil-ink-stopsthe shadow half's three stops plus two arc waypoints, without an anglethe same, for the 800 band
--foil-glas-n, --foil-sky-n, --foil-ink-sky-nthe renormalised stop positions as bare numbersso each waypoint's position is the average of its leg's ends rather than a third picked figure — see The arc
--spectrum-hot18 % — the white hot spotshared by both foil gradients; not used alone
--spectrum-cool64 % — where Sky takes overshared by both foil gradients; not used alone
--surface-page-washlinear 180°, the foil reversed at chroma 0.005the page itself — .page-wash. See The page wash
--wash-stopsthe wash's four stops, without an anglethe same split as the two foils, so the oklab branch restates nothing
--grainSVG fractal noise tilethe second half of the wash's dithering, at 8 %; the same filter is inlined in all three wallpapers, at 4 % on the two dark ones. See The page wash
--hero-dissolvemask, linear 180°, an (1 − t)^1.45 falloff over the last 25.6 %how full-bleed artwork ends — see Where artwork meets the page
--hero-scrimlinear 63.43°, CF-Grau at --scrim-depth to nothingthe light layer under hero type; same section
--scrim-reachmin(39rem, 62 %) plus the content column's own displacement, projected onto the axishow far the scrim holds full before falling off. The second term is zero up to a viewport of 1438.2 and sin(63.43°) of --column-inset − --gutter above it: past --container-max the column centres and slides right, and a constant plateau leaves the headline behind. Held by scripts/check-hero-scrim.py
--scrim-depth0.42, and 0.5 at or below 56.25 remhow deep the scrim is across the plateau. The reach answers the width axis; this answers the aspect one, because cover on a square artwork puts a different part of it under the type as the box changes shape. Below the breakpoint the hero's media box is 567 px tall and the kicker sits on the square's dark band — 4.03:1 at 375 × 812 against a 4.5:1 floor. Recomputed by scripts/check-hero-contrast.py

Where artwork meets the page

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

The dissolve

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

The designer's mockup has no edge at all. The artwork decays continuously into CF-Grau and the page simply carries on. Measured off assets/source/mockups/landing-page.jpg as the mean absolute channel deviation from #CFCFCF over each full row — which is what alpha is, when the thing composites onto CF-Grau — the decay runs from y 598 to y 804 in an 804 px hero: the last 25.6 %. Fitting a = (1 − t)^k across the band gives k = 1.31, 1.43, 1.46, 1.46 at the four quartiles, so the token is that power law at k = 1.45, sampled at the quartiles.

It is a mask, not an overlay, and that is why it can be one token. An overlay has to be painted the colour of whatever is behind it, so it would have to know that the hero sits on the page wash and at what point of the ramp. A mask hands the question back to the page: the artwork stops being there, and whatever the page is at that height is what you see. .cf-hero therefore carries no background of its own — an opaque one would only have swapped the artwork's seam for a second, flatter one, since the wash is fixed-attachment and no colour declared on the hero can match it at that height.

The reading scrim

The hero sets black display type, a black hairline and a 12 px mono line over a loop nobody controls the frames of. Sampled off the rendered page in the text column, the artwork behind the headline reached rgb(48,55,59) and behind the kicker rgb(70,70,81). Both failed their floors, on the page as shipped.

ElementFloorBeforeAfter
Headline — display, large text3:11.74:14.13 – 4.46:1
Kicker — 12 px mono, body4.5:12.26:18.50 – 14.81:1
Rule — UI contour3:12.26:17.93 – 14.08:1

Worst backdrop pixel anywhere inside each element's box, with the type hidden, at 375 / 768 / 1280 / 1920. The still frame the designer composed does not fail: in the mockup the left column is pale grey-blue the whole way down and the colour is all on the right. The loop drifts a dark violet plane under the type instead. The scrim restores the composition rather than correcting it, and how much of it there is was set by what the mockup already looks like, not by the contrast target.

CF-Grau and nothing else — the page's own surface, bleeding up into the artwork exactly where the type sits. White would frost it and black would bruise it; both would read as a plate laid on top, which is the failure mode this has to avoid. It runs at 63.43°, --angle-a, so it rises from the bottom-left corner where the text block is and is gone by the top right, where the artwork is at its best and the glass button needs something complicated behind it to be worth being glass.

Its stops are lengths, not percentages, and that is the one thing here that had to be got right rather than merely chosen. A percentage stop is a percentage of the gradient line, whose length is 0.894·W + 0.447·H — so the same 38 % is 587 px in a 1280 × 896 hero and 402 px in a 768 × 828 one. But the scrim has to cover a text column, and a text column is 44ch wide and four display lines tall whatever the viewport is. Projecting its far corner onto the 63.43° axis puts it at 578 px at 768, 603 px at 1280 and 603 px at 1920 — one number, because the column is one size. With percentage stops the headline read 4.46:1 at 1280 but only 3.58:1 at 768; with 39rem it reads 4.13 – 4.46:1 at every width. The min(39rem, 62 %) is for phones, where the whole gradient line is 623 px and a flat 39rem plateau would sheet the entire picture.

The foil

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

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

Weiss is the deliberate exception. At L 1.000 it sits above that band, and that is precisely why it reads as a specular hot spot rather than as one more step in the ramp. Counting it, the full span is 0.180. Both numbers matter: the band is what keeps the ramp iridescent, the hot spot is what makes it catch.

Gradient in the letters

.text-foil is the opt-in way to put a gradient inside letterforms. It uses the cool half of the spectrum — no lime — so a screen that has already spent its one lime moment can still carry it. Every sample along it clears 11.9:1 on black, which is past AA at any size; on a light surface it fails, so it is an inverse-surface material only. Its counterpart for light surfaces is .text-foil--ink, below.

One foil moment per screen — the brand's own unit for the lime rule, and the right unit here too, because the constraint is what the eye takes in at once rather than what a URL contains. A page whose light-surface headline and whose footer never share a screen may carry one of each, which is the case on both designed pages. Two on one screen is one too many.

Three places the foil lands, governed differently

WhereHowBudget
A display headline on dark .text-foil, opt-in per element Together, one foil moment per screen.
A display headline on light .text-foil--ink, opt-in per element
The black button's label .cf-btn--solid, automatic — no class, no span Unbudgeted. It is what a black button is, not an emphasis on one.

The exemption is not a loophole. A headline in foil is a decision about that headline; a black button in foil is the component's own colour, the same way the primary button's colour is the light ramp. A 12 px mono label does not compete with a display headline, and three black buttons on a page are three buttons rather than three emphases — none of which has spent the page's lime, which is the entire reason the foil exists. The precedent is the manual's app icon (Farben > Anwendung): a black plate whose mark carries the ramp while the plate stays black.

What the budget does still forbid is a foil headline and a black button in one eyeful. At that point the headline is no longer the only iridescent thing on screen, which is the condition the rule exists to protect. Neither designed page hits this today — the footer's headline is paired with the primary button, not the solid one. → Buttons

The button does not take width: fit-content, and that is deliberate. Sizing the clip box to the text is the right fix for a headline in a block, where the box is the whole column and the ramp dies a third of the way in. On the button the gradient box is the whole button on purpose: the label takes the Weiss → Glas → Sky leg and the arrow at the far right sits in Violett, so light enters at the first letter and arrives coloured at the arrow. Shrink that box to the label and every button ramps the full spectrum inside ten characters, which reads as a rainbow rather than as light crossing a plate. Do not propagate the headline's fix to it.

Its stops are not chosen separately. They are the spectrum's own, with lime removed and the remaining band renormalised onto 0–100 %: Weiss (18−18)/82 = 0 %, Glas (32−18)/82 = 17 %, Sky (64−18)/82 = 56 %, Violett (100−18)/82 = 100 %. Keeping the white hot spot is the point — it gives the letterform an edge for the light to land on. Started at Glas instead, the same ramp reads as a flat mint tint rather than as foil.

That arithmetic is written into the token as calc(), not pasted in as finished numbers, so the two gradients cannot drift apart: move --spectrum-cool or --rake-near and the foil follows. This is why the rakes and the spectrum's stops are each declared twice — once as a bare number (--rake-near-n), once as the percentage built from it — because calc() cannot divide a percentage by a percentage, and the renormalisation needs to. Weiss and Violett are 0 % and 100 % by construction, being the ends of the band itself.

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

Jetzt Projekt starten!

<p class="cf-footer__title text-foil">Jetzt Projekt starten!</p>

The shadow half — .text-foil--ink

Every stop in the foil above sits above OKLab L 0.82, so on the page's own CF-Grau it lands between 1.1:1 and 1.5:1. That left the two designed pages — a CF-Grau-to-white wash almost end to end — with no gradient type at all, and the only foil moment on either of them down in the black footer. --gradient-foil-ink is the same material seen from the other side. Not a second gradient: the other half of the one that already exists.

It is built by the rule the foil is built by. Iridescence is hue travel inside a narrow luminance band, so the band moves and the hue travel does not. The ramps are white-mixes and black-mixes of one palette, so the dark mirror falls straight out of them:

StopLit halfLShadow halfLHue
1Glas 5000.910Glas 8000.426179.4° / 180.0°
2Sky 3000.865Sky 8000.353243.6° / 242.5°
3Violett 3000.820Violett 8000.311283.9° / 282.2°
band depth0.0900.115104.5° / 102.2°

Two decimal places of hue apart across three stops, and a band 0.025 wider. It reads as the same material because it is. The stops are the spectrum's own once again — the lit half drops Weiss and rebases off the hot spot, so the shadow half drops the hot spot too and rebases off the near rake: Glas (32−32)/(100−32) = 0 %, Sky (64−32)/(100−32) = 47 %, Violett 100 %. Same 116.57° rake, same reason.

It has no hot spot, and that is the one place the mirror is not symmetric. The foil's Weiss sits deliberately above its band as a specular. The ink mirror of that would be a stop below L 0.311, and there is no room for its opposite either — the lightest ink that still clears 4.5:1 on CF-Grau is about L 0.44, which is where Glas 800 already sits. So the shadow half is the falloff without the highlight, which is what the shadow side of a foil actually looks like. Completing it with Schwarz at 0 % would put a 0.426 span on a ramp whose whole claim is a 0.115 one, and it would read as a fade to black rather than as foil.

The 800s are not a taste choice. They are the lightest rung of the ramp that clears AA on the page's own grey: Glas 700, the obvious pick for a lighter and prettier ink, is 1.5:1 on CF-Grau. Measured on the shipped page header at 375 / 768 / 1280 / 1920, worst sample against the real backdrop: 5.45 / 5.51 / 5.56 / 5.56:1. Against flat CF-Grau it is 5.24:1 and against white 8.16:1, so the worst case is the top of the page wash — this ink darkens as it travels, so unlike the lit half it gets safer further down the page.

Über uns

<h1 class="cf-page-header__title text-foil text-foil--ink">Über uns</h1>
The clip box is sized to the text.

background-clip: text clips a background painted across the whole element box, and a display headline in a block is as wide as its column however short the words are. Measured on the footer title before this was fixed: at a 1920 viewport the box was 1000 px against 378 px of inked text, so the letterforms sampled only the first 36.5 % of a four-stop ramp. Weiss and Glas rendered, Sky barely arrived, and Violett never appeared on the page at all at any viewport above roughly 400 px. The claim above that the shallow rake "spreads it across the whole line" was describing the intent rather than the paint.

.text-foil now takes width: fit-content inside the same @supports block. fit-content and not max-content: it resolves to min(max-content, available), so a short headline shrink-wraps and a long one still wraps and fills its column exactly as before. It sits inside @supports because outside it nothing is clipped, and shrinking the box there would be a layout change for no reason.

What it falls back to.

The clip lives inside @supports, so the unenhanced state is simply the inherited solid colour — nothing is transparent until the browser has proved it can clip. Print and forced-colors both reset to solid type. Both variants share every one of those rules: --ink is a modifier that swaps one custom property, --foil-ramp, so it cannot drift out of the safety net.

A highlight painted over a foil has to state its own fill.

-webkit-text-fill-color is what draws the clip, and it is inherited — where it is set it beats the color of every descendant. So text under a foil headline or inside a .cf-btn--solid label arrives at any highlight already filled transparent, and a highlight that sets only color paints its plate and no word.

Measured before this was fixed: .cf-mark--current — the current match, the rung this brand is named after — rendered as an empty lime block inside every foil headline and every solid button on the site, at every size, on both designed pages. ::selection had been caught earlier and answered the expensive way, with .text-foil and .cf-btn--solid each carrying a private ::selection rule restating the global one plus the missing property. Two clipping contexts, two copies, and the found state never got its own.

The rule is now stated once on the highlight rather than once per clipping context: ::selection, .cf-mark--current, ::target-text and ::highlight(cf-found-current) each restate their ink as a fill, and both private copies are gone. The non-current rung deliberately does not: :where(mark) takes color: inherit because a match the reader has not arrived at is the running text with a ground line under it — inside a foil, the running text is the foil. Only a rung that supplies its own ink has an ink to lose. Held by scripts/check-highlight-fill.py.

The rake swings

Holographic stock is not a picture of a spectrum. It is a surface that hands you a different part of one from every angle, which is why a foil sample book is photographed tilting and a printed one looks dead. Static, the foil is the photograph. The swing is the tilt: the angle the light crosses the letterforms at opens and closes by exactly one brand angle as the type travels up the viewport, scrubbed from scroll position so it tracks the reader's hand and runs backwards when they scroll back.

The contrast floor is unchanged by construction, and that is the reason it is an angle and not a slide. Turning a linear gradient changes where each stop lands, never which stops are present — the whole ramp is inside the box at every frame. So the worst sample of the swinging foil is the worst sample of the static one: 11.9:1 on black, 5.24:1 on CF-Grau. Nothing needs re-measuring per frame, which is a property of the mechanism rather than a lucky result. The two alternatives both would have needed it: a specular band composited over the type raises contrast on the lit half and lowers it on the ink half, and a ramp painted wider than its clip box and slid across leaves the glyphs standing in two thirds of the ramp at any instant, which reads as changing colour rather than as catching light.

90° to 116.57°, and the swing is itself a brand angle. Both endpoints are sanctioned — 90° is the flat rake and 116.57° is 90° + 26.57° — and the distance between them is 26.57°, the isometric angle. The swing cannot drift off the four permitted angles wherever it is stopped, because it is bounded by two of them. It stays shallow throughout on purpose: a headline is a wide short box, and the foil took the shallow rake in the first place because at 132.36° the ramp completes inside the first two or three letters.

116.57° is what you see head-on, which is why the swing turns round at the halfway mark instead of running straight through. You see a material as drawn when you are square to it, so timeline 50 % — the type at the middle of the viewport — is where the rake is the designer's value, and either edge of the screen is an oblique line of sight where it flattens to 90°. That is also the value everything falls back to when the swing cannot run: no @property, no scroll timeline, reduced motion, print, forced colours.

That model has two axes and this is one of them. A view() timeline can say how far the type is above or below the middle of the screen; it cannot say how far to the side of the reader it is, because it measures the element and the model is about the reader. The second axis is a second squareness, multiplied into the same rake so the band cannot be left, and it is measured rather than assumed. → The Line of Sight

Neither foil moment on the two designed pages reaches head-on.

Both sit at a document extreme — a page header at the top, a footer at the bottom — where a view() timeline only has part of its range to work with. Measured at 1280 × 900: the Über uns title runs 74.6 % → 100 % of the timeline, so 103.5° → 90°; the footer title runs 0 % → 34 %, so 90° → 108.2°. Both therefore read between 90° and 108°, opening towards the designed rake as they approach the middle of the screen and never quite arriving. That is the honest behaviour of the model rather than a shortfall in it — a headline the reader cannot bring to the centre of the screen is a headline they never look at square on. A foil moment placed mid-page, such as a blog article title, gets the whole journey.

Those are the vertical figures. The horizontal axis multiplies into them and can only take the rake further from head-on, never closer, so 108.2° remains the ceiling on both pages and is now reached only by a reader standing in front of the type as well as level with it.

.cf-btn--solid does not swing. Its foil is the component's own colour rather than an emphasis, its clip box is the whole button on purpose, and a label whose light shifts while the reader is aiming at it is a different thing from a headline catching it. It reads --gradient-foil from :root, which is static, so it is untouched.

The foil, moving

Everything above is light that has settled. There is one member of the family that has not: the band that crosses a placeholder while the thing it stands for is still on its way. It is the same three chromatic stops as the foil, taken down to 18 % alpha and swept across the object at 26.57° on a 2.4-second loop, and it is documented here rather than only on its component because every decision in it is a colour decision.

Beiträge werden geladen

Three stops, and they are the cool half on purpose

Weiss, Glas and Sky 300 — the foil's own chromatic run with lime removed. That is not a shortening for convenience, it is the same reason --gradient-foil drops lime: this band loops for as long as the wait lasts, and lime is a moment. A lit stop that returns every 2.4 seconds until the network answers is the one thing the dosage rule cannot survive — it would be the longest-running lime on the site, on the one element that is not even content yet.

Weiss · 18 %the hot spot, still the hot spot
Glas 500 · 18 %the leading edge of the band
Sky 300 · 18 %the trailing edge

The three are mixed with transparent in oklab, and the band itself interpolates in oklab behind an @supports test, the same way every other gradient in the family does. This is also the only member of the family that is transparent rather than opaque: the foil, the wash and the three rakes all paint a surface, and this one passes over one. That is what puts it above the contour it crosses — light is layer 5 and a contour is layer 4 — and it is why the alpha is the parameter that had to be argued.

18 %, and why the ceiling is 52 %

A band that lightens a contour can only take contrast away from it, so the cap is worth being exact about. Worst case is the whitest stop over the blackest contour on the page's own CF-Grau, composited the way CSS actually composites it — in sRGB, not in linear light:

AlphaContour composites toGround composites toContrast
0.18 — shipped462169.51:1
0.28712416.76:1
0.501282313.22:1
0.523.03:1 — the floor for a non-text contour

Measured off rendered pixels across eight frames of one period, the worst inked pixel on the shipped ghost is 9.57:1. So contrast did not choose 18 %: anything under 52 % clears the floor. What the arithmetic gives is the ceiling, not the value — 18 % is chosen for restraint, because lime is not the only light in this system that has to stay a moment rather than become a surface. Raise the token past 0.52 and the ghost fails quietly, which is exactly the kind of failure a written-down ceiling exists to prevent.

The movement is part of the colour

A holographic surface hands you a different part of the spectrum from every angle, and the foil's rake swing is that tilt scrubbed from the reader's scroll. This band is the other way a foil moves: the surface holds still and the light travels across it. Three numbers carry it, and each is a decision:

ValueIsBecause
26.57°the angle the band crosses at--angle-b, the isometric rake. The light on a placeholder arrives on the same axis as the light on everything else in the brand.
−30 % → 130 %the travelthe band leaves the object completely at both ends, so a period reads as one pass and a rest rather than as a strobe.
2400 ms, linearthe periodtwice --duration-scene, the slowest thing the system otherwise animates. A fast loop reads as impatience, and easing would imply the wait has a shape.

This is the one place in the system where a continuous loop is honest. Everything else that moves here is answering the reader — a hover, a scroll, a page changing. A loop with no input behind it is usually decoration pretending to be feedback. Here something genuinely is still happening, and it stops when it stops being true.

50 % is the designed still. The band at the middle of the object is where every path that cannot run the loop arrives: no @property, reduced motion, print, forced colours. It is the same rest value the swinging foil and the field's pool land on — a system that agrees with itself about where light sits when nothing is moving. The ghost itself stays in all of them, because the thing is still not here yet. A browser with no color-mix() loses the light and keeps the ghost, which is the correct half to lose.

The component, its markup and the rest of the presence ladder are on Arrival. Everything above is the part that belongs to colour.

The page wash

The page is not flat grey. It is a very slow vertical gradient from CF-Grau at the top to white at the bottom — in the original across the full page height of roughly 5,800 px. It lets the content come into the light as you scroll, and it is why individual sections never need a background colour of their own.

It is the foil, reversed, at the amplitude of atmosphere

The wash used to be the one gradient in the system with no hue in it at all — two neutral stops, zero chroma, a member of no family. It is now the foil's own ramp run backwards, because the page descends from shadow into light: CF-Grau at the top, Weiss at the bottom, so Violett → Sky → Glas → Weiss. The stop positions are --foil-stops' own, subtracted from 100 % — that is what “reversed” means arithmetically, and it is why nothing here is picked by hand. Weiss lands at 100 % by construction, so the bottom of the page is the same #FFFFFF it has always been.

The lightness path is unchanged, exactly and by construction. Each chromatic stop is the neutral wash's own OKLab L at that position, so the interpolated lightness of the four-stop ramp is identical to the two-stop one it replaces. Only the hue moves. That is this brand's definition of iridescence taken to its limit — hue travel inside a narrow luminance band, with the band held at zero width relative to the ramp that was already there.

StopPositionLChRenders
Violett0.00 %0.85450.005283.86°#CFCFD2
Sky43.90 %0.91840.005243.65°#E1E4E7
Glas82.93 %0.97520.005179.38°#F3F8F7
Weiss100.00 %1.00000#FFFFFF

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

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

Banding — and why hue is the first half of the fix

Over 5,800 px, 8-bit sRGB cannot resolve a ramp between two colours this close and the wash bands into visible steps. Two layers work on that, and they are not substitutes.

The first is the chroma above. A neutral ramp is quantization-bound: #CFCFCF to #FFFFFF is 49 greys and there is no fiftieth, because R, G and B are locked together and cross every 8-bit boundary in the same place. That is also why greyscale ramps are the standard diagnostic for banding — with no hue component, nothing masks the step. Put chroma on the ramp and the three channels cross at different places, so it has more rungs to stand on.

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

Neutral washIridescent wash
distinct colours down the ramp51130
widest flat run40 px36 px
transitions moving all three channels99.8 %23.6 %

Two and a half times the levels, and three quarters of the steps are now one or two channels moving instead of three — a partial contour rather than a full one. The neutral ramp's 51 is not a shortcoming of the renderer. It is the ceiling: 49 greys plus dither noise.

The second is --grain — one tile of static, greyscale fractal noise generated inline in SVG — which rides on top of the ramp inside the same background and hides the edges that are left. It sits under the content, never over it, so it cannot touch the text. Decorrelation weakens each edge; grain hides what remains. Use the .page-wash utility and you get both layers.

Grain is not the wash's alone, and its strength is not one number. Any member of the light family painted across a whole screen has the wash's problem, so all three wallpapers carry the same filter primitive for primitive — the two dark ones since the run that measured them, the light one since it was drawn. What does not carry over is the 8 %. The grain's own mean is a floor lift wherever it lands: on a ramp that ends at white that costs nothing and the strength can be chosen for the grain, and on a near-black ground it is the first thing anybody sees. So the rule is the least grain that clears the banding, which is 8 % on the wash and 4 % on a dark ground. Measured over the video-call frame, the share of it sitting in a flat plateau of eight pixels or more goes 53.1 % → 0.6 % at 4 % and 0.4 % at 8 %, while mean luminance goes 9.49 → 12.84 → 16.31. The second four per cent buys two tenths of a point and doubles the lift.

Nor is the first trick free to apply anywhere. It costs a hue budget, and the wash could afford one only because it had the foil's own 104.5° of travel going spare.

<!-- on <body> or the outermost wrapper -->
<body class="page-wash">

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

Lime is never flat

Lime never appears as a flat fill. Wherever it fills an area, it fills it with a gradient — one of the light family above, running out of lime into Glas and away to CF-Grau. A rhombus painted #E1FF00 edge to edge is not the light layer; it is a yellow shape. The ramp is what makes it light.

This follows from the rule the whole palette is built on and had simply never been written down as its own line. Light in this brand is a source and a falloff — that is what the three rakes are, what the bloom is, what the foil is. A flat lime plane has no source and no falloff, so it has nothing to say about where the light is coming from, and it reads as the one thing the manual never draws: a coloured surface.

Don't · a lime plane. No source, no falloff.
Do · the near rake. Lime is where the light lands.

Which ramp, on a small mark

The objection to the rule is always the same: a 40 px cap or an 8 px marker has no room for a ramp. It has exactly as much room as it needs, because the ramp is sized to the element and not to the page — the process cards' lit faces are 120 units in a 640 box and read as light at 352 px. Pick by what the mark is:

The mark isTakeBecause
a flat face — a cap, a top face, a tile--gradient-light, near rakethe face light actually lands on. Same choice as card 01.
a curved body turning away--gradient-light-raking, mid rakethe falloff is longer because the surface leaves the light gradually.
a rim, a ring, a nucleus--gradient-bloom / -isolime sits on the rim and the centre falls to CF-Grau — cards 02 and 04.
a horizontal run — an edge, a head, a bar--gradient-light-90the same ramp mirrored onto a level axis.
a mark on a dark surfacethe same ramp, stopped at GlasCF-Grau over black is a mid grey, so the far end of the ramp reads as a grey pill rather than as light falling off. Ending at Glas keeps the whole mark inside the light half.

The last row is the one that is easy to get wrong, because it looks like an exception and is not: it is the same ramp, run over a shorter distance, for the same reason the three rakes exist. What changes on black is not the material — it is where the falloff stops being light and starts being a surface.

Aim the axis at a vertex, not across an edge

This is the part that decides whether a small lit face reads as light or as a yellow shape, and it is pure geometry. A linear gradient's stops run perpendicular to its axis — so if the axis is perpendicular to one of the shape's own edges, that entire edge sits on a single stop. Put lime at offset 0 and the whole edge is pure lime: a band, not a highlight.

On the isometric rhombus this is easy to walk into, because the shape's edges are at 26.57° and 63.43° — the two angles a rake is most likely to be drawn on. The plot's cap hit it exactly: an axis at 63.43° put its full upper-left edge on the lime stop, and a 40 px mark with a lime edge is a yellow mark. Running the same ramp down the rhombus's vertical diagonal instead — 90°, still a brand angle — puts lime on the apex alone and lets it fall away with the shape. Same ramp, same rake, a third of the lime.

The glow is the ramp as well. A glow is light spilling past the thing that is lit, so it is an area of lime like any other. A single drop-shadow in lime falls off in alpha and not in hue, which at any usable radius reads as a yellow cloud around the mark. The system's glow is --glow-light: two shadows, lime tight to the shape and Glas spreading past it, so the spill shifts colour as it fades the way the rakes do. --glow-r sets the reach; lime always takes 45 % of it.
In SVG the ramp is a paint server, and it carries the oklab waypoint. A CSS gradient cannot be an SVG fill, so a lit face in a drawing declares its own <linearGradient> — with #DBFC60 at 19 % of the lime→Glas leg, because SVG interpolates in sRGB and the rest of the family interpolates in oklab. → Illustration

Three exceptions, and each is a technical one

WhereWhy it stays flat
the palette swatch on this pagea swatch of a colour has to be the colour. It is the specimen, not an application of it.
the found state::selection, ::target-text, .cf-mark--current, ::highlight()background-image is ignored on a highlight pseudo-element, so no ramp of this family can be painted into one. There is no gradient form of the declaration to reach for. .cf-mark--current is an element and could take a ramp; it is held to the pseudo-element's means on purpose, because a match must be one drawing wherever it is drawn.
a stroke, an outline, a focus ringnot a fill. A line has no plane to ramp across, and the rule is about area.

The fourth thing that looks like an exception is not one: the flat lime cube on Illustration is a labelled don't, drawn wrong on purpose.

Lime is light, not ink

No lime letterforms on a dark surface. Not a headline, not a label, not a nav marker, not a syntax colour. Type on black is white — at whatever strength the hierarchy needs — or it is the foil, which is the palette's own answer to colour inside letterforms and is built from the cool half of the spectrum precisely so that it never spends the screen's lime.

Lime keeps everything it was already doing on black, because none of it is type: the lit contour of one icon, the cap of one plot column, the focus ring, a 2 px edge along a banner. The line is between a source of light in the composition and the colour of the words. Lime is the first and never the second.

Why, given that it passes

Lime on black measures 18.5:1. It clears AA at any size with room to spare, and it is still wrong — which is the useful part of this rule, because it is a case where the contrast check has nothing to say and the type is bad anyway.

#E1FF00 sits at 87.5 % of white's luminance while carrying the highest chroma in the palette. On a pure-black ground that is close to the maximum luminance step and the maximum saturation step at the same time — the exact combination that blooms. Light-on-dark type already fringes for readers with astigmatism, which is a large share of them; adding maximum chroma to a near-white luminance widens the halo instead of narrowing it, and the eye cannot bring all of a saturated edge into focus at once. Dark-theme guidance elsewhere reaches the same conclusion by another route: saturated accents visually vibrate against a dark surface, so they get desaturated for dark UI rather than carried across from the light one.

Desaturating lime is the move this system does not make. Lime is the brand's light — one moment per screen, the thing everything else is measured against — and a dimmed, dark-mode-safe lime would be a second lime that is neither the light nor the brand. So lime stays exactly itself and stops being used for a job it was never good at. The seven colours do not change; the rule about where the letters take their colour from does.

What replaces it

Ink and shape, which is what the system uses everywhere else. The site nav has stated this since it was written — the current page is marked by weight of ink, not by a lime ground — and the documentation sidebar you are reading was, until this change, the loudest counter-example to it. Three steps of one white carry a whole navigation column:

Foundations Colour Typography Layout & Grid Geometry & Lines
RoleInkOn blackTold apart by
Group titlewhite 100 %21.0:111 px mono, uppercase, tracked
Link, at restwhite 66 %8.8:115 px sans
Current pagewhite 100 %15.5:1 on its own platea white 14 % plate at --radius-sm

The title and the marker share an alpha and still cannot be confused, because they are different objects: a tracked mono label and a sans link carrying a plate. That is the general shape of the replacement — where lime was doing the work of distinguishing one thing from another, the distinction moves into ink strength, type texture or a plate, all of which survive greyscale, forced colours and a monochrome print.

Why the marker is not the foil either. The foil is the sanctioned way to put colour in letterforms on black, and it is budgeted: one foil moment per screen. A navigation marker is on every screen, so spending the budget there would leave no foil for the thing the page is actually about — and a gradient clipped into a 15 px link is a gradient nobody can see. The foil stays for display type; the chrome takes white. → The foil
Where the rule already applied without being written down. The black button's label is the foil rather than lime, for the neighbouring reason that a lime-tipped label would spend the screen's one lime moment on a secondary action (Buttons). The site nav marks the current page with ink and a plate (Navigation). Both were already following this rule; it is stated here so the next dark component does not have to rediscover it.

Dosage

Do

  • Use lime as light: one edge, one dot, one button gradient.
  • Keep Glas, Sky and Violett to illustrations and gradients.
  • Take surfaces from the neutral scale and meaning from the contour.
  • On black: white type at whatever ink strength the hierarchy needs, white 18 % contours — and the foil for one display headline.

Don't

  • No body copy on lime — and no lime as a section background.
  • No lime letterforms on black — lime is light, not ink. → the rule
  • No Sky or Violett as UI colours (links, buttons, status).
  • Don't mix new greys — nine steps is enough.
  • Never let colour be the only carrier of meaning; add contrast, position or a label.

Contrast

Checked combinations. The target is WCAG 2.1 AA — 4.5:1 for text, 3:1 for large text and UI contours.

ForegroundBackgroundRatioUse
BlackCF-Grau #CFCFCF13.1:1unrestricted
grey-700 #484848CF-Grau5.3:1body copy, 14 px and up
grey-400 #919191CF-Grau1.9:1decorative only — never for text that must be read
grey-400 #919191White3.0:1mono labels 14 px and up, counters, meta
BlackLime #E1FF0018.5:1button labels
WhiteBlack #00000021.0:1footer, nav, consent
LimeBlack18.5:1never as type — light only: a lit contour, a plot cap, a focus ring, an edge. → Lime is light, not ink
White 72 %Black10.6:1nav links, mono meta on dark
White 66 %Black8.8:1documentation sidebar links
Violett 300 #BEBFF0Black11.9:1the foil's worst sample
Careful with mono labels on CF-Grau. The Figma design sets counters like “01 / 04” in #919191 on grey — that is 1.9:1 and unreadable for many people. In this implementation those labels either sit on a light surface or switch to --text-secondary. Purely decorative counters additionally get aria-hidden="true".