---
name: "designs-system"
description: "Design tokens with light/dark/system appearance, responsive device scales, type, spacing, and stacked design modules from Designs. Use for any UI that should match the Designs export. Use this skill whenever implementing UI for a project that follows the Designs token system: colors (light/dark), typography, spacing, radius, shadows, responsive device scales, and stacked design modules. Triggers on design tokens, theme, light mode, dark mode, spacing scale, type scale, design system, Designs."
metadata:
  short-description: "Designs tokens — dual theme, type, space, responsive"
  version: "1.0.0"
  source: "Designs export"
---

# Designs System

Agent instructions: treat this file as the **source of truth** for visual design.
Do not invent parallel palettes, type scales, or spacing systems. Prefer these tokens
in CSS variables / Tailwind `@theme` / component styles.

## When to use

- Building or restyling any product UI that should match this system
- Implementing light/dark/system appearance
- Choosing type scale, spacing rhythm, radii, shadows, motion, or icon size/stroke
- Scaffolding layouts for mobile (390), tablet (768), and desktop

## Appearance modes

Support three preferences that resolve to two appearances:

| Preference | Resolves to |
| --- | --- |
| `light` | Light tokens |
| `dark` | Dark tokens |
| `system` | `prefers-color-scheme` |

Set `data-theme="light"` | `"dark"` on `<html>` (or the app root). When preference
is `system`, listen to `prefers-color-scheme` and update `data-theme`.

## Design principles

1. **Tokens first** — no ad-hoc hex in components if a token exists
2. **Dual theme** — every color token has light and dark values
3. **One type scale** — use `typeScale` ratio for steps; display + body fonts only
4. **Concentric radii** — outer radius ≈ inner + padding
5. **8-based space** — `spaceUnit` multiples (device scale may adjust)
6. **Touch targets** — ≥44px on mobile/tablet
7. **Export parity** — CSS variables below match the Designs export

## Taste bar

- **Hierarchy** — one primary action and a readable type ladder; do not flatten display and body
- **Contrast** — WCAG AA or better for `fg` on `bg` / `surface` in both themes
- **Motion** — honor `prefers-reduced-motion`; never decorative-only animation. Optional WGSL/vgpu effects must fall back to CSS when WebGPU is missing and must not run a frame loop when reduced motion is preferred. Optional Paper Shaders (WebGL2) methods must fall back to CSS grain/filter when WebGL2 is missing or reduced motion is preferred. Optional Motion.dev (`motion`) playback maps published `--lab-duration-*` / `--lab-ease-*` only; do not call `animate` when reduced motion is preferred.
- **Density** — readable padding and type; neither cramped rows nor oversized empty chrome
- **Responsive** — verify layouts at 390, 768, and 1200
- **Components** — pick from this package's `guidance.components` when that list is present

## Motion playback

