Skip to content

1. Colour in light and dark

1. Dark is one solid black family, with no green tint.

  • Problem. The first dark theme (Deep Ink plus Maker Teal surfaces, then a “teal-lit” version) put green-tinted layers next to pure black elsewhere (landing, docs). The owner said mixing pure black with green-tinted surfaces “is not working out”: surfaces seamed against the page and the product looked like two themes.
  • Rule. Dark is a neutral grey ladder on a true #000 page. Teal, Citron and Orange are accents, buttons, focus and status only, never a surface. The same family applies to every screen: landing, docs, sign-in, member area, staff console, admin, TV displays.
  • Values. bg #000000, surface #0B0B0B, panel #141414, hover #1C1C1C, line #2C2C2C, outline #787878. Old green-black set for reference: bg #142C2A, surface #193935, panel #1F433F.
  • Enforced. frontend/src/styles/darkTokens.test.ts asserts every layer has equal R, G and B and that hover is lighter than panel, which is lighter than surface. Design repo changelog lists old to new values.
  • Source. Decisions 225 (AMOLED black), 275, 300 (teal-lit, now superseded), 316 (solid black); design repo 5cf062e, 894cf8c, 8060ba0.

2. Layer by lightness steps, not by borders or shadows.

  • Problem. On black, big steps look like glowing slabs and tiny steps are invisible. The ladder is therefore small and even: 0, 11, 20, 28.
  • Rule. Each raised layer is slightly lighter than the one under it (bg < surface < panel < hover). Never skip a step to make something “pop”; use accent or a border instead.
  • Enforced. darkTokens.test.ts checks the order. Ratios: surface 1.07, panel 1.14, hover 1.23 against bg.
  • Source. Decision 316; design repo 8060ba0.

3. Hairlines on black are decoration. Control edges use a separate token.

  • Problem. A divider that is comfortable on cream (about 1.4:1) is the same ratio on black, so a field with only a line border is not visibly a field, and anyone with low vision cannot find it.
  • Rule. line is a quiet divider only (1.3 to 1.5:1 in dark) and never the only thing that identifies a control. Inputs, segmented controls and any boundary a person must find use outline, which is at least 3:1 on every layer.
  • Values. Dark line #2C2C2C (1.50 on bg, 1.32 on panel), dark outline #787878 (4.76 / 4.17). Light line #D5D0C6 (1.42), light outline #828A83 (3.27 / 3.55).
  • Enforced. Contrast guard test “control boundary”: outline at least 3 on bg, surface and panel.
  • Source. Design repo docs/ui-brand-spec.md D8, section A.2; aa251ed.

4. Cards on black need a border; cards on cream do not.

  • Problem. On cream a white card separates from the page with no border at all. On black a 1.14:1 layer step is not enough, and “no shadows” is a brand rule, so the dark sidebar and cards lost their edges.
  • Rule. In dark, give panels and the sidebar a 1 px line border. Do not add shadows to fix it.
  • Values. Sidebar: dark:border-r dark:border-line. Citron search field in light becomes a surface field with a Citron border and icon in dark (see lesson 20).
  • Enforced. Component classes in components/shell/Sidebar.tsx and ui/FilterBar.tsx.
  • Source. App 6e60c20c4.

5. Teal is a fill, not a text colour, on dark. Use accent-ink.

  • Problem. Maker Teal #006E63 is fine as a button fill, but as text, an icon or a chart line on dark it is too dim: 3.41:1 on black and 2.99:1 on the panel (it was 1.76:1 on the earlier green-lit panel, which is where the “1.76” note in chrome.css comes from). Early dark screens used the fill as text and read as washed out.
  • Rule. Standalone coloured text and small marks use the *-ink tokens, never the fill. In light accent-ink equals the fill; in dark it is a lifted tint of teal. A selected teal fill on dark carries a label or other cue and is never the only signal.
  • Values. Light accent-ink #006E63 (6.16 on panel); dark accent-ink #80B1A6 (7.68 on panel). It is Teal mixed 50% with Cream.
  • Enforced. Source guard bans text-accent, text-success and similar fill-as-text utilities; the contrast guard checks every *-ink on every background.
  • Source. App 8b57246d1 (split on-fill ink from text ink), d619267b1; decision 316; design spec D2.

6. Muted text is a neutral grey chosen for the worst background, not “ink at 60%”.

  • Problem. text-ink/60 is translucent: its real colour depends on what is behind it, so contrast changed per surface and failed on several (for example text-on-accent/75 on teal measured 3.90:1).
  • Rule. No translucent text, including opacity on a text element. Muted is its own opaque token, tuned to 4.5:1 or better on every surface it can sit on, including soft fills and hover.
  • Values. Light muted #5B6863 (worst case 4.72 on hover). Dark muted #ACACAC, neutral grey 172 (worst case 7.07 on soft fills).
  • Enforced. Source guard rejects text-*/NN patterns. Contrast guard covers all text tokens on bg, surface, panel, hover and the five soft fills.
  • Source. Design spec D5 and H.1; decision 316 (“muted at least 4.5”).

