# TrainerDay Design System

> Auto-generated on 2026-04-19 from the per-platform AI docs.
> Regenerate: `cd apps/td-style-guide && npm run ai-docs`

Complete design reference for all TrainerDay platforms. Use this when building or modifying any TrainerDay UI.

---

## Brand Identity

**Friendly. Simple. Open.** TrainerDay feels like a helpful training buddy, not enterprise software.

### Core Principles

1. **Friendly, not corporate** -- rounded corners, pill buttons, generous whitespace
2. **Simple and open** -- one font, one primary color, one zone palette
3. **Readable while suffering** -- large numbers, high contrast, minimal info on workout screens
4. **One system, two themes** -- light (web/marketing), dark (mobile/desktop)

---

## Font

**DM Sans** -- the ONLY font across all platforms. Weights 300-700. Never use Inter, Roboto, Arial, or system fonts.

Desktop exception: **JetBrains Mono** for numeric data values only.

---

## Shared Color Tokens

These are identical across all platforms.

### Primary

| Token | Hex | Usage |
|-------|-----|-------|
| Primary | `#2563eb` | Buttons, links, active states |
| Primary Light | `#dbeafe` | Focus rings, hover backgrounds |
| Primary Dark | `#1d4ed8` | Hover/pressed states |

### Semantic

| Token | Hex | Usage |
|-------|-----|-------|
| Success | `#10b981` | Positive states, confirmations |
| Danger | `#e7140f` | Destructive actions, errors ONLY |
| Warning | `#eab308` | Caution states |
| Info | `#4ea1fe` | Informational states |

### Training Zone Colors (Zwift standard)

Source: `@trainerday/cycling-converter` ZONE_COLORS

| Zone | Color | Background (light) |
|------|-------|--------------------|
| Z1 Recovery | `#7F7F7F` | `#e8e8e8` |
| Z2 Endurance | `#3F8FCE` | `#dcebf6` |
| Z3 Tempo | `#49C072` | `#def4e6` |
| Z4 Threshold | `#FFCC3F` | `#fff6dc` |
| Z5 VO2max | `#F46D41` | `#fde5dd` |
| Z6 Anaerobic | `#D6270B` | `#f8d8d3` |

### Periodization Colors

| Phase | Hex |
|-------|-----|
| Base | `#02c9af` |
| Build | `#ffba4a` |
| Peak | `#f86f2b` |
| Event | `#1a9af6` |

### Light Theme (web + marketing)

| Token | Hex |
|-------|-----|
| Text | `#1a202c` |
| Text Muted | `#64748b` |
| Text Light | `#94a3b8` |
| Surface | `#f6f8fb` |
| Card | `#ffffff` |
| Border | `#e2e8f0` |
| Sidebar | `#1e293b` |

### Dark Theme (mobile + desktop)

| Token | Hex |
|-------|-----|
| BG | `#171626` (mobile) / `#0a0b10` (desktop) |
| Card | `#161820` |
| Elevated | `#1e2028` |
| Element | `#20232f` |
| Border | `rgba(255,255,255,0.07)` |
| Text Primary | `#e5e5e8` |
| Text Secondary | `#8a8a95` |
| Text Muted | `#55555f` |

---

## Golden Rules

### Always

- Use DM Sans (all weights 300-700)
- Rounded corners everywhere
- Generous whitespace
- One primary blue button per screen
- High contrast for workout-active screens

### Never

- Use any font besides DM Sans
- Use red for primary actions or branding (danger/destructive ONLY)
- Sharp, corporate design
- Cramped layouts
- More than 3-4 data points visible on workout-active screens

---

## Platform: Web App

**Tech:** Vue 3, Vite, Tailwind CSS v4
**Theme:** Light only

### Buttons

All buttons are **pill-shaped** (`border-radius: 100px`).

