# SurveyStream — Colour System

**Version** 1.0.0 · **Direction** Interlocking Loop / Active Signal · **Standard** WCAG 2.1 AA minimum

A dual-sided platform needs one palette that carries two emotional registers: the
**aggregator** side wants instrument-panel credibility, the **taker** side wants the
reward to feel like it arrived. This system uses a single deep anchor and separates
those registers by *which accent is allowed to appear*, not by changing the palette.

Every ratio below is computed, not estimated. 56 pairs checked, 0 failures.

---

## 1. Core brand colours

### 1.1 Primary brand hue — Deepwater

A desaturated teal-navy. Reads as navy at a glance, but the teal bias ties it to the
cyan accent so the two never look like separate systems.

| Step | Hex | Role |
| --- | --- | --- |
| `deepwater-950` | `#04171F` | Ground. Darkest canvas, dark-mode page background. |
| `deepwater-900` | `#08222E` | Primary brand colour. Dark-mode surface, print anchor. |
| `deepwater-800` | `#0D2E3D` | Dark-mode elevated surface. |
| `deepwater-700` | `#064A5B` | Light-mode interactive active state. |
| `deepwater-600` | `#085A6E` | Light-mode interactive hover. |
| `deepwater-500` | `#0A6E86` | Light-mode primary action, links, focus ring. |

### 1.2 Accent / action hue — Stream

The live current. This is the interactive colour: buttons, links, focus, active state.
It is bright enough to carry dark-mode actions and dark enough at 500 to sit on white.

| Step | Hex | Role |
| --- | --- | --- |
| `stream-600` | `#0BA3B7` | Dark-mode active/pressed. |
| `stream-500` | `#10BBD1` | Accent. Dark-mode primary action. |
| `stream-400` | `#3ACFE1` | Dark-mode hover. |
| `stream-300` | `#5FE3EC` | Signal. Dark-mode links and focus ring. |

### 1.3 Reward hues — Pulse and Payout

Two separate ideas that are routinely collapsed into one and should not be:

- **Pulse** (mint) — *flow*. Something is moving, live, healthy. Ambient and structural.
- **Payout** (amber) — *money landed*. Reserved for value transfer. If Payout appears
  where no money moved, it stops meaning anything.

| Step | Hex | Role |
| --- | --- | --- |
| `pulse-500` | `#2FE3A2` | Flow accent, live-data states, the mark. |
| `pulse-600` | `#12987A` | Light-mode Pulse text/edge. |
| `payout-500` | `#F2A23C` | Payout fills and dark-mode payout text. |
| `payout-600` | `#C77A1E` | Light-mode edge for amber fills (SC 1.4.11). |
| `payout-700` | `#8A4E08` | Light-mode payout text. |

### 1.4 Neutral scale — 6 steps

Teal-biased rather than pure grey, so neutrals read as chosen rather than inherited.
A 7th step (`neutral-500`) exists solely because light-mode borders need 3:1 against
the *tinted* canvas, which `neutral-400` misses — see §3.

| Step | Hex | Primary use |
| --- | --- | --- |
| `neutral-50` | `#F4F8F9` | Light canvas. |
| `neutral-200` | `#D3E0E2` | Light dividers, sunken surface, disabled fill. |
| `neutral-400` | `#8199A0` | Light disabled text (exempt from contrast). |
| `neutral-500` | `#758D94` | Light default border. Meets 3:1 on canvas *and* surface. |
| `neutral-600` | `#566A70` | Light muted text. Dark-mode borders derive from here. |
| `neutral-800` | `#2E4A55` | Light secondary text. |
| `neutral-950` | `#04171F` | Light primary text. Same value as `deepwater-950`. |

---

## 2. Design token architecture

Three layers. Components may only reference layer 3.

```
primitive  ->  semantic (per mode)  ->  component
deepwater-500  ->  interactive      ->  button.primary.bg
```

### 2.1 Background tokens

| Token | Light | Dark |
| --- | --- | --- |
| `bg-canvas` | `#F4F8F9` | `#04171F` |
| `bg-surface` | `#FFFFFF` | `#08222E` |
| `bg-elevated` | `#FFFFFF` | `#0D2E3D` |
| `bg-sunken` | `#D3E0E2` | `#020E14` |

