Perceptual lightness is not contrast.
Every CEDX product declares two numbers: a primary hue and a secondary one. Everything else (the eleven-step tonal ramp, the gradients, the tints, the three colours that carry text) is generated from those. Colour is authored in OKLCH, which is the part most teams already do. OKLCH holds perceived lightness roughly even across hue, so a ramp built at a fixed lightness reads as one family rather than a set of unrelated swatches. Set every product's 600 step to a lightness of 0.583 and the yellows stop shouting and the blues stop disappearing.
That solves appearance. It does not solve legibility, and the two are not the same problem. WCAG contrast is computed from relative luminance, and relative luminance weights the three primaries very unevenly: 0.2126 for red, 0.7152 for green, 0.0722 for blue. Green counts for nearly ten times what blue does. A green and a blue at the same OKLCH lightness therefore do not have the same contrast against the same background, and the gap between them is routinely more than a full step on the ratio scale.
With one brand this is invisible. You pick the accent, check it once and move on. With a hundred and seven it is a defect generator: somewhere in the hue circle there is a band where a fixed-lightness accent drops under 4.5:1 on the lightest surface, and nobody notices until an audit does.
Fix the ratio, let lightness move.
The change is small and it inverts the usual instinct. Rather than fixing lightness and measuring whatever contrast results, fix the contrast required and solve for the lightness that produces it, per hue.
Why a bisection is exact here.
Contrast against a fixed surface is monotonic in lightness on either side of that surface: move away from the surface and the ratio only rises. That is precisely the condition a bisection needs. The solver takes a hue, a chroma, the surface's luminance and a target ratio, brackets the search on the correct side of the surface, and halves the interval twenty-four times. Over a range of about 0.47 that lands within roughly three parts in a hundred million of the boundary. Far below anything an eye or an eight-bit channel can resolve.
Why chroma is held constant.
Chroma is held constant per role rather than searched, which keeps this a one-dimensional problem. Three roles, three chromas: 0.184 for the accent that carries text on light surfaces, 0.146 for the accent on ink, 0.158 for the marker highlight. Holding chroma also keeps the accents recognisably one colour family across products, which a two-dimensional solve would not.
Which surface do you solve against?
There are three light surfaces in the system (paper, ivory and mist) and an accent can land on any of them. Solving against paper would be the flattering choice and the wrong one: paper is pure white, the easiest of the three, and an accent tuned against it can fail on mist. The solver uses mist, the darkest of the light surfaces, so the guarantee holds everywhere the colour can actually appear.
The marker highlight is solved from the other direction, because it is a background rather than text. Ink type sits on top of it, so its lightness is solved upward from ink's luminance until the type on it clears the target.
The targets are 4.9:1, not 4.5:1. Half a step of margin costs almost nothing perceptually and removes an entire class of regression.
That margin is deliberate. Text is antialiased, so its edge pixels are a blend rather than the nominal colour. Accents sometimes sit over a translucent tint. And a future adjustment to a surface token should not silently push a hundred hues under the line at once.
Measure the colour that gets painted.
One detail matters more than it looks. The OKLCH-to-sRGB conversion clamps each linear-light component into the zero-to-one range before encoding: the same clipping a browser performs when an OKLCH value falls outside the display gamut. Highly chromatic colours at extreme lightness do fall outside it.
If the solver measured the ideal, unclipped colour, it would occasionally certify a ratio the screen never shows. Measuring after clipping means the luminance under test is the luminance of the pixel that will actually be painted. The guarantee is about the rendered result, not about the arithmetic that produced it.
What it costs, and what it buys.
Three solved colours per product, twenty-four iterations each, a hundred and seven products. Results are cached on hue, chroma, surface and target, and hues repeat across the catalogue, so in practice this is a few thousand polynomial evaluations at module load and nothing at all afterwards.
What it buys is that contrast stops being a review item. Nobody signs off an accent. A new product declares a hue, the ramp is generated, and the three text-bearing colours come out at a known ratio against the hardest surface they can land on. The only failure mode left is somebody bypassing the tokens and writing a hex value, which is why there are no hex values in this codebase.


