# Minimalism Design Language System (DLS)

Version 1.0 · Web and mobile · CSS variables, Tailwind CSS, Figma tokens

## 1. Design Philosophy & Core Principles

**Ethos: less, but better.** Every pixel must earn its place. If removing an element does not reduce comprehension or capability, remove it. Content is the interface; the container only serves it.

| Tenet | Meaning | Practical test |
|---|---|---|
| Essentialism | Ship only what the task needs | Can this be deleted without loss? |
| Functional hierarchy | Importance is shown by size, weight and opacity, not decoration | Squint test: is the primary element still obvious? |
| Deliberate negative space | Space is a material, not leftover area | Is spacing systematic, not accidental? |
| Radical simplicity | One primary action per view, one accent color, one idea per section | Can a new user name the main action in 3 seconds? |
| Noise reduction | No gradients, heavy shadows, ornaments or redundant borders | Does each border or fill separate something spacing cannot? |

**Decision order:** (1) Remove it. (2) Express it with space. (3) Then with type weight/opacity. (4) Then with a hairline. (5) Only last, with color or elevation.

## 2. Spatial Engine & Layout System

### 2.1 Spacing scale (4pt base, 8pt rhythm)

| Token | px | rem | Tailwind | Use |
|---|---|---|---|---|
| space-1 | 4 | 0.25 | 1 | Icon-to-label gap |
| space-2 | 8 | 0.5 | 2 | Inline gaps, tight stacks |
| space-3 | 12 | 0.75 | 3 | Input padding (y) |
| space-4 | 16 | 1 | 4 | Component padding, mobile gutter |
| space-6 | 24 | 1.5 | 6 | Card padding, paragraph gap |
| space-8 | 32 | 2 | 8 | Group separation |
| space-12 | 48 | 3 | 12 | Section inner spacing |
| space-16 | 64 | 4 | 16 | Section spacing (mobile) |
| space-24 | 96 | 6 | 24 | Section spacing (desktop) |
| space-32 | 128 | 8 | 32 | Hero and page-level breathing room |

### 2.2 Proximity rule (1 : 2 : 4)

- Related items (label and field, title and meta): `space-1` to `space-2`
- Siblings in a group: 2x the related gap (`space-4`)
- Separate groups: 4x (`space-8` to `space-12`)
- Sections: 8x or more (`space-16` to `space-32`)

### 2.3 Breakpoints and dynamic padding

| Breakpoint | Min width | Page gutter | Section padding (y) | Max columns |
|---|---|---|---|---|
| base | 0 | 16px | 64px | 4 |
| sm | 640px | 24px | 64px | 4 |
| md | 768px | 32px | 96px | 8 |
| lg | 1024px | 48px | 96px | 12 |
| xl | 1280px | 64px | 128px | 12 |

```css
:root {
  --gutter: clamp(1rem, 4vw, 4rem);
  --section-y: clamp(4rem, 10vw, 8rem);
  --measure: 65ch;
  --container: 72rem;
  --container-narrow: 40rem;
}
.container { max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.section   { padding-block: var(--section-y); }
.prose-col { max-width: var(--measure); }
```

### 2.4 Layout rules

1. Default to a single column. Reading and forms use `--container-narrow`.
2. Multi-column: 12-column grid on desktop, collapse to 1 column below `md`. Never more than 4 content columns.
3. Asymmetry is welcome: a narrow label column next to a wide content column creates editorial calm.
4. Dividers: a 1px hairline (`--border-subtle`) only where spacing fails. Never stack border, fill and shadow.
5. A strong left edge; avoid centered body text longer than two lines.
6. Touch targets at least 44x44px on mobile.

## 3. Typography & Hierarchy

### 3.1 Typefaces

| Role | Primary | Alternatives | Fallback stack |
|---|---|---|---|
| Sans (UI + body) | Inter | SF Pro, Neue Haas Grotesk, Roboto, Geist | `ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` |
| Serif accent (editorial headings, pull quotes) | Newsreader | Source Serif 4, Fraunces | `ui-serif, Georgia, serif` |
| Mono (code, data) | JetBrains Mono | SF Mono, IBM Plex Mono | `ui-monospace, 'SF Mono', Menlo, monospace` |

One sans family plus at most one serif accent. Serif is never used for UI controls.

### 3.2 Type scale (about 1.25 ratio, fluid)

| Token | Mobile to desktop | Weight | Line height | Tracking | Use |
|---|---|---|---|---|---|
| display | 40 to 72px | 500 (or serif 400) | 1.05 | -0.03em | Hero statement |
| h1 | 32 to 48px | 500 | 1.1 | -0.025em | Page title |
| h2 | 24 to 32px | 500 | 1.2 | -0.02em | Section title |
| h3 | 20 to 24px | 500 | 1.3 | -0.01em | Subsection |
| body-lg | 18 to 20px | 400 | 1.65 | 0 | Lead paragraphs |
| body | 16px | 400 | 1.6 | 0 | Default text |
| body-sm | 14px | 400 | 1.5 | 0.005em | Secondary text |
| caption | 12px | 400 | 1.4 | 0.02em | Meta, helper text |