| Class | Style |
|-------|-------|
| `.td-btn-primary` | display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 24px; border-radius: 100px; font-weight: 600; font-size: 0.875rem; font-family: 'DM Sans', sans-serif; background: #2563eb; color: white; border: none; cursor: pointer; transition: all 0.15s |
| `.td-btn-primary:hover` | background: #1d4ed8 |
| `.td-btn-primary:disabled` | opacity: 0.5; cursor: not-allowed |
| `.td-btn-secondary` | display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 24px; border-radius: 100px; font-weight: 600; font-size: 0.875rem; font-family: 'DM Sans', sans-serif; background: white; color: #1a202c; border: 1.5px solid #e2e8f0; cursor: pointer; transition: all 0.15s |
| `.td-btn-secondary:hover` | border-color: #2563eb; color: #2563eb; background: #dbeafe |
| `.td-btn-secondary:disabled` | opacity: 0.5; cursor: not-allowed |
| `.td-btn-secondary-accent` | display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 24px; border-radius: 100px; font-weight: 600; font-size: 0.875rem; font-family: 'DM Sans', sans-serif; background: white; color: #2563eb; border: 1.5px solid #2563eb; cursor: pointer; transition: all 0.15s |
| `.td-btn-secondary-accent:hover` | background: #2563eb; color: white |
| `.td-btn-secondary-accent:disabled` | opacity: 0.5; cursor: not-allowed |
| `.td-btn-tertiary` | display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 24px; border-radius: 100px; border: 1px solid #e2e8f0; background: transparent; font-size: 13px; font-weight: 600; color: #64748b; cursor: pointer; transition: all 0.15s |
| `.td-btn-tertiary:hover` | border-color: #2563eb; color: #2563eb |
| `.td-btn-tertiary:disabled` | opacity: 0.5; cursor: not-allowed |
| `.td-btn-danger` | display: inline-flex; align-items: center; gap: 8px; color: #e7140f; font-weight: 600; font-size: 0.875rem; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; transition: all 0.15s; background: none; border: none |
| `.td-btn-danger:hover` | opacity: 0.7 |

Default padding: `12px 24px`. Font: `600 0.875rem DM Sans`.
Small variant: add `.is-small` (`7px 16px`, `13px` font).

### Cards

| Class | Style |
|-------|-------|
| `.td-card` | border-radius: 12px; background-color: white; border: 1px solid #e2e8f0; box-shadow: 0 1px 2px rgba(0,0,0,0.05); padding: 16px |
| `.td-base-card` | background-color: white; border: 1px solid #e2e8f0; padding: 16px; border-radius: 12px |

### Form Inputs

| Class | Style |
|-------|-------|
| `.td-field` | display: flex; flex-direction: column |
| `.td-field-label` | display: block; font-size: 0.875rem; font-weight: 600; color: #1a202c; margin-bottom: 6px |
| `.td-field-hint` | font-size: 0.75rem; color: #64748b; margin-top: 4px |
| `.td-field-error` | font-size: 0.75rem; color: #e7140f; margin-top: 4px |
| `.td-input-text` | width: 100%; padding: 10px 18px; border-radius: 100px; border: 1px solid #e2e8f0; background: white; color: #1a202c; font-size: 0.875rem; font-family: 'DM Sans', sans-serif; outline: none; transition: all 0.15s |
| `.td-input-text:focus` | border-color: #2563eb; box-shadow: 0 0 0 3px #dbeafe |
| `.td-input-text--error` | border-color: #e7140f |
| `.td-input-text--error:focus` | border-color: #e7140f; box-shadow: 0 0 0 3px rgba(231,20,15,0.2) |
| `.td-input-text` | border-radius: 20px; padding: 12px 18px |

### Links

| Class | Style |
|-------|-------|
| `.td-link` | color: #2563eb; text-decoration: underline dashed; text-underline-offset: 4px; cursor: pointer |
| `.td-link:hover` | text-decoration: none; color: #1a202c |
| `.td-link-muted` | color: #64748b; text-decoration: underline dashed; text-underline-offset: 4px; cursor: pointer |
| `.td-link-muted:hover` | text-decoration: none; color: #1a202c |
| `.td-link-danger` | color: #e7140f; text-decoration: underline dashed; text-underline-offset: 4px; cursor: pointer |
| `.td-link-danger:hover` | text-decoration: none; color: #1a202c |
| `.td-link-success` | color: #10b981; text-decoration: underline dashed; text-underline-offset: 4px; cursor: pointer |
| `.td-link-success:hover` | text-decoration: none; color: #1a202c |

### Headings

| Class | Style |
|-------|-------|
| `.td-h2` | font-size: 1.5rem; font-weight: 700; color: #1a202c |
| `.td-h3` | font-size: 1.25rem; font-weight: 700; color: #1a202c |
| `.td-h4` | font-size: 1.125rem; font-weight: 700; color: #1a202c |

### Web Rules

