---
id: glyco-foundations
title: Foundations — Tokens, Typography, Motion, Glass, Icons
chapter: 02
source-of-truth: src/app/globals.css
token-format: OKLCH CSS custom properties, exposed to Tailwind v4 via "@theme inline"
themes: { default: dark, mechanism: ".dark class on <html>", storage: localStorage["glyco-theme"] }
machine-readable: tokens.json
---

# 02 — Foundations

All design tokens live in **`src/app/globals.css`** as OKLCH CSS custom properties, with
light values on `:root` and dark overrides on `.dark`. Tailwind v4 maps them via the
`@theme inline` block in the same file (no `tailwind.config.js`). A machine-readable
export lives in [tokens.json](tokens.json).

**Rule zero: never hardcode a color, radius, shadow, duration, or z-index. Use a token.**

## 1. Color

### 1.1 Core palette

| Token | Light | Dark | Role |
|-------|-------|------|------|
| `--background` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | Page canvas |
| `--foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | Primary text |
| `--card` | `oklch(1 0 0)` | `oklch(0.18 0 0)` | Solid surface |
| `--popover` | `oklch(1 0 0)` | `oklch(0.18 0 0)` | Floating surface |
| `--primary` | `oklch(0.52 0.21 252)` | `oklch(0.55 0.22 252)` | **Brand blue** (iOS systemBlue, WCAG AA corrected) |
| `--primary-foreground` | `oklch(0.985 0 0)` | `oklch(1 0 0)` | Text on primary |
| `--secondary` | `oklch(0.97 0 0)` | `oklch(0.21 0 0)` | Secondary surface |
| `--muted` | `oklch(0.93 0 0)` | `oklch(0.21 0 0)` | Muted surface |
| `--muted-foreground` | `oklch(0.45 0 0)` | `oklch(0.68 0 0)` | Secondary text (AA vs background) |
| `--accent` | `oklch(0.97 0 0)` | `oklch(0.21 0 0)` | Hover wash |
| `--accent-muted` | `oklch(0.95 0.04 254)` | `oklch(0.35 0.08 254)` | Soft blue tint |
| `--destructive` | `oklch(0.52 0.22 25)` | `oklch(0.58 0.23 27)` | iOS systemRed, AA-darkened |
| `--border` | `oklch(0.85 0 0)` | `oklch(0.4 0 0)` | Decorative borders (1.4.11-exempt) |
| `--border-subtle` | `oklch(0.9 0 0)` | `oklch(0.27 0 0)` | Hairlines, chart grids |
| `--input` | `oklch(0.55 0 0)` | `oklch(0.6 0 0)` | Essential UI borders — keeps 3:1 vs background |

### 1.2 Sidebar tokens

`--sidebar` (`0.985` / `0.155`), `--sidebar-hover`, `--sidebar-active`,
`--sidebar-active-bg` (blue wash `oklch(0.93 0.04 252)` / `oklch(0.22 0.06 252)`),
`--sidebar-active-fg` (`oklch(0.45 0.22 252)` / `oklch(0.78 0.18 252)` — AA on both the
active wash and the sidebar base), `--sidebar-border`, `--sidebar-ring`.

### 1.3 Clinical feedback semantics

The success scale is split by **use** so each variant clears its WCAG target:

| Token | Light | Dark | Use | Contrast target |
|-------|-------|------|-----|-----------------|
| `--success-fg` | `#047857` (emerald-700) | `oklch(0.78 0.17 152)` | Text labels | 4.5:1 AA |
| `--success-num` | `#059669` (emerald-600) | = fg | Large KPI numbers | 3:1 AA-large |
| `--success-fill` | `#10b981` (emerald-500) | = fg | Bars, dots, borders | decorative |
| `--success-bg` | `oklch(0.95 0.05 145)` | `oklch(0.28 0.1 145)` | Washes | n/a |
| `--success-tint` / `-strong` / `-border` | `color-mix(… 10% / 15% / 22%)` | same | Translucent chips | n/a |
| `--warning-fg` / `--warning-bg` | `oklch(0.5 0.16 70)` / `oklch(0.96 0.05 70)` | `oklch(0.85 0.15 70)` / `oklch(0.3 0.1 60)` | Warnings | AA |
| `--danger-fg` / `--danger-bg` | `oklch(0.5 0.22 25)` / `oklch(0.96 0.05 25)` | `oklch(0.78 0.2 25)` / `oklch(0.3 0.12 25)` | Errors/risk | AA |
| `--info-fg` / `--info-bg` | `oklch(0.5 0.18 254)` / `oklch(0.96 0.04 254)` | `oklch(0.85 0.16 254)` / `oklch(0.3 0.1 254)` | Info | AA |

