# SurveyStream — design tokens

Four artefacts, one source. Generated from `color-system/tokens.json` and
`typography/type-tokens.json`, so they cannot drift from the specs they encode.

| File | Format | Consume with |
| --- | --- | --- |
| `design-tokens.json` | W3C DTCG | Style Dictionary 4+, Tokens Studio, Figma Variables |
| `theme.css` | CSS custom properties | Any stack. Import once, before your framework's base layer. |
| `tailwind.config.js` | Tailwind v3 | `content` + `theme.extend` |
| `tailwind-v4.css` | Tailwind v4 `@theme` | `@import` after `theme.css` |

**202 tokens · 119 aliases, all resolving · 46 primitives · 27 semantic roles per theme.**

## Quick start

```css
/* 1. tokens first — they define the variables everything else reads */
@import "./tokens/theme.css";

/* 2a. Tailwind v4 */
@import "tailwindcss";
@import "./tokens/tailwind-v4.css";
```

```js
// 2b. Tailwind v3
module.exports = require('./tokens/tailwind.config.js');
```

Then write ordinary utilities — they follow the theme with **no `dark:` prefix**, because the
semantic variables are re-declared per theme rather than duplicated per utility:

```html
<div class="bg-canvas text-primary rounded-lg shadow-md p-6">
  <h1 class="font-display text-h1">Turn feedback into value</h1>
  <p class="text-body text-muted">Body copy on a 4px grid.</p>
  <span class="bg-fill-payout text-on-accent border-hairline border-fill-payout-edge rounded-full px-3">
    Payout cleared
  </span>
</div>
```

## Architecture

Three layers. **Components may only reference layer 3.**

```
primitive              →  semantic (per theme)  →  utility / component
color.deepwater.500    →  theme.light.color.action.interactive  →  bg-interactive
color.stream.500       →  theme.dark.color.action.interactive   →  bg-interactive
```

Every semantic token is an **alias**, never a literal — the generator throws if a semantic value
has no primitive to point at. That rule is why `color.slate.*` exists: the dark UI's structural
values (dividers, disabled text, the sunken surface) had been authored straight into the semantic
layer. A semantic token that cannot resolve to a primitive is not a token, it is a hard-coded
value wearing a name.

```jsonc
// theme.light.color.text.primary  →  {color.deepwater.950}   #04171F
// theme.dark.color.text.primary   →  {color.slate.100}       #EDF3F4
```

## Theming

Dark mode is applied by **any of three mechanisms**, and `theme.css` handles all three because a
real product needs all three:

| Mechanism | Selector | Use |
| --- | --- | --- |
| OS preference | `@media (prefers-color-scheme: dark)` | The default for a visitor who has never chosen |
| Explicit choice | `[data-theme="dark"]` / `[data-theme="light"]` | An in-product toggle |
| Framework class | `.dark` / `.light` | Tailwind's `darkMode: 'class'` |

The media query is guarded — `:root:not([data-theme="light"]):not(.light)` — so an explicit light
choice still wins over a dark OS. **Never declare a colour only inside a media or `[data-theme]`
block:** the un-stamped state is the one most visitors are in, and a colour defined only in the
stamped state simply will not apply to them.

`tailwind.config.js` sets `darkMode: ['class', '[data-theme="dark"]']` so the `dark:` variant also
honours the attribute, for the cases where you do want to branch explicitly.

## Naming: why the CSS variable and the utility differ

CSS variables keep their property prefix because that is what reads correctly in raw CSS:

```css
background: var(--color-bg-canvas);
color: var(--color-text-primary);
```

Tailwind must **not** repeat the prefix, or you end up typing `text-text-primary`. In v3 the fix is
per-property theme keys (`backgroundColor`, `textColor`, `borderColor`); in v4, which has a single
`--color-*` namespace, the prefix is dropped in the `@theme` mapping. Either way the utility reads
as `bg-canvas` and `text-primary`.

Fills and feedback keep their prefix in both — `success` alone is ambiguous between the text colour
and the solid fill, and those are genuinely different values.