### 3.3 Weight and opacity distribution

- Use weights 400 and 500; 600 only for one emphasis tier; never 700+.
- Three text tones create depth: primary (full ink), secondary (about 70%), tertiary (about 55%, never below AA).
- Large type gets tighter tracking; small type gets looser tracking.
- Body measure 55 to 75 characters. Paragraph gap `space-6`. Heading to text `space-3`.

```css
:root {
  --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-serif: 'Newsreader', ui-serif, Georgia, serif;
  --text-display: clamp(2.5rem, 1.6rem + 4vw, 4.5rem);
  --text-h1: clamp(2rem, 1.5rem + 2vw, 3rem);
  --text-h2: clamp(1.5rem, 1.25rem + 1vw, 2rem);
  --text-h3: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
  --text-body: 1rem;
  --text-sm: 0.875rem;
  --text-xs: 0.75rem;
}
body { font-family: var(--font-sans); line-height: 1.6; color: var(--text-primary); -webkit-font-smoothing: antialiased; }
h1 { font-size: var(--text-h1); line-height: 1.1; letter-spacing: -0.025em; font-weight: 500; }
```

## 4. Color Palette & Surface Tokens

### 4.1 Neutral foundation

| Token | Light | Dark | Role |
|---|---|---|---|
| neutral-0 | #FFFFFF | #0B0B0A | Raised surface |
| neutral-25 | #FAFAF8 | #0F0F0E | Page background |
| neutral-50 | #F4F4F1 | #171716 | Subtle tint |
| neutral-100 | #EAEAE6 | #1F1F1E | Hairlines |
| neutral-200 | #D9D8D3 | #2B2B29 | Strong hairline |
| neutral-400 | #9A9892 | #6F6E69 | Placeholder (decorative only) |
| neutral-500 | #6B6A66 | #9E9D98 | Tertiary text (about 5:1) |
| neutral-600 | #52514D | #B8B7B2 | Secondary text (about 8:1) |
| neutral-900 | #141413 | #EDEDEA | Primary text (about 17:1) |

### 4.2 Single accent rule

One accent per product, only on: primary CTA, selected state, links, focus ring, progress or success.

| Option | Base (light) | Hover | Tint | On-accent | Base (dark) |
|---|---|---|---|---|---|
| Cobalt (default) | #2B4EFF (about 5.7:1) | #1F3BD9 | #EEF1FF | #FFFFFF | #7C93FF |
| Muted sage | #3E6B55 | #2F5742 | #EEF4F0 | #FFFFFF | #8FBFA4 |
| Electric amber | #B45309 text / #F59E0B fill | #92400E | #FFF7E6 | #141413 on fill | #FBBF24 |