```
AI REFERENCE — WEB APP STYLE GUIDE
===================================
Tech: Vue 3, Vite, Tailwind CSS v4
Token file: apps/main-app-web/src/assets/css/tailwind.css
Component dir: apps/main-app-web/src/shared/components/

BUTTON TIERS:
- .td-btn-primary — Blue bg, white text, pill. One per screen.
- .td-btn-secondary — White bg, border, pill. Most common.
- .td-btn-secondary-accent — White bg, blue border/text, pill. Hover inverts.
- .td-btn-tertiary — Transparent, border, muted text. Low emphasis.
- .td-btn-danger — Red underlined text only. Destructive actions.
- Size: add .is-small for compact variant.

INPUT SYSTEM (Updated 2026-04-19 — pill shape):
- .td-input-text — Full-width pill-shaped input, 100px radius, 10px 18px padding. Focus: blue ring.
- .td-input-text--error — Red border + red ring.
- Textareas reuse .td-input-text but override to 20px radius (pill is wrong on multi-line).
- Selects also use rounded-pill + pl-[18px] pr-10 py-2.5 (pr-8 for compact).
- .td-field — Flex column wrapper.
- .td-field-label — Label above input.
- .td-field-hint — Helper text below input (muted).
- .td-field-error — Error text below input (red).

PAGE HEADER (New 2026-04-19):
- White band at top of every top-level page, full-width with bottom border.
- Tab-style title: px-4 py-3 text-[13px] uppercase font-bold tracking-[0.06em].
- Active tab: text-primary + border-b-[3px] border-primary.
- Single-title pages use the same tab styling (always active) for visual consistency.
- Replaces the old .page-title <h1> pattern.
- Pulled up over grey gap via margin-top: -24px; padding-top: 32px; margin-bottom: 32px.

READOUT PILL (New 2026-04-19):
- .td-readout-pill — 40px pill container. Top-notch label via data-label + ::before.
- .td-readout-col — One label/value column (label + value or label + badge).
- .td-readout-label / .td-readout-value / .td-readout-badge — Inner pieces.
- .td-readout-divider — 1px × 18px divider between columns.
- Use for contextual readouts (selected row, active interval). Not for static KPIs.

CARDS:
- .td-card — White bg, 12px radius, border, shadow, 16px padding.
- .td-base-card — Same but no shadow.

LINKS:
- .td-link — Primary blue, dashed underline, hides on hover.
- .td-link-muted — Same but muted gray.
- .td-link-danger — Same but red.
- .td-link-success — Same but green.

HEADINGS:
- .td-h2 / .td-h3 / .td-h4 — DM Sans bold heading utilities.

RULES:
1. Tailwind CSS only — no new SCSS.
2. Use design token colors (text-primary, bg-surface, border-border).
3. rounded-pill for buttons AND form inputs (text, select, etc.), rounded-md for cards, 20px for textareas.
4. DM Sans is global — no class needed.
5. Check the style guide before inventing patterns.
6. One primary blue button per screen.
7. Red is for danger only.
```

---

## Platform: Mobile App

**Tech:** React Native 0.80, TypeScript, Styled Components, Zustand + MMKV
**Theme:** Dark only

### Styling Rules

- **Styled Components ONLY** -- never `StyleSheet.create`, never inline styles
- Shared styles in `services/css/`
- All UI strings via `t('key')` (i18next, 11 languages)
- TestID format: `testID="[component][action/content][parent]"`

### Buttons

All buttons are **pill-shaped** (`border-radius: 9999px`). Always use `TDButton`, never raw `TouchableOpacity`.

| Class | Style |
|-------|-------|
| `.btn-main` | height: 46px; padding: 0 24px; background: #2563eb; color: #fff; font-size: 15px; min-width: 80px |
| `.btn-main-lg` | height: 52px; padding: 0 28px; background: #2563eb; color: #fff; font-size: 17px |
| `.btn-secondary` | height: 46px; padding: 0 24px; background: transparent; color: #F4F5F9; border: 1px solid #555770; font-size: 15px; min-width: 80px |
| `.btn-warning-link` | height: 46px; padding: 0 24px; background: transparent; color: #F4F5F9; border: 1px solid transparent; font-size: 15px |
| `.btn-success` | height: 46px; padding: 0 24px; background: #10b981; color: #fff; font-size: 15px |
| `.btn-disabled` | height: 46px; padding: 0 24px; background: #4A4B5F; color: #6A6B7F; font-size: 15px; cursor: not-allowed |

### Text Input