## Token sets

### Colour

46 primitives across `deepwater`, `stream`, `pulse`, `payout`, `neutral`, `slate`, `success`,
`warn`, `error`, `base`. 27 semantic roles per theme, grouped `bg` · `text` · `border` · `action` ·
`feedback` · `fill`.

**Primitive utilities do not follow the theme.** `bg-stream-500` is a literal. Reach for those only
when a colour must be pinned regardless of mode — a specimen plate, a fixed piece of artwork.

### Typography

Ten steps on a 1.200 minor third from a 16px base, eight on-scale and two flagged off-scale.
Emitted three ways: individual `fontSize` / `lineHeight` / `letterSpacing` tokens, DTCG composite
`typography` tokens, and Tailwind `fontSize` tuples so `text-h1` carries its leading and tracking
automatically.

### Spacing

A **4px grid** — the same grid the type system snaps line-heights to, which is why the two sit
together without half-pixel drift. The built pages had accumulated 6 / 7 / 9 / 18 / 26 / 30px
values; those are rationalised here rather than enshrined.

### Radii

Rationalised from what shipped (4 / 9 / 12 / 14 / 18 / 999) into
`xs sm md lg xl 2xl 3xl full`.

### Elevation

Five steps, **different values per theme**. This is deliberate: in light mode lift is carried by
shadow, but in dark mode shadow is nearly invisible against a dark ground, so the dark set is
heavier *and* the surfaces themselves step up in lightness. A single shadow scale would make dark
mode look flat.

### Motion

The most lightly-evidenced set here, and worth saying so. `180ms` is the only UI transition the
system actually shipped; the rest of the scale is derived from it, and `60s` is the pattern drift
cycle. Five easing curves and five composite `transition` tokens are provided, but **there is no
motion system yet** — no choreography, no documented entrance/exit pairing per component. Treat
these as sound defaults, not as a specification.

`theme.css` ships a `prefers-reduced-motion` block that collapses all animation and transition
durations to 1ms.

## Spec compliance notes

- **Dimensions are strings** (`"16px"`), not the `{ value, unit }` object form in the current DTCG
  editor's draft. Strings are what Style Dictionary, Tokens Studio and Figma Variables actually
  consume today. If your pipeline wants the object form, one transform over `$type: "dimension"`
  converts the whole file.
- `$type` is present on **every** token — 202 of 202.
- Composite types used: `typography`, `shadow`, `transition`, `cubicBezier`, `fontFamily`,
  `fontWeight`, `duration`, `dimension`, `color`.
- DTCG has no standardised theming mechanism yet. Themes here are a `theme.light` / `theme.dark`
  group whose leaves are aliases into the primitives — portable, and trivially splittable into
  per-theme files if your tooling prefers that.

## Verification

Regenerating runs these checks and fails loudly:

| Check | Result |
| --- | --- |
| Every `{alias}` resolves | 119 / 119 |
| Every token has `$type` | 202 / 202 |
| Light and dark semantic sets are identical in shape | ✓ |
| Every semantic value resolves to a primitive | ✓ (throws otherwise) |
| `slate` ramp is monotonically darkening | ✓ |
| CSS braces balanced, no undefined `var()` | ✓ |
| Tailwind v3 config compiles and emits every utility | ✓ |

The last one is a real compile, not a lint: the config is run through Tailwind against a fixture
using `bg-canvas`, `text-h1`, `shadow-md`, `border-fill-payout-edge`, `duration-base`,
`ease-standard` and the rest, and each utility is confirmed in the output.

## Relationship to the other files

`color-system/tokens.json` and `typography/type-tokens.json` remain the **domain sources** — they
carry the contrast validation and the font evidence that justify the values. This folder is the
**consolidated export** that adds spacing, radii, elevation and motion, and is what application
code should consume. Change a value in the domain source, regenerate, and everything downstream
follows.

`BRAND-GUIDELINES.md` is generated from the same domain sources, so the document and the tokens
cannot disagree.