7. Ink in dark is Cream, not white.

  • Rule. Dark body text is Warm Cream #FFF4E8 (19.36 on black), which keeps the brand warmth and avoids the harsh glare of pure white on pure black. The page background in light is the same Cream, so the two themes are visual inverses of one pair.
  • Source. Decisions 225, 316.

8. Focus is Citron in dark and Teal in light, and always opaque.

  • Problem. A teal focus ring is 3.41:1 on black: legal but weak. A ring that is an outline with some alpha blends with whatever is under it. focus:outline-none had been used and left keyboard users with no ring.
  • Rule. Every interactive control shows a 2 px ring with a 2 px offset in the dedicated focus token, which is opaque. Dark uses Citron #DFFF8F (18.84 on black). Never remove the outline without replacing it.
  • Values. Light focus #006E63, dark focus #DFFF8F. Class pattern: focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus.
  • Enforced. Contrast guard: focus at least 3 on bg, surface, panel, hover, sidebar. focus:outline-none is banned by a source guard. A11y e2e (axe, Chromium) fails the build on any violation, with no allow-list.
  • Source. Design repo 5cf062e; app 2d606d4e1 (accessible controls), ede1f0052; INVARIANTS “accessibility floor”.

9. Small status marks are solid in both themes; big tinted blocks use soft fills in dark.

  • Problem. Pills first used a solid pastel in light and a soft fill plus ink in dark. In dark the soft-pill version looked dim and inconsistent next to the same pill in light, and users scanning a table saw different colours for the same status. But solid pastel on a full-width callout glared on black.
  • Rule. Pills, avatars, date chips and identity tiles (small marks) use the solid fill with its fixed dark label in both themes. Large areas (callouts, banners) use the solid fill in light and *-soft with the tone’s ink in dark. A status always includes a word; colour is never the only cue.
  • Values. Success #DFFF8F, warn #FFD447, info #B5CDFF, label #142C2A (13.23, 10.37, 9.23). Dark callout: success-soft #20221B with success-ink #E9FCAA (14.52), and the same pattern for warn and info.
  • Enforced. components/ui/tones.ts exports toneMarkClasses (small) and toneSurfaceClasses (large).
  • Source. App 6e60c20c4; design spec D6 and section C.

Current rule (v2.0.1, 2026-11-13): Lesson 9 is the current small-mark rule; the older D6/C dark soft-pill cells are superseded. Success/warn/info use solid fills and on-* labels in both themes; large dark callouts use soft fills. Accent/danger/neutral retain their explicit exceptions. (1A-N17) — supersedes the line above; see docs/current-rules.md#cr-status-01.

10. Fill labels are fixed per tone; text-on-tint colours change per theme. Do not mix the two token sets.

  • Problem. The -ink tokens were first made theme-adaptive (light in dark mode) and were then used on the solid pastel fills too, so labels on a Citron pill turned light in dark and became unreadable. A second vocabulary of duplicate colour names (tone-*) made it worse: which name a component used was accidental.
  • Rule. There are two separate families. on-* (fixed Deep Ink, or Cream on teal/danger) is only for labels sitting on that tone’s solid fill. *-ink (theme-adaptive) is only for standalone text and for text on a *-soft background. One vocabulary, no duplicates.
  • Enforced. Contrast guard: on-X on X, X-hover and X-active at least 4.5 for every tone.
  • Source. App 8b57246d1, d619267b1.

11. Danger flips both its fill and its label in dark.

  • Rule. Light danger is a deep red #B91C1C with a Cream label (5.96). Dark danger is a soft red #FCA5A5 with a Deep Ink label (7.77); the same red on black would be 2.8:1 or worse, and a light red text on the dark panel is 9.71. Danger is only for destructive actions and errors, never for ordinary attention (use emphasis).
  • Source. Design spec D4, D10; the brand has no red, so these were sampled from the reference boards.

12. Orange emphasis is a fill with a dark label, once per layout, and never text.

  • Problem. Workshop Orange as text on Cream is 2.47:1.
  • Rule. Orange is Space Works’ own, never tenant-overridable, one bold moment per layout (counters, low-stock tags, one call to action). Always a pill or button with the fixed on-emphasis label (5.51). Orange standalone text is banned.
  • Values. emphasis #FF7447, label #142C2A; soft #FFEEE9 light, #221A17 dark.
  • Enforced. Source guard on emphasis text utilities; a test of one emphasis button per page.
  • Source. INVARIANTS “colour vocabulary”; design spec D3, D10.

Part of Design lessons learned, published from docs/lessons-learned.md. Edit it there.