Deprecated aliases (`--success`, `--warning`, `--danger`, `--ring`, `--accent-blue`,
`--glass-header-bg`) exist for backward compatibility. **Do not use in new code.**

### 1.4 Clinical state semantics (patient data)

| Token | Light | Dark | Meaning |
|-------|-------|------|---------|
| `--color-ok` | = `--success-fg` | = `--success-fg` | Within target ("dentro do alvo") |
| `--color-attention` | `#f5a622` | `oklch(0.85 0.15 70)` | Borderline / needs attention |
| `--color-risk` | = `--danger-fg` | = `--danger-fg` | Out of range / risk |
| `--color-info` | = `--info-fg` | = `--info-fg` | Neutral/informative |
| `--color-inactive` | `oklch(0.45 0.01 220)` | `oklch(0.55 0.01 220)` | No data |

**Color is never the only channel**: clinical states always pair color with text and/or an
icon, plus `aria-label`.

### 1.5 Protocol identity tints

| Token | Value | Protocol |
|-------|-------|----------|
| `--protocol-trt` | `hsl(225 80% 55%)` | TRT / androgens |
| `--protocol-glp1` | `hsl(145 65% 42%)` | GLP-1 / metabolism |
| `--protocol-recomp` | `hsl(260 70% 58%)` | Body recomposition |
| `--protocol-performance` | `hsl(195 85% 48%)` | Performance |

### 1.6 Chart palette

Recharts, themed via CSS (`--chart-1…5`); grid lines use `--border-subtle`, labels
`--muted-foreground` at 11px (global override in globals.css).

| Token | Light | Dark |
|-------|-------|------|
| `--chart-1` | `oklch(0.646 0.222 41)` warm orange | `oklch(0.62 0.18 254)` blue |
| `--chart-2` | `oklch(0.6 0.118 185)` teal | `oklch(0.65 0.18 145)` green |
| `--chart-3` | `oklch(0.398 0.07 227)` dark blue | `oklch(0.74 0.16 70)` yellow |
| `--chart-4` | `oklch(0.828 0.189 84)` yellow-green | `oklch(0.62 0.22 25)` red |
| `--chart-5` | `oklch(0.769 0.188 70)` amber | `oklch(0.6 0.22 285)` magenta |

### 1.7 Focus ring

`--focus-ring`: `oklch(0.59 0.21 252)` light / `oklch(0.68 0.21 252)` dark (brighter on
dark). `--focus-ring-offset` matches the background. Global rules: buttons/links get
`outline: 2px solid var(--focus-ring); outline-offset: 2px`; inputs use offset 0; switches
offset 3px.

### 1.8 Elevation & z-index

| Token | Light | Dark |
|-------|-------|------|
| `--elevation-0` | none | none |
| `--elevation-1` | `0 1px 2px oklch(0 0 0 / 0.06)` | opacity 0.3 |
| `--elevation-2` | `0 2px 4px /0.08 + 0 1px 2px /0.04` | 0.4 / 0.2 |
| `--elevation-3` | `0 4px 16px /0.1 + 0 2px 4px /0.06` | 0.5 / 0.3 |
| `--elevation-4` | `0 8px 32px /0.12 + 0 4px 8px /0.06` | 0.6 / 0.4 |

Z-index scale: `--z-sidebar: 10`, `--z-header: 20`, `--z-dropdown: 30`, `--z-modal: 40`,
`--z-toast: 50`, `--z-tooltip: 60`. Never use arbitrary z-index values.

### 1.9 High contrast & reduced transparency

- `.high-contrast` on `<html>`: pure black/white surfaces, white primary, clinical hues at
  max brightness, glass becomes opaque.
