Foundations
The tokens, measurements, and laws from which every component and pattern is derived. Nothing in later volumes may contradict this volume. If a problem isn't answered here, resolve it in the spirit of the principles, document it, and propose it for the next revision — never improvise silently.
Principles
Four laws. Every rule in this book serves one of them. When the book is silent, decide in their favour.
Nothing approximate.
Engineered geometry, tokenised values, exact alignment. If a measurement isn't on the scale, it's wrong. Precision earns trust before a word is read.
Calm under load.
Feel like something that stays up when things go down. Solid surfaces, steady rhythm, no visual panic — even at P1 severity.
State, shown plainly.
Legibility over decoration. Surface truth — green when healthy, red when not — never obscured by ornament or ambiguity.
Always watching.
The pulse is the soul. Motion and signal show the estate is observed in real time. Used with restraint, it makes the product feel awake.
Rule grammar
Rules are graded. Keywords are absolute, read as in RFC 2119.
Token names (--p500, --s4) are normative. Reference tokens, never raw values.
Color
Obsidian dark, one signal green. Green is the pulse — used sparingly so it always carries meaning. Click any swatch to copy.
1.1.1 Core ramps
Obsidian — surfaces
Pulse — the accent
Neutrals — text & light mode
1.1.2 Elevation by tint
Dark UIs express depth with surface tint and a hairline border, not heavy shadow. Each step up the surface ramp is one level of elevation.
--hair border. Tint alone reads flat on low-contrast displays.1.1.3 Semantic & data
| Token | Hex | Meaning | Use |
|---|---|---|---|
| --p500 | #22C55E | Success / healthy / live | Operational state, primary action, the pulse |
| --info | #38BDF8 | Informational | Neutral notices, links, in-progress |
| --warn | #F59E0B | Caution / degraded | Degraded nodes, soft warnings |
| --danger | #F43F5E | Error / offline / destructive | Failures, destructive actions |
| --violet | #8B5CF6 | Data series 4 | Charts only — never UI chrome |
Data-viz series order: --p500, --info, --warn, --violet, then tints. Status colours keep their meaning in charts — green is always "good."
1.1.4 Contrast
Measured, not estimated (WCAG 2.x relative luminance). The v1.0 table quoted approximations; two of them were wrong in the direction that matters, so every pairing below is the number a checker will give you.
| Pairing | Ratio | Verdict |
|---|---|---|
| Snow on Obsidian-950 | 17.0:1 | AAA · body |
| Snow-2 on Obsidian-950 | 14.2:1 | AAA · running text |
| Mist on Obsidian-950 | 9.8:1 | AAA · secondary |
| Slate on Obsidian-950 (v1.0, #6B7585) | 4.3:1 | Fails AA at 13px — retired |
| Slate on Obsidian-950 (v1.1, #7A8598) | 5.3:1 | AA · labels only |
| Pulse-400 on Obsidian-950 | 11.4:1 | AAA · status ink |
| Pulse-500 on Obsidian-950 | 8.7:1 | AAA · fills, large text |
| Danger on Obsidian-950 | 5.4:1 | AA · fills only — text uses --danger-ink (10.5:1) |
| Warn / Info on Obsidian-950 | 9.2:1 | AAA · inks 11.9:1 |
| Obsidian-900 on Paper | 17.3:1 | AAA · body (light) |
| Paper status inks on their 12% tints over Snow-2 (worst Paper surface) | positive 4.76 · warn 4.80 · danger 5.03 · info 5.10 · violet 5.71 | AA · measured where status text actually sits |
| On-accent on Pulse-500 | 7.5:1 | AAA · primary button, both themes |
--danger) is for dots, borders and tints; text and icons on a page surface use the ink (--danger-ink). A fill used as text fails contrast on one of the two surfaces — and the console's hig-lint gate rejects it.1.1.5 Paper — the light theme
Obsidian is the brand and the default. Paper exists for daylight, print, and the customer portal's calmer register. It is the same system: every role keeps its name, every status keeps its hue; only the surfaces flip and the inks darken so every pairing still clears AA. Toggle it with the switch in the top bar of this book.
| Role | Obsidian | Paper |
|---|---|---|
| --bg | obs-950 #080A0F | paper #F4F6FA |
| --surface · --surface-2 · --surface-3 | obs-900 · obs-850 · obs-800 | white · paper-2 · snow-2 |
| --text · --text-2 · --text-3 | snow · snow-2 · slate | obs-900 · #2B3441 · #4B5563 |
| --border · --border-strong | hair · hair-2 | snow-2 · mist |
| --accent (primary button) | p500 + on-accent ink | p500 + on-accent ink — unchanged |
| --positive-ink | p400 | #166534 |
| --warn-ink · --danger-ink · --info-ink | #FBBF24 · #FCA5B4 · #7DD3FC | #92400E · #9F1239 · #075985 |
| --surface-code | #06080C | obs-900 — code stays on obsidian |
| --shadow-* | black, .40–.50 | ink, .08–.16 |
--obs-900 directly is wrong on Paper; one that reads --surface is right on both.Typography
Three families, three jobs, no overlap.
Wordmark & ≥24px headings only
All readable copy & UI
Data, code, labels, metadata
assets/aros-wordmark.svg, traced once from Archivo at wdth 125 / wght 800) — an app that shows the wordmark and no display headings ships no display font at all.1.2.1 Type scale
| Token | Family | Size / line | Weight · tracking | Use |
|---|---|---|---|---|
| display-l | Display | 52 / 56 | 800 · +0.01em | Page hero |
| display-m | Display | 40 / 44 | 700 · +0.02em | Section hero |
| h1 | Sans | 32 / 40 | 700 | Page title |
| h2 | Sans | 26 / 32 | 600 | Subsection |
| h3 | Sans | 22 / 30 | 600 | Block heading |
| h4 | Sans | 18 / 26 | 600 | Minor heading |
| body-l | Sans | 20 / 32 | 400 | Lead paragraph |
| body | Sans | 17 / 28 | 400 | Default text |
| body-s | Sans | 16 / 26 | 400 | Secondary |
| caption | Sans | 15 / 22 | 400 | Captions, help |
| label | Mono | 13 / 20 | 500 · +0.16em · UPPER | Eyebrows, tags — the floor |
| code | Mono | 15 / 24 | 400 | Code, data values |
| axis | Mono | 11 / 16 | 500 · tabular | Chart axis ticks only — never running text |
axis for chart ticks, which are read as marks, not as text. The operator console's hig-lint gate rejects any arbitrary pixel size.1.2.3 Density: the book vs the instrument
This book is set at comfortable (body 17). The operator console is an instrument and renders at dense: UI text 14, labels 13, controls 36px tall, table cells 12×14. Same families, same floor, same tokens — only the scale moves. The customer portal sits between the two.
| Role | Comfortable (book, portal prose) | Dense (console) |
|---|---|---|
| UI text | 16 / 26 | 14 / 20 |
| Page title (h1) | 32 / 40 · 700 | 24 / 32 · 600 |
| Card title | 18 / 26 · 600 | 16 / 22 · 600 |
| Label / eyebrow | 13 / 20 · mono | 13 / 20 · mono — never lower |
| Control height | 40px | 36px (sm 32 · lg 44) |
| Table cell | 14 × 16 | 12 × 14 (compact 8 × 12) |
1.2.2 Numerics & truncation
Spacing & layout
Everything is a multiple of 4px. One base unit is what makes unrelated screens feel like one product.
1.3.1 The scale
13px, 15px, 30px do not exist.1.3.2 Inset · stack · inline
Padding inside a container. Cards --s6. Compact controls --s3×--s4.
Vertical gap. Default --s4; related --s2; unrelated blocks --s8+.
Horizontal gap. Icon-label --s2; button group --s3; toolbar sections --s6.
1.3.3 Density
Comfortable is the book's default; the console runs dense (§1.2.3), and compact tables step each inset/stack token down one more. Density is a property of the whole surface, chosen once.
Grid & breakpoints
A 12-column fluid grid inside a fixed content well, with a persistent navigation rail.
| Columns | 12 | Subdivide 2 / 3 / 4 / 6 |
| Gutter | --s4 → --s6 | Compact → comfortable |
| Content max | 1080px | Reading & app well |
| Page margin | --s6 → --s11 | Scales with viewport |
| Nav rail | 288px fixed | Persistent ≥ lg; off-canvas below |
1.4.1 Breakpoints
| Token | Min | Behaviour |
|---|---|---|
| sm | 480px | Single column, stacked controls |
| md | 768px | Two-column grids permitted |
| lg | 1024px | Nav rail persistent; full grid |
| xl | 1280px | Comfortable gutters & margins |
| xxl | 1536px | Well centred, margins grow |
lg, the nav rail collapses to an off-canvas drawer. The content well never sits under a fixed rail it can't clear.Elevation & shape
Depth is layered and ordered; corners follow one radius scale.
1.5.1 Z-layers
| z-base | 0 | Page content |
| z-raised | 10 | Cards, hovered rows |
| z-sticky | 30 | Sticky headers, toolbars |
| z-rail | 40 | Navigation rail / drawer |
| z-modal | 50 | Dialogs & scrim |
| z-toast | 90 | Toasts, transient alerts |
1.5.2 Shadow tokens
| shadow-sm | 0 1px 2px rgba(0,0,0,.4) | Hover lift |
| shadow-md | 0 6px 16px rgba(0,0,0,.45) | Popovers, menus |
| shadow-lg | 0 18px 40px rgba(0,0,0,.5) | Modals only |
1.5.3 Corner radius
| --r-xs (4) | Inputs in cards, chips, code blocks |
| --r-md (9) | Buttons, inputs — default |
| --r-lg (14) | Cards, panels, modals |
| --r-xl (20) | Hero surfaces, app-icon tiles |
| pill / full | Status pills, avatars, the live dot |
Iconography
Icons are a system, not a pile of drawings. They share one grid, one stroke, and a strict convention for when green appears — so the set reads as a single hand and every glyph is distinguishable from its neighbours.
1.6.1 The convention
--ink. Domain icons (node, deploy, audit — things with meaning) carry exactly one green --accent element, placed on the part that is the live / active / verified thing.1.6.2 Construction grid & keylines
Every icon is drawn on a 24px grid with 1px padding (drawable area 22px). Forms snap to the keyline shapes so square, round and tall icons feel optically equal in weight.
Square 20×20 — blocky forms (host, container).
Circle 22Ø — round forms run slightly larger to match weight (node, observe).
Rectangle wide forms (terminal, database).
1.6.3 Stroke spec
| Stroke weight | 2px @ 24px (scales proportionally) |
| Caps & joins | Round |
| Interior corners | 2px radius |
| Structure / signal | --ink · one --p500 element |
| Fill | None — outline (default variant) |
1.6.4 Chrome set · monochrome
1.6.5 Domain set · two-tone
1.6.6 Deliberate metaphors
Where there is a convention, we use it. Where the obvious glyph would collide with a neighbour or mislead, we modify it or design a new one. The reasoning is part of the system.
1.6.7 Variants
Each glyph ships as outline (default) and, where it appears in navigation, solid (filled silhouette) for the active/selected state. The green signal element is retained in both.
Optical sizing
1.6.8 Rules
1.6.9 In the product: Lucide
The bespoke set above is the brand's domain vocabulary and the reference for any new glyph. The operator console and the customer portal draw their UI icons from Lucide — the same 24px grid, the same 2px optical stroke, the same outline-only discipline — because a product with four hundred icon sites needs a set that already exists. The reconciliation is a rule, not a compromise:
currentColor and never tinted by hand. The one green element arrives by state: the active rail item's icon turns --positive-ink, a live status carries the pulsing dot beside its glyph. Green is earned by liveness or selection, never painted on.ArosMark) as the only two-tone glyph on any product surface.Motion
Motion is purposeful, quick, and eased — it communicates state and causality, never decoration. The heartbeat is the one signature animation and the only thing permitted to loop indefinitely.
1.7.1 Principles
Purposeful
Every animation answers "what changed and why." If it explains nothing, remove it.
Quick
UI transitions land in 120–240ms. Slow motion feels broken, not premium.
Eased
Decelerate into rest. Linear is reserved for the continuous pulse loop only.
Restrained
One focal motion per view. Many small animations compete and cheapen.
1.7.2 Duration tokens
| Token | Value | Use |
|---|---|---|
| d-instant | 0ms | Direct manipulation (drag) |
| d-fast | 120ms | Taps, toggles, hover colour |
| d-quick | 180ms | Buttons, small reveals |
| d-base | 240ms | Panels, cards, menus |
| d-slow | 360ms | Modals, page transitions |
| d-pulse | 2800ms loop | The mark's pulse trace |
| d-ping | 1800ms loop | The live dot's ring |
| d-shimmer | 1400ms loop · linear | Skeleton sweep — the only other loop, and it must resolve |
1.7.3 Easing tokens
1.7.4 Per-component motion
| Element | Trigger | Property | Duration · easing |
|---|---|---|---|
| Button | Hover / press | bg, transform 1px | fast · standard |
| Card / row | Hover | border, translateY 2px | quick · standard |
| Menu / popover | Open | opacity + 4px rise | base · decelerate |
| Modal | Open | opacity + scale .98→1 | slow · emphasized |
| Toast | Enter / exit | opacity + 8px slide | base · standard |
| Tooltip | Open | opacity + scale .95→1 | fast · standard · 300ms delay, none on focus |
| Live dot | Always (healthy only) | ring scale 1→2.6, opacity .7→0 | ping · standard |
| Page | Route change | opacity + 14px rise (stagger) | base · decelerate |
1.7.5 Choreography
When several elements enter together, stagger them 40–60ms in reading order. One orchestrated entrance beats scattered independent animations.
1.7.6 The signature pulse
The heartbeat traces the pulse path over 2800ms on a continuous loop with --e-standard. It is visual proof that the estate is being watched.
1.7.7 Accessibility
prefers-reduced-motion. When set, disable loops and large transforms; keep opacity only. The pulse becomes a static green dot, the skeleton a static block — state stays legible without motion. Remove the animation, never shorten it: a 1ms slide is still a slide.Next: Components
Foundations are locked. Volume II — Components → builds every element on these tokens and laws.