Optional [Motion](https://motion.dev) (`motion`, MIT) playback maps published `--lab-duration-*` and `--lab-ease-*` onto Motion `transition` (duration seconds + easing). Use this snippet only when the package CSS already defines those variables (hosted `motion` field). `--lab-transition-*` stay CSS shorthands — this snippet does not read them. Do not invent a second scale. Pin `motion` in the consumer app only — Studio chrome stays on CSS. Skip decorative playback when `prefers-reduced-motion: reduce` — do not call `animate`. React: `motion/react` only after `useReducedMotion()` is false; do not fade on first render.

```ts
import { animate } from "motion";
// React: motion/react + useReducedMotion() only. Never getComputedStyle/matchMedia during render.

function labVar(name: string): string {
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}

function durationToSeconds(value: string): number {
  const match = /^(0|[1-9]\d*)(?:\.(\d+))?(ms|s)$/.exec(value.trim());
  if (!match) return 0;
  const n = Number(match[1]) + (match[2] ? Number(`0.${match[2]}`) : 0);
  return match[3] === "s" ? n : n / 1000;
}

function cssEaseToMotion(value: string): string | [number, number, number, number] {
  const match =
    /^cubic-bezier\(\s*([+-]?\d*\.?\d+)\s*,\s*([+-]?\d*\.?\d+)\s*,\s*([+-]?\d*\.?\d+)\s*,\s*([+-]?\d*\.?\d+)\s*\)$/i.exec(
      value.trim(),
    );
  if (!match) return value.trim();
  return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])];
}

export function labTransition(
  durationVar = "--lab-duration-normal",
  easeVar = "--lab-ease-standard",
) {
  return {
    duration: durationToSeconds(labVar(durationVar)),
    ease: cssEaseToMotion(labVar(easeVar)),
  };
}

export function prefersReducedMotion(): boolean {
  return (
    typeof window !== "undefined" &&
    typeof window.matchMedia === "function" &&
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  );
}

export function playLabMotion(
  target: Element,
  keyframes: Record<string, unknown>,
  transition?: { duration: number; ease: string | [number, number, number, number] },
) {
  if (typeof document === "undefined") return;
  if (prefersReducedMotion()) return;
  return animate(target, keyframes, transition ?? labTransition());
}
```


## Icon

Size, stroke, and color tokens for the default [Lucide](https://lucide.dev) catalog (`lucide-react`, ISC; Feather-derived glyphs remain MIT). Pin Lucide in the consumer app and tree-shake named imports — do not paste SVGs and do not load every icon. Color reuses `currentColor` / `fg` / `fgMuted`; do not invent a parallel palette. Tailwind remains the default styling system.

| Role | CSS variable | Value |
| --- | --- | --- |
| size.sm | `--lab-icon-size-sm` | `16px` |
| size.md | `--lab-icon-size-md` | `20px` |
| size.lg | `--lab-icon-size-lg` | `24px` |
| stroke | `--lab-icon-stroke` | `2` |
| color | `--lab-icon-color` | `currentColor` |

Catalog: `lucide`. Semantic aliases resolve to Lucide names (`icon.delete` → `trash`):

| Alias | Lucide | `lucide-react` |
| --- | --- | --- |
| `icon.add` | `plus` | `Plus` |
| `icon.check` | `check` | `Check` |
| `icon.close` | `x` | `X` |
| `icon.delete` | `trash` | `Trash` |
| `icon.edit` | `pencil` | `Pencil` |
| `icon.home` | `house` | `House` |
| `icon.menu` | `menu` | `Menu` |
| `icon.search` | `search` | `Search` |
| `icon.settings` | `settings` | `Settings` |
| `icon.user` | `user` | `User` |

## Active stack modules

- **Dark product chrome** (`dark-product`) · 100% · system
- **8\-point grid** (`8pt-grid`) · 100% · method
- **Major third scale** (`major-third`) · 100% · type

## Shader methods

Stack these Paper Shaders catalog methods like filters. Opacity, scale, and color come from existing tokens (`bg`, `surface`, `accent`, `fg`, `spaceUnit`, stack intensity). `paper-grain` reuses stack grain intensity (`grain` / `--lab-grain`) — do not add a second grain slider. CSS duration / easing / transition tokens remain the package motion contract; packages do not need to ship GLSL.

| Method | Catalog id | Params |
| --- | --- | --- |
| Mesh gradient | `paper-mesh-gradient` | colors: bg / surface / accent; opacity: stack intensity |
| Grain | `paper-grain` | grain: stack grain intensity; opacity: stack intensity |
| Dots | `paper-dots` | colors: fg / accent; scale: spaceUnit; opacity: stack intensity |

When WebGL2 is missing **or** `prefers-reduced-motion: reduce`, keep CSS grain/filter fallback and do not start a WebGL/rAF loop. Optional WGSL (vgpu) film grain is a separate overlay and is not replaced by these methods.



## Light theme tokens

| Token | Value |
| --- | --- |
| `bg` | `#f7f7f8` |
| `surface` | `#ffffff` |
| `surfaceElevated` | `#f0f0f2` |
| `fg` | `#121214` |
| `fgMuted` | `#5c5c66` |
| `fgSubtle` | `#8a8a96` |
| `border` | `rgba(18, 18, 20, 0.12)` |
| `accent` | `#2a2a32` |
| `accentFg` | `#f7f7f8` |
| `radiusSm` | `6px` |
| `radiusMd` | `12px` |
| `radiusLg` | `20px` |
| `spaceUnit` | `8.8px` |
| `fontDisplay` | `"Segoe UI", system-ui, sans-serif` |
| `fontBody` | `"Segoe UI", system-ui, sans-serif` |
| `fontWeightDisplay` | `600` |
| `fontWeightBody` | `400` |
| `typeScale` | `1.25` |
| `shadow` | `0 8px 24px rgba(18,18,20,0.08)` |
| `borderWidth` | `1px` |
| `letterSpacing` | `-0.020em` |
| `lineHeight` | `1.50` |

## Dark theme tokens

| Token | Value |
| --- | --- |
| `bg` | `#0c0c0e` |
| `surface` | `#141416` |
| `surfaceElevated` | `#1c1c20` |
| `fg` | `#f2f2f3` |
| `fgMuted` | `#a0a0a8` |
| `fgSubtle` | `#6e6e78` |
| `border` | `rgba(242, 242, 243, 0.12)` |
| `accent` | `#d4d4d8` |
| `accentFg` | `#0c0c0e` |
| `radiusSm` | `6px` |
| `radiusMd` | `12px` |
| `radiusLg` | `20px` |
| `spaceUnit` | `8.8px` |
| `fontDisplay` | `"Segoe UI", system-ui, sans-serif` |
| `fontBody` | `"Segoe UI", system-ui, sans-serif` |
| `fontWeightDisplay` | `600` |
| `fontWeightBody` | `400` |
| `typeScale` | `1.25` |
| `shadow` | `0 8px 24px rgba(0,0,0,0.35)` |
| `borderWidth` | `1px` |
| `letterSpacing` | `-0.020em` |
| `lineHeight` | `1.50` |

## Typography

- **Display:** `"Segoe UI", system-ui, sans-serif`
- **Body:** `"Segoe UI", system-ui, sans-serif`
- **Display weight:** `600`
- **Body weight:** `400`
- **Scale ratio:** `1.25`
- **Tracking:** `-0.020em`
- **Line height:** `1.50`

Type steps come from `typeSteps(deviceBasePx, typeScale)`, the same helper that emits `--lab-text-*`:

- `sm` is a fixed readable fraction of the device base (`0.875`)
- `base` is the device base (15px mobile, 16px tablet/desktop) and does not change with the ratio
- `lg` / `xl` / `2xl` use positive powers of the ratio

| Step | Formula | Desktop px |
| --- | --- | --- |
| sm | base × 0.875 | 14px |
| base | 15–16px by device | 16px |
| lg | base × scale | 20px |
| xl | base × scale² | 25px |
| 2xl | base × scale³ | 31.25px |

## Spacing & shape

| Role | Token |
| --- | --- |
| Space unit | `8.8px` (device-scaled in preview) |
| Radius SM / MD / LG | `6px` / `12px` / `20px` |
| Border width | `1px` |
| Shadow (dark) | `0 8px 24px rgba(0,0,0,0.35)` |
| Shadow (light) | `0 8px 24px rgba(18,18,20,0.08)` |

## Responsive device scales

Export device context: **Desktop** (Fluid wide · multi-column, full chrome)

| Device | Width | Cards | Split | Base type | Notes |
| --- | --- | --- | --- | --- | --- |
| Mobile | 390 | 1 | 1 | 15px | Stack CTAs, compact nav, ≥44px targets |
| Tablet | 768 | 2 | 2 | 16px | Two-up grids, full nav |
| Desktop | 1200 | 3 | 2 | 16px | Multi-column product chrome |

Implement with **container queries** or an explicit `data-device` attribute on the
preview/root — do not rely only on viewport media queries when embedding framed UIs.

## CSS variables (copy into the project)

```css
/* Light */
:root,
[data-theme="light"] {
  --lab-device: desktop;
  --lab-base-font: 16px;
  --lab-pad-scale: 1.25;
  --lab-gap-scale: 1.15;
  --lab-target-min: 40px;
  --lab-cols-cards: 3;
  --lab-cols-split: 2;
  --lab-hero-split: 1;
  --lab-cta-stack: 0;
  --lab-bg: #f7f7f8;
  --lab-surface: #ffffff;
  --lab-surface-elevated: #f0f0f2;
  --lab-fg: #121214;
  --lab-fg-muted: #5c5c66;
  --lab-fg-subtle: #8a8a96;
  --lab-border: rgba(18, 18, 20, 0.12);
  --lab-accent: #2a2a32;
  --lab-accent-fg: #f7f7f8;
  --lab-radius-sm: 6px;
  --lab-radius-md: 12px;
  --lab-radius-lg: 20px;
  --lab-space: 8.8px;
  --lab-font-display: "Segoe UI", system-ui, sans-serif;
  --lab-font-body: "Segoe UI", system-ui, sans-serif;
  --lab-font-weight-display: 600;
  --lab-font-weight-body: 400;
  --lab-type-scale: 1.25;
  --lab-shadow: 0 8px 24px rgba(18,18,20,0.08);
  --lab-border-width: 1px;
  --lab-tracking: -0.020em;
  --lab-leading: 1.50;
  --lab-text-sm: 14px;
  --lab-text-base: 16px;
  --lab-text-lg: 20px;
  --lab-text-xl: 25px;
  --lab-text-2xl: 31.25px;
  --lab-icon-size-sm: 16px;
  --lab-icon-size-md: 20px;
  --lab-icon-size-lg: 24px;
  --lab-icon-stroke: 2;
  --lab-icon-color: currentColor;
}

/* Dark */
:root,
[data-theme="dark"] {
  --lab-device: desktop;
  --lab-base-font: 16px;
  --lab-pad-scale: 1.25;
  --lab-gap-scale: 1.15;
  --lab-target-min: 40px;
  --lab-cols-cards: 3;
  --lab-cols-split: 2;
  --lab-hero-split: 1;
  --lab-cta-stack: 0;
  --lab-bg: #0c0c0e;
  --lab-surface: #141416;
  --lab-surface-elevated: #1c1c20;
  --lab-fg: #f2f2f3;
  --lab-fg-muted: #a0a0a8;
  --lab-fg-subtle: #6e6e78;
  --lab-border: rgba(242, 242, 243, 0.12);
  --lab-accent: #d4d4d8;
  --lab-accent-fg: #0c0c0e;
  --lab-radius-sm: 6px;
  --lab-radius-md: 12px;
  --lab-radius-lg: 20px;
  --lab-space: 8.8px;
  --lab-font-display: "Segoe UI", system-ui, sans-serif;
  --lab-font-body: "Segoe UI", system-ui, sans-serif;
  --lab-font-weight-display: 600;
  --lab-font-weight-body: 400;
  --lab-type-scale: 1.25;
  --lab-shadow: 0 8px 24px rgba(0,0,0,0.35);
  --lab-border-width: 1px;
  --lab-tracking: -0.020em;
  --lab-leading: 1.50;
  --lab-text-sm: 14px;
  --lab-text-base: 16px;
  --lab-text-lg: 20px;
  --lab-text-xl: 25px;
  --lab-text-2xl: 31.25px;
  --lab-icon-size-sm: 16px;
  --lab-icon-size-md: 20px;
  --lab-icon-size-lg: 24px;
  --lab-icon-stroke: 2;
  --lab-icon-color: currentColor;
}


@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* optional: mirror dark vars when using system preference without JS */
  }
}
```

## JSON snapshot

```json
{
  "name": "Designs System",
  "dual": {
    "light": {
      "bg": "#f7f7f8",
      "surface": "#ffffff",
      "surfaceElevated": "#f0f0f2",
      "fg": "#121214",
      "fgMuted": "#5c5c66",
      "fgSubtle": "#8a8a96",
      "border": "rgba(18, 18, 20, 0.12)",
      "accent": "#2a2a32",
      "accentFg": "#f7f7f8",
      "shadow": "0 8px 24px rgba(18,18,20,0.08)"
    },
    "dark": {
      "bg": "#0c0c0e",
      "surface": "#141416",
      "surfaceElevated": "#1c1c20",
      "fg": "#f2f2f3",
      "fgMuted": "#a0a0a8",
      "fgSubtle": "#6e6e78",
      "border": "rgba(242, 242, 243, 0.12)",
      "accent": "#d4d4d8",
      "accentFg": "#0c0c0e",
      "shadow": "0 8px 24px rgba(0,0,0,0.35)"
    }
  },
  "tokens": {
    "light": {
      "bg": "#f7f7f8",
      "surface": "#ffffff",
      "surfaceElevated": "#f0f0f2",
      "fg": "#121214",
      "fgMuted": "#5c5c66",
      "fgSubtle": "#8a8a96",
      "border": "rgba(18, 18, 20, 0.12)",
      "accent": "#2a2a32",
      "accentFg": "#f7f7f8",
      "radiusSm": "6px",
      "radiusMd": "12px",
      "radiusLg": "20px",
      "spaceUnit": "8.8px",
      "fontDisplay": "\"Segoe UI\", system-ui, sans-serif",
      "fontBody": "\"Segoe UI\", system-ui, sans-serif",
      "fontWeightDisplay": 600,
      "fontWeightBody": 400,
      "typeScale": 1.25,
      "shadow": "0 8px 24px rgba(18,18,20,0.08)",
      "borderWidth": "1px",
      "letterSpacing": "-0.020em",
      "lineHeight": "1.50"
    },
    "dark": {
      "bg": "#0c0c0e",
      "surface": "#141416",
      "surfaceElevated": "#1c1c20",
      "fg": "#f2f2f3",
      "fgMuted": "#a0a0a8",
      "fgSubtle": "#6e6e78",
      "border": "rgba(242, 242, 243, 0.12)",
      "accent": "#d4d4d8",
      "accentFg": "#0c0c0e",
      "radiusSm": "6px",
      "radiusMd": "12px",
      "radiusLg": "20px",
      "spaceUnit": "8.8px",
      "fontDisplay": "\"Segoe UI\", system-ui, sans-serif",
      "fontBody": "\"Segoe UI\", system-ui, sans-serif",
      "fontWeightDisplay": 600,
      "fontWeightBody": 400,
      "typeScale": 1.25,
      "shadow": "0 8px 24px rgba(0,0,0,0.35)",
      "borderWidth": "1px",
      "letterSpacing": "-0.020em",
      "lineHeight": "1.50"
    }
  },
  "locks": {
    "fonts": false,
    "type": false,
    "colors": false,
    "shape": false,
    "shadow": false
  },
  "device": "desktop",
  "filter": "none",
  "grain": 0,
  "shaderMethods": [
    {
      "id": "paper-mesh-gradient",
      "name": "Mesh gradient",
      "kind": "mesh-gradient",
      "params": {
        "colors": [
          "bg",
          "surface",
          "accent"
        ],
        "opacity": "stack intensity"
      }
    },
    {
      "id": "paper-grain",
      "name": "Grain",
      "kind": "grain",
      "params": {
        "grain": "stack grain intensity",
        "opacity": "stack intensity"
      }
    },
    {
      "id": "paper-dots",
      "name": "Dots",
      "kind": "dots",
      "params": {
        "colors": [
          "fg",
          "accent"
        ],
        "scale": "spaceUnit",
        "opacity": "stack intensity"
      }
    }
  ],
  "stack": [
    {
      "moduleId": "dark-product",
      "enabled": true,
      "intensity": 100,
      "order": 0
    },
    {
      "moduleId": "8pt-grid",
      "enabled": true,
      "intensity": 100,
      "order": 1
    },
    {
      "moduleId": "major-third",
      "enabled": true,
      "intensity": 100,
      "order": 2
    }
  ],
  "icon": {
    "catalog": "lucide",
    "size": {
      "sm": "16px",
      "md": "20px",
      "lg": "24px"
    },
    "stroke": 2,
    "color": "currentColor",
    "aliases": {
      "add": "plus",
      "check": "check",
      "close": "x",
      "delete": "trash",
      "edit": "pencil",
      "home": "house",
      "menu": "menu",
      "search": "search",
      "settings": "settings",
      "user": "user"
    }
  }
}
```

## Implementation checklist

- [ ] Wire `data-theme` light/dark/system
- [ ] Map all surfaces to `bg` / `surface` / `surfaceElevated`
- [ ] Text uses display/body fonts + scale steps only
- [ ] Spacing from `spaceUnit` scale; no magic numbers
- [ ] Radii concentric; shadows from tokens
- [ ] Motion from duration / easing / transition tokens; honor reduce-motion
- [ ] Optional Motion.dev playback maps `--lab-duration-*` / `--lab-ease-*`; skip `animate` when reduced motion is preferred
- [ ] Icons from Lucide (`lucide-react`); size/stroke/color from `--lab-icon-*`; tree-shake; do not paste SVGs
- [ ] Mobile layout single-column; targets ≥44px
- [ ] Contrast AA for `fg` on `bg`/`surface` in both themes

## Project structure suggestion

```text
src/
  styles.css          # @theme + data-theme vars from this skill
  components/ui/      # primitives consuming tokens only
  lib/theme.ts        # preference → appearance → data-theme
```

---

_Generated by Designs. Point agents at this skill URL for token context and structure._
