# T-009: CSS Design Token System — Semantic Tokens, Component Tokens, Utility Classes
**Type:** Task
**Phase:** 0 — Foundation
**Autonomy:** `agent:autonomous` — Pure CSS, fully specified, no decisions required.
**Stack:** `stack:typescript`
**Version:** v0.1
**Iteration:** iter-1
**Effort:** XS (1 hour)
---
> ⚠️ **Agent Scope:** T-001 defined primitive tokens in `tokens.css`. T-009 adds the second layer: semantic component tokens, reusable utility classes, and keyframe animations. Do not modify the primitive tokens from T-001 — extend only. Do not wire any interactivity or JavaScript — pure CSS.
---
## Context
T-001 created `src/styles/tokens.css` with raw primitives: colors, spacing, typography scale. That layer answers "what colors exist." This task adds the layer that answers "what does a panel header look like" — semantic tokens and utility classes that UI components reference directly instead of reaching back to raw primitives.
Having both layers means a component like `TrackMixer.ts` writes `class="panel-header"` and picks up the correct font, color, spacing, and border in one class — not a sequence of raw token references that must be manually kept in sync.
---
## Prerequisites
- [ ] T-001 merged — `src/styles/tokens.css` exists with all primitive tokens
- [ ] Fonts bundled (`JetBrains Mono`, `Space Grotesk`) per T-001 spec
---
## Acceptance Criteria
- [ ] `src/styles/tokens.css` — T-001's primitive tokens intact and verified complete (add any missing from the list below)
- [ ] `src/styles/components.css` — all semantic token blocks defined and imported in `index.html` after `tokens.css`
- [ ] `src/styles/animations.css` — all keyframes defined and imported after `components.css`
- [ ] `pnpm typecheck` still passes (CSS-only change, no TS impact)
- [ ] No raw hex values in `components.css` or `animations.css` — all colors reference `var(--color-*)` tokens
- [ ] No `!important` anywhere in any style file
- [ ] CSS loads in correct order: `reset.css` → `tokens.css` → `components.css` → `animations.css` → `app.css`
---
## Verify T-001 Primitive Tokens Are Complete
Before writing anything new, check `tokens.css` has all of the following. Add any that are missing:
```css
/* Required primitives from T-001 — verify each exists */
--color-bg, --color-surface, --color-surface-2, --color-border
--color-text, --color-text-dim, --color-text-bright
--color-accent, --color-accent-dim, --color-accent-2
--color-red, --color-green, --color-yellow, --color-orange
--color-rust, --color-cpp, --color-ts, --color-python, --color-ml
--font-mono, --font-ui
--font-size-xs, --font-size-sm, --font-size-base, --font-size-md, --font-size-lg
--font-weight-light, --font-weight-normal, --font-weight-medium, --font-weight-semi, --font-weight-bold
--space-1 through --space-8
--radius-sm, --radius-md, --radius-lg
--transition-fast, --transition-base
```
Add one missing primitive that T-001 did not include:
```css
/* Add to :root in tokens.css */
/* Layout heights — used by app.css grid and component positioning */
--height-header: 56px;
--height-bottom: 100px;
/* Panel widths */
--width-left-panel: 240px;
--width-right-panel: 280px;
/* Z-index scale */
--z-base: 0;
--z-overlay: 10;
--z-modal: 100;
--z-toast: 200;
```
---
## `src/styles/components.css`
```css
/* ─── Semantic tokens ─────────────────────────────────────────────────────── */
/* These are aliases of primitives scoped to UI roles. */
/* Change a primitive → all its semantic aliases update automatically. */
:root {
/* Panel structure */
--panel-bg: var(--color-surface);
--panel-bg-alt: var(--color-surface-2);
--panel-border: var(--color-border);
--panel-padding: var(--space-5);
/* Panel header row (e.g. "TRACK MIXER [MODEL: MUSICVAE]") */
--panel-header-font: var(--font-mono);
--panel-header-size: var(--font-size-xs);
--panel-header-color: var(--color-text-dim);
--panel-header-weight: var(--font-weight-normal);
--panel-header-gap: var(--space-2);
--panel-header-track: 0.1em;
/* Bracket-style label e.g. [MODEL: MUSICVAE], [ENGINE: WEBGPU] */
--label-bracket-color: var(--color-text-dim);
--label-bracket-font: var(--font-mono);
--label-bracket-size: var(--font-size-xs);
/* Large display values — Mood, Energy, Texture panels */
--display-value-size: var(--font-size-lg);
--display-value-weight: var(--font-weight-bold);
--display-value-color: var(--color-text-bright);
--display-label-size: var(--font-size-xs);
--display-label-color: var(--color-text-dim);
--display-label-track: 0.12em;
/* Channel strips (LEAD, PAD, BASS) */
--channel-name-size: var(--font-size-base);
--channel-name-weight: var(--font-weight-bold);
--channel-name-color: var(--color-text-bright);
--channel-label-color: var(--color-text-dim);
--channel-label-size: var(--font-size-xs);
/* Fader / slider track */
--fader-track-bg: var(--color-surface-2);
--fader-track-border: var(--color-border);
--fader-fill-color: var(--color-accent);
--fader-thumb-color: var(--color-text-bright);
--fader-height: 4px;
/* Transport display */
--transport-bpm-size: 28px;
--transport-bpm-weight: var(--font-weight-bold);
--transport-bpm-font: var(--font-mono);
--transport-meta-size: var(--font-size-xs);
--transport-meta-color: var(--color-text-dim);
/* Status pill (WebGPU Active, LIVE) */
--pill-bg: var(--color-accent-dim);
--pill-border: var(--color-accent);
--pill-color: var(--color-accent);
--pill-font: var(--font-mono);
--pill-size: var(--font-size-xs);
--pill-padding: var(--space-1) var(--space-3);
--pill-radius: var(--radius-sm);
/* REC indicator */
--rec-color: var(--color-red);
--rec-dot-size: 8px;
/* Accent button (FREEZE) */
--btn-accent-bg: transparent;
--btn-accent-border: var(--color-border);
--btn-accent-color: var(--color-text);
--btn-accent-bg-hover: var(--color-surface-2);
--btn-accent-border-hover: var(--color-accent);
/* Icon button (settings gear, +/-) */
--btn-icon-color: var(--color-text-dim);
--btn-icon-color-hover: var(--color-text-bright);
/* Macro knob/fader label */
--macro-label-size: var(--font-size-xs);
--macro-label-color: var(--color-text-dim);
--macro-label-font: var(--font-mono);
--macro-value-color: var(--color-text);
/* Radar chart */
--radar-line-color: rgba(0, 200, 212, 0.2);
--radar-fill-color: rgba(0, 200, 212, 0.08);
--radar-point-color: var(--color-accent);
--radar-label-size: var(--font-size-xs);
--radar-label-color: var(--color-text-dim);
}
/* ─── Utility classes ─────────────────────────────────────────────────────── */
/* Bracket label: [MODEL: MUSICVAE] */
.label-bracket {
font-family: var(--label-bracket-font);
font-size: var(--label-bracket-size);
color: var(--label-bracket-color);
letter-spacing: 0.08em;
}
/* Panel header row */
.panel-header {
display: flex;
align-items: center;
gap: var(--panel-header-gap);
font-family: var(--panel-header-font);
font-size: var(--panel-header-size);
font-weight: var(--panel-header-weight);
color: var(--panel-header-color);
letter-spacing: var(--panel-header-track);
text-transform: uppercase;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--panel-border);
}
/* Status pill */
.pill {
display: inline-flex;
align-items: center;
gap: var(--space-2);
background: var(--pill-bg);
border: 1px solid var(--pill-border);
border-radius: var(--pill-radius);
color: var(--pill-color);
font-family: var(--pill-font);
font-size: var(--pill-size);
padding: var(--pill-padding);
}
.pill-dot {
width: var(--rec-dot-size);
height: var(--rec-dot-size);
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
/* REC indicator */
.rec-indicator {
display: inline-flex;
align-items: center;
gap: var(--space-2);
color: var(--rec-color);
font-family: var(--font-mono);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
}
/* Large display value (e.g. "MYSTERIOUS", "0.8 TeV", "DENSE") */
.display-value {
font-size: var(--display-value-size);
font-weight: var(--display-value-weight);
color: var(--display-value-color);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.display-label {
font-family: var(--font-mono);
font-size: var(--display-label-size);
color: var(--display-label-color);
letter-spacing: var(--display-label-track);
text-transform: uppercase;
}
/* Channel name in Track Mixer */
.channel-name {
font-size: var(--channel-name-size);
font-weight: var(--channel-name-weight);
color: var(--channel-name-color);
}
/* Accent button (FREEZE) */
.btn-accent {
display: inline-flex;
align-items: center;
gap: var(--space-2);
background: var(--btn-accent-bg);
border: 1px solid var(--btn-accent-border);
border-radius: var(--radius-sm);
color: var(--btn-accent-color);
font-family: var(--font-mono);
font-size: var(--font-size-xs);
padding: var(--space-1) var(--space-3);
cursor: pointer;
transition: border-color var(--transition-fast), background var(--transition-fast);
}
.btn-accent:hover {
background: var(--btn-accent-bg-hover);
border-color: var(--btn-accent-border-hover);
}
/* Icon button (+/-, gear) */
.btn-icon {
background: transparent;
border: none;
color: var(--btn-icon-color);
cursor: pointer;
padding: var(--space-1);
transition: color var(--transition-fast);
line-height: 1;
}
.btn-icon:hover { color: var(--btn-icon-color-hover); }
/* Monospace data label (BPM, bar/beat counter, parameter values) */
.mono-data {
font-family: var(--font-mono);
font-size: var(--font-size-xs);
color: var(--color-text-dim);
letter-spacing: 0.05em;
}
/* Divider line */
.divider {
border: none;
border-top: 1px solid var(--panel-border);
margin: 0;
}
```
---
## `src/styles/animations.css`
```css
/* ─── Keyframe definitions ───────────────────────────────────────────────── */
/* All animation durations use CSS custom properties so they can be overridden */
/* by JS when synced to BPM (e.g. --beat-ms set from transport_tick events). */
:root {
--anim-pulse-duration: 2s; /* Default heartbeat pulse — overridden by BPM */
--anim-blink-duration: 1s; /* REC indicator blink */
--anim-fade-in: 200ms;
}
/* Status dot pulse (WebGPU Active indicator) */
@keyframes dot-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* REC indicator blink */
@keyframes rec-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* Beat flash — applied to the transport area on each downbeat */
@keyframes beat-flash {
0% { opacity: 1; }
15% { opacity: 0.4; }
100% { opacity: 1; }
}
/* Fade in from transparent — used for panels mounting */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Subtle slide-up on mount */
@keyframes slide-up {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* ─── Animation utility classes ──────────────────────────────────────────── */
.animate-pulse {
animation: dot-pulse var(--anim-pulse-duration) ease-in-out infinite;
}
.animate-rec {
animation: rec-blink var(--anim-blink-duration) step-start infinite;
}
.animate-beat-flash {
animation: beat-flash 200ms ease-out forwards;
}
.animate-fade-in {
animation: fade-in var(--anim-fade-in) ease both;
}
.animate-slide-up {
animation: slide-up var(--anim-fade-in) ease both;
}
```
---
## Update `index.html` Import Order
```html
```
---
## Testing
```bash
pnpm typecheck # No TS impact — must still pass
pnpm format:check
```
Manual: open `cargo tauri dev`, open DevTools → Elements tab. Verify:
- `--color-accent` resolves to `#00c8d4` on `:root`
- `.panel-header` class applied to Track Mixer header shows correct uppercase monospace style
- `.animate-pulse` on the WebGPU status dot produces a visible fade
- `.animate-rec` on the REC indicator produces a hard blink
---
## GitHub CLI
```bash
gh issue create \
--title "T-009: CSS design token system — semantic tokens, component tokens, utility classes" \
--label "type:task,stack:typescript,agent:autonomous,priority:high,status:ready,day:1" \
--body-file T-009.md
```
---
**Parent:** GENESIS
**Blocks:** T-036 (skeleton renderer uses `.animate-pulse`), T-052 (Gemma panels use `.display-value`, `.display-label`), T-058 (waveform renderer uses radar tokens), T-113 (Unity data bridge overlay labels)
**Blocked By:** T-001
**Version:** v0.1 · **Iteration:** iter-1 · **Effort:** XS (1 hour)