Color
Bone is OPERATOR's only theme, and it is dark. It declares color-scheme: dark, so scrollbars,
select popups, and form controls follow the page. pnpm verify:contrast measures the contrast
and separation figures on this page, and the token tests recompute every ratio quoted here.
Surfaces
Keep surfaces achromatic so UI chrome cannot resemble affiliation fills. Most content sits on
bg to surface-3. Surfaces 4 to 6 raise panels and overlays, and carry fewer text colors.
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-bg | #0A0A0A | Application background | |
--oprt-surface-1 | #101010 | Cards, sidebar | |
--oprt-surface-2 | #161616 | Elevated surfaces, inputs | |
--oprt-surface-3 | #1C1C1C | Hover, active surfaces | |
--oprt-surface-4 | #222222 | Raised panels | |
--oprt-surface-5 | #282828 | Overlays on panels | |
--oprt-surface-6 | #2E2E2E | Highest elevation | |
--oprt-border-subtle | #1E1E1E | Hairline separators | |
--oprt-border | #252525 | Layout lines: tables, tabs, cards, dividers, panels | |
--oprt-border-strong | #333333 | Emphasized layout lines | |
--oprt-border-control | #6B6B6B | Control outline — the boundary of anything you click or type into (text fields, selects, checkboxes, radios, toggles, segmented and disabled buttons); 3:1 non-text contrast |
Control outline
--oprt-border is for layout lines: tables, tabs, cards, dividers, and panels. It is deliberately
quiet, and it is not a control boundary. The boundary of anything you click or type into uses
--oprt-border-control (border-oprt-border-control in Tailwind), which keeps 3:1 non-text
contrast (WCAG 1.4.11) against the surface the control sits on. pnpm verify:contrast checks it
on bg to surface-3.
| Surface | --oprt-border-control |
|---|---|
bg | 3.72 |
surface-1 | 3.57 |
surface-2 | 3.40 |
surface-3 | 3.20 |
Text
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-text | #E0E0E0 | Primary text — 15.00:1 on bg (AAA) | |
--oprt-text-dim | #8A8A8A | Secondary text — 5.73:1 on bg; allowed on fewer surfaces, see below | |
--oprt-text-muted | #444444 | Decorative only — 2.03:1 on bg. Never text a reader needs, including placeholders and disabled labels |
Where text may go
Text needs 4.5:1 against what it sits on (WCAG 1.4.3). This is the allowed-text matrix, and
pnpm verify:contrast requires every cell to pass; a test fails when a cell passes but is
missing, so the matrix is exactly what the tokens measure. A state layer is a translucent token,
so its contrast is measured with the layer composited over the surface beneath it. The hover,
active, and selected tokens are state layers, and so are the two accent washes.
| Text color | Allowed on surfaces | Allowed on state layers |
|---|---|---|
--oprt-text | All | All |
--oprt-text-dim | bg to surface-4 | Hover: bg to surface-2. Active: bg. Selected: none. Accent-ghost: bg to surface-1. Accent-bg: bg to surface-2. |
--oprt-accent | All | All |
--oprt-accent-hover | All | All |
--oprt-accent-active | All | All |
--oprt-success | All | All |
--oprt-warning | All | All |
--oprt-error | bg to surface-3 | Hover: bg to surface-1. Active: bg. Selected: none. Accent-ghost: bg to surface-1. Accent-bg: bg to surface-2. |
--oprt-info | All | All |
--oprt-accent, --oprt-accent-hover, --oprt-accent-active, --oprt-success,
--oprt-warning, and --oprt-info measure at least 5.44:1 on every surface. Where a color is
not allowed, use --oprt-text. "All" on a state layer means over bg to surface-3.
Contrast on each surface
| Surface | --oprt-text | --oprt-text-dim | --oprt-error |
|---|---|---|---|
bg | 15.00 | 5.73 | 5.59 |
surface-1 | 14.41 | 5.51 | 5.37 |
surface-2 | 13.71 | 5.24 | 5.11 |
surface-3 | 12.91 | 4.94 | 4.81 |
surface-4 | 12.05 | 4.61 | 4.49 fail |
surface-5 | 11.17 | 4.27 fail | 4.16 fail |
surface-6 | 10.29 | 3.93 fail | 3.83 fail |
A cell marked fail is below 4.5:1. Secondary text and error text lose contrast as surfaces rise, which is why they stop where the matrix says they stop.
Contrast on state layers
| Layer over surface | --oprt-text | --oprt-text-dim | --oprt-error |
|---|---|---|---|
hover over bg | 13.71 | 5.24 | 5.11 |
hover over surface-1 | 12.91 | 4.94 | 4.81 |
hover over surface-2 | 12.05 | 4.61 | 4.49 fail |
hover over surface-3 | 11.32 | 4.33 fail | 4.22 fail |
active over bg | 12.34 | 4.72 | 4.60 |
active over surface-1 | 11.46 | 4.38 fail | 4.27 fail |
active over surface-2 | 10.73 | 4.10 fail | 4.00 fail |
active over surface-3 | 10.00 | 3.82 fail | 3.72 fail |
selected over bg | 11.47 | 4.39 fail | 4.27 fail |
selected over surface-1 | 10.74 | 4.11 fail | 4.00 fail |
selected over surface-2 | 9.98 | 3.82 fail | 3.72 fail |
selected over surface-3 | 9.27 | 3.55 fail | 3.45 fail |
accent-ghost over bg | 13.19 | 5.05 | 4.92 |
accent-ghost over surface-1 | 12.46 | 4.76 | 4.64 |
accent-ghost over surface-2 | 11.62 | 4.44 fail | 4.33 fail |
accent-ghost over surface-3 | 10.84 | 4.15 fail | 4.04 fail |
accent-bg over bg | 13.72 | 5.25 | 5.11 |
accent-bg over surface-1 | 12.92 | 4.94 | 4.81 |
accent-bg over surface-2 | 12.20 | 4.66 | 4.54 |
accent-bg over surface-3 | 11.33 | 4.33 fail | 4.22 fail |
Layers lighten the surface beneath them, so secondary text and error text lose contrast on them.
Put --oprt-text on a selected row. --oprt-accent-ghost and --oprt-accent-bg are state layers
too: they are the quiet hover and selection washes, and where secondary or error text fails on
them it measures between 4.04:1 and 4.44:1. Accent, success, warning, and info text clear 4.5:1 on
every layer over bg to surface-3.
Accent
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-accent | #D4D0C8 | Primary interactive — 12.87:1 on bg (AAA) | |
--oprt-accent-light | #EAE8E4 | Highlights, button borders | |
--oprt-accent-hover | #C0BCB4 | Hover | |
--oprt-accent-active | #AAA69E | Active / pressed | |
--oprt-accent-ghost | rgba(212, 208, 200, 0.08) | Quiet hover and selection wash — 8% accent | |
--oprt-accent-bg | rgba(212, 208, 200, 0.06) | Subtle accent fill — 6% accent | |
--oprt-accent-border | rgba(212, 208, 200, 0.25) | Accent-tinted border — 25% accent |
Semantic
Use -subtle for background washes, the base for text and borders, and the darker
-strong fill with --oprt-text. Pair every semantic color with a non-color
indicator (WCAG 1.4.1).
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-success-subtle | #0F1E17 | Active, online, healthy — background wash | |
--oprt-success | #7DBEA0 | Active, online, healthy | |
--oprt-success-strong | #216F51 | Active, online, healthy — solid fill | |
--oprt-warning-subtle | #1F190D | Degraded, threshold exceeded — background wash | |
--oprt-warning | #D4A844 | Degraded, threshold exceeded | |
--oprt-warning-strong | #7D5D13 | Degraded, threshold exceeded — solid fill | |
--oprt-error-subtle | #241614 | Failed, offline, critical — background wash | |
--oprt-error | #D86464 | Failed, offline, critical | |
--oprt-error-strong | #9B4A44 | Failed, offline, critical — solid fill | |
--oprt-info-subtle | #111B25 | Informational, neutral — background wash | |
--oprt-info | #88A8C0 | Informational, neutral | |
--oprt-info-strong | #1B4975 | Informational, neutral — solid fill |
Categorical
For charts and legends, use the first three colors first: their minimum separation
is 28.6 CIEDE2000. Always include non-color cues such as dashes or markers,
especially for series 5–8. Red and pink are reserved for error and 2525 hostile.
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-cat-1 | #B87347 | Series 1 | |
--oprt-cat-2 | #B1BDDC | Series 2 | |
--oprt-cat-3 | #E8D4A5 | Series 3 | |
--oprt-cat-4 | #599795 | Series 4 | |
--oprt-cat-5 | #9F72B1 | Series 5 | |
--oprt-cat-6 | #98A887 | Series 6 | |
--oprt-cat-7 | #BFE5D5 | Series 7 | |
--oprt-cat-8 | #A79BDD | Series 8 |
Sequential and chart roles
Ordered magnitudes rise in lightness and keep 3:1 on the chart surfaces, bg to surface-3.
Numeric labels on a step use --oprt-bg at 4.5:1. Axis labels, grids, and thresholds inherit
foundation tokens rather than carrying a palette of their own; see
Data visualization.
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-data-seq-1 | #808080 | Lowest magnitude | |
--oprt-data-seq-2 | #999999 | Low magnitude | |
--oprt-data-seq-3 | #B0B0B0 | Middle magnitude | |
--oprt-data-seq-4 | #C8C8C8 | High magnitude | |
--oprt-data-seq-5 | #E0E0E0 | Highest magnitude | |
--oprt-chart-grid | var(--oprt-border-strong) | Decorative grid lines | |
--oprt-chart-axis | var(--oprt-text-dim) | Axis labels and ticks | |
--oprt-chart-threshold | var(--oprt-warning) | A labeled, application-supplied reference |
Interaction states
State tokens are translucent layers, so the surface beneath shows through and text contrast depends on both: see the matrix above. Prefer them over per-component opacity values, and check the composite before placing secondary text on one.
| TOKEN | VALUE | USAGE | SWATCH |
|---|---|---|---|
--oprt-surface-hover | rgba(255, 255, 255, 0.05) | Hover over any surface | |
--oprt-surface-active | rgba(255, 255, 255, 0.09) | Pressed / active | |
--oprt-surface-selected | rgba(212, 208, 200, 0.14) | Selected row or item | |
--oprt-overlay-scrim | rgba(10, 10, 10, 0.72) | Modal and drawer scrim |
Using the colors in Tailwind
Every token above is a Tailwind color through @operator-ds/tailwind: bg-oprt-surface-2,
text-oprt-text-dim, border-oprt-border-control. Opacity modifiers work (bg-oprt-bg/80) and compile
to color-mix(), because Tailwind 3 cannot split a var() into channels.
Separation from map symbology
UI colors must remain distinguishable from the saturated MIL-STD-2525 fills in
@operator-ds/tokens-operational. The target
is 13 CIEDE2000 from all fifteen affiliation values, and pnpm verify:contrast measures it
for every Bone color. Two existing exceptions rely on low chroma: --oprt-error
(9.8 from hostile-light) and --oprt-info (11.5 from friendly). Their floors are pinned, so
they cannot move closer. Never increase their chroma.
Keep -strong fills darker than their base. Lightening error to a mid-light fill
reduces its hostile-light margin to 6.1 and risks resembling map affiliation.