Elevation works differently per mode, and this is deliberate: in **light** mode
`bg-elevated` equals `bg-surface` and elevation is carried entirely by shadow;
in **dark** mode shadow is nearly invisible, so elevation is carried by lightness.
Adjacent surfaces are not UI components and are exempt from the 3:1 rule — but a
card that needs to read as a distinct object still gets `border-subtle`.

### 2.2 Text tokens

| Token | Light | Dark |
| --- | --- | --- |
| `text-primary` | `#04171F` | `#EDF3F4` |
| `text-secondary` | `#2E4A55` | `#AEC4C9` |
| `text-muted` | `#566A70` | `#7E969C` |
| `text-disabled` | `#8199A0` | `#3F6273` |
| `text-link` | `#0A6E86` | `#5FE3EC` |
| `text-on-brand` | `#FFFFFF` | `#FFFFFF` |
| `text-on-accent` | `#04171F` | `#04171F` |
| `text-on-interactive` | `#FFFFFF` | `#04171F` |

`text-on-accent` vs `text-on-brand` vs `text-on-interactive` is the part teams get
wrong. The rule is luminance, not semantics:

- **`text-on-brand`** = white. For deep fills (`deepwater-500/600/700`, `error-500`).
- **`text-on-accent`** = ink. For bright fills (`stream-500`, `pulse-500`, `payout-500`, `success-500`).
- **`text-on-interactive`** = mode-resolved. Light mode's action fill is dark, so white;
  dark mode's action fill is bright cyan, so ink. Using one value for both modes fails —
  white on `stream-500` is 2.32:1.

### 2.3 Interactive and feedback tokens

| Token | Light | Dark |
| --- | --- | --- |
| `interactive` | `#0A6E86` | `#10BBD1` |
| `interactive-hover` | `#085A6E` | `#3ACFE1` |
| `interactive-active` | `#064A5B` | `#0BA3B7` |
| `interactive-disabled` | `#D3E0E2` | `#1C3440` |
| `focus-ring` | `#0A6E86` | `#5FE3EC` |
| `border-subtle` | `#D3E0E2` | `#1C3440` |
| `border-default` | `#758D94` | `#4E7687` |

| Token | Light | Dark |
| --- | --- | --- |
| `feedback-success` | `#1B7A57` | `#4FD69A` |
| `feedback-payout` | `#8A4E08` | `#F2A23C` |
| `feedback-warning` | `#A85B10` | `#F0B45E` |
| `feedback-error` | `#A82F27` | `#F0847B` |
| `fill-success` | `#2E9E6B` | `#2E9E6B` |
| `fill-payout` | `#F2A23C` | `#F2A23C` |
| `fill-payout-edge` | `#C77A1E` | `#F2A23C` |
| `fill-error` | `#CF463D` | `#CF463D` |

Two notes:

1. **Warning and Payout share a hue family.** In the source guidelines both were amber
   (`#F2A23C` payout, `#DE8A2A` warning) — a real collision, since "you earned this" and
   "something is wrong" would look alike. Resolved by pushing warning to `warn-600`
   `#A85B10` in light mode and `warn-400` `#F0B45E` in dark,
   both distinctly more orange-brown than Payout, and by never rendering the two in the
   same component. Colour alone must not carry the distinction — always pair with an icon.
2. **`success-payout` was split.** The request named one token; it is two. `feedback-success`
   (mint) means the system succeeded. `feedback-payout` (amber) means money moved. Merging
   them spends the strongest colour in the palette on "settings saved".

---

## 3. Accessibility validation

Computed against WCAG 2.1: **1.4.3** text (4.5:1 normal / 3:1 large; AAA at 7:1 / 4.5:1)
and **1.4.11** non-text contrast (3:1 for component boundaries and state indicators).
Disabled controls and purely decorative dividers are exempt and marked as such.