| Class | Style |
|-------|-------|
| `.td-input` | position: relative; height: 56px; border: 1px solid rgba(255,255,255,0.2); border-radius: 28px; background: #020202; padding: 0 20px |
| `.td-input-focused` | border-color: rgba(255,255,255,0.4) |
| `.td-input-error` | border-color: rgba(220,80,80,0.6) |
| `.td-input-label` | position: absolute; left: 20px; color: #B0B2BC; pointer-events: none |
| `.td-input-value` | position: absolute; top: 22px; left: 20px; font-size: 18px; color: #E0E0E4 |

### Workout Screen Rules

- Numbers readable from 1 meter
- Touch targets minimum `56px`
- Max 3-4 data points visible
- High contrast colors

### Key Components

Always use these -- never raw RN equivalents:
- `TDButton` -- for all buttons
- `TDText` -- for all text
- `TDTextInput` -- for all inputs
- Components in `src/shared/components/`

### Mobile Reference

```
AI REFERENCE -- MOBILE APP STYLE GUIDE
======================================
Tech: React Native 0.80, TypeScript, Styled Components, Zustand + MMKV
Location: apps/mobile-app-rn/

STYLING: Styled Components ONLY -- no StyleSheet.create, no inline styles.
Shared styles in services/css/

TOKEN FILE: src/services/css/themes/designTokens.ts
COLOR FILE: src/services/css/colors.ts
FONT FILE: src/services/css/font.ts
SIZES FILE: src/services/css/sizes.ts

KEY COMPONENTS:
- TDButton -- always use this, never raw TouchableOpacity for buttons
- TDText -- always use this, never raw Text
- Components in src/shared/components/

CONVENTIONS:
- PascalCase for components/files, camelCase for variables/functions
- All UI strings via t('key') (i18next, 11 languages)
- TestID format: testID="[component][action/content][parent]"
- Dark theme ONLY

WORKOUT SCREENS:
- Numbers readable from 1 meter
- Touch targets minimum 56px
- Max 3-4 data points visible
- High contrast colors

ZONE COLORS: from @trainerday/cycling-converter (same across all platforms)
```

---

## Platform: Desktop App

**Tech:** React, Electron, TypeScript, Zustand, inline CSSProperties
**Theme:** Dark only

### Critical Rule

**No Tailwind classes in JSX** -- ALL styling is inline `React.CSSProperties` objects. Tailwind is only used in `globals.css` for keyframe animations.

### The C Token Object

```typescript
const C = {
  bg: '#0a0b10',
  card: '#161820',
  elevated: '#1e2028',
  border: 'rgba(255,255,255,0.07)',
  borderSubtle: 'rgba(255,255,255,0.04)',
  text: '#e5e5e8',
  textSecondary: '#8a8a95',
  textMuted: '#55555f',
}
```

### Desktop Reference