- `@media (prefers-reduced-transparency: reduce)`: all `--lg-*` glass tokens fall back to
  opaque `var(--card)` / `var(--border)`; highlights become transparent.

## 2. Liquid Glass material

Glyco's signature surface (iOS 26 "Liquid Glass" adapted for web). Tokens:

| Token | Light | Dark |
|-------|-------|------|
| `--lg-blur` | `28px` | `28px` |
| `--lg-saturate` | `200%` | `200%` |
| `--lg-brightness` | `1` | `1.08` |
| `--lg-bg-regular` | `oklch(1 0 0 / 0.72)` | `oklch(0.17 0 0 / 0.72)` |
| `--lg-bg-clear` | `oklch(1 0 0 / 0.48)` | `oklch(0.17 0 0 / 0.48)` |
| `--lg-border` | `oklch(1 0 0 / 0.6)` | `oklch(1 0 0 / 0.12)` |
| `--lg-highlight` | `oklch(1 0 0 / 0.85)` | `oklch(1 0 0 / 0.18)` |
| `--lg-shadow-color` | `oklch(0 0 0 / 0.08)` | `oklch(0 0 0 / 0.3)` |

**Glass recipe** (what `Card variant="glass-regular"` / `GlassSurface` implement):

```css
background: var(--lg-bg-regular);
backdrop-filter: blur(var(--lg-blur)) saturate(var(--lg-saturate)) brightness(var(--lg-brightness));
border: 1px solid var(--lg-border);
box-shadow: inset 0 1px 0 var(--lg-highlight),          /* specular top edge */
            0 8px 32px var(--lg-shadow-color);
border-radius: 20px;                                     /* glass cards */
```

Variants: **regular** (0.72 alpha — default chrome, cards) vs **clear** (0.48 alpha —
media-rich contexts, the composer). Scoped variants exist for LGPD surfaces
(`--lgpd-glass-*`, Apple system colors, radius scale 24/20/14/7/pill), the canvas toolbar
(`--canvas-toolbar-*`), onboarding (`--glyco-glass-*`), and the Home dashboard
(`--home-surface-bg`, `--composer-inner`, `--home-glow`, `--home-tile*`, `--home-chip*`).
Use the scoped tokens inside those features; use `--lg-*` everywhere else.

**Always provide the reduced-transparency fallback** (§1.9) — components get it for free
if they use the tokens.

## 3. Typography

- **Family:** SF Pro Display, local `@font-face` from `public/fonts/SF-Pro-Display-*.otf`
  — weights 100/300/400/500/600/700/800. Stack: `"SF Pro Display", -apple-system,
  BlinkMacSystemFont, system-ui, sans-serif`.
- **Mono:** `ui-monospace, "SF Mono", "Geist Mono", "JetBrains Mono", monospace`.
  (Geist fonts are loaded as CSS variables in `layout.tsx` but SF Pro Display is the
  rendered sans face.)
- Global `font-feature-settings: "kern" 1, "liga" 1, "calt" 1`.
- Data/metrics: add `.tabular-nums` (`font-variant-numeric: tabular-nums`).

### Scale (iOS 26 Dynamic Type)

| Style | Size | Weight | Line-height | Letter-spacing |
|-------|------|--------|-------------|----------------|
| h1 / Large Title | 34px | 700 | 1.12 | −0.022em |
| h2 / Title 1 | 28px | 700 | 1.14 | −0.02em |
| h3 / Title 2 | 22px | 700 | 1.18 | −0.016em |
| h4 / Title 3 | 20px | 600 | 1.2 | −0.014em |
| h5 / Headline | 17px | 600 | 1.29 | −0.012em |
| Body (`p`, `text-base`) | 15px | 400 | 1.47 | −0.009em |
| `text-sm` | 13px | 400 | 1.38 | −0.004em |
| `text-xs` | 12px | 400 | 1.33 | 0 |
| Footnote (replaces uppercase labels) | 11px | 600 | — | −0.004em, muted |

**No uppercase tracking labels**: `.uppercase.tracking-wider/widest` styles are globally
neutralized; use the Footnote style instead.