| Mode | Token | Foreground | Background | Ratio | Applied as | Result |
| --- | --- | --- | --- | --- | --- | --- |
| Light | `text-primary` | `#04171F` | `#F4F8F9` | 17.12:1 | Normal text | AAA |
| Light | `text-primary` | `#04171F` | `#FFFFFF` | 18.30:1 | Normal text | AAA |
| Light | `text-secondary` | `#2E4A55` | `#FFFFFF` | 9.43:1 | Normal text | AAA |
| Light | `text-secondary` | `#2E4A55` | `#F4F8F9` | 8.82:1 | Normal text | AAA |
| Light | `text-muted` | `#566A70` | `#FFFFFF` | 5.69:1 | Normal text | AA |
| Light | `text-muted` | `#566A70` | `#F4F8F9` | 5.32:1 | Normal text | AA |
| Light | `text-link` | `#0A6E86` | `#FFFFFF` | 5.85:1 | Normal text | AA |
| Light | `text-disabled` | `#8199A0` | `#FFFFFF` | 3.00:1 | Exempt | n/a |
| Light | `text-on-interactive` | `#FFFFFF` | `#0A6E86` | 5.85:1 | Normal text | AA |
| Light | `text-on-interactive` | `#FFFFFF` | `#085A6E` | 7.78:1 | Normal text | AAA |
| Light | `text-on-interactive` | `#FFFFFF` | `#064A5B` | 9.82:1 | Normal text | AAA |
| Light | `text-on-accent` | `#04171F` | `#F2A23C` | 8.72:1 | Normal text | AAA |
| Light | `text-on-accent` | `#04171F` | `#2E9E6B` | 5.42:1 | Normal text | AA |
| Light | `text-on-brand` | `#FFFFFF` | `#CF463D` | 4.57:1 | Normal text | AA |
| Light | `feedback-success` | `#1B7A57` | `#FFFFFF` | 5.29:1 | Normal text | AA |
| Light | `feedback-payout` | `#8A4E08` | `#FFFFFF` | 6.62:1 | Normal text | AA |
| Light | `feedback-warning` | `#A85B10` | `#FFFFFF` | 5.04:1 | Normal text | AA |
| Light | `feedback-error` | `#A82F27` | `#FFFFFF` | 6.77:1 | Normal text | AA |
| Light | `interactive` | `#0A6E86` | `#FFFFFF` | 5.85:1 | UI / non-text | AA |
| Light | `interactive` | `#0A6E86` | `#F4F8F9` | 5.47:1 | UI / non-text | AA |
| Light | `border-default` | `#758D94` | `#FFFFFF` | 3.50:1 | UI / non-text | AA |
| Light | `border-default` | `#758D94` | `#F4F8F9` | 3.28:1 | UI / non-text | AA |
| Light | `fill-payout-edge` | `#C77A1E` | `#FFFFFF` | 3.37:1 | UI / non-text | AA |
| Light | `focus-ring` | `#0A6E86` | `#FFFFFF` | 5.85:1 | UI / non-text | AA |
| Light | `focus-ring` | `#0A6E86` | `#F4F8F9` | 5.47:1 | UI / non-text | AA |
| Light | `border-subtle` | `#D3E0E2` | `#FFFFFF` | 1.35:1 | Exempt | n/a |
| Light | `bg-surface` | `#FFFFFF` | `#F4F8F9` | 1.07:1 | Exempt | n/a |
| Light | `bg-elevated` | `#FFFFFF` | `#FFFFFF` | 1.00:1 | Exempt | n/a |
| Dark | `text-primary` | `#EDF3F4` | `#04171F` | 16.32:1 | Normal text | AAA |
| Dark | `text-primary` | `#EDF3F4` | `#08222E` | 14.65:1 | Normal text | AAA |
| Dark | `text-secondary` | `#AEC4C9` | `#08222E` | 9.02:1 | Normal text | AAA |
| Dark | `text-secondary` | `#AEC4C9` | `#04171F` | 10.06:1 | Normal text | AAA |
| Dark | `text-muted` | `#7E969C` | `#08222E` | 5.26:1 | Normal text | AA |
| Dark | `text-muted` | `#7E969C` | `#04171F` | 5.86:1 | Normal text | AA |
| Dark | `text-link` | `#5FE3EC` | `#08222E` | 10.70:1 | Normal text | AAA |
| Dark | `text-disabled` | `#3F6273` | `#08222E` | 2.51:1 | Exempt | n/a |
| Dark | `text-on-interactive` | `#04171F` | `#10BBD1` | 7.89:1 | Normal text | AAA |
| Dark | `text-on-interactive` | `#04171F` | `#3ACFE1` | 9.75:1 | Normal text | AAA |
| Dark | `text-on-interactive` | `#04171F` | `#0BA3B7` | 6.05:1 | Normal text | AA |
| Dark | `text-on-accent` | `#04171F` | `#F2A23C` | 8.72:1 | Normal text | AAA |
| Dark | `text-on-accent` | `#04171F` | `#2E9E6B` | 5.42:1 | Normal text | AA |
| Dark | `text-on-brand` | `#FFFFFF` | `#CF463D` | 4.57:1 | Normal text | AA |
| Dark | `feedback-success` | `#4FD69A` | `#08222E` | 8.93:1 | Normal text | AAA |
| Dark | `feedback-payout` | `#F2A23C` | `#08222E` | 7.83:1 | Normal text | AAA |
| Dark | `feedback-warning` | `#F0B45E` | `#08222E` | 8.91:1 | Normal text | AAA |
| Dark | `feedback-error` | `#F0847B` | `#08222E` | 6.48:1 | Normal text | AA |
| Dark | `interactive` | `#10BBD1` | `#08222E` | 7.08:1 | UI / non-text | AA |
| Dark | `interactive` | `#10BBD1` | `#04171F` | 7.89:1 | UI / non-text | AA |
| Dark | `border-default` | `#4E7687` | `#08222E` | 3.34:1 | UI / non-text | AA |
| Dark | `border-default` | `#4E7687` | `#04171F` | 3.72:1 | UI / non-text | AA |
| Dark | `fill-payout-edge` | `#F2A23C` | `#08222E` | 7.83:1 | UI / non-text | AA |
| Dark | `focus-ring` | `#5FE3EC` | `#08222E` | 10.70:1 | UI / non-text | AA |
| Dark | `focus-ring` | `#5FE3EC` | `#04171F` | 11.93:1 | UI / non-text | AA |
| Dark | `border-subtle` | `#1C3440` | `#08222E` | 1.26:1 | Exempt | n/a |
| Dark | `bg-surface` | `#08222E` | `#04171F` | 1.11:1 | Exempt | n/a |
| Dark | `bg-elevated` | `#0D2E3D` | `#08222E` | 1.15:1 | Exempt | n/a |

