Colors

Six OKLCH ramps sharing one lightness/chroma spine — only the hue changes. Primary 257.4°, danger 25°, warning 85°, success 145°, info 210°; neutral is the primary hue with chroma flattened.

primary

--color-primary-*

50

100

200

300

400

500

600

700

800

900

950

danger

--color-danger-*

50

100

200

300

400

500

600

700

800

900

950

warning

--color-warning-*

50

100

200

300

400

500

600

700

800

900

950

success

--color-success-*

50

100

200

300

400

500

600

700

800

900

950

info

--color-info-*

50

100

200

300

400

500

600

700

800

900

950

neutral

--color-neutral-*

50

100

200

300

400

500

600

700

800

900

950

Categorical palette

off-spine by design

Twelve hues whose only job is to be told apart. They are not semantic: a semantic colour means something (danger is bad), a categorical colour only says "not the same as that one". Tags, scheduler job categories and the per-module sidebar accent draw from here.

This is the one documented exception to the spine invariant. Each hue is pulled to the spine's 500-level weight and then gamut-mapped, so lightness drifts 0.551–0.560 and chroma varies by hue. Yellow and amber cannot hold C 0.196 at L 0.55 on any real display — forcing them onto the nominal spine would push them out of gamut and let the browser clip them somewhere unpredictable. Every 500 step carries ≥ 4.3:1 against white; the closest pair is ΔE2000 7.7 apart.

cat-red
Red
cat-orange
Orange
cat-amber
Amber
cat-yellow
Yellow
cat-lime
Lime
cat-green
Green
cat-teal
Teal
cat-cyan
Cyan
cat-blue
Blue
cat-indigo
Indigo
cat-purple
Purple
cat-magenta
Magenta

The .cat-* classes are tint carriers, not components — each only sets --cat-tint and --cat-ink. Put one on a chip, a calendar event or a legend swatch. The chip recipe (100 tint background, 500 text) is structurally the legacy statusBox, so status chips migrate almost invisibly.

Module accents

--color-module-accent

The legacy 41px icon rail is tinted per module, so "which module am I in" is answered by colour before it is answered by a label — users who live in two or three modules navigate by that tint. Set data-bms-module on any ancestor and everything beneath it that reads the token picks up the accent. Default is neutral, so an app that sets nothing is unchanged.

crm

cat-red

sales

cat-orange

billing

cat-amber

service

cat-green

stock

cat-blue

debtors

cat-purple

admin

neutral

Semantic tokens

flip with dark mode
--color-canvas
--color-surface
--color-surface-sunken
--color-border
--color-border-strong
--color-accent
--color-accent-hover
--color-accent-subtle
--color-text
--color-text-muted
--color-text-subtle
--color-focus-ring

Components reference these, never the ramps directly. Toggle dark mode in the topbar and watch this page re-skin itself without a single component change.