# Miltinson Design System — full corpus > Miltinson Design System — tokens, React 19 components, and AI patterns for Miltinson Technologies products. Ink-led palette with one amber accent, sharp radii, hairline borders, WCAG AA by default. Packages: @elirobinson/tokens, @elirobinson/react, @elirobinson/ai-patterns (GitHub Packages registry). ## System constraints (machine-checkable ids from @elirobinson/ai-patterns/contracts) - **no-barrel-imports** — Import via package subpaths only — @elirobinson/react/components//, @elirobinson/react/hooks/, @elirobinson/tokens/tokens.css. There are no barrel files; a bare @elirobinson/react import does not resolve. Check: Every import from an @elirobinson package names a subpath (components/*, hooks/*, styles/*, styles.css, tokens.css, tokens.json, tokens-data, patterns, contracts). - **forward-ref** — Every component that renders a focusable or interactive native element uses forwardRef, forwarding to the outermost interactive element the component owns. Check: The component is wrapped in React.forwardRef and the forwarded ref resolves to the interactive DOM node (or a documented handle, e.g. VirtualListHandle). - **touch-target-primary** — Primary interactive controls — buttons, pagination items, segmented-control options, nav items — have a minimum 44x44px touch target. Where visual density matters, keep the painted glyph small and expand the hit area with padding or a bounded overlay rather than inflating the visible control. Check: The computed hit area of each primary control is at least 44x44px. - **touch-target-dense** — Dense inline affordances — a chip's remove glyph, a search field's clear button, rating stars, calendar day cells — follow shadcn/MUI-scale sizing rather than 44px. Reference: MUI Chip is 32px tall (24px small) with a 22px (16px small) delete icon; shadcn Badge is ~20px tall with 12px icons. Check: Dense affordances match the shadcn/MUI reference scale instead of the 44px rule. - **hit-area-no-overlap** — An expanded hit area must never overlap sibling content. Bound the hit area — for example stretch it to the container's height, not a symmetric negative inset — so a control's overlay cannot cover an adjacent label. Check: No interactive hit area intersects a sibling element's painted or interactive area. - **tier-boundary** — Components live under packages/react/src/components//. If a component renders into a portal, traps focus, or manages open/closed state across multiple sub-elements, it is an organism. If it is assembled from 2+ atoms with no such orchestration, it is a molecule. Otherwise it is an atom. Check: The component's directory (atoms | molecules | organisms) matches the boundary rule for its behavior. UI contracts: minimum touch target 44x44 (scoped — see touch-target-dense), focus-visible required: true, contrast level WCAG AA. ## Import rules There are no barrel files. Every import names a subpath: import '@elirobinson/tokens/tokens.css'; import '@elirobinson/react/styles.css'; import { Button } from '@elirobinson/react/components/atoms/Button'; import { useRovingFocus } from '@elirobinson/react/hooks/useRovingFocus'; A bare import from '@elirobinson/react' does not resolve. ## Design tokens (from @elirobinson/tokens/tokens.css) - `--ink-0: #ffffff` — pure white - `--ink-50: oklch(98.4% 0.002 247)` — off-white surface - `--ink-100: oklch(96.2% 0.003 247)` — hairline backgrounds - `--ink-200: oklch(92.8% 0.004 247)` — dividers - `--ink-300: oklch(86% 0.005 247)` — borders - `--ink-400: oklch(70% 0.006 247)` — disabled / placeholder - `--ink-500: oklch(55% 0.008 247)` — tertiary text - `--ink-600: oklch(42% 0.01 247)` — secondary text - `--ink-700: oklch(30% 0.01 247)` — high-contrast secondary - `--ink-800: oklch(20% 0.01 247)` — near-black surfaces - `--ink-900: oklch(12% 0.008 247)` — deep surface - `--ink-950: oklch(6% 0.006 247)` — card on dark - `--ink-1000: #000000` — pure black - `--signal-50: oklch(97.5% 0.025 75)` - `--signal-100: oklch(94% 0.055 75)` - `--signal-200: oklch(89% 0.1 75)` - `--signal-300: oklch(83% 0.14 72)` - `--signal-400: oklch(78% 0.16 68)` - `--signal-500: oklch(72.5% 0.175 65)` — primary — Miltinson Amber - `--signal-600: oklch(63% 0.165 55)` - `--signal-700: oklch(52% 0.145 45)` - `--signal-800: oklch(40% 0.115 40)` - `--signal-900: oklch(28% 0.08 38)` - `--anchor-50: oklch(96% 0.018 165)` - `--anchor-100: oklch(91% 0.035 165)` - `--anchor-200: oklch(82% 0.055 162)` - `--anchor-300: oklch(70% 0.075 160)` - `--anchor-400: oklch(55% 0.085 158)` - `--anchor-500: oklch(42% 0.08 156)` — primary — Miltinson Forest - `--anchor-600: oklch(34% 0.065 156)` - `--anchor-700: oklch(26% 0.05 156)` - `--anchor-800: oklch(19% 0.035 156)` - `--anchor-900: oklch(13% 0.025 156)` - `--status-success: oklch(62% 0.16 155)` - `--status-warning: oklch(78% 0.16 85)` - `--status-danger: oklch(58% 0.22 25)` - `--status-info: oklch(60% 0.13 240)` - `--bg: var(--ink-0)` — page background - `--bg-subtle: var(--ink-50)` — zebra / panel - `--bg-muted: var(--ink-100)` — code blocks, inset wells - `--bg-inverse: var(--ink-1000)` — dark hero, footer - `--surface: var(--ink-0)` — cards - `--surface-2: var(--ink-50)` — nested cards - `--surface-3: var(--ink-100)` — deeper inset - `--border: var(--ink-200)` — default hairline - `--border-strong: var(--ink-300)` — emphasized - `--border-inverse: oklch(100% 0 0 / 0.12)` — on dark - `--fg: var(--ink-1000)` — primary text - `--fg-1: var(--ink-1000)` — alias - `--fg-2: var(--ink-600)` — secondary text - `--fg-3: var(--ink-500)` — tertiary / meta - `--fg-4: var(--ink-400)` — disabled - `--fg-inverse: var(--ink-0)` — on dark - `--fg-on-signal: var(--ink-1000)` — text on lime - `--accent: var(--signal-500)` — Miltinson Amber - `--accent-hover: var(--signal-400)` - `--accent-press: var(--signal-600)` - `--accent-fg: var(--ink-1000)` - `--accent-tint: var(--signal-50)` — very-light amber backgrounds - `--anchor: var(--anchor-500)` — Miltinson Forest - `--anchor-hover: var(--anchor-400)` - `--anchor-press: var(--anchor-600)` - `--anchor-fg: var(--ink-0)` - `--anchor-tint: var(--anchor-50)` - `--focus-ring: var(--ink-1000)` — high-contrast focus, ink-on-bg - `--link: var(--ink-1000)` - `--link-hover: var(--signal-700)` - `--link-visited: var(--ink-700)` - `--status-success: var(--anchor-500)` - `--status-warning: var(--signal-600)` - `--font-sans: 'Geist', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` - `--font-display: 'Geist', ui-sans-serif, system-ui, -apple-system, sans-serif` - `--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace` - `--fs-3xs: 10px` - `--fs-2xs: 11px` - `--fs-xs: 12px` - `--fs-sm: 14px` - `--fs-md: 16px` — base - `--fs-lg: 18px` - `--fs-xl: 20px` - `--fs-2xl: 24px` - `--fs-3xl: 30px` - `--fs-4xl: 38px` - `--fs-5xl: 48px` - `--fs-6xl: 64px` - `--fs-7xl: 84px` - `--fs-8xl: 112px` - `--lh-tight: 1.05` - `--lh-snug: 1.2` - `--lh-normal: 1.5` - `--lh-relaxed: 1.65` - `--tr-tight: -0.025em` - `--tr-snug: -0.015em` - `--tr-normal: 0em` - `--tr-wide: 0.04em` - `--tr-caps: 0.08em` - `--fw-light: 300` - `--fw-regular: 400` - `--fw-medium: 500` - `--fw-semibold: 600` - `--fw-bold: 700` - `--fw-black: 800` - `--space-0: 0` - `--space-px: 1px` - `--space-1: 4px` - `--space-2: 8px` - `--space-3: 12px` - `--space-4: 16px` - `--space-5: 20px` - `--space-6: 24px` - `--space-7: 32px` - `--space-8: 40px` - `--space-9: 48px` - `--space-10: 64px` - `--space-11: 80px` - `--space-12: 96px` - `--space-13: 128px` - `--space-14: 160px` - `--space-15: 192px` - `--radius-none: 0` - `--radius-xs: 2px` - `--radius-sm: 4px` — default for inputs, buttons - `--radius-md: 6px` — cards - `--radius-lg: 10px` — modals - `--radius-xl: 16px` — heroes - `--radius-pill: 999px` — tags only - `--shadow-xs: 0 1px 0 0 oklch(0% 0 0 / 0.04)` - `--shadow-sm: 0 1px 2px 0 oklch(0% 0 0 / 0.06), 0 0 0 1px oklch(0% 0 0 / 0.04)` - `--shadow-md: 0 4px 12px -2px oklch(0% 0 0 / 0.08), 0 0 0 1px oklch(0% 0 0 / 0.04)` - `--shadow-lg: 0 12px 32px -8px oklch(0% 0 0 / 0.14), 0 0 0 1px oklch(0% 0 0 / 0.05)` - `--shadow-xl: 0 24px 56px -12px oklch(0% 0 0 / 0.2), 0 0 0 1px oklch(0% 0 0 / 0.05)` - `--shadow-inset: inset 0 1px 0 0 oklch(0% 0 0 / 0.04)` - `--shadow-focus: 0 0 0 2px var(--bg), 0 0 0 4px var(--focus-ring)` - `--ease-out: cubic-bezier(0.22, 1, 0.36, 1)` - `--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1)` - `--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1)` - `--dur-instant: 80ms` - `--dur-fast: 140ms` - `--dur-normal: 220ms` - `--dur-slow: 420ms` - `--container-sm: 640px` - `--container-md: 768px` - `--container-lg: 1024px` - `--container-xl: 1280px` - `--container-2xl: 1440px` - `--gutter: max(20px, 4vw)` - `--z-base: 0` - `--z-raised: 10` - `--z-sticky: 100` - `--z-overlay: 1000` - `--z-modal: 1100` - `--z-toast: 1200` - `--z-tooltip: 1300` ## Foundations # Color The palette is ink-led: black type on white surfaces, hairline borders, and one loud color — Miltinson Amber — used as a signal, never a fill. Every value on this page is read live from `@elirobinson/tokens/tokens.css`; the swatches and contrast ratios below cannot drift from what the package ships. ## How to think about it - **Ink does the work.** Headings are pure black, body text near-black, and hierarchy comes from the ink scale — not from adding color. - **Amber is the only loud color.** Primary CTAs, the dot in the wordmark, eyebrow underlines, key stats, link hovers. If two things on a screen are amber, one of them is wrong. - **Forest is the quiet second.** Trust marks, success states, and the earnest surfaces (coaching guides). It anchors; it doesn't shout. - **No gradients, no purple.** Flat color only. Reference semantic tokens (`--fg`, `--surface`, `--accent`) in components, never raw scale values (`--ink-500`) — the semantic layer is what makes a future dark theme possible without touching component code. ## Ink — the neutral scale ## Signal — Miltinson Amber Warm, craftsman, honest. `--signal-500` is the brand amber; the darker steps exist because raw amber fails AA on white — text uses `--signal-700` and darker. ## Anchor — Miltinson Forest ## Semantic tokens These are the names components actually use. Each references a base-scale value, so retuning the palette is a one-line change per token. ## Contrast Computed from the live token values with WCAG 2.x math. Two pairings deserve a note: raw amber (`--accent`) fails AA as text on white — which is exactly why button labels use `--accent-fg` (ink on amber, 8.7:1) and text links only shift to amber's dark step (`--signal-700`) on hover. `--status-warning` passes at large sizes only; pair it with an icon or keep warning text at `--fg` with a warning-colored border. # Typography Two families do everything: Geist for the interface, JetBrains Mono for code, eyebrows, pricing, and anywhere numbers need to line up. Sizes, weights, and spacing below are read live from `@elirobinson/tokens/tokens.css`. ## Families Geist covers Latin, Latin-Extended, Cyrillic, and Greek. The package currently loads both families from Google Fonts via `@import` — the stylesheet's own comment flags this as a placeholder: if you have licensed `.woff2` files, self-host them and replace the import. ## The scale A perfect-fourth-ish ramp on a 16px base. Body text never goes below `--fs-md` — the smaller steps are for captions, eyebrows, and metadata, not paragraphs. ## Weights 400 for body, 500 for UI labels, 600 for headings, 700+ for display peaks. Light (300) exists but earns its place rarely. ## Line height Body text sits at 1.65 — relaxed and forgiving on purpose; accessibility-first beats dense. Display type tightens to 1.05. ## Tracking Display sizes tighten (−0.025em), body stays neutral, and mono eyebrows open up (+0.08em, uppercase). ## Rules that keep it coherent - **Sentence case for headings.** Title Case is reserved for product names and the tagline. - **Mono for numbers that compare** — tables, pricing, stats — with tabular figures. - **`clamp()` for display sizes** so long translations don't break layouts (the `t-display-*` classes in tokens.css already do this). - **Never justify text.** Left-aligned, ragged right. # Spacing A 4px base, 8px-friendly, sixteen steps. Use the scale and nothing else — a magic number is a bug waiting for a redesign. ## The scale ## How the scale gets used - **Generous vertical rhythm.** Major sections on landing pages breathe — `--space-13` (128px) between them. Cramped sections read as clutter, not density. - **Tight horizontal density inside cards.** `--space-4` (16px) is the default inner padding for compact components; `--space-6` (24px) for roomier cards. - **Gaps within a control cluster:** `--space-2` or `--space-3`. Between distinct groups: `--space-6` and up. - When in doubt, take the larger step for vertical space and the smaller for horizontal. ## Layout containers Content clamps at `--container-xl` (1280px); editorial body copy clamps tighter at `--container-md` (768px) for a readable measure. `--gutter` scales with the viewport. ## Asymmetry is encouraged 7–5 and 8–4 column splits beat 6–6. A perfectly symmetric layout reads as template-made; the brand prefers a deliberate lean. # Radii and elevation Sharp by default, and borders do most of the work shadows usually do. A card in this system is a 1px hairline on white — not a floating drop shadow. ## Radii 4px for buttons and inputs, 6px for cards, 10px for modals. The pill radius is reserved for tags — category labels like "Food" or "Education" — and never for CTAs: a pill button reads as a different brand. ## Shadows The scale is deliberately restrained. Default cards take `--shadow-sm` — a hairline tint that reads as paper, not levitation. `--shadow-lg` and up are for modals only. `--shadow-inset` marks input wells. ## Borders - `--border` (1px hairline) is the primary divider — between cards, table rows, sections. - `--border-strong` for emphasis and hover states. - On dark surfaces, `--border-inverse` (12% white) keeps hairlines visible without glowing. ## What to avoid - No glassmorphism, no frosted panels — the one sanctioned blur is the sticky header at 92% white when scrolled. - No left-border accent stripes on cards. - No stacking shadows to fake depth; if hierarchy isn't reading, fix the spacing. # Motion Calm and purposeful. Motion here confirms what happened — it never performs. Hovers answer in 140ms, state changes in 220ms, and nothing bounces across the screen. ## Durations ## Easings `--ease-out` is the default for almost everything. `--ease-spring` is reserved for icon micro-interactions — a check mark settling into place — never for panels or pages. ## Feel it ## Rules - **Hovers:** `--dur-fast`, color/background only — no scale, no opacity tricks. - **State changes** (open, select, expand): `--dur-normal` with `--ease-out`. - **No bouncy page transitions, no parallax.** The content is the show. - **`prefers-reduced-motion` is honored globally** — tokens.css collapses every transition and animation to 0.01ms when the user asks for reduced motion. You get this for free; don't opt out of it. # Accessibility Accessibility is a foundation here, not a review step. The tokens, the components, and the brand rules are all shaped by it — if you build with the system's defaults, you inherit most of this for free. ## The standard - **WCAG 2.1 AA at minimum.** Every color pairing the system ships passes AA for its intended use — the [color page](/foundations/color) computes the ratios live so the claim stays checkable. - **16px minimum body text.** The smaller type steps exist for captions and metadata, never paragraphs. - **Visible focus, always.** A high-contrast ink ring (`2px solid var(--focus-ring)`, 2px offset) on every interactive element, applied globally via `:focus-visible` in tokens.css. It's ink on both light and dark — never amber on amber. - **Reduced motion honored globally.** tokens.css collapses all animation under `prefers-reduced-motion: reduce`. ## Touch targets are scoped, not blanket Two classes of control, two rules — this is the system's most misunderstood policy: - **Primary interactive controls** — buttons, pagination items, segmented-control options, nav items — get a minimum **44×44px** hit area. Where visual density matters, the painted control stays small and the hit area expands via padding or a bounded overlay. - **Dense inline affordances** — a chip's remove glyph, a search field's clear button, rating stars, calendar day cells — follow the shadcn/MUI dense scale instead. A 44px remove button inside a 24px chip is how you get overlapping hit areas. - **In both cases** an expanded hit area must never overlap sibling content — a hit area that covers a neighboring label fires the wrong action from a correct click. ## Keyboard, by construction - Native elements first: `