```
AI REFERENCE — DESKTOP APP STYLE GUIDE
=======================================
Tech: React, Electron, TypeScript, Zustand, inline CSSProperties
Location: apps/td-electron/

CRITICAL: No Tailwind classes in JSX — ALL styling is inline React.CSSProperties objects.
Tailwind is configured but only used in globals.css for keyframe animations.

COLOR TOKEN PATTERN (from ProgressPage.tsx):
const C = {
  bg: '#0a0b10',
  card: '#161820',
  elevated: '#1e2028',
  border: 'rgba(255,255,255,0.07)',
  borderSubtle: 'rgba(255,255,255,0.04)',
  text: '#e5e5e8',
  textSecondary: '#8a8a95',
  textMuted: '#55555f',
}

SURFACE HIERARCHY:
- App bg: #0a0b10 (C.bg)
- Card: #161820 (C.card)
- Elevated: #1e2028 (C.elevated)
- Element: #20232f
- Control: #252836
- Active/pressed: #292D3D
- Popup border: #3a3d4a

TEXT HIERARCHY:
- Primary: #e5e5e8 (C.text) or #eaeaea
- Secondary: #8a8a95 (C.textSecondary) or #b8bac0
- Muted: #55555f (C.textMuted) or #83858b
- Dim: #5B5D65

ACCENT COLORS:
- Primary action: #2C68DE
- Chart line: #4a9eff
- Error/danger: #fe5959
- Success: #4cdd8a
- BLE Controllable: #2C68DE
- BLE Power: #e8a838
- BLE Heart Rate: #fe5959
- BLE Cadence: #4cdd8a
- BLE Speed: #a78bfa

INTENSITY COLORS:
- Easy: #4a7fff
- Moderate: #d4c94a
- Hard: #d45454

ZONE COLORS (Zwift standard):
- Z1: #7F7F7F, Z2: #3F8FCE, Z3: #49C072, Z4: #FFCC3F, Z5: #F46D41, Z6: #D6270B

BUTTONS: borderRadius 6 (NOT pill). Primary: bg #2C68DE. Outline: border #2C68DE.
CARDS: borderRadius 12, border rgba(255,255,255,0.07), padding 28.
NAV TABS: borderRadius 6, active bg #292D3D, height 36px, fontSize 13, fontWeight 500.
FONT: DM Sans for all text. JetBrains Mono for numeric values only.
FONT SMOOTHING: -webkit-font-smoothing: antialiased
CHARTS: Canvas-based (CanvasChart.tsx + DrawChart.ts). D3 for data, Canvas 2D for rendering.
CHART COLORS: line #4a9eff, bar fill rgba(74,158,255,0.12)
ZONE COLORS: from @trainerday/cycling-converter via splitRampByZones.

SHADOWS:
- Dropdown: 0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3)
- Toggle active: 0 1px 3px rgba(0,0,0,0.2)
- Tooltip: 0 4px 16px rgba(0,0,0,0.4)

POPUP/MODAL: bg #252836, border 1px solid #3a3d4a, borderRadius 12, heavy shadow.

LAYOUT:
- Page container: height 100%, overflow auto, padding 32, maxWidth 960, margin 0 auto
- Card grid: display grid, gap 24
- Two-column: gridTemplateColumns '1fr 1fr'

KEY COMPONENTS:
- Layout.tsx — root flex column
- TopNav.tsx — 36px tab navigation
- BottomBar.tsx — live workout metrics (867 lines, most complex UI)
- ProgressPage.tsx — fitness dashboard (675 lines, best style reference)
- ConnectPopup.tsx — BLE device connection modal
- SettingsPage.tsx — user config (SettingsSection, Row, NumberSetting, PillGroup, ToggleSwitch)

DARK THEME ONLY. No light mode.
```

---

## Platform: Marketing Site

**Tech:** Nuxt 3, Tailwind CSS v4, SSR
**Theme:** Light (matches web app) with dark header/footer

### Marketing CTA Button

| Class | Style |
|-------|-------|
| `.td-btn-marketing` | display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 14px 36px; border-radius: 100px; font-weight: 600; font-size: 17px; font-family: 'DM Sans', sans-serif; background: #2563eb; color: white; border: none; cursor: pointer; transition: all 0.15s |
| `.td-btn-marketing:hover` | background: #1d4ed8 |

### Marketing Reference

```
AI REFERENCE — MARKETING SITE STYLE GUIDE
==========================================
Tech: Nuxt 3, Tailwind CSS v4, SSR
Location: apps/td-home/

TOKEN FILE: apps/td-home/assets/css/tailwind.css
SYNC SOURCE: apps/main-app-web/src/assets/css/tailwind.css (subset)

BUTTONS:
- Same .td-btn-* classes as web app
- Marketing CTAs are LARGER: text-[17px] px-9 py-3.5
- All buttons pill-shaped (rounded-pill)

HEADER/FOOTER:
- Fixed dark header: bg-dark-bg (#171626)
- Footer: same dark bg
- Nav links: #b4b4c5 text
- CTA button in header: bg-primary blue

SECTIONS:
- bg-white — default content
- bg-surface (#f6f8fb) — alternating sections
- bg-dark-bg (#171626) — dark CTA blocks, header, footer
- bg-dark-marketing (#000000) — full-bleed dark sections

FITSCORE:
- Separate dark color set for FitScore sections (fs-bg, fs-card, fs-accent, etc.)
- These are NOT the same as the header dark colors

BLOG:
- .prose-blog class with @tailwindcss/typography
- Heading font: DM Sans (font-heading token)
- Link underline color: primary-light, darkens to primary on hover
- Code blocks: surface bg, border, rounded-sm
- Images: full width, rounded-md, generous margin

FORMS:
- Same input system as web app (.td-input-text, .td-field, etc.)
- Used on login, register, password reset pages

RULES:
1. Tailwind utility classes only — no new SCSS or CSS custom properties
2. Do NOT use red for primary actions — use bg-primary (blue)
3. Red is only for danger states (text-danger, bg-danger)
4. Token changes: check main-app-web source first, then sync
5. Do NOT run npm install locally — breaks Docker volume mounts
```