**Result: 0 failures across 56 pairs.**

Three findings worth recording, because each one changed a value:

- **Light borders needed a 7th neutral.** `neutral-400` `#8199A0` gives 3.00:1
  on white but only 2.81:1 on the tinted canvas `#F4F8F9` — a fail. Borders now use
  `neutral-500` `#758D94` (3.28:1 on canvas, 3.50:1 on surface).
- **Amber fills fail 1.4.11 on white.** `payout-500` is 2.10:1 against white, so an amber badge
  has no discernible boundary. It requires a `payout-600` edge, which is 3.37:1. Enforced in
  `tokens.css` as `.ss-badge-payout`.
- **Dark-mode borders were far too dark.** A conventional `#2B4653` hairline is 1.65:1 against
  `bg-surface` — invisible to SC 1.4.11. Real borders need `#4E7687` (3.34:1).
  The `#1C3440` hairline survives only as a decorative divider, which is exempt.

---

## 4. Digital to print

### 4.1 Conversions

CMYK below is an **unmanaged mathematical conversion** — a starting point, not a press
spec. Any real job must be converted through the actual output profile (GRACoL 2013 for
coated, SWOP for web, PSO Uncoated for uncoated stock), because naive conversion produces
unprintable builds for the darks: `deepwater-900` computes to C83 M26 Y0 K82, which is a
muddy, high-ink build no printer would accept.

