Skip to content

Colour

RisingSwamp uses a dark-first palette driven by clinical argument (Moderate Brain Arousal model), not preference. Colour space is OKLCH for perceptually uniform lightness across hues. Contrast validation uses APCA (Advanced Perceptual Contrast Algorithm), not WCAG 2.1.

The palette is multi-theme: every surface, text, and accent token flips per [data-theme] on <html>. Today there are three themes — Delight (night, cool dark, cyan), Relight (dawn, warm off-white, teal), Gilt (golden hour, warm dark, amber) — with Vesper and Bleach planned.

  • Day-to-day component code: consume Semantic Tokens (the first three sections below). They flip per theme automatically.
  • Adding a new theme or extending the palette: see Per-theme Primitive Palettes further down. Don’t reach into these from feature code — that bypasses the multi-theme contract.
  • The full naming rules live in notes/design/TOKEN-NAMING.md.
  • Plane 1 (UI): Only the active accent (accent.default) as chromatic accent. No phenomenon colours — phenomena.* (plural) tokens are forbidden in any Plane-1 file.
  • Plane 2 (Environment): Atmospheric — phenomenon slots live here exclusively (phenomena.atmospheric-base, phenomena.scatter-points, phenomena.directional-wash, phenomena.cool-edge, phenomena.long-tail-glow, optional phenomena.warning-edge). Each theme fills the slots with its own phenomenon (foxfire / firefly / ember / etc.).

Phenomenon colours in a UI element permanently destroy the scarcity-based neurological impact. The exception is the singular phenomenon.marker semantic alias used by Tag (variant="phenomenon") and Toast (type="phenomenon") to signal Plane-2 source from Plane-1 chrome — same value as phenomena.long-tail-glow per theme, but namespaced so the Plane-1 lint guard targets only the plural form.


Each token below resolves through a [data-theme] condition; the hex shown is the Delight resolution. Switch the active theme to see the value flip live.

Surface · Plane 1

Semantic tokenCSS varUse
bg.canvas--colors-bg-canvasPage background — deepest layer
bg.default--colors-bg-defaultDefault surface — cards, panels
bg.subtle--colors-bg-subtleSubtle lift over canvas
bg.muted--colors-bg-mutedMuted panel — inputs, nested elements
bg.emphasized--colors-bg-emphasizedEmphasised surface — hover state, active panel
bg.disabled--colors-bg-disabledDisabled control background
border.default--colors-border-defaultDefault border colour

RisingSwamp aliases (all themes-aware): bg.surface, bg.surface.alt, bg.dim, bg.secondary. See panda.config.ts for the full set.

Text · Plane 1

Semantic tokenCSS varMin sizeUse
text.DEFAULT--colors-textAnyPrimary text
text.secondary--colors-text-secondary--fs-baseSecondary body text
text.tertiary--colors-text-tertiary--fs-smLabels, descriptive copy
text.muted--colors-text-muted--fs-xsMetadata, hints. Never below --fs-xs.

The Park UI–aligned aliases (fg.default, fg.muted, fg.subtle, fg.disabled, fg.error) resolve to the same values; either namespace is acceptable in component code.

The accent is the most visible per-theme token: cyan in Delight, teal in Relight, amber in Gilt.

Accent · Saltie's colour (Delight resolution shown)

Semantic tokenCSS varUse
accent.default--colors-accent-defaultSaltie’s hue; focus rings; interactive affordances
accent.fg--colors-accent-fgText on top of accent surfaces (e.g. solid primary button)
accent.dim--colors-accent-dimSubtle accent backgrounds; focus ring fill (alpha ≈ 0.12)
accent.glow--colors-accent-glowGlow shadow on primary buttons (alpha ≈ 0.25)
accent.subtle--colors-accent-subtleLightest accent fill — sidebar nav active state (alpha ≈ 0.08)
colors.saltie--colors-saltieSaltie’s primary presence colour (rare ceremonial state)

