Admin UI design system
Audience: contributors changing anything served from internal/server/static/.
Aperture's admin UI shell follows the BERA Design System. These rules are not style preferences — several are load-bearing, and reviewers enforce them. Nothing here is derivable from the code, which is why it lives in the docs.
Voice
An analyst's tool: information density, analytical seriousness, proof-led copy.
Address the reader in second person ("your grants"), never "users". Sentence case
everywhere, with one exception — small form-field labels are uppercase with
0.06em letter-spacing.
Colour
Use named tokens, never raw hex.
- BERA Blue ramp
--bera-25…--bera-900. Primary action is--bera-600, hover--bera-700. - AI-pink
#ff3399is reserved for AI affordances only — the assistant button, an "Ask BERA" pill, AI badges. It is forbidden for primary actions, alerts, and decoration. This one is load-bearing: a pink primary button is a bug, not a taste difference. - Data-visualisation families (grass, strawberry, blueberry, plum, rose, orange, apricot, bera) each carry a 25→900 ramp. A series keeps its colour identity across every chart it appears in. Never invent a chart colour.
- Neutrals: page background
--neutral-200(#f8fafc), card#fff, body text#333333. Pure black is reserved for the wordmark. - Semantic roles:
--color-primary/-success/-warning/-danger/-info,--color-bg/-surface,--color-fg/-muted,--color-border(slate-200),--color-focus-ring(bera-500).
Typography
- Display: BwGradual (commercial). If the
.otfis not shipped, fall back to the system sans — never substitute Inter or Roboto. - Body: IBM Plex Sans.
- Mono: IBM Plex Mono, tabular numerals. Use it for every object-identity
string, entity id, and token-like value —
account:acme/project:atlas/document:42is mono, always. - Scale
--text-2xs…--text-6xl; semantic classes.bera-h1….bera-h4,.bera-body,.bera-label,.bera-numeric.
Spacing, radius, motion
4px base unit; when in doubt take the tighter value (analyst density). Radius sm 4 / md 8 / lg 12 / xl 20 / full. Shadows: card, raised, floating, plus the focus ring. Motion is fade plus a 4–8px translate — no bounces, no springs. Durations: fast 150ms, base 250ms, chart 600ms.
Layout
Dashboard is a fixed left sidebar (~232px) plus a top utility bar (~52px) over flex content. 12-column grid, 8–12px gutters. Modals cap at 720px, or 1080px for heavy workflows.
Iconography
Lucide, stroke-only at 1.5px. 16px inline, 20px default. Always pair an icon with text. AI surfaces may use sparkles / bot / wand-2 in AI-pink.
Hard nos
- No emoji anywhere — including ✅ and 🎯 in confirmations and empty states.
- No exclamation points, no hype words, no "users".
- No invented chart colours.
- No gradients as decoration.
- The primary button is not pink.
Vendored assets, and why there is no build step
Everything the UI needs is committed pre-built under
internal/server/static/vendor/ and //go:embed-ed: Alpine, Tailwind, DaisyUI,
and the Rete.js dist bundle (generated by rete-kit/rete-cli). There is no
node build pipeline in development or CI — the binary ships self-contained, and
CI is node-free by design. Regenerating a vendored bundle is an occasional,
explicit, manual step, never something a build performs.
That node-free property is also why internal/server/static/js/rules-serializer.test.js
never runs in the pipeline, and why rules/editor_js_contract_test.go — which
reads the JavaScript from disk and diffs its tables against the Go rule AST — is
the real parity gate. See Build, test & lint gates.