Semantic colors (error #C62828 light / #FF8A80 dark) are functional only, always paired with an icon or text.

### 4.3 Surface rules

1. Flat planes only: no gradients, glass or textures.
2. At most two surface levels visible at once.
3. Separate with space first, then a hairline, then a tint. Elevation last.
4. Images and content carry the color; chrome stays neutral.

```css
:root {
  --bg: #FAFAF8; --surface: #FFFFFF; --surface-tint: #F4F4F1;
  --border-subtle: #EAEAE6; --border-strong: #D9D8D3;
  --text-primary: #141413; --text-secondary: #52514D; --text-tertiary: #6B6A66;
  --accent: #2B4EFF; --accent-hover: #1F3BD9; --accent-tint: #EEF1FF; --on-accent: #FFFFFF;
  --danger: #C62828;
  --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0F0F0E; --surface: #171716; --surface-tint: #1F1F1E;
    --border-subtle: #2B2B29; --border-strong: #3A3A37;
    --text-primary: #EDEDEA; --text-secondary: #B8B7B2; --text-tertiary: #9E9D98;
    --accent: #7C93FF; --accent-hover: #9BADFF; --accent-tint: #1A2040; --on-accent: #0F0F0E; --danger: #FF8A80;
  }
}
```

### 4.4 Tailwind configuration

```js
module.exports = {
  darkMode: 'media',
  theme: { extend: {
    colors: {
      bg: 'var(--bg)', surface: 'var(--surface)', tint: 'var(--surface-tint)',
      line: { DEFAULT: 'var(--border-subtle)', strong: 'var(--border-strong)' },
      ink: { DEFAULT: 'var(--text-primary)', 2: 'var(--text-secondary)', 3: 'var(--text-tertiary)' },
      accent: { DEFAULT: 'var(--accent)', hover: 'var(--accent-hover)', tint: 'var(--accent-tint)', on: 'var(--on-accent)' },
      danger: 'var(--danger)',
    },
    fontFamily: { sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], serif: ['Newsreader', 'ui-serif', 'Georgia', 'serif'] },
    borderRadius: { sm: '4px', DEFAULT: '6px', lg: '10px' },
    maxWidth: { measure: '65ch', narrow: '40rem' },
    boxShadow: { overlay: '0 8px 32px rgba(20,20,19,0.08), 0 1px 2px rgba(20,20,19,0.04)' },
    transitionTimingFunction: { out: 'cubic-bezier(0.2, 0, 0, 1)' },
    transitionDuration: { fast: '100ms', base: '150ms', slow: '200ms' },
  } },
};
```

## 5. Interactive Component Specifications

Shared rules: radius 6px (controls) and 10px (containers); one border weight (1px); no shadows except overlays; every interactive element has hover, focus-visible, active and disabled states.

### 5.1 Buttons

| Variant | Fill | Border | Text | Hover | Active | Use |
|---|---|---|---|---|---|---|
| Primary | accent | none | on-accent | accent-hover | opacity 0.9 | One per view |
| Outline | transparent | 1px border-strong | ink | border to ink, tint bg | deeper tint | Supporting actions |
| Ghost | transparent | none | ink-2 | text to ink, tint bg | deeper tint | Tertiary, toolbars |
| Link | none | none | accent | underline | opacity 0.8 | Inline navigation |

Sizes: sm 32px, md 40px (default), lg 48px. Label 14 or 16px, weight 500, sentence case.

```css
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 40px; padding-inline: 16px;
  font: 500 14px/1 var(--font-sans); border-radius: 6px; border: 1px solid transparent; cursor: pointer;
  transition: background-color 150ms cubic-bezier(0.2,0,0,1), border-color 150ms cubic-bezier(0.2,0,0,1), opacity 100ms; }
.btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-outline { border-color: var(--border-strong); color: var(--text-primary); }
.btn-outline:hover { border-color: var(--text-primary); background: var(--surface-tint); }
.btn-ghost { color: var(--text-secondary); }
.btn-ghost:hover { color: var(--text-primary); background: var(--surface-tint); }
```

Tailwind (primary):

```html
<button class="h-10 px-4 rounded text-sm font-medium bg-accent text-accent-on hover:bg-accent-hover active:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg transition-colors duration-base ease-out">Continue</button>
```

### 5.2 Cards & containers

- Default: borderless, separated by space. Use a tint only to emphasise a group.
- When separation is needed: `1px solid var(--border-subtle)`, radius 10px, padding `space-6` (mobile `space-4`).
- No box-shadow. Hover on interactive cards shifts the border to `--border-strong`; never lift or scale.
- Content order: title, one line of secondary text, at most one action.

```css
.card { padding: 24px; border: 1px solid var(--border-subtle); border-radius: 10px; background: var(--surface); transition: border-color 150ms cubic-bezier(0.2,0,0,1); }
a.card:hover { border-color: var(--border-strong); }
```

### 5.3 Form inputs & controls

| Style | Rest | Hover | Focus | Error |
|---|---|---|---|---|
| Bordered (default) | 1px border-strong, radius 6px, 44px high | border ink-3 | accent border plus 3px ring at 20% | danger border plus message |
| Underline (editorial) | 1px bottom border, no fill | bottom border ink-3 | 2px bottom accent | 2px bottom danger |

Labels are always visible above the field; helper text uses caption in tertiary tone; errors pair an icon or the word "Error" with `aria-describedby`; checkboxes, radios and switches use the accent only when on.

```css
.input { height: 44px; padding: 0 12px; font: 400 16px/1.5 var(--font-sans); color: var(--text-primary);
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 6px;
  transition: border-color 150ms cubic-bezier(0.2,0,0,1), box-shadow 150ms cubic-bezier(0.2,0,0,1); }
.input:hover { border-color: var(--text-tertiary); }
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
.input[aria-invalid='true'] { border-color: var(--danger); }
.input--underline { border: 0; border-bottom: 1px solid var(--border-strong); border-radius: 0; padding-inline: 0; background: transparent; }
.input--underline:focus { border-bottom: 2px solid var(--accent); box-shadow: none; }
```

### 5.4 Modals, tooltips & overlays

Overlays are the only place shadows are allowed, because they signal z-index.

| Element | Surface | Elevation | Radius | Padding | Notes |
|---|---|---|---|---|---|
| Modal | surface | `0 8px 32px rgba(20,20,19,.08), 0 1px 2px rgba(20,20,19,.04)` | 10px | 24px (mobile 16px) | Scrim rgba(20,20,19,.4), no blur; max width 28 to 32rem |
| Popover / menu | surface | same, shadow at 0.06 | 8px | 4px list, 8px 12px items | Items 36px tall |
| Tooltip | ink bg | none | 4px | 6px 10px | 12px text, 150ms delay, never essential info |
| Bottom sheet | surface | top hairline only | 16px top | 16px, safe-area aware | 32x4px drag handle |

In dark mode replace shadows with a 1px `--border-strong` hairline.

## 6. Micro-Interactions, Motion & Accessibility

### 6.1 Motion tokens

| Token | Value | Use |
|---|---|---|
| duration-instant | 100ms | Opacity shifts, press feedback |
| duration-base | 150ms | Color, border, background |
| duration-slow | 200ms | Overlay enter, accordion |
| ease-out | cubic-bezier(0.2, 0, 0, 1) | Enter and most transitions |
| ease-in | cubic-bezier(0.4, 0, 1, 1) | Exit (100 to 150ms) |

Principles: motion explains, never entertains; animate only opacity, transform and color properties; translate 4 to 8px, scale 0.98 to 1; no bounce, parallax or looping decoration.

```css
@keyframes fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.overlay-enter { animation: fade-in 200ms cubic-bezier(0.2,0,0,1) both; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
}
```

### 6.2 Contrast for light, quiet typography

| Content | Minimum (AA) | Target (AAA) | Token |
|---|---|---|---|
| Body and UI text | 4.5:1 | 7:1 | text-primary, text-secondary |
| Tertiary / helper text | 4.5:1 | n/a | text-tertiary (never lighter) |
| Large text (24px+, or 18.66px bold) | 3:1 | 4.5:1 | any text token |
| UI boundaries, focus ring, icons | 3:1 | n/a | border-strong, accent |
| Decorative hairlines | exempt | n/a | border-subtle |
| Placeholder carrying meaning | 4.5:1 | n/a | text-tertiary; never neutral-400 |

- Never use weight below 400; weight 300 or lighter is prohibited below 24px.
- Text below 14px must meet 4.5:1 and is limited to captions and meta.
- Do not dim text with opacity without recalculating contrast; use the tertiary token.
- Never convey state by color alone.
- Verify both light and dark themes.

### 6.3 Accessibility checklist (WCAG 2.1 AA, AAA where noted)

- Focus: always visible via `:focus-visible`, 2px accent ring with 2px offset, 3:1 against neighbours.
- Targets: 44x44px minimum on touch; 8px between adjacent targets.
- Keyboard: logical tab order; modals trap and restore focus; Escape dismisses overlays.
- Semantics: real buttons and links, labels for every control, `aria-live='polite'` for async status.
- Text spacing: survive 200% zoom and user spacing overrides; use rem, never fixed-height text boxes.
- Reflow: no horizontal scroll at 320px.
- Motion: honor `prefers-reduced-motion`; no flashing above 3 per second.
- Language: set `lang`; strict heading order; line length at most 80 characters.

## 7. Figma Token Specification

Three tiers: primitive, semantic, component. Components consume only semantic tokens; Light/Dark modes swap the semantic layer.

```json
{
  "primitive": {
    "neutral": { "25": { "value": "#FAFAF8", "type": "color" }, "900": { "value": "#141413", "type": "color" } },
    "space": { "2": { "value": "8", "type": "spacing" }, "4": { "value": "16", "type": "spacing" } },
    "radius": { "md": { "value": "6", "type": "borderRadius" } }
  },
  "semantic": {
    "bg": { "value": "{primitive.neutral.25}", "type": "color" },
    "text": { "primary": { "value": "{primitive.neutral.900}", "type": "color" } }
  },
  "component": {
    "button": { "height": { "value": "40", "type": "sizing" }, "radius": { "value": "{primitive.radius.md}", "type": "borderRadius" } }
  }
}
```

| Collection | Modes | Contents |
|---|---|---|
| Primitives | none | Color ramps, spacing, radius, durations |
| Semantic | Light, Dark | bg, surface, border, text, accent, danger |
| Layout | Mobile, Tablet, Desktop | gutter, section-y, columns, container width |
| Typography | Mobile, Desktop | display to caption per section 3 |

Naming: `category/role/variant/state` (for example `color/text/secondary`, `space/6`, `motion/duration/base`). Use auto-layout everywhere with spacing bound to variables.

## 8. Governance: The Minimalism Audit

Before shipping a screen, answer yes to all:

1. Is there a single, obvious primary action?
2. Is the accent used only for action, selection, links and focus?
3. Could any border, fill, shadow or icon be removed with no loss of meaning?
4. Does every spacing value come from the scale?
5. Do all text and UI elements meet the contrast table?
6. Are there at most two weights and one serif accent?
7. Do transitions stay within 100 to 200ms and respect reduced motion?

A "no" is a defect, not a preference. Resolve it by removing before adding.
