the compiler

How these colours were made

This page is about the program, not the palette. If you came for a stylesheet, everything you need is on the front page — this is here because colours are only worth trusting if the method is.

Back to the colours

How it works

Relative chroma

Chroma is stored as a fraction of what the target gamut can actually show at a given lightness and hue — never as an absolute number. One definition renders correctly on sRGB and more vividly on a wide-gamut display, without being redefined.

srgbC 0.0985
display-p3C 0.1132

Different numbers, not the same color repeated. The wider gamut resolves the same relative chroma against more room.

Contrast-anchored steps

A step's lightness is solved, not authored. Each role declares what it must achieve against a reference — APCA contrast for text and solids, perceptual lightness separation for surfaces and borders — and the engine finds the lightness that hits it.

APCA rather than WCAG 2.x, because WCAG passes light-grey-on-black pairs that are genuinely hard to read. Designing against it produces dark themes that satisfy an audit and hurt to use.

Hue torsion

Hue shifts deliberately along a ramp — shadows cooler, highlights warmer, or the reverse. It is what makes a ramp look designed rather than computed.

Kept in a separate field from the corrective term that compensates Oklab's blue-toward-purple drift. Merging intent with a workaround makes "was this on purpose?" unanswerable a year later.

Contrast

Every pair an application actually ships, measured in APCA. Text on a card, a label on a filled button, a focus ring against the page. Most of these cross a family boundary, which is exactly where a per-family check cannot see.

Lc is APCA's lightness contrast, running from 0 to about 108. It is not a ratio and does not compare to WCAG's: 90 is the threshold for body text, 75 for larger body text, 60 for headlines, 45 for large or bold text, 30 for a non-text element that still has to be seen, and 15 is the floor at which anything is discernible at all.

The numbers below are measured in your browser, from the pixels this page is actually painted with — not read from a table. They are shown to four decimal places because that is what the palette is quantized to, so a pair that misses its target by a ten-thousandth says so rather than rounding to the number it missed. Expect the last decimal to differ from the compiler's: it measures its own values, this measures eight-bit pixels.

APCA contrast for every shipped pair
foregroundbackgroundforLcneeds
--nc-color-fg--nc-color-surfacebody text90
--nc-color-fg--nc-color-surface-raisedbody text on a card90
--nc-color-fg-muted--nc-color-surfacesecondary text60
--nc-color-on-accent--nc-color-accentlabel on a filled button45
--nc-color-ring--nc-color-surfacefocus ring30
--nc-color-border-strong--nc-color-surfacestrongest border15
--nc-color-accent--nc-color-surfaceaccent as text45
--nc-color-danger--nc-color-surfacea semantic fill30
--nc-color-fg--nc-color-danger-bgtext inside a callout90
--nc-color-on-danger--nc-color-dangera fill's foreground30
--nc-color-danger-border--nc-color-surfacea status border8
--nc-color-fg--nc-color-surface-coolbody text90
APCA contrast for every shipped pair
foregroundbackgroundforLcneeds
--nc-color-fg--nc-color-surfacebody text90
--nc-color-fg--nc-color-surface-raisedbody text on a card90
--nc-color-fg-muted--nc-color-surfacesecondary text60
--nc-color-on-accent--nc-color-accentlabel on a filled button45
--nc-color-ring--nc-color-surfacefocus ring30
--nc-color-border-strong--nc-color-surfacestrongest border15
--nc-color-accent--nc-color-surfaceaccent as text45
--nc-color-danger--nc-color-surfacea semantic fill30
--nc-color-fg--nc-color-danger-bgtext inside a callout90
--nc-color-on-danger--nc-color-dangera fill's foreground30
--nc-color-danger-border--nc-color-surfacea status border8
--nc-color-fg--nc-color-surface-coolbody text90

Computed live in the browser from the emitted tokens, using the same APCA implementation the compiler gates on.

What the gates found, and could not fix

The quality gates were wired up after the emitters and immediately reported 175 failures against a palette that had passed everything else. The cause was structural: every semantic solid was anchored to the same contrast target, so they all landed at the same lightness and differed only in hue — and hue is precisely the axis dichromacy removes.

Families now separate in lightness as well as hue, which is the only lever that survives. That is not a complete fix, and the system says so rather than pretending. Searched across every combination subject to fills staying visible and ramps staying sane, the best achievable worst-case separation for a six-family semantic set is 0.0163 — under one just-noticeable difference, and there are now ten families.

So the gate reports margins and warns; it fails only when two colours are literally the same. Every number is published in system/reports/colour-vision.md. This is the reason WCAG 1.4.1 exists: never convey information by colour alone. The palette gets you as far as colour can, and tells you exactly how far that is.

The same measurement decided the categorical scale. Eight generated colours bottom out at 0.0416 separation; six reach 0.0724. The default is six, and asking for more warns rather than silently shipping a chart a dichromat cannot read.