accent.fg design note: picks contrast deliberately per theme so text on accent surfaces never disappears. Delight and Gilt use a deep dark (gray.950 / gray-gilt.950); Relight uses gray-relight.50 (the inverted “extreme” of the Relight scale, #0c1217) — naive gray.950 conditioning would resolve to #f0ebe0 in Relight (the page bg) and render the text invisible.


Per-theme Primitive Palettes — reference

Section titled “Per-theme Primitive Palettes — reference”

App code should never reach into these directly — go through a semantic token above. These exist so theme designers can see the concrete values backing each accent and verify contrast / harmony decisions.

Accent palettes across all themes

The same shape (DEFAULT, emphasized, dim, glow, plus alpha40 / alpha08) repeats per palette. Each theme picks values that fit the hour:

  • Delight: electric cyan against night-water blacks.
  • Relight: desaturated teal — the same hue seen through morning atmosphere.
  • Gilt: amber gold — Saltie gilded at golden hour, not bioluminescent.

Per-theme gray scales (gray-delight, gray-relight, gray-gilt) follow the same pattern: 13 steps each (95050), mirrored indices, theme-specific values. The semantic surface and text tokens above route through them.

Phenomena are role-named slots, not phenomenon-named tokens. Plane-2 components reach for a slot (phenomena.scatter-points) and get the right concrete phenomenon for the active theme — foxfire in Delight, foxfire-muted in Relight, firefly in Gilt. Adding a future theme (Vesper, Bleach) is a single condition entry per slot in panda.config.ts; no Plane-2 component needs a code change.

Phenomena slots · the Delight resolution shown live

Slot tokenCSS varRoleDelightRelightGilt
phenomena.atmospheric-base--colors-phenomena-atmospheric-base-*Radial gradient defining where the light comes frombioluminescence (below)dawn glow (above)low sun (side)
phenomena.scatter-points--colors-phenomena-scatter-pointsSmall discrete bright points, often animatedfoxfirefoxfire-mutedfirefly
phenomena.directional-wash--colors-phenomena-directional-washDiffuse warm or cool wash from one directionaurora-gaurora-g-mutedember
phenomena.cool-edge--colors-phenomena-cool-edgeThe “periwinkle weird-side” phenomenonelmoelmo-mutedcopper
phenomena.long-tail-glow--colors-phenomena-long-tail-glowSoft diffuse glow hanging in airgloryglory-mutedpollen
phenomena.warning-edge--colors-phenomena-warning-edgeBurning sky edge (Gilt only — others fall back quietly to long-tail-glow)(silent)(silent)dusk-r

These slot tokens must never be referenced from a Plane-1 file. A pnpm test guard (src/lib/no-phenomena-in-plane-1.test.ts) fails the build if any Plane-1 source references phenomena.* (plural). Plane-1 components that need a phenomenon-coloured signal use the singular phenomenon.marker alias instead — see the Two-Plane Rule above.

The atmospheric-base slot is split into three sub-tokens (position, inner, outer) consumed by AtmosphericLayout to assemble the full radial gradient inline. The remaining slots resolve to a single colour each.


No shadows. Depth via background tone only:

bg.canvas → bg.default → bg.muted → bg.emphasized

Three to four levels max. No further nesting.

All text must pass APCA at its rendered size and weight:

  • Body text: Lc 60+
  • Large text (≥24px / ≥18.5px bold): Lc 45+
  • text.muted at --fs-xs is the floor — do not go smaller
  • Consume semantic tokens in component styles, never primitives
  • Validate contrast with APCA, not WCAG 2.1 ratio
  • Trust the per-theme conditioning — do not hardcode hex fallbacks for a specific theme
  • Reach into the per-theme primitive palettes (gray-relight.X, accent-gilt.Y) from feature code
  • Use raw hex values in component code (the focus-ring exception in custom.css is a docs-site artefact, not a pattern to copy)
  • Use phenomena.* (plural) slot tokens in any Plane-1 file (use the singular phenomenon.marker alias for phenomenon-signalling Tag/Toast variants)
  • Use text.muted below --fs-xs
  • Use drop shadows for elevation