design-system
Establish typography, an OKLCH color palette, and spacing as canonical design
tokens. Typography drives the spacing scale; the palette is generated from the
design briefing (or a supplied brand seed) into perceptually uniform OKLCH
tint scales. Light mode only in v1 — dark mode is an open item for
research-plan.md, never an invented palette.
generate-scale.mjs (in this skill directory, no dependencies) does the
mechanics: typography <base> <ratio> · color <kind> <hue> <chroma> [lAdjust] · contrast <hex1> <hex2>.
Modes
Exactly one fires per invocation — declare which in your first output line.
- Pipeline — conducted by
ateam-design. Never asks a question: every input derives fromcontext.md's## Design contextper the derivation table below; what cannot derive becomes a visibleTBDand a flag. Variant choice is the human's at the design gate; under a non-blockgate_policythe skill self-selects the recommended variant only if the design bank's self-select consent was captured at the grill, recording it as a provisional call inresearch-plan.md. - Standalone — a human invoked it directly. Run the upstream interactive flow exactly as written in Process below: ask, confirm, then write. Durable writes still require the read-back — standalone is not a back door.
Where tokens live (A-Team paths)
Source of truth, durable layer of the target repo — gate-reviewed, never silently overwritten (CONTRACT.md durable rules):
docs/product/design-system/scale.ts— typography + spacing tokensdocs/product/design-system/palette.ts— color tokensdocs/product/design-system/shadcn-theme.css— the shadcn role-variable bridge (A-Team extension, below)
Materialised for the lofi (when docs/features/<slug>/lofi/ exists):
docs/features/<slug>/lofi/src/styles/tokens.css—:root { --... }blockdocs/features/<slug>/lofi/tailwind.config.mjs— referencesvar(--...)docs/features/<slug>/lofi/scale-variants/{name}.cssandpalette-variants/{name}.css— alternate token blocks for?scale=/?palette=comparison
If no lofi exists yet, write only the source-of-truth files; the lofi picks
them up when build-lofi runs.
Production materialisation (the target's src/, Tailwind config, shadcn
install) is never this skill's write — the dev phase materialises against
spec.md's mapping. Whether the target uses Tailwind v3 config or v4
@theme is a dev-time fact read from the target repo; the CSS variables here
are version-agnostic.
Create-once
Before generating anything, check for an existing system:
docs/product/design-system/exists, or A-Team Config'sdesign system pathpoints at a real token source → consume, don't regenerate. Read the tokens as settled facts; report them; stop.- If
## Design contextnow contradicts the existing tokens (e.g. "avoid blue" whilebrand-500is blue), do not auto-regenerate — surface the contradiction as a flag at the design gate and let the human decide. - Regeneration has exactly two triggers: an explicit human instruction (gate revision notes, or standalone invocation saying so). Never spontaneous.
Pipeline derivation — replaces the interview
The upstream questions are answered from durable context, not asked. Scan the target first (the design bank's own rule): existing tokens/CSS variables, brand assets, style guides — a fact the repo already answers is read, not derived.
| Upstream input | A-Team source | When absent |
|---|---|---|
| Specimen category (sans/serif/mono/display) | ## Design context typography answer (design-intake bank) |
default sans-serif (upstream default); mark derived |
| Scale preset (base, ratio) | ## Design context density/feel answer |
derive from aesthetic direction — calm/airy/editorial → Relaxed · dense/fast/technical → Compact · spacious/luxury → Spacious · else Balanced; mark derived, emit 2–3 scale variants for the gate |
| Brand seed (hex/hue/brand name) | ## Design context color constraints; brand assets found in the target |
TBD-draft (below) + blocking flag |
| Domain/industry | context.md ## Overview |
— |
| Mood axes | ## Design context users & emotional goals + brand personality |
derived from adjacent fields; flag |
| Reference / anti-reference brands | ## Design context aesthetic direction |
skip nudging toward references; note the gap |
| Audience sophistication | ## Design context users |
derived; flag |
| Colors to avoid | ## Design context color constraints |
— |
| WCAG bar | ## Design context accessibility |
default AA (4.5:1 body) |
TBD-draft (no brand seed anywhere): the design lead's red flag —
never pick colors from nothing — holds in substance: generate everything that
needs no brand opinion (neutral scale at a default hue bias, the four
fixed-hue semantics, the full typography pass), write brand/accent as
explicit TBD: no brand seed in Design context, and report it as a
blocking flag in the phase report. The lofi stays greyscale regardless,
so nothing downstream is blocked from rendering — only from shipping a
brand nobody chose.
Every derived value is listed in design.md's options/reasoning section and
appended to research-plan.md's ## Assumptions tagged · [design phase]
with a confidence level.
Process
The guideline content below is the design lead's, preserved from upstream. "Ask" steps fire in standalone mode; pipeline mode answers them from the derivation table.
Pass 1: Typography
Specimen category. Ask: sans-serif (default), serif, monospace, or display? Don't ask for a specific font yet — the
fontFamilyblock gets aTODO: project fontplaceholder; system fallback stack is filled per category.Pick presets. Show the four curated presets plus an optional custom tuple:
Preset Base Ratio Character Compact 16 1.2 fast, dense Balanced 16 1.333 steady Relaxed 18 1.414 calm Spacious 20 1.5 airy Custom user-supplied (base, ratio) — Ask the user to pick one preset, or "show me a few to compare." Multiple → emit as variants under
scale-variants/.Compute the scale. Invoke
node generate-scale.mjs typography <base> <ratio>. The script returns:sizes: { caption, body, h3, h2, h1 } — each snapped to a multiple of 4, with anti-collision applied.lineHeights: matching, each a multiple of 8 (baseline grid is half, so a multiple of 4).spacing: { xs:4, s:8, m:16, l:24, xl:32, xxl:48 } — fixed for v1.warnings: any string describing collisions that required ≥2 bumps.
Surface warnings. If
warningsis non-empty, print the message verbatim. Soft warning only — do not block, do not auto-pick another preset. Let the user decide (pipeline: keep the preset, carry the warning into the gate report).
Pass 2: Color
Brand seed check. Ask: "Do you already have brand colour(s) you need to lock — hex, hue, or a brand name?" If yes → skip the vibe interview; convert the supplied input into a seed hue/chroma pair and continue.
Vibe interview (only if no seed; pipeline reads these from
## Design context). Five questions, in order:- What domain or industry is the product in?
- Mood on three axes: warm↔cool, calm↔energetic, sophisticated↔playful.
- Reference brand to lean toward + one to feel distinct from.
- Audience sophistication.
- Anything to actively avoid.
Propose seed hues (reasoning from the interview). Surface these before generating (pipeline: in
design.md's reasoning, reviewed at the gate).- Brand hue (H 0–360°), chroma 0.10–0.20 (low for sophisticated/calm, high for energetic/playful).
- Neutral hue, chroma 0.01–0.03, biased ±15° from brand for tonal harmony.
- Optional accent hue (90° or 180° from brand) if the vibe needs a counter-colour.
- Semantic placeholders: success 145°, warning 70°, error 25°, info 240°. These are fixed per-language convention; only the lightness curve adapts.
Generate tint scales. Invoke
node generate-scale.mjs color <kind> <hue> <chroma>for each scale (brand, neutral, accent if any, four semantics). The script returns 10 steps (50, 100, …, 900) as{ oklch, hex }pairs.WCAG nudge loop (max 2 iterations).
- Check contrast:
brand-500onneutral-50, andneutral-50onbrand-500. - If either is below 4.5:1, regenerate with the L curve adjusted down 0.02
at the relevant steps (
lAdjust). - After 2 nudges, if still failing, surface the failure. Don't ship a low-contrast palette silently.
- Check contrast:
Generate variants if requested. "Show me 3 palettes" (pipeline: the diverge-by-default requirement) → generate two or three with slightly varied hue/chroma seeds, write to
palette-variants/, and record indesign.mdwhy each exists and which is recommended.
Anti-collision rule (typography)
Body is the anchor. Snap each scale level to nearest multiple of 4. Walk levels above body upward: each must be ≥ previous + 4 (bump if not). Walk levels below body downward: each must be ≤ previous − 4 (bump if not).
If any level required ≥2 bumps, surface a soft warning: "Ratio X at base Y collides too tight. Try a wider ratio or larger base." Continue writing — do not hard-stop. The user picks.
Token format
docs/product/design-system/scale.ts
export const scale = {
fontFamily: {
// TODO: project font
sans: 'ui-sans-serif, system-ui, -apple-system, sans-serif',
},
base: 16,
ratio: 1.333,
sizes: { caption: 12, body: 16, h3: 20, h2: 24, h1: 32 },
lineHeights: { caption: 16, body: 24, h3: 28, h2: 32, h1: 40 },
spacing: { xs: 4, s: 8, m: 16, l: 24, xl: 32, xxl: 48 },
} as const;
docs/product/design-system/palette.ts
export const palette = {
brand: {
50: { oklch: 'oklch(0.985 0.02 240)', hex: '#f6faff' },
// ... 100–900
},
neutral: { /* same shape */ },
semantic: {
success: { default: { oklch: '...', hex: '...' }, bg: { oklch: '...', hex: '...' } },
warning: { /* ... */ },
error: { /* ... */ },
info: { /* ... */ },
},
} as const;
docs/features/<slug>/lofi/src/styles/tokens.css
:root {
/* Typography */
--font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
--size-body: 16px;
--line-body: 24px;
/* ...other sizes/line-heights/spacing */
/* Brand scale (OKLCH triplets — space-separated, no commas) */
--brand-50: 0.985 0.02 240;
--brand-100: 0.965 0.04 240;
/* ...200–900 */
/* Neutral, semantic — same pattern */
}
docs/features/<slug>/lofi/tailwind.config.mjs
import { scale } from '../../../product/design-system/scale.ts';
export default {
content: ['./src/**/*.{astro,html,js,ts}'],
theme: {
extend: {
fontFamily: { sans: 'var(--font-sans)' },
fontSize: {
caption: ['var(--size-caption)', { lineHeight: 'var(--line-caption)' }],
body: ['var(--size-body)', { lineHeight: 'var(--line-body)' }],
h3: ['var(--size-h3)', { lineHeight: 'var(--line-h3)' }],
h2: ['var(--size-h2)', { lineHeight: 'var(--line-h2)' }],
h1: ['var(--size-h1)', { lineHeight: 'var(--line-h1)' }],
},
spacing: scale.spacing,
colors: {
brand: {
50: 'oklch(var(--brand-50))',
100: 'oklch(var(--brand-100))',
// ...
},
neutral: { /* ... */ },
success: 'oklch(var(--success))',
warning: 'oklch(var(--warning))',
error: 'oklch(var(--error))',
info: 'oklch(var(--info))',
},
},
},
};
shadcn bridge — A-Team extension
Not upstream content. This section exists because shadcn/ui is the A-Team's
declared-default component library (design-intake ## Declared defaults);
shadcn themes through semantic role variables, and someone has to bind roles
to scale steps exactly once. The binding is contrast-driven mechanics — every
aesthetic opinion already lives in the scales above.
Write docs/product/design-system/shadcn-theme.css: a :root block binding
shadcn's role variables to OKLCH scale steps. Default mapping (light mode):
| Role var | Scale step | Role var | Scale step |
|---|---|---|---|
--background |
neutral-50 |
--muted |
neutral-100 |
--foreground |
neutral-900 |
--muted-foreground |
neutral-600 |
--card / --popover |
neutral-50 |
--accent |
accent-100, else neutral-100 |
--card-foreground / --popover-foreground |
neutral-900 |
--accent-foreground |
accent-900, else neutral-900 |
--primary |
brand-600 |
--destructive |
error default |
--primary-foreground |
neutral-50 |
--destructive-foreground |
neutral-50 |
--secondary |
neutral-100 |
--border / --input |
neutral-200 |
--secondary-foreground |
neutral-900 |
--ring |
brand-500 |
Rules:
- Every foreground/background pair must pass the WCAG bar (default AA
4.5:1) via
generate-scale.mjs contrast. A failing pair walks its scale step darker/lighter one step at a time, max 2 steps — the same nudge discipline as the palette. Still failing → surface it, don't ship it. --radiushas no source in the subvisual tokens: write--radius: 0.5rem; /* TBD: no radius opinion in the design system */and record the open item inresearch-plan.md.- Light mode only: no
.darkblock. Dark mode is an open item inresearch-plan.md, not an invented palette. - File header comments record the mapping table and the contrast results, so the gate reviews the binding, not just the values.
Common mistakes
- Hard-coding hex without OKLCH source. Always store OKLCH primary, hex fallback. Editing hex by hand drifts the scale.
- Forgetting the WCAG nudge. Generated palettes can fail AA on key pairings. Run the check — on the palette and on the shadcn bridge pairs.
- Letting the user pick "any font". Push for the specimen category. Even "system sans-serif" is more useful than "TBD".
- Generating both passes silently. Standalone is interactive: ask,
confirm, then write. Pipeline is derived: every non-obvious call lands in
design.md's reasoning and the assumptions ledger — derived is not silent. - Skipping the brand-seed branch. Most real projects have at least a
brand color already. Always check
## Design contextand the target's assets before deriving anything. - Regenerating instead of consuming. An existing system is a settled fact. Re-running the generator over it is a silent overwrite of a durable artifact.
Red flags — STOP
- Standalone: user says "skip the vibe interview, just pick colors" without supplying a seed. Decline; ask for at least domain + mood.
- Pipeline:
## Design contexthas no brand/aesthetic data. Do the TBD-draft; never invent a seed to keep the run tidy. - Generated palette fails WCAG after 2 nudges. Surface the failure; don't ship low-contrast tokens.
- Custom typography tuple produces ≥2 collision bumps on any level. Warn explicitly; offer (standalone) or fall back to (pipeline, flagged) a curated preset.
- Existing
docs/product/design-system/and no explicit regeneration instruction. Consume it; report; stop.