## 4. Shape & spacing

- Base radius `--radius: 0.625rem` (10px). Scale: `sm` 6px · `md` 8px · `lg` 10px ·
  `xl` 14px · `2xl` 18px · pill `9999px`.
- Glass cards: 20px. Canvas cards: 16px. LGPD: 24/20/14/7px scale.
- **Concentric nesting:** child radius = parent radius − gutter (Card uses a 4px gutter:
  `rounded-[calc(var(--radius)-4px)]`).
- Spacing: Tailwind default 4px scale (no custom spacing tokens). Common rhythm: `gap-2/3/4`,
  `px-4`, `py-3/4`, `px-6` for page gutters.
- Breakpoints: Tailwind defaults (sm 640 / md 768 / lg 1024 / xl 1280 / 2xl 1536).

## 5. Motion

Source: `src/lib/animations.ts` (GSAP) + keyframes in `globals.css` + Motion (Framer) v12
for component animation. **Every animation must respect `prefers-reduced-motion`** —
a global media query collapses all animation/transition durations to 1ms, and JS animations
must gate via `gsap.matchMedia()` or `useReducedMotion()`.

### Duration tokens

| Token | Value | Use |
|-------|-------|-----|
| `instant` | 0.08s | Button press feedback |
| `fast` | 0.18s | Hover states |
| `base` | 0.28s | Standard transitions |
| `slow` | 0.42s | Page-level entrances |

### Easing tokens

| Token | Value | Use |
|-------|-------|-----|
| `EASE.enter` | `power2.out` | Content entrances |
| `EASE.exit` | `power2.in` | Content exits |
| `EASE.spring` | `back.out(1.4)` | Press release, playful overshoot |
| `EASE.smooth` | `power1.inOut` | Ambient/continuous (sparingly) |
| `--glyco-ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | CSS entrances |
| `--glyco-ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | CSS springs |

### Canonical behaviors

- **Button press:** `active → scale(0.95)`, 100ms spring; hover lift `translateY(-1px)`.
- **Card entrance:** `card-rise` (fade + 16px rise + scale 0.98→1); sets stagger via
  `.card-stagger` (40–340ms delays) or `staggerCards()` (70ms stagger).
- **Glass shimmer:** `.glass-shimmer` hover sweep, 600ms.
- **Number changes:** `@number-flow/react` digit-flip on KPI tiles.
- Ambient loops (orb-drift 18–26s, blob-float 20s, pulse-ring 2s) only for the composer
  orb, timeline pulses, and CGM badges — never on data content.

## 6. Iconography

| Library | Use |
|---------|-----|
| `lucide-react` | Generic UI: chevrons, close, loader, alerts |
| `iconsax-react` | Dashboard/clinical icons: documents, calendar, people, pills |
| `src/lib/sf-icons.tsx` | Custom SF Symbols set (~2,600 paths) — patient timeline, medication panels. `IconProps { size?=18, color?=currentColor, className?, style? }` |
| `glyco-icon.tsx` / `logo.tsx` | Brand marks only |

Icons inherit `currentColor` by default; size via the component prop, not CSS scaling.
Pick one library per surface (don't mix lucide and iconsax in the same row of controls).

## 7. Known token gaps (do not copy these patterns)

Hardcoded values that exist in the codebase and are **debt, not precedent**:

- Skill-tile hexes in `src/components/home/skills-grid.tsx` and `composer.tsx`:
  `#1eb3a8`, `#a06bff`, `#34d07f`, `#3b9eff`, `#f5a623` (≈ `--color-attention`), `#ff5fa2`.
- Composer orb gradient: `#dcecff → #92beec → #4f89d0 → #20467d`, strokes `#3b82f6`/`#0ea5e9`.
- `patients-card.tsx` green `#309668`; `bento.tsx` `#3b9eff` / `#1eb3a8`.
- Apple blues `#007AFF`/`#0A84FF` in `lgpd-policy-link.tsx` (should use `--lgpd-blue`).
- Legacy parallel token file `src/design-system/tokens/colors.css` (teal ramp) — only
  `glass.module.css` consumes it; do not extend.

When touching these files, migrate the hardcoded value to a token if low-risk.
