Atelier — Design-System Foundations
The engine of the suite. "Expensive" is not a trick — it's a system showing through. Coherent
color, spacing, and type tokens are what separate bespoke-looking work from a pile of one-off values.
This skill turns a Direction Doc into a real, reusable token system that atelier-typography and
atelier-layout (and your components) consume.
Project memory: if ATELIER.md exists, read it first and honor its register, palette mood, and
anti-references. Identity preservation wins: if the project already has committed brand colors/tokens
(existing CSS vars, a theme file, recognizable brand hexes), preserve them and build the system around
them — don't regenerate a palette over a real brand. Capture the result back in ATELIER.md's Tokens
section. Set up memory with /atelier init (the atelier router).
Inputs: the Direction Doc from atelier-direction (palette mood, type voice, density, light/dark).
Outputs: tokens as CSS vars / Tailwind v4 @theme / DTCG JSON. Deep reference:
references/fundamentals-deepdive.md (§7 Color, §8 Layout & Composition → spacing, §9 Design Systems → DTCG tokens).
Data — atelier-data (seed only): per-product-type shadcn-token palettes via scripts/search.py "<product>" --domain color. These are raw Tailwind hex stops — use as a starting hue set, then rebuild the system here in OKLCH and re-check contrast before shipping.
Token architecture (build in this order)
Three tiers, always. This is what makes theming and dark mode trivial later.
- Primitive / ramp — raw values, no meaning:
--blue-50 … --blue-950, --space-1 … --space-12.
- Semantic / alias — intent, referencing primitives:
--bg, --surface, --border, --text,
--text-muted, --primary, --danger. This tier carries the brand and enables theming —
dark mode = remap semantics, never touch components.
- Component (optional) — component-scoped:
--button-bg, --card-padding, referencing semantics.
Rule: components consume tier 2/3, never hard-code a primitive. Get this right and dark mode is a
20-line remap instead of a rewrite.
1. Color — build in OKLCH
Build the whole color system in OKLCH, not HSL/hex. HSL lies about lightness (same L% looks far
brighter for yellow than blue), so HSL ramps are perceptually uneven and dark themes go muddy. OKLCH is
perceptually uniform, hue-stable, and P3-capable. Full method, ready-to-tweak ramps, and the Radix
12-step role mapping are in references/color-oklch.md. The short version:
- Brand ramp: fix the hue
H, sweep lightness L evenly from ~0.97 (step 50) to ~0.20 (step 950),
and curve chroma C — low at the light end, peaking around step 500–600, tapering at the dark end.
oklch(L C H).
- Neutrals: a gray ramp with a tiny chroma tinted toward the brand temperature (
C ≈ 0.01–0.02).
Pure-gray neutrals read clinical; a whisper of brand tint reads designed.
- Accent: usually one. Monochrome + a single saturated accent is the safest "premium" move.
- Avoid the cream/sand/beige default. The warm-neutral band (OKLCH
L 0.84–0.97, C < 0.06, hue 40–100)
reads as paper/parchment no matter what you name it, and is the saturated AI body background of 2026.
Token names like --paper, --cream, --sand, --bone, --linen, --parchment, --ivory, --oat
are tells in themselves (the atelier-perf-a11y detector flags them). If the brief says "warm / editorial
/ cozy," carry the warmth in the accent + type + imagery, not the body bg: pick a saturated brand
body, a true off-white at chroma ~0, or a clearly-branded mid-tone. Tint neutrals toward the brand hue by
only C ≈ 0.005–0.02, deliberately — never "warm by default."
- Map semantics to ramp steps using the Radix 12-step roles (1 app bg → 9 solid/CTA → 12 high-
contrast text). See the reference for the full table.
- Relative color for variants:
oklch(from var(--primary) calc(l - 0.08) c h) for a hover state —
keeps hue/chroma locked. Or color-mix(in oklch, …) (Baseline Widely Available Nov 2025) to derive state/tint
ramps from a semantic token (color-mix(in oklch, var(--primary) 12%, transparent) for a scrim) — no
Sass. Mix in oklch, not sRGB, or midpoints muddy.
- Support: base
oklch() ~93%, relative-color syntax ~86% — both production-safe behind a
plain-color fallback declaration. Keep the brand ramp inside sRGB; treat extra chroma as a P3 opt-in
(@media (color-gamut: p3)), since out-of-gamut values gamut-map per-engine and can shift.
- Data-viz palettes — categorical (rotate hue at fixed L/C), sequential, diverging, and colorblind-safe
rules + the
--chart-N tokens live in references/color-oklch.md, built for atelier-dataviz to consume.
2. Type scale (tokens only — craft lives in atelier-typography)
Emit the scale here as tokens; atelier-typography picks the actual fonts and detailing.
- Modular scale: base 16–18px × a ratio (1.2 conservative UI, 1.25 web default, 1.333+ marketing,
1.618 dramatic). Keep it to ~5–7 steps — fewer sizes = more consistent.
- Fluid: make each step
clamp(min, intercept_rem + slope·100vw, max). Always keep a rem term
in the preferred value or you break browser zoom (WCAG 1.4.4). The math + a generator is in
references/tokens-and-output.md; or compute via utopia.fyi.
- Token names:
--text-xs … --text-5xl, plus --leading-tight/normal/relaxed and --tracking-tight.
3. Spacing, radii, depth
- Spacing: 8-point grid (
--space-1: 4px sub-unit, then 8/16/24/32/48/64…). Viewport widths divide
cleanly by 8; it kills decision fatigue and gives consistent rhythm. Tight UIs may use the 4px sub-unit
more.
- Density (from the Direction Doc) sets where on the scale you live + the
--section-y macro rhythm:
airy → bias to the larger steps, wide --section-y (clamp toward the top); balanced → mid;
packed → lean on the 4px sub-unit and tighter steps. Same scale, different default reach.
- Radii: a small scale (
--radius-sm/md/lg/xl, e.g. 6/10/16/24px). Pick one family and stick to it
(sharp = brutalist/Swiss; large/continuous = soft/premium).
- Depth: elevation via soft, layered, low-opacity shadows (ambient + direct) in light mode; in
dark mode use lighter surfaces, not shadows (shadows are nearly invisible on dark). See
references/dark-mode-and-depth.md.
- Grain: the single highest-ROI "expensive" tactic — an SVG
feTurbulence overlay at opacity
0.03–0.08 with mix-blend-mode: overlay. Kills gradient banding, adds analog depth. Recipe in the
depth reference.
4. Dark mode
If the direction is dark (or supports both), build it as a semantic remap, not a second palette:
- Base is charcoal/ink, never
#000 (oklch(~0.16–0.22 …) ≈ #0a0a0f–#141416). Pure black causes
text halation and leaves no room to show elevation.
- Elevation by lightness: surfaces step lighter (
rgba(255,255,255,.03) → .06 → .09 overlays,
or stepped L), not by shadow.
- Desaturate accents ~10–15% — full-saturation brand colors vibrate on dark.
- Text: off-white (~`L 0.92
), muted ~L 0.70`. Re-check contrast (APCA is better here than WCAG 2 ratios).
- Authoring: the cleanest way to express each semantic token is
light-dark() (Baseline; needs
color-scheme: light dark on :root) — color: light-dark(<light>, <dark>) follows the theme toggle
with no [data-theme] media query.
- Beyond dark: the same semantic remap yields multi-theme for free — sub-brands, a first-class
high-contrast variant, and a one-hue user-customizable accent. Recipes in
references/dark-mode-and-depth.md.
5. Output
Detect the stack first. If the project already uses Tailwind (a tailwindcss dependency or
@import "tailwindcss"), default to Tailwind v4 + shadcn/ui (the suite's house stack) — emit tokens as
@theme in CSS and map them to shadcn's semantic token names so components inherit the system for free.
If there's no Tailwind (vanilla, Rails, Laravel, Django, Astro-no-Tailwind), default to CSS custom
properties instead — don't impose Tailwind on a codebase that isn't using it. Use DTCG JSON when a design
team / multi-platform pipeline exists. Full copy-paste templates in
references/tokens-and-output.md:
- Tailwind v4 + shadcn (DEFAULT) —
@theme { --color-…; --spacing-…; --font-… } in CSS (v4 is
CSS-first, OKLCH-native, no tailwind.config.js) + the shadcn --background/--foreground/--primary/…
mapping.
- CSS custom properties (
:root + [data-theme="dark"]) — portable fallback for vanilla / other
frameworks.
- DTCG / W3C design tokens JSON (+ Style Dictionary) — when a design team / multi-platform pipeline
exists.
6. Contrast gate (don't ship without it)
- Conformance: WCAG 2.2 AA — body text ≥ 4.5:1, large text & UI/icons/focus ≥ 3:1 (WebAIM checker).
- Design with APCA for fidelity, especially dark mode (Lc 90 body / 75 min / 45 headlines). APCA is
not yet a conformance standard — certify with 2.x, tune with APCA.
- Never rely on color alone (pair with text/icon/shape). Verify the
--primary solid and all text
tokens against their backgrounds before handoff.
- For a dynamic/data-driven accent, let the browser pick the readable foreground:
contrast-color(var(--brand)) (Baseline Apr 2026) returns black or white — kills the "is white
text readable on this accent?" guess for buttons/badges. Returns black/white only; provide a static
@supports fallback for older engines.
- This is the design-time check;
atelier-perf-a11y re-verifies contrast at ship time (computed
from rendered pixels, in every theme) — the accent-as-text-on-light pair is the usual failure.
Operating principles
- Three tiers or it isn't a system. Hard-coded hex/px scattered through components is the #1 cause of
incoherent, cheap-looking UI.
- OKLCH everywhere. It's why the ramps look evenly stepped and the dark theme isn't muddy.
- One accent, tinted neutrals, real grain. These three quietly do most of the "expensive" work.
- Default to Tailwind v4
@theme + shadcn token names; drop to CSS custom properties or DTCG only
when the project clearly isn't Tailwind. One source of truth either way — components reference
semantic names, never primitives.
1---2name: atelier-foundations3description: Atelier suite — the design-system engine. Build the token foundation that makes a frontend look coherent and "expensive": a perceptual OKLCH color system (ramps + semantic tokens + accessible dark mode), a modular + fluid type scale, an 8-point spacing scale, and depth (radii, shadows, grain). Outputs ready-to-use CSS custom properties, Tailwind v4 @theme (mapped to shadcn/ui theme variables), or W3C/DTCG design tokens. Use this whenever starting any frontend that needs a consistent visual system, when building a color palette / design tokens / chart & data-viz palettes / theme / dark mode, or when colors, spacing, or sizing feel inconsistent, muddy, or generic. Run after atelier-direction; before building components. (Choosing and setting the actual fonts → atelier-typography; scaffolding and building components → atelier-components.)4---56# Atelier — Design-System Foundations78The engine of the suite. **"Expensive" is not a trick — it's a system showing through.** Coherent9color, spacing, and type tokens are what separate bespoke-looking work from a pile of one-off values.10This skill turns a Direction Doc into a real, reusable token system that `atelier-typography` and11`atelier-layout` (and your components) consume.1213> **Project memory:** if **`ATELIER.md`** exists, read it first and honor its register, palette mood, and14> anti-references. **Identity preservation wins:** if the project already has committed brand colors/tokens15> (existing CSS vars, a theme file, recognizable brand hexes), preserve them and build the system *around*16> them — don't regenerate a palette over a real brand. Capture the result back in `ATELIER.md`'s Tokens17> section. Set up memory with **`/atelier init`** (the **`atelier`** router).18>19> **Inputs:** the Direction Doc from `atelier-direction` (palette mood, type voice, density, light/dark).20> **Outputs:** tokens as CSS vars / Tailwind v4 `@theme` / DTCG JSON. Deep reference:21> `references/fundamentals-deepdive.md` (§7 Color, §8 Layout & Composition → spacing, §9 Design Systems → DTCG tokens).22>23> **Data — `atelier-data` (seed only):** per-product-type shadcn-token palettes via `scripts/search.py "<product>" --domain color`. These are raw Tailwind hex stops — use as a *starting* hue set, then rebuild the system here in OKLCH and re-check contrast before shipping.2425---2627## Token architecture (build in this order)2829Three tiers, always. This is what makes theming and dark mode trivial later.30311. **Primitive / ramp** — raw values, no meaning: `--blue-50 … --blue-950`, `--space-1 … --space-12`.322. **Semantic / alias** — intent, *referencing* primitives: `--bg`, `--surface`, `--border`, `--text`,33 `--text-muted`, `--primary`, `--danger`. **This tier carries the brand and enables theming** —34 dark mode = remap semantics, never touch components.353. **Component** (optional) — component-scoped: `--button-bg`, `--card-padding`, referencing semantics.3637Rule: components consume tier 2/3, **never hard-code a primitive**. Get this right and dark mode is a3820-line remap instead of a rewrite.3940---4142## 1. Color — build in OKLCH4344Build the whole color system in **OKLCH**, not HSL/hex. HSL lies about lightness (same `L%` looks far45brighter for yellow than blue), so HSL ramps are perceptually uneven and dark themes go muddy. OKLCH is46perceptually uniform, hue-stable, and P3-capable. Full method, ready-to-tweak ramps, and the Radix4712-step role mapping are in **`references/color-oklch.md`**. The short version:4849- **Brand ramp:** fix the hue `H`, sweep lightness `L` evenly from ~0.97 (step 50) to ~0.20 (step 950),50 and *curve* chroma `C` — low at the light end, peaking around step 500–600, tapering at the dark end.51 `oklch(L C H)`.52- **Neutrals:** a gray ramp with a *tiny* chroma tinted toward the brand temperature (`C ≈ 0.01–0.02`).53 Pure-gray neutrals read clinical; a whisper of brand tint reads designed.54- **Accent:** usually one. Monochrome + a single saturated accent is the safest "premium" move.55- **Avoid the cream/sand/beige default.** The warm-neutral band (OKLCH `L 0.84–0.97, C < 0.06, hue 40–100`)56 reads as paper/parchment no matter what you name it, and is *the* saturated AI body background of 2026.57 Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--linen`, `--parchment`, `--ivory`, `--oat`58 are tells in themselves (the `atelier-perf-a11y` detector flags them). If the brief says "warm / editorial59 / cozy," carry the warmth in the **accent + type + imagery**, not the body bg: pick a saturated brand60 body, a true off-white at chroma ~0, or a clearly-branded mid-tone. Tint neutrals toward the brand hue by61 only `C ≈ 0.005–0.02`, *deliberately* — never "warm by default."62- **Map semantics** to ramp steps using the Radix 12-step roles (1 app bg → 9 solid/CTA → 12 high-63 contrast text). See the reference for the full table.64- **Relative color** for variants: `oklch(from var(--primary) calc(l - 0.08) c h)` for a hover state —65 keeps hue/chroma locked. Or **`color-mix(in oklch, …)`** (Baseline Widely Available Nov 2025) to derive state/tint66 ramps from a semantic token (`color-mix(in oklch, var(--primary) 12%, transparent)` for a scrim) — no67 Sass. Mix *in `oklch`*, not sRGB, or midpoints muddy.68- **Support:** base `oklch()` ~93%, relative-color syntax ~86% — both production-safe behind a69 plain-color fallback declaration. Keep the brand ramp inside sRGB; treat extra chroma as a P3 *opt-in*70 (`@media (color-gamut: p3)`), since out-of-gamut values gamut-map per-engine and can shift.71- **Data-viz palettes** — categorical (rotate hue at fixed L/C), sequential, diverging, and colorblind-safe72 rules + the `--chart-N` tokens live in `references/color-oklch.md`, built for **`atelier-dataviz`** to consume.7374## 2. Type scale (tokens only — craft lives in atelier-typography)7576Emit the *scale* here as tokens; `atelier-typography` picks the actual fonts and detailing.7778- **Modular scale:** base 16–18px × a ratio (1.2 conservative UI, **1.25 web default**, 1.333+ marketing,79 1.618 dramatic). Keep it to ~5–7 steps — fewer sizes = more consistent.80- **Fluid:** make each step `clamp(min, intercept_rem + slope·100vw, max)`. **Always keep a `rem` term**81 in the preferred value or you break browser zoom (WCAG 1.4.4). The math + a generator is in82 `references/tokens-and-output.md`; or compute via utopia.fyi.83- Token names: `--text-xs … --text-5xl`, plus `--leading-tight/normal/relaxed` and `--tracking-tight`.8485## 3. Spacing, radii, depth8687- **Spacing: 8-point grid** (`--space-1: 4px` sub-unit, then 8/16/24/32/48/64…). Viewport widths divide88 cleanly by 8; it kills decision fatigue and gives consistent rhythm. Tight UIs may use the 4px sub-unit89 more.90- **Density (from the Direction Doc) sets where on the scale you live** + the `--section-y` macro rhythm:91 *airy* → bias to the larger steps, wide `--section-y` (`clamp` toward the top); *balanced* → mid;92 *packed* → lean on the 4px sub-unit and tighter steps. Same scale, different default reach.93- **Radii:** a small scale (`--radius-sm/md/lg/xl`, e.g. 6/10/16/24px). Pick one family and stick to it94 (sharp = brutalist/Swiss; large/continuous = soft/premium).95- **Depth:** elevation via **soft, layered, low-opacity shadows** (ambient + direct) in light mode; in96 **dark mode use lighter surfaces, not shadows** (shadows are nearly invisible on dark). See97 `references/dark-mode-and-depth.md`.98- **Grain:** the single highest-ROI "expensive" tactic — an SVG `feTurbulence` overlay at opacity99 0.03–0.08 with `mix-blend-mode: overlay`. Kills gradient banding, adds analog depth. Recipe in the100 depth reference.101102## 4. Dark mode103104If the direction is dark (or supports both), build it as a semantic remap, not a second palette:105106- Base is **charcoal/ink, never `#000`** (`oklch(~0.16–0.22 …)` ≈ `#0a0a0f`–`#141416`). Pure black causes107 text halation and leaves no room to show elevation.108- **Elevation by lightness:** surfaces step *lighter* (`rgba(255,255,255,.03)` → `.06` → `.09` overlays,109 or stepped L), not by shadow.110- **Desaturate accents ~10–15%** — full-saturation brand colors vibrate on dark.111- Text: off-white (~`L 0.92`), muted ~`L 0.70`. Re-check contrast (APCA is better here than WCAG 2 ratios).112- **Authoring:** the cleanest way to express each semantic token is **`light-dark()`** (Baseline; needs113 `color-scheme: light dark` on `:root`) — `color: light-dark(<light>, <dark>)` follows the theme toggle114 with no `[data-theme]` media query.115- **Beyond dark:** the same semantic remap yields **multi-theme** for free — sub-brands, a first-class116 high-contrast variant, and a one-hue user-customizable accent. Recipes in `references/dark-mode-and-depth.md`.117118## 5. Output119120**Detect the stack first.** If the project already uses Tailwind (a `tailwindcss` dependency or121`@import "tailwindcss"`), **default to Tailwind v4 + shadcn/ui** (the suite's house stack) — emit tokens as122`@theme` in CSS and map them to shadcn's semantic token names so components inherit the system for free.123**If there's no Tailwind** (vanilla, Rails, Laravel, Django, Astro-no-Tailwind), default to **CSS custom124properties** instead — don't impose Tailwind on a codebase that isn't using it. Use DTCG JSON when a design125team / multi-platform pipeline exists. Full copy-paste templates in126**`references/tokens-and-output.md`**:127- **Tailwind v4 + shadcn** (DEFAULT) — `@theme { --color-…; --spacing-…; --font-… }` in CSS (v4 is128 CSS-first, OKLCH-native, no `tailwind.config.js`) + the shadcn `--background/--foreground/--primary/…`129 mapping.130- **CSS custom properties** (`:root` + `[data-theme="dark"]`) — portable fallback for vanilla / other131 frameworks.132- **DTCG / W3C design tokens** JSON (+ Style Dictionary) — when a design team / multi-platform pipeline133 exists.134135## 6. Contrast gate (don't ship without it)136137- **Conformance: WCAG 2.2 AA** — body text ≥ 4.5:1, large text & UI/icons/focus ≥ 3:1 (WebAIM checker).138- **Design with APCA** for fidelity, especially dark mode (Lc 90 body / 75 min / 45 headlines). APCA is139 not yet a conformance standard — certify with 2.x, tune with APCA.140- **Never rely on color alone** (pair with text/icon/shape). Verify the `--primary` solid and all text141 tokens against their backgrounds before handoff.142- For a *dynamic/data-driven* accent, let the browser pick the readable foreground:143 **`contrast-color(var(--brand))`** (Baseline Apr 2026) returns black or white — kills the "is white144 text readable on this accent?" guess for buttons/badges. Returns black/white only; provide a static145 `@supports` fallback for older engines.146- This is the *design-time* check; **`atelier-perf-a11y` re-verifies contrast at ship time** (computed147 from rendered pixels, in *every* theme) — the accent-as-text-on-light pair is the usual failure.148149---150151## Operating principles152153- **Three tiers or it isn't a system.** Hard-coded hex/px scattered through components is the #1 cause of154 incoherent, cheap-looking UI.155- **OKLCH everywhere.** It's why the ramps look evenly stepped and the dark theme isn't muddy.156- **One accent, tinted neutrals, real grain.** These three quietly do most of the "expensive" work.157- **Default to Tailwind v4 `@theme` + shadcn token names**; drop to CSS custom properties or DTCG only158 when the project clearly isn't Tailwind. One source of truth either way — components reference159 semantic names, never primitives.