Colour
Nine semantic tokens and three brand colours. Every value below is read live from the stylesheet, so this page cannot drift from the tokens — a colour chart that lies is worse than no chart.
Semantic tokens
Name things by role, never by colour. `--primary` can be re-tuned for contrast; `--teal-500` cannot, without a find-and-replace across four repos. Click any swatch to copy its var() reference.
Brand
Sampled from the Averoa mark. These are for identity — a logo, a gradient accent, a hero number. They are not UI colours: none of them is guaranteed to carry text at an accessible contrast, so never put copy on one without checking.
In both themes
The same tokens, two sets of values. Nothing below hardcodes a colour.
light
Surface
Secondary text on a card, above a hairline.
dark
Surface
Secondary text on a card, above a hairline.
Contrast, measured live
Computed from the values your browser actually resolved, in the theme you are actually in — switch the theme and watch them change. A design system that publishes a colour chart without these is asking to be trusted on a claim it never checked.
The border token is deliberately not listed. WCAG's 3:1 rule covers non-text content needed to understand the interface; a hairline between rows is decoration, and pushing it to 3:1 would make every list look caged.
One place the palette was wrong
This is what the table above caught, and it is the reason it exists.
The light theme's --primary was #0db39e. White label text on it measured 2.64:1 against a 4.5:1 requirement — on the most-used control in the platform. As link text it was 2.51:1, and as the focus ring 2.51:1 against a 3:1 bar.
It is now #098071: the same hue and saturation, dropped in lightness until both pass. It reads as the same teal. --muted moved for the same reason, from 4.47:1 to 4.65:1.
That means Hana looks very slightly deeper than before — a deliberate exception to the “tokens lifted verbatim” rule. A design system that preserves an accessibility defect to preserve an appearance has the priorities backwards. The dark theme already passed everything and is untouched.
Rules
- Never hardcode a hex in a component. If you need a colour that is not here, the answer is usually a token that does not exist yet.
- Define every colour on bare
:rootfirst, then redefine it in the dark blocks. A colour whose only definition lives inside a media query is invisible in the other theme — the most common way a themed component breaks. - Status colours are not in this palette on purpose. They live in
StatusBadge, where meaning and colour are decided together.