REALBRAIN · DESIGN SYSTEM · V2.0
Premium comes from restraint.
One accent per screen. Flat surfaces separated by hairlines. Type that carries hierarchy through size, not weight. Every value a named token, shipped as one plain CSS file that any RealBrain project can link in a minute.
Getting started
The system is one CSS file with no build step and no framework coupling. There is no npm package on purpose: a URL is easier to keep in sync than a version matrix.
1. Link it
For prototypes, link the canonical URL. Tokens, base reset, fonts, every component, and the composed patterns all come with it.
<link rel="stylesheet" href="https://realbrain.cc/design-system/tokens.css">
2. Vendor it for anything that ships
A deploy should never depend on another origin. Copy the file into the repository and import it once, globally. The version line at the top is what to compare when you update.
curl -o src/tokens.css https://realbrain.cc/design-system/tokens.css
3. Use the classes
The classes go straight into markup, the way utility classes would. Thin wrapper components are fine when you want prop-driven variants, but keep the styles in the one shared stylesheet, never in a scoped block that will drift.
<!-- 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>
4. Themes and density
Dark is the default. Light is one attribute on <html>. Density is one class on a container, chosen once per product. Try both with the switches in the top bar: every component on this page responds.
document.documentElement.setAttribute('data-theme', 'light') // light
document.documentElement.removeAttribute('data-theme') // back to dark
document.body.classList.add('density-compact') // 32px controls for dense tooling
Tailwind v4
Tailwind can consume the tokens without duplicating a value. @theme inline makes each utility use the variable, so the theme switch keeps working. With --spacing: 4px, Tailwind's numeric scale lines up with the space tokens: p-4 is 16px, p-6 is 24px, p-12 is 48px.
@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;
--radius-sm: var(--radius-sm);
--radius-md: var(--radius-md);
--radius-lg: var(--radius-lg);
--radius-full: var(--radius-full);
--ease-rb: var(--ease);
}
@import line from the vendored copy. The stacks fall back to system-ui and ui-monospace, so a blocked font is a degraded page, not a broken one.
Colour
Sixteen tokens, two themes, one accent. Chips are painted with the live CSS variables; the hex shown is the value for the active theme. Switch the theme in the top bar to compare.
--accent-hover and --accent-pressed are not hardcoded. They are color-mix() formulas relative to --accent, which is why the light theme does not redefine them. Prefer this over hand-picking a shade whenever a colour is derived from another.
Both themes
| Token | Dark | Light | Role |
|---|
Verified contrast
WCAG relative luminance. AA thresholds: 4.5:1 for body text, 3:1 for large text. Every pair the system relies on passes in both themes. Hold any new colour to the same bar before it becomes a token.
| Pair | Dark | Light | Result |
|---|
Typography
Two families: IBM Plex Sans for UI and IBM Plex Mono for code, data, and IDs. Weights 400, 500, and 600 only. The font import does not load 700, so nothing can accidentally use it.
The weight column is 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. Check the component's CSS rather than assuming from the ramp.
Spacing
A 4px base unit with nine non-linear steps: tight at the low end for component internals, wide at the high end for section rhythm.
Radius & shadow
Four radii. Three shadows that exist only to say "this surface is above the page". Never a glow.
Radius
Shadow
Structural depth cues for elevated surfaces: menus, popovers, modals, toasts. A card at rest has no shadow. The light theme uses a tinted rgba(20,20,30) at lower alpha instead of pure black.
Motion
One easing curve, three durations. Under prefers-reduced-motion: reduce all three collapse to 0ms and every animation and transition on the page is forced to 0ms. No springs, no bounce, no tilt-on-hover.
--dur-1: 120ms; /* 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 */
.btn { transition: background var(--dur-1) var(--ease); }
Layout & density
Two content widths and two densities. A marketing page is comfortable, a dense admin console is compact, and a single screen is never both.
| Token | Value | Use |
|---|
<body class="density-comfortable"> <!-- 44px controls: marketing, consumer apps (default) -->
<body class="density-compact"> <!-- 32px controls: admin consoles, dev tools -->
Brand chrome
The system's own mark is CSS only: an accent square with a mono, weight-600 "R" in --accent-fg. The company's neural-network logo is a separate asset; on v2 surfaces use a single-colour version filled with currentColor so it reads in both themes.
Button
Four variants. One .btn-primary per screen; everything else is secondary, ghost, or neutral text. Height and padding come from the density tokens, so set density on an ancestor, not per button.
- States: default, hover,
:focus-visible(2px accent ring, 2px offset), active, disabled, loading. - Loading uses
aria-disabled="true"plus.spinner, not the nativedisabledattribute, so the button stays focusable and announced. - Copy: sentence case, verbs not nouns. "Sign in", never "Sign In" or "Login". Destructive actions name the object: "Delete workspace", not "Delete".
<button class="btn btn-primary" aria-disabled="true">
<span class="spinner" aria-hidden="true"></span>Signing in…
</button>
Field
A label, a control, and help or error text wired together with aria-describedby. Error copy says what is wrong and shows the fix. No "Invalid input" dead ends.
We only use this to sign you in.
That email is missing a domain. Add one after the @, like [email protected].
.selectis a native<select>withappearance: noneand a CSS-drawn chevron. No JS dropdown for the basic case.- Always
aria-describedbyto the help or error text, andaria-invalid="true"on error.
Checkbox & radio
Real native inputs with appearance: none, redrawn in CSS. Keyboard and screen-reader behaviour come free.
Toggle
A visually hidden real checkbox under a track and knob. opacity: 0, not display: none, so focus and keyboard work. Always give the input an aria-label or a visible label.
Card
Standard cards are flat: surface on bg, hairline border, --radius-md, --space-5 padding, no shadow at rest. .elevated is for content that is actually above the page, not for making a card stand out.
Standard card
Surface on bg, 1px border, radius-md, space-5 padding. No shadow at rest.
Elevated card
Elevated surface with shadow-md. Reserve for overlays and popped content.
Modal
A native <dialog> on purpose. Focus trap and Esc-to-close come from the browser, and the backdrop is the ::backdrop pseudo-element rather than an overlay div.
<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>
<script>
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
</script>
Toast
A permanent role="status" live region; toasts are appended and removed. Self-dismisses after five seconds or on the dismiss button. The left border colour signals type, always with the icon.
<div class="toast-region" role="status" aria-live="polite" id="toasts"></div>
<script>
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)
}
</script>
Tabs
The ARIA tabs pattern with roving tabindex: the selected tab is 0, the rest are -1. Arrow keys, Home, and End move selection and focus together. Click a tab, then try the arrow keys.
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])
})
Table
No zebra striping. Rows separate with hairlines. IDs and numbers use .mono and .num. Compact density tightens the cell padding; try it in the top bar.
| Scan | Target | Status | Findings |
|---|---|---|---|
| #4812 | api.realbrain.cc | Passed | 0 |
| #4811 | forum.realbrain.cc | Degraded | 2 |
| #4810 | accounts.realbrain.cc | Failed | 3 |
| #4809 | realbrain.cc | Queued | — |
Status
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
Top bar, side nav, and main area: the shell for a product console. The active link is signalled by background plus an inset left accent bar, not colour alone. Not for marketing pages.
No projects yet
Create your first project to start building. It takes about a minute.
Empty state
Dashed border, centred, and always paired with an action. Never a dead end.
No scans yet
Run your first scan to see findings here. Most take under two minutes.
Skeleton
Loading placeholders on the elevated surface with a shimmer that respects reduced motion through the global rule.
Composed screens
Four starting layouts, each built only from the tokens and components above. Use a product shell for a product surface. Do not use one for a marketing page.
Marketing hero
Comfortable density. Eyebrow, one display heading, one lead paragraph, a primary and a secondary action.
REALBRAIN · SOFTWARE LAB
We build intelligent software for the world.
From AI tools to enterprise software: products that think, adapt, and deliver.
Editor shell
Comfortable density. A dark canvas with a checkerboard transparency preview, a narrow side panel, and one primary action. The shape of a single-canvas tool such as BG Remover.
Compiler
Compact density, mono type. Source and output panes over a status bar. The shape of any dev tool with a split view.
Dashboard
Compact density. Stat cards over an alert table, with semantic colour always paired with a dot and text. The shape of a security or operations console.
| Severity | Finding | Asset |
|---|---|---|
| Critical | Expired TLS certificate | api.example.com |
| Medium | Missing security headers | www.example.com |
| Low | Outdated dependency | worker-01 |
Content & interaction rules
The condensed version. Skim this even if nothing else.
Do
- Use
--accentfor 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 TOKENwith a proposal. - Pair semantic colours with a dot or icon and text.
- Write errors as what happened plus 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.
- No gradients, glassmorphism, blur, or decorative shadows.
- No white text on accent. Always
--accent-fg. - No raw hex or px in component code. Tokens only.
- No mixing densities within one product. No "Log in". No Title Case Buttons.
Error copy
"That email is missing a domain. Add one after the @, like [email protected]." Not "Invalid email". The pattern is what is wrong + a concrete example of the fix, and it is baked into the field example above.
Accessibility
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 at
--fs-6and above). The token pairs in Colour 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 is for pointer-precision tooling only.
- Fields have a visible label,
aria-describedbyto help or error text, andaria-invalidon error. - Loading buttons use
aria-disabled, notdisabled, so they stay in the tab order and are announced. - Modals are native
<dialog>. Toasts live in arole="status"region. Tabs use the ARIA tabs pattern with roving tabindex. - Status is dot plus text. Charts and badges follow the same rule.
- Motion goes through the duration tokens, which collapse to zero under reduced motion.
color-schemeis declared per theme, so native form controls and scrollbars match.
Governance
If you need a value that does not exist in tokens.css:
- Do not invent a raw hex or px inline "just this once".
- Check whether an existing token is close enough semantically, not just numerically. A new destructive-adjacent colour probably wants
--error, not a new red. - If it is genuinely new, flag it in your output as
⚑ NEW TOKENwith the proposed name (--{category}-{step}or--{semantic-name}), the value, and why nothing existing fits. - Do not silently fix drift you notice elsewhere either. Flag it the same way so the pattern stays visible.
Tokens change in one place: tokens.css in the realbrain.cc repository. A deploy republishes every file in Files. Projects that vendored the file update by re-running the curl and diffing. Breaking changes (a renamed or removed token) bump the major version; new tokens or components bump the minor.
For AI agents
Everything on this page exists in a form an agent can fetch. The short index fits in a few hundred tokens; the full guide is the same content as this page in Markdown.
- Fetch
llms.txtfirst. It has the rules and a quick reference for both themes. - Fetch
tokens.cssand use its classes and variables directly. Do not restyle a component that already exists there. - Read the relevant section of
design-system.mdfor the component or pattern you are building. - If the repository has no agent instructions yet, copy
AGENTS.mdin asAGENTS.mdorCLAUDE.mdand adjust the vendored-file path. - Before finishing, run the self-check in that file.
Point an agent at it
This project uses the RealBrain Design System. Before writing any UI or CSS:
1. Fetch https://realbrain.cc/design-system/llms.txt and follow its rules.
2. Use the classes and variables from https://realbrain.cc/design-system/tokens.css directly.
3. For component markup and states, read the matching section of https://realbrain.cc/design-system/design-system.md.
Tokens only, one accent per screen, no weight above 600, no gradients. Flag anything missing as ⚑ NEW TOKEN instead of inventing a value.
Self-check
From the repository root, adjusting src. Every command should print nothing.
# raw colours or px outside the token file
grep -rnE --include=*.css --include=*.vue --include=*.jsx --include=*.tsx --include=*.svelte \
--exclude=tokens.css '#[0-9a-fA-F]{3,8}\b|rgba?\(|\b[0-9]+px\b' src
# forbidden weight and effects
grep -rnE --include=*.css --include=*.vue --include=*.jsx --include=*.tsx --include=*.svelte \
--exclude=tokens.css 'font-weight:\s*(700|800|900|bold)|linear-gradient|radial-gradient|backdrop-filter|text-shadow' src
# copy rules
grep -rniE --exclude-dir=node_modules 'log ?in|Sign In\b|Log In\b' src
Files
Every file is published from the realbrain.cc repository on deploy, generated from the one canonical tokens.css.