Skip to content
Gloss v2.0.0
Source
Foundations

Color

Two neutral ramps, five accents, two fixed semantic colors, seventeen semantic aliases. Low-chroma warm-gray, never stark white or black.

The two layers

The system separates what a color is from what a color is for.

The ramps below are raw material: ten steps of neutral (light), the same ten inverted for dark mode, and five accent hues. No component references them directly.

The aliases underneath are the working vocabulary — --gl-color-text-primary, --gl-color-surface-card, --gl-color-accent. Every component stylesheet reads from these. Retheming, or switching to dark, means repointing aliases at different values — which is exactly what the dark block does.

The neutral ramps were authored in oklch(). Every value below is that same color converted to flat sRGB hex — not re-picked, not rounded. The contrast figures on this page are computed from those hex values with the standard WCAG 2.1 relative-luminance formula, then transcribed here as fact rather than typed by eye.

Neutral

Low-chroma warm-gray, never stark white or black. Ten steps, not twelve or sixteen — the palette defines exactly these.

Low-chroma warm-gray. Not stark white or black. Ten steps, not twelve: exactly 0/50/100/150/200/300/500/600/800/900 — steps 400 and 700 do not exist.

The same low-chroma warm-gray scale, inverted for dark mode. Same ten step numbers as `neutral`; `0` is the darkest step here (the dark-theme page background) rather than the lightest.

Accent

One swappable accent per project — never two in the same view. Signal Teal is the default; the other four are the sanctioned menu a project can switch to.

All five clear AA (4.5:1) for white text at their own value, but Ochre is the tightest of the five at roughly 4.6:1 — closer to the floor than the others (Ink Blue and Plum both clear 8.5:1). That is a real property of a dry, papery ochre and is not being rounded away; a project choosing Ochre as its accent should re-check any place it sets small white text directly on the accent fill, rather than through --gl-color-accent-fg on a full-size Button.

Semantic aliases

Both themes, side by side. The left half of each chip is the light value and the right half is the dark one.

Surfaces

Backgrounds, from the page itself up to a card sitting on it.

Token Light Dark Use for
--gl-color-surface-page
neutral.0 neutral_dark.0 The page itself. Nothing sits behind it.
--gl-color-surface-card
neutral.50 neutral_dark.50 Cards, dialogs, toasts — anything that reads as a sheet on the page.
--gl-color-surface-fill
neutral.100 neutral_dark.100 Recessed fill — a hovered row, a segmented-tab track, a photo placeholder.
--gl-color-surface-fill-2
neutral.150 neutral_dark.150 A second, deeper fill step — the diagonal-grain stripe inside an empty photo slot.

Borders

Two weights. Hairlines do most of the elevation work in this system, so there was never a reason for a third.

Token Light Dark Use for
--gl-color-border-hairline
neutral.200 neutral_dark.200 Card edges, row dividers, dialog dividers. The default edge everywhere.
--gl-color-border-default
neutral.300 neutral_dark.300 Input and button borders — anything a reader has to recognize as a control.

Text

Three levels of emphasis, deliberately not four. Past three, a reader stops perceiving a hierarchy and starts perceiving noise.

Token Light Dark Use for
--gl-color-text-primary
neutral.900 neutral_dark.900 Body copy, headings, and every control label.
--gl-color-text-secondary
neutral.600 neutral_dark.600 Supporting copy, hints, mono meta values, type-label's color.
--gl-color-text-tertiary
neutral.500 neutral_dark.500 Placeholders and de-emphasized metadata only — never body text. This step is intentionally sub-AA against the page; see the caveat on the Color page.

Accent

One swappable accent per project, held constant across a whole view. `-ink` and `-soft` re-derive from `--gl-color-accent` via `color-mix()`, so retheming the accent retones both automatically.

Token Light Dark Use for
--gl-color-accent
accent.teal accent.teal Primary buttons, links, focus affordance, the active tab underline. Never two accents in one view.
--gl-color-accent-fg
#ffffff #ffffff Label and icon color on a filled accent surface. Fixed white, not a ramp reference.
--gl-color-accent-ink
color-mix(in oklch, var(--gl-color-accent) 78%, black) color-mix(in oklch, var(--gl-color-accent) 78%, black) Text sitting on an `-soft` accent tint — an IconButton's active state, an accent avatar's initials.
--gl-color-accent-soft
color-mix(in oklch, var(--gl-color-accent) 12%, white) color-mix(in oklch, var(--gl-color-accent) 12%, white) The tint behind an accent Badge, a toggled IconButton, an accent avatar chip.

Status

Exactly two states, both fixed — they read the same in every project and are never swapped the way the accent is.

Token Light Dark Use for
--gl-color-success
#3f7a4a #3f7a4a Success text and icons — a completed task's check, a saved-state badge.
--gl-color-success-bg
color-mix(in oklch, var(--gl-color-success) 12%, white) color-mix(in oklch, var(--gl-color-success) 12%, white) Tint behind a success Badge. Mixed against literal white, so it stays constant across themes.
--gl-color-danger
#ad3a2c #ad3a2c Overdue/destructive text, the Toast undo action, a danger Button's fill.
--gl-color-danger-bg
color-mix(in oklch, var(--gl-color-danger) 10%, white) color-mix(in oklch, var(--gl-color-danger) 10%, white) Tint behind a danger Badge. Mixed against literal white, so it stays constant across themes.

Contrast that has to hold

Color is the one part of this system that is not arbitrary: a ratio is a fact about two colors and a pair of eyes, and it does not care what the palette wants. The pairings below are fixed facts about the token values above, computed once with the standard WCAG 2.1 relative-luminance formula and checked by hand rather than by a build-time test.

Text — 4.5:1 minimum

WCAG 2.1 SC 1.4.3 (normal-size text)

Pairing Where it shows up Light Dark
color-text-primary
on color-surface-page
Body copy 18.21 15.86
color-text-secondary
on color-surface-page
Supporting copy 6.24 7.26
color-accent-fg
on color-accent
The label on a primary or dark-variant Button. 6.14 6.14
color-success
on color-accent-fg
Success text on a light backdrop — proxied by color-accent-fg 5.13 5.13
color-danger
on color-accent-fg
Overdue/destructive text on a light backdrop 6.14 6.14

Two things this contract deliberately does not assert. Text tertiary--gl-color-text-tertiary (neutral.500) is roughly 3.46:1 against the page in light mode, under the 4.5:1 floor. That step is documented as muted-faint text — placeholders and de-emphasized metadata, never body copy — so this is a real, intentional property of the palette, not a bug to patch by darkening the step. Dividers--gl-color-border-hairline and --gl-color-border-default sit well under the 3:1 that WCAG 1.4.11 asks of a meaningful boundary. That's deliberate: a bounded surface in this system reads by surface contrast, not a border, so these tokens are reserved for dividers inside a surface (a CardRow, a table rule) — never a boundary around one — and this system never claims a 3:1 bar for them, so this contract does not invent one.

Rules