| Colour | Hex | RGB | CMYK (naive C/M/Y/K) | Nearest PMS | ΔE2000 | Alternate |
| --- | --- | --- | --- | --- | --- | --- |
| Deepwater 900 — primary brand | `#08222E` | 8, 34, 46 | 83 / 26 / 0 / 82 | PMS 5395 C | ΔE 1.7 | PMS 5463 C |
| Deepwater 950 — ground | `#04171F` | 4, 23, 31 | 87 / 26 / 0 / 88 | PMS 5395 C | ΔE 2.4 | PMS Black 6 C |
| Deepwater 500 — deep action | `#0A6E86` | 10, 110, 134 | 93 / 18 / 0 / 47 | PMS 3145 C | ΔE 3.8 | PMS 3165 C |
| Stream 500 — accent | `#10BBD1` | 16, 187, 209 | 92 / 11 / 0 / 18 | PMS 3125 C | ΔE 2.1 | PMS 311 C |
| Stream 300 — signal | `#5FE3EC` | 95, 227, 236 | 60 / 4 / 0 / 7 | PMS 318 C | ΔE 7.3 | PMS 3252 C |
| Pulse 500 — flow | `#2FE3A2` | 47, 227, 162 | 79 / 0 / 29 / 11 | PMS 3385 C | ΔE 5 | PMS 3405 C |
| Payout 500 — warmth | `#F2A23C` | 242, 162, 60 | 0 / 33 / 75 / 5 | PMS 1375 C | ΔE 3.4 | PMS 137 C |
| Neutral 600 — slate | `#566A70` | 86, 106, 112 | 23 / 5 / 0 / 56 | PMS 7545 C | ΔE 8.6 | PMS 3145 C |
| Neutral 200 — fog | `#D3E0E2` | 211, 224, 226 | 7 / 1 / 0 / 11 | PMS 7541 C | ΔE 2 | PMS 427 C |
| Neutral 50 — mist | `#F4F8F9` | 244, 248, 249 | 2 / 0 / 0 / 2 | PMS 7541 C | ΔE 5.4 | PMS 427 C |

ΔE2000 is measured between the sRGB hex and the published screen approximation of each
Pantone solid coated colour. **ΔE under 2 is a strong match, 2–4 is usable, above 5 means
there is no good spot equivalent.** These are candidates for specification, not final
values — Pantone matching is done on a physical Bridge or Formula Guide under D50, never
on a monitor.

### 4.2 Stationery recommendation

**Business cards and letterhead should print two spot colours, not four-colour process.**

- **Spot 1 — PMS 5395 C** (ΔE 1.7 to `deepwater-900`). Carries the field, the wordmark, and
  all body setting. An excellent match, and a large flat dark field is exactly where CMYK
  betrays you: registration shifts and banding show on a solid dark card in a way they never
  do in a spot ink.
- **Spot 2 — PMS 3125 C** (ΔE 2.1 to `stream-500`). The mark and the rule. Saturated
  cyan-turquoise sits outside the coated CMYK gamut, so process would render it visibly
  duller than the screen brand.

If a job must run process — a brochure, anything with photography — use a controlled rich
black for the Deepwater field rather than the naive build: **C70 M50 Y40 K100**, total area
coverage 260%, which is inside GRACoL's 320% limit and holds detail in reversed type. Confirm
with a contract proof on the actual stock; dark teals shift green on uncoated.

### 4.3 Colours that must not go to press

- **`stream-300` Signal `#5FE3EC`** — nearest spot is ΔE 7.3. No usable match.
- **`pulse-500` Pulse `#2FE3A2`** — nearest spot is ΔE 5.0.

Both are high-luminance saturated cyan-greens, the region where neither the CMYK gamut nor
the Pantone solid range reaches. That the nearest spot inks are this far away is the evidence.
**Treat both as screen-only.** Where print needs a light accent, tint Spot 2 (PMS 3125 C at
25–40%) instead of chasing the on-screen colour.

---

## 5. Files

| File | Purpose |
| --- | --- |
| `tokens.json` | DTCG-format tokens. Source of truth for Style Dictionary / Figma Variables. |
| `tokens.css` | Custom properties, all three theme states wired. Drop-in. |
| `COLOR-SYSTEM.md` | This document. |

## 6. Open decisions

1. **Pulse enters the palette.** The source guidelines had no neon mint; `pulse-500` is
   `success-500` `#2E9E6B` pushed into the neon range. It needs sign-off, or
   the system runs on Stream alone.
2. **Neutral count.** Shipped as 7 steps, not the 6 requested, because light-mode borders
   cannot clear 3:1 on a tinted canvas with 6. The alternative is an untinted white canvas,
   which loses the teal bias.
3. **Dual-sided accent split.** Currently both sides share Stream for actions and differ only
   in whether Payout appears. The stronger move may be giving the taker side Pulse as its
   action colour outright — a bigger identity decision than a colour one.
