Skip to content

ADR-0009: The light console theme

Target architecture — not the current operating contract

This decision records the implemented visual system of the console. Use Current release for operating procedures.

Status

Accepted — 2026-08-29. Supersedes decision D3 of the 2026-08-27 console UX review.

Context

docs/product/experience/design-system.md has always specified a light system: a #f7f7f4 canvas, white panels, a green Enkode accent, and one semantic meaning per colour. The console shipped the opposite — a #020617 canvas with a cyan accent — and used that accent for eyebrow labels, primary actions, and link hover, which is three meanings for one colour.

D3 of the 2026-08-27 console UX review considered adopting the documented light theme and chose not to. It recorded: "tokenize semantics, keep dark; update design-system.md to state that dark is the console default; a light/branded theme is a separate later project." This record is that project.

Two things changed between then and now. The tokenization D3 asked for was completed, so every colour in the console resolves through a semantic name and none is written into a component. And measuring the shipped palette showed the dark theme was not accessible: text-muted, the console's most-used token at 279 call sites, rendered at 4.24:1 on the canvas and 3.75:1 on panels, below the 4.5:1 the design system commits to.

Decision

  • The console adopts the documented light system. globals.css holds the twelve semantic tokens and nothing else in the console holds a colour. The dark theme is removed rather than made switchable.
  • accent is a fill, not an interface colour. accent.primary #00c892 is 2.17:1 against a white panel. It never carries text, a focus ring, or a border that conveys state. accent.strong #007f5e does that work at 5.00:1, and accent.deep #063b31 is the label on an accent fill at 5.77:1.
  • The primary action is an accent fill with an accent.deep label. Its hover darkens the fill to accent.strong and lightens the label to surface.panel in the same rule, because accent.deep on accent.strong falls to 2.50:1.
  • Three state colours are darkened from their original publication so that each clears AA as text on the canvas: state.success #168a63 → #14805c, state.warning #b97900 → #9b6600, state.danger #c34f4f → #bd4d4d. They measured 4.03:1, 3.37:1, and 4.30:1, and all three are used predominantly as text. The design system's own rule licenses this: implementations derive accessible variants from a semantic token while preserving its meaning.
  • Contrast is a build gate. contrast.test.ts parses the token values out of globals.css and asserts WCAG 2.2 AA for every pair the console renders. Adding a colour relationship means adding a row; no threshold may be lowered.

Consequences

  • The console is accessible where it was not. text-muted moves from 4.24:1 to 5.08:1, and every state colour clears AA as text on both surfaces.
  • Reintroducing a dark theme is a second set of values behind the same token names. The non-inline @theme block emits the tokens as custom properties on :root, which is what a second palette would override. Nothing about this decision blocks that; it declines to carry two palettes before anyone has asked for one.
  • Customer branding works the same way, and remains bounded by the existing rule that a customer theme may change brand accents but not remap state semantics.
  • The design system's token table is now a description of shipped behavior rather than a target, for colour. Its component, state-model, and layout sections remain target architecture.
  • border.default #d7dcd8 is 1.39:1 against white. That is correct for a decorative divider and below the 3:1 that WCAG 1.4.11 asks of an input border. Form-control boundaries are an open question for the plan that builds the shared input and table primitives.

Rejected alternatives

  • Keep dark, tokenize only (D3, 2026-08-27). Correct at the time and a precondition for this record: removing the last raw palette utility is what reduced the flip to twelve values. Retained no longer, because the documented system was always the target and the shipped palette was not accessible.
  • Ship both themes behind a user toggle. Doubles the visual verification surface — every badge, chart, code block, and focus ring checked twice — for a capability nobody requested. This was option C of D3 and was rejected then for the same reason.
  • Add success.strong, warning.strong, and danger.strong text variants rather than darkening the three tokens. It preserves the published hex values exactly and mirrors the accent / accent.strong split, at the cost of three more tokens and a rule every future call site has to apply correctly. Rejected because those three colours are used as text far more often than as fills, so the common case should be the accessible one.
  • Drop colour from state text entirely, showing near-black labels beside a coloured dot. Always accessible and arguably more legible, but it is a redesign of every badge and status surface rather than a theme change.

Current governance mapping

The current source release uses Organizations, Teams, real user/service-account virtual keys and separate human management roles. Agent is managed application content; deployments own secretless execution access and publisher funding. Observe uses Requests, Traces, Sessions and Usage with separate metadata/content/export permissions. Historical identity terms in this target proposal are not the operating contract. See Current release.