# RealBrain Design System

Version 2.0 · Published at [realbrain.cc/design-system](https://realbrain.cc/design-system/)

The visual language for every RealBrain product and site. This document is the complete guide: what each token is for, how every component is built, the rules that keep screens consistent, and how to wire it into a new project in a few minutes.

**Source of truth:** [`tokens.css`](https://realbrain.cc/design-system/tokens.css). Every table in this guide is generated from that file, so if the two ever disagree, the CSS wins.

| Resource | URL | For |
|---|---|---|
| Style guide (this content, rendered with live components) | https://realbrain.cc/design-system/ | People |
| `tokens.css` — canonical CSS, tokens + components + patterns | https://realbrain.cc/design-system/tokens.css | Everyone |
| `tokens.json` — W3C Design Tokens (DTCG 2025.10) | https://realbrain.cc/design-system/tokens.json | Tooling |
| `design-system.md` — this guide as Markdown | https://realbrain.cc/design-system/design-system.md | AI agents |
| `llms.txt` — short agent index | https://realbrain.cc/design-system/llms.txt | AI agents |
| `AGENTS.md` — drop-in instructions for a new repo | https://realbrain.cc/design-system/AGENTS.md | AI agents |
| `starter.html` — minimal page wired to the system | https://realbrain.cc/design-system/starter.html | New projects |

## 0. How to use this guide

1. Read **§2 (non-negotiables)** before writing any UI. They prevent the mistakes that are hardest to undo later.
2. Starting a project? **§3** gets you from zero to a themed page in one `<link>` tag.
3. Building a component? Jump to **§6**. Building a full screen? Check **§7 (patterns)** first, it may already exist.
4. Need a raw value? Use **§4** or grep `tokens.css`. Never eyeball a value from a screenshot.
5. Can't find a token that fits? That's a stop condition, not a "pick the closest one" condition. See **§10**.

## 1. Philosophy

*Premium comes from restraint.* One accent colour per screen. No decorative effects: no gradients, no glassmorphism, no glow. A type scale capped at weight 600. Every visual value traces back to a named token. The system ships two themes (dark by default, light) from one token map, and two densities (comfortable, compact) that are never mixed inside one product.

The result is a look that stays quiet behind the product. The accent draws the eye to exactly one thing on a screen, surfaces separate by hairlines rather than shadows, and type does the hierarchy work through size and spacing rather than weight.

## 2. Non-negotiables

**Do**

- Use `--accent` for the single primary action on a screen. Everything else stays neutral.
- Snap every value to the nearest token. If none fits, stop and flag it `⚑ NEW TOKEN` with a proposal (§10).
- Pair semantic colours (success, warning, error, info) with a dot or icon *and* text. Colour is never the only signal.
- Write errors as *what happened + what to do next*, in sentence case.
- Say "Sign in", not "Log in". Pick one density per product and keep it.

**Don't**

- No font weight above 600, anywhere. The font import only loads 400, 500, and 600.
- No gradients, no glassmorphism, no blur, no decorative shadows.
- No white text on accent. Always `--accent-fg`. The dark-theme accent is a light periwinkle, and white on it fails contrast.
- No raw hex or px in component code. Tokens only. Drift like `#3b82f6` gets replaced with `--accent`.
- No mixing densities within one product. No "Log in". No Title Case Buttons.

## 3. Getting started

The system is one plain CSS file with no build step, no framework coupling, and no npm package (on purpose: a URL is easier to keep in sync than a version matrix).

### 3.1 Plain HTML

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://realbrain.cc/design-system/tokens.css">
</head>
<body>
  <main class="hero">
    <p class="eyebrow">REALBRAIN</p>
    <h1>A themed page in one link tag.</h1>
    <p>Everything on this page is a token or a component class from tokens.css.</p>
    <div class="cta-row">
      <button class="btn btn-primary">Get started</button>
      <button class="btn btn-secondary">Read the guide</button>
    </div>
  </main>
</body>
</html>
```

[`starter.html`](https://realbrain.cc/design-system/starter.html) is a fuller version of this with a card grid, a form, and a status row.

### 3.2 Vendoring the file

Link the URL for prototypes. For anything that ships, vendor the file so a deploy never depends on another origin:

```sh
curl -o src/tokens.css https://realbrain.cc/design-system/tokens.css
```

Import it once, globally (`import './tokens.css'` in your entry file, or `@import './tokens.css'` at the top of your stylesheet). Do not scope it into a component. The version line at the top of the file is what to compare when you update.

### 3.3 Vue, React, Svelte, or any framework

The classes (`.btn`, `.field`, `.card`, …) go straight into template markup, the way utility classes would. Thin wrapper components are reasonable when you want prop-driven variants:

```vue
<!-- RbButton.vue -->
<template>
  <button class="btn" :class="`btn-${variant}`" :aria-disabled="loading || undefined">
    <span v-if="loading" class="spinner" aria-hidden="true"></span>
    <slot />
  </button>
</template>
<script setup>
defineProps({ variant: { type: String, default: 'primary' }, loading: Boolean })
</script>
```

Keep component styles in the one shared stylesheet. If a component needs a piece that is not in `tokens.css`, build it from tokens and put it next to the others, not in a scoped block that will drift.

### 3.4 Tailwind v4

Tailwind can consume the tokens without duplicating any values. `@theme inline` makes each utility use the variable, so the theme switch keeps working:

```css
@import "tailwindcss";
@import "./tokens.css";

@theme inline {
  --color-*: initial;                 /* drop Tailwind's palette: tokens only */
  --color-bg: var(--bg);
  --color-surface: var(--surface);
  --color-elevated: var(--elevated);
  --color-text-1: var(--text-1);
  --color-text-2: var(--text-2);
  --color-text-3: var(--text-3);
  --color-accent: var(--accent);
  --color-accent-fg: var(--accent-fg);
  --color-success: var(--success);
  --color-warning: var(--warning);
  --color-error: var(--error);
  --color-info: var(--info);
  --color-border: var(--border);
  --color-border-strong: var(--border-strong);

  --font-sans: var(--font-ui);
  --font-mono: var(--font-mono);

  --spacing: 4px;                     /* p-1 = 4px … p-4 = 16px = --space-4 */

  --radius-sm: var(--radius-sm);
  --radius-md: var(--radius-md);
  --radius-lg: var(--radius-lg);
  --radius-full: var(--radius-full);

  --ease-rb: var(--ease);
}
```

With `--spacing: 4px`, Tailwind's numeric scale lines up with the space tokens: `p-1`=4, `p-2`=8, `p-3`=12, `p-4`=16, `p-6`=24, `p-8`=32, `p-12`=48, `p-16`=64, `p-24`=96.

### 3.5 Themes and density

Dark is the default. Light is one attribute:

```js
document.documentElement.setAttribute('data-theme', 'light')  // light
document.documentElement.removeAttribute('data-theme')        // back to dark
```

No class toggling, no duplicate component CSS. `--accent-hover` and `--accent-pressed` are `color-mix()` formulas relative to `--accent`, so they recompute for whichever theme is active.

Density is a class on a container, chosen once per product:

```html
<body class="density-comfortable">  <!-- 44px controls: marketing, consumer apps (default) -->
<body class="density-compact">      <!-- 32px controls: admin consoles, dev tools -->
```

### 3.6 Fonts

`tokens.css` imports IBM Plex Sans and IBM Plex Mono from Google Fonts at weights 400, 500, and 600. If your CSP blocks that origin, self-host the same three weights and delete the `@import` line from your vendored copy. The `--font-ui` and `--font-mono` stacks fall back to `system-ui` and `ui-monospace`, so a blocked font is a degraded page, not a broken one.

## 4. Tokens

All values live in `tokens.css` §1. The tables are generated from that file.

### 4.1 Colour — dark (default)

<!-- BEGIN GENERATED: color-dark -->
| Token | Value | Role |
|---|---|---|
| `--bg` | `#080808` | Page background |
| `--surface` | `#121216` | Cards, inputs, the default component surface |
| `--elevated` | `#1A1A20` | Modals, popovers, toasts — anything popped above surface |
| `--text-1` | `#F4F4F6` | Primary text |
| `--text-2` | `#A9A9B4` | Secondary text |
| `--text-3` | `#83838F` | Tertiary text: metadata, placeholders, captions |
| `--accent` | `#8FA8FF` | The one primary-action colour per screen |
| `--accent-fg` | `#080808` | Text and icons placed ON accent. Dark in the dark theme — never white on periwinkle |
| `--accent-hover` | `color-mix(in oklab, var(--accent), white 14%)` → `#9EB5FF` | Derived: color-mix(in oklab, accent, white 14%). Recomputes per theme |
| `--accent-pressed` | `color-mix(in oklab, var(--accent), black 24%)` → `#6173B1` | Derived: color-mix(in oklab, accent, black 24%). Recomputes per theme |
| `--success` | `#3ECF8E` | Semantic: success. Always paired with a dot/icon and text |
| `--warning` | `#E8B23D` | Semantic: warning. Always paired with a dot/icon and text |
| `--error` | `#F26D6D` | Semantic: error / destructive. Always paired with a dot/icon and text |
| `--info` | `#62B0F5` | Semantic: informational. Always paired with a dot/icon and text |
| `--border` | `#26262E` | Default hairline |
| `--border-strong` | `#3A3A46` | Input borders and emphasised dividers |
<!-- END GENERATED: color-dark -->

**Technique worth reusing:** `--accent-hover` and `--accent-pressed` are not hardcoded hexes. They are `color-mix()` formulas relative to `--accent`, which is why the light theme does not redefine them. If you introduce a second accent-driven colour, prefer this pattern over hand-picking a shade.

### 4.2 Colour — light (`[data-theme="light"]`)

<!-- BEGIN GENERATED: color-light -->
| Token | Value |
|---|---|
| `--bg` | `#F7F7F9` |
| `--surface` | `#FFFFFF` |
| `--elevated` | `#FFFFFF` |
| `--text-1` | `#16161C` |
| `--text-2` | `#4C4C58` |
| `--text-3` | `#6E6E7A` |
| `--accent` | `#3450C8` |
| `--accent-fg` | `#FFFFFF` |
| `--accent-hover` | `color-mix(in oklab, var(--accent), white 14%)` → `#4C6BD2` |
| `--accent-pressed` | `color-mix(in oklab, var(--accent), black 24%)` → `#21358A` |
| `--success` | `#177A4E` |
| `--warning` | `#8A5A00` |
| `--error` | `#C2373C` |
| `--info` | `#1D62A8` |
| `--border` | `#E3E3E9` |
| `--border-strong` | `#C6C6D0` |
<!-- END GENERATED: color-light -->

The light accent is a deeper indigo because periwinkle does not hold contrast on white. `--accent-fg` flips to white for the same reason.

### 4.3 Verified contrast

WCAG relative luminance, AA thresholds: 4.5:1 body text, 3:1 large text. Every pair below passes AA in both themes. Hold any new colour to the same bar before it becomes a token.

<!-- BEGIN GENERATED: contrast -->
| Pair | Dark | Light |
|---|---|---|
| `text-1` on `bg` | 18.23:1 | 16.84:1 |
| `text-1` on `surface` | 17.01:1 | 18.02:1 |
| `text-1` on `elevated` | 15.77:1 | 18.02:1 |
| `text-2` on `bg` | 8.60:1 | 7.91:1 |
| `text-2` on `surface` | 8.03:1 | 8.46:1 |
| `text-3` on `bg` | 5.35:1 | 4.70:1 |
| `text-3` on `surface` | 4.99:1 | 5.03:1 |
| `accent-fg` on `accent` | 8.79:1 | 6.71:1 |
| `accent` on `surface` | 8.20:1 | 6.71:1 |
| `accent` on `bg` | 8.79:1 | 6.28:1 |
| `success` on `surface` | 9.36:1 | 5.34:1 |
| `warning` on `surface` | 9.67:1 | 5.93:1 |
| `error` on `surface` | 6.39:1 | 5.38:1 |
| `info` on `surface` | 8.05:1 | 6.24:1 |
<!-- END GENERATED: contrast -->

### 4.4 Typography

Two families: `--font-ui` (IBM Plex Sans) and `--font-mono` (IBM Plex Mono). Weights 400, 500, 600 only.

<!-- BEGIN GENERATED: type -->
| Step | Size / line-height | Weight | Use for |
|---|---|---|---|
| `--fs-display` / `--lh-display` | 60 / 64 | 600 | Hero display text only |
| `--fs-8` / `--lh-8` | 40 / 48 | 600 | Page title |
| `--fs-7` / `--lh-7` | 30 / 38 | 600 | Section title |
| `--fs-6` / `--lh-6` | 24 / 32 | 600 | Card heading, stat values |
| `--fs-5` / `--lh-5` | 20 / 28 | 500 | Subheading |
| `--fs-4` / `--lh-4` | 17 / 26 | 400 | Lead paragraph |
| `--fs-3` / `--lh-3` | 15 / 24 | 400 | Body text — the default reading size and the <body> default |
| `--fs-2` / `--lh-2` | 13 / 20 | 400 | UI text: controls, labels, table cells (controls and labels use 500) |
| `--fs-1` / `--lh-1` | 12 / 16 | 400 | Caption, metadata |
| mono | 13 / 20 | 400 | Code, data, IDs |
<!-- END GENERATED: type -->

The weight column shows typical usage per step. Components pin their own weight explicitly: buttons and field labels use `--fs-2` at 500 even though body copy at the same step is 400.

### 4.5 Spacing

4px base unit, 9 steps, non-linear: tighter at the low end for component internals, wider at the high end for section rhythm.

<!-- BEGIN GENERATED: spacing -->
| Token | Value |
|---|---|
| `--space-1` | 4px |
| `--space-2` | 8px |
| `--space-3` | 12px |
| `--space-4` | 16px |
| `--space-5` | 24px |
| `--space-6` | 32px |
| `--space-7` | 48px |
| `--space-8` | 64px |
| `--space-9` | 96px |
<!-- END GENERATED: spacing -->

### 4.6 Radius and shadow

<!-- BEGIN GENERATED: radius -->
| Token | Value | Use for |
|---|---|---|
| `--radius-sm` | 6px | Buttons, inputs, checkboxes |
| `--radius-md` | 10px | Cards, tables |
| `--radius-lg` | 16px | Modals |
| `--radius-full` | 999px | Pills, toggles, dots, avatars |
<!-- END GENERATED: radius -->

Shadows are structural only, a depth cue for elevated surfaces. Never a glow.

<!-- BEGIN GENERATED: shadow -->
| Token | Dark | Light |
|---|---|---|
| `--shadow-sm` | `0 1px 2px rgba(0,0,0,.45)` | `0 1px 2px rgba(20,20,30,.08)` |
| `--shadow-md` | `0 4px 14px rgba(0,0,0,.5)` | `0 4px 14px rgba(20,20,30,.10)` |
| `--shadow-lg` | `0 16px 40px rgba(0,0,0,.55)` | `0 16px 40px rgba(20,20,30,.14)` |
<!-- END GENERATED: shadow -->

### 4.7 Motion

One easing curve, three durations. Under `prefers-reduced-motion: reduce` all three collapse to 0ms and every animation and transition duration is forced to 0ms globally. No springs, no bounce, no tilt-on-hover.

<!-- BEGIN GENERATED: motion -->
| Token | Value | Use for |
|---|---|---|
| `--dur-1` | 120ms | Micro-interactions: hover and press colour shifts |
| `--dur-2` | 200ms | Toggles, small state changes |
| `--dur-3` | 360ms | Toast enter, larger transitions |
| `--ease` | `cubic-bezier(.22,.61,.36,1)` | The only curve in the system |
<!-- END GENERATED: motion -->

### 4.8 Layout and density

<!-- BEGIN GENERATED: layout -->
| Token | Value |
|---|---|
| `--max-marketing` | 1200px |
| `--max-app` | 1440px |
| `--control-h` (comfortable, default) | 44px |
| `--control-h` (`.density-compact`) | 32px |
| `--control-pad-x` (comfortable) | `var(--space-4)` (16px) |
| `--control-pad-x` (compact) | `var(--space-3)` (12px) |
<!-- END GENERATED: layout -->

Apply `.density-comfortable` or `.density-compact` at a container level. A marketing page is comfortable, a dense admin console is compact, a single screen is not both.

## 5. Brand chrome

**The R mark** is the system's own chrome mark, CSS only: an `--accent` square with a mono, weight-600 "R" in `--accent-fg`. `.r-mark` (28px, `--radius-sm`) for top bars and inline use; `.r-mark-lg` (96px, `--radius-lg`) for covers and heroes.

```html
<span class="r-mark" aria-hidden="true">R</span>
<span class="r-mark-lg" aria-hidden="true">R</span>
```

**The theme toggle** (`.theme-toggle`) is a pill button with an accent dot. Wire it to the `data-theme` attribute from §3.5.

**The RealBrain neural mark** (the node-and-line logo on realbrain.cc) is the company logo, not a system component. On v2 surfaces use a single-colour version filled with `currentColor` so it reads in both themes.

## 6. Components

Every component is defined in `tokens.css` §4. Classes are plain CSS. Each entry below gives the markup, the states, and the rules that are easy to miss.

### Button — `.btn` + `.btn-primary` / `.btn-secondary` / `.btn-ghost` / `.btn-destructive`

```html
<button class="btn btn-primary">Sign in</button>
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-ghost">Learn more</button>
<button class="btn btn-destructive">Delete project</button>

<!-- loading: swap the label, add a spinner, use aria-disabled not disabled -->
<button class="btn btn-primary" aria-disabled="true">
  <span class="spinner" aria-hidden="true"></span>Signing in…
</button>
```

- States: default, hover, `:focus-visible` (2px `--accent` ring, 2px offset), active, `disabled`, loading.
- Loading buttons use `aria-disabled="true"` plus `.spinner`, not the native `disabled` attribute, so they stay focusable and announced.
- Height and padding come from `--control-h` / `--control-pad-x`. Set density on an ancestor, not per button.
- Copy: sentence case, verbs not nouns ("Sign in", not "Sign In" or "Login"). Destructive actions name the object ("Delete workspace", not "Delete").
- One `.btn-primary` per screen.

### Field — `.field` wrapping `.input` or `.select`

```html
<div class="field">
  <label for="email">Email</label>
  <input class="input" id="email" type="email" placeholder="you@company.com" aria-describedby="email-help">
  <p class="help" id="email-help">We only use this to sign you in.</p>
</div>

<div class="field">
  <label for="email-2">Email</label>
  <input class="input is-error" id="email-2" type="email" value="you@company" aria-invalid="true" aria-describedby="email-2-err">
  <p class="error-msg" id="email-2-err">That email is missing a domain. Add one after the @, like you@company.com.</p>
</div>

<div class="field">
  <label for="region">Region</label>
  <select class="select" id="region">
    <option>Europe (Frankfurt)</option>
    <option>US East (Virginia)</option>
  </select>
</div>
```

- Error copy: what is wrong plus a concrete example of the fix, sentence case. No "Invalid input" dead ends.
- `.select` is a native `<select>` with `appearance: none` and a CSS-drawn chevron. No JS dropdown for the basic case.
- Always wire `aria-describedby` to the help or error text, and `aria-invalid="true"` on error.

### Checkbox and radio — `.checkbox` / `.radio` inside `.check-row`

```html
<label class="check-row"><input type="checkbox" class="checkbox" checked> Email me release notes</label>
<label class="check-row"><input type="radio" name="density" class="radio" checked> Comfortable</label>
<label class="check-row"><input type="radio" name="density" class="radio"> Compact</label>
```

Both are real native inputs with `appearance: none`, redrawn in CSS. Keyboard and screen-reader behaviour comes free.

### Toggle — `.toggle` wrapping a checkbox, `.track`, and `.knob`

```html
<label class="check-row">
  <span class="toggle">
    <input type="checkbox" checked aria-label="Auto-save">
    <span class="track"><span class="knob"></span></span>
  </span>
  Auto-save
</label>
```

The real checkbox is visually hidden (`opacity: 0`, not `display: none`) under the track so focus and keyboard work. Always give the input an `aria-label` or a visible label.

### Card — `.card` / `.card.elevated`

```html
<div class="card">
  <h4>Standard card</h4>
  <p>Surface on bg, 1px border, radius-md, space-5 padding. No shadow at rest.</p>
</div>
<div class="card elevated">
  <h4>Elevated card</h4>
  <p>Elevated surface with shadow-md. Reserve for overlays and popped content.</p>
</div>
```

Standard cards are flat. `.elevated` is for content that is actually above the page (popovers, menus), not for making a card "stand out".

### Modal — native `<dialog class="modal">`

```html
<dialog class="modal" id="confirm" aria-labelledby="confirm-title">
  <h4 id="confirm-title">Delete this workspace?</h4>
  <p>All 14 projects and their build history will be removed. This can't be undone. Export anything you need first.</p>
  <div class="actions">
    <button class="btn btn-secondary" data-close>Cancel</button>
    <button class="btn btn-destructive">Delete workspace</button>
  </div>
</dialog>
```

```js
const modal = document.getElementById('confirm')
modal.showModal()
modal.querySelector('[data-close]').onclick = () => modal.close()
modal.addEventListener('click', e => { if (e.target === modal) modal.close() })  // backdrop
```

Native `<dialog>` on purpose: focus trap and Esc-to-close come from the browser. The backdrop is styled through `::backdrop`, not an extra overlay element.

### Toast — `.toast-region` live region + injected `.toast`

```html
<div class="toast-region" role="status" aria-live="polite" id="toasts"></div>
```

```js
function toast(message) {
  const el = document.createElement('div')
  el.className = 'toast'
  el.innerHTML = '<span class="icon" aria-hidden="true">✓</span><span></span><button aria-label="Dismiss notification">×</button>'
  el.children[1].textContent = message
  el.querySelector('button').onclick = () => el.remove()
  document.getElementById('toasts').appendChild(el)
  setTimeout(() => el.remove(), 5000)
}
```

The region stays in the DOM permanently; toasts are appended and removed. Self-dismisses after 5s. The left border colour signals type, always with the icon. For anything error-level use `.status.err` styling inside the toast rather than relying on the border alone.

### Tabs — ARIA tabs pattern, arrow-key operable

```html
<div class="tabs">
  <div role="tablist" aria-label="Project views">
    <button role="tab" id="tab-overview" aria-selected="true" aria-controls="panel-overview">Overview</button>
    <button role="tab" id="tab-activity" aria-selected="false" aria-controls="panel-activity" tabindex="-1">Activity</button>
  </div>
  <div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" tabindex="0">…</div>
  <div role="tabpanel" id="panel-activity" aria-labelledby="tab-activity" tabindex="0" hidden>…</div>
</div>
```

```js
const list = document.querySelector('[role="tablist"]')
const tabs = [...list.querySelectorAll('[role="tab"]')]
function select(tab) {
  tabs.forEach(t => {
    const on = t === tab
    t.setAttribute('aria-selected', on)
    t.tabIndex = on ? 0 : -1
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on
  })
  tab.focus()
}
list.addEventListener('click', e => e.target.matches('[role="tab"]') && select(e.target))
list.addEventListener('keydown', e => {
  const i = tabs.indexOf(document.activeElement)
  if (i < 0) return
  const next = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]
  if (next === undefined) return
  e.preventDefault()
  select(tabs[(next + tabs.length) % tabs.length])
})
```

Roving `tabindex`: the selected tab is `0`, the rest `-1`. Arrow keys, Home, and End move selection and focus together.

### Table — `.table-wrap > .table`, border dividers only

```html
<div class="table-wrap">
  <table class="table">
    <thead><tr><th scope="col">Scan</th><th scope="col">Status</th><th scope="col" style="text-align:right">Findings</th></tr></thead>
    <tbody>
      <tr><td class="mono">#4812</td><td><span class="status ok">Passed</span></td><td class="num">0</td></tr>
      <tr><td class="mono">#4811</td><td><span class="status err">Failed</span></td><td class="num">3</td></tr>
    </tbody>
  </table>
</div>
```

No zebra striping. Rows separate with `--border` hairlines. IDs and numbers use `.mono` / `.num` (mono, right-aligned for numbers). `.density-compact` tightens cell padding.

### Status — `.status.ok` / `.warn` / `.err` / `.info`

```html
<span class="status ok">Passed</span>
<span class="status warn">Degraded</span>
<span class="status err">Failed</span>
<span class="status info">Queued</span>
```

A `currentColor` dot plus text. This is the "colour is never the only signal" rule made concrete. Never ship a bare swatch or coloured text alone to mean pass, fail, or warn.

### App scaffold — `.scaffold` (top bar + side nav + main)

```html
<div class="scaffold density-compact">
  <div class="topbar">
    <span class="r-mark" aria-hidden="true">R</span>
    <span class="title">Console</span>
    <button class="btn btn-primary" style="--control-h:28px; margin-left:auto">New project</button>
  </div>
  <div class="body">
    <nav class="sidenav" aria-label="Console">
      <a href="#" aria-current="page">Projects</a>
      <a href="#">Builds</a>
      <a href="#">Settings</a>
    </nav>
    <div class="main">…</div>
  </div>
</div>
```

The active link is signalled by background plus an inset left accent bar, not colour alone. This is the shell for a product console, not for a marketing page.

### Empty state — `.empty`

```html
<div class="empty">
  <div class="glyph" aria-hidden="true">∅</div>
  <h4>No projects yet</h4>
  <p>Create your first project to start building. It takes about a minute.</p>
  <button class="btn btn-primary">New project</button>
</div>
```

Dashed border, centred, and always paired with a primary action. Never a dead end.

### Skeleton — `.skeleton`

```html
<div class="skeleton" style="width:40px; height:40px; border-radius:var(--radius-full)"></div>
<div class="skeleton" style="height:12px; width:60%"></div>
```

The shimmer respects `prefers-reduced-motion` through the global rule in §4.7.

## 7. Patterns (composed screens)

Full CSS in `tokens.css` §5. Each composes only the tokens and components above.

| Pattern | Density | Use for |
|---|---|---|
| Marketing hero (`.hero`) | comfortable | Landing pages: eyebrow, one display heading, one lead paragraph, a primary and a secondary action |
| Editor shell (`.bgr*`, `.checkerboard`) | comfortable | Single-canvas tools such as BG Remover: dark canvas, checkerboard transparency preview, one primary action |
| Compiler (`.compiler*`, `.code-line`) | compact, mono | Dev tools with a source/output split view |
| Dashboard (`.secdash*`, `.stat-card`) | compact | Consoles and security dashboards: stat cards plus an alert table, semantic colour always paired with icon and text |

Use these as the starting layout for a product surface. Do not use the product shells for a marketing page.

## 8. Content and interaction rules

- **Single accent per screen.** `--accent` marks the one primary action. Secondary buttons, body links, and nav stay neutral.
- **Snap to tokens or stop.** Never eyeball a "close enough" hex or px value.
- **Semantic colour is never solo.** Dot or icon plus text, always.
- **Errors say what happened and what to do next**, in sentence case. "That email is missing a domain. Add one after the @, like you@company.com." Not "Invalid email".
- **"Sign in", never "Log in".** Buttons are sentence case, never Title Case.
- **One density per product.** Comfortable for marketing and consumer surfaces, compact for dense tooling. Decide once.
- **No weight above 600. No gradients, glassmorphism, or decorative shadows.**
- **Drift gets filed, not silently fixed.** See §10.

## 9. Accessibility checklist

The system is built so most of this is free. Verify it stays that way on each screen:

- Text meets 4.5:1 on the surface it sits on (3:1 for text at `--fs-6` and above). The token pairs in §4.3 already do; new combinations must be checked.
- Every interactive element shows the 2px `--accent` focus ring on `:focus-visible`. Do not remove outlines.
- Controls are at least 44px tall in comfortable density (32px in compact, for pointer-precision tooling only).
- Form fields have a visible `<label>`, `aria-describedby` pointing at help or error text, and `aria-invalid` on error.
- Loading buttons use `aria-disabled`, not `disabled`, so they remain in the tab order and are announced.
- Modals are native `<dialog>`; toasts live in a `role="status"` region; tabs use the ARIA tabs pattern with roving tabindex.
- Status is dot plus text. Charts and badges follow the same rule.
- Motion respects `prefers-reduced-motion`. Anything animated must go through the duration tokens.
- Colour scheme is declared (`color-scheme: dark` / `light`) so native form controls and scrollbars match the theme.

## 10. Governance — proposing a new token

If you need a value that does not exist in `tokens.css`:

1. Do not invent a raw hex or px inline "just this once".
2. Check whether an existing token is close enough *semantically*, not just numerically. A new destructive-adjacent colour probably wants `--error`, not a new red.
3. If it is genuinely new, flag it in your output as `⚑ NEW TOKEN` with the proposed name (follow the existing naming: `--{category}-{step}` or `--{semantic-name}`), the value, and why nothing existing fits.
4. Do not silently fix visual drift you notice elsewhere either. Flag it the same way (offending value plus nearest token) so the pattern stays visible.

Tokens change in one place: `tokens.css` in the realbrain.cc repository. A deploy republishes every file listed at the top of this guide. Consumers that vendored the file update by re-running the `curl` in §3.2 and diffing.

## 11. For AI agents

If you are an agent building or editing a RealBrain surface:

1. Fetch https://realbrain.cc/design-system/llms.txt first. It has the rules and a quick reference that fits in a few hundred tokens.
2. Fetch https://realbrain.cc/design-system/tokens.css and use its classes and variables directly. Do not restyle a component that already exists there.
3. Read the relevant section of this guide for the component or pattern you are building.
4. If the repository has no agent instructions yet, copy https://realbrain.cc/design-system/AGENTS.md in as `AGENTS.md` or `CLAUDE.md` and adjust the vendored-file path.
5. Before finishing, run the self-check in that file: no raw hex or px outside `tokens.css`, no weight above 600, no gradient or blur, no "Log in", no Title Case buttons, one `.btn-primary` per screen.

## 12. Versioning

Version 2.0.0, first published 2026-09-05 (the system itself dates from July 2026). The version appears at the top of `tokens.css`, in `tokens.json` under `$extensions["cc.realbrain.meta"].version`, and at the top of this guide. Breaking changes (a renamed or removed token) bump the major version; new tokens or components bump the minor.
