# 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)