SUBSTRATE
The layer everything is built on. Obsidian canvas, one hot signal, and motion that only ever means something — rendered by the running theme, not screenshotted from it.
Three layers. No shortcuts.
A component may never reference a primitive. It reads a component token, which reads a semantic token, which reads a primitive. That indirection is what lets you re-theme, ship a light mode, or change the accent in one line without auditing 400 files.
Raw values
Named for what they are. No meaning attached. Never used directly in a component.
/* the palette */
--volt-400: #C8FF3D;
--void-900: #0A0B0D;
--slate-200:#BDC2CB;
Purpose aliases
Named for what they do. This is the layer that themes — the Customizer writes here. Swap these, and everything downstream follows.
/* the meaning */
--accent: var(--volt-400);
--bg-base:var(--void-900);
--fg-muted:var(--slate-200);
Local contracts
Named for where they apply. Lets one component deviate without forking the system.
/* the contract */
--btn-primary-bg: var(--accent);
--btn-primary-fg: var(--accent-fg);
--card-bg: var(--bg-raised);
// ✗ WRONG — a component reaching past two layers .button { background: #C8FF3D; } .button { background: var(--volt-400); } // ✓ RIGHT — every hop is explicit .button { background: var(--btn-primary-bg); } // └─ --accent ─── --volt-400 ─── #C8FF3D
One signal. One atmosphere. Everything else is obsidian.
Roughly 92% of every screen is neutral. Voltage appears only where something is live, active, or actionable. If the accent is on more than about 6% of the pixels, the page has stopped meaning anything.
The obsidian ladder
Four elevations, not two. Pure black is banned: it smears on OLED, and it leaves no room to go darker for recessed surfaces like inputs and code blocks.
Text ramp
Voltage — the signal
These read live from the theme. The accent is currently #C8FF3D — change it in Customize → Substrate and this row moves with it.
Plasma and status
✓ Do
- Let plasma live at ≤22% alpha, behind everything, pointer-events:none.
- Use voltage for exactly one primary action per viewport.
- Reach for bg-void when a surface should feel recessed — inputs, code, terminals.
✗ Don't
- Set plasma as text or a button fill. It is 2.75:1. It will fail an audit and it will look cheap.
- Add a fifth accent. Two colours carry meaning here; a third destroys both.
- Use border-hair to outline an input. It is decorative — it cannot define a control.
The Substrate
Not a node graph. Not a logo constellation. Not two boxes with an arrow between them. Those depict plumbing, and nobody wakes up wanting plumbing.
The device is a field of particles in noise that resolves into an ordered lattice — and holds. It is the whole company in one gesture: we take an ambiguous problem and return a structured system. The cursor is an attractor, so the field visibly responds. Move your mouse across it.
| State | Visual | Meaning |
|---|---|---|
| Noise | Particles scattered, dim, drifting on a random walk | The brief. Ambiguity. What the client arrives with. |
| Resolving | Particles ease toward lattice anchors; edges begin to draw between neighbours | The work. expo.out, staggered — order emerging, not snapping. |
| Resolved | Lattice holds, edges at full voltage, entropy reads 0.00 | The system. Running. This is the terminal state. |
| Perturbed | Cursor pushes particles off-anchor; they spring back | Production. It takes a hit and returns to true. |
Reduced motion: the canvas renders the resolved lattice statically on first paint, entropy already at zero. The argument survives with the motion removed entirely, which is the only honest test of whether an animation earns its place. No JS: the canvas is skipped and the section collapses to its caption — no empty box, no stalled “RESOLVING” label.
Three faces, three jobs
Archivo carries every display line — it has genuine mechanical character where Inter has none. Inter does all the reading. JetBrains Mono owns every number, label, identifier and line of code.
| Role | Face | Size / LH | Tracking | Rule |
|---|---|---|---|---|
| Hero | Archivo 900 · 85% | clamp(40 → 78) / .94 | −.022em | Max 6 words. Tight enough to look engineered. |
| H2 | Archivo 900 · 85% | clamp(30 → 50) / 1.02 | −.024em | One per section. |
| H3 / card | Archivo 800 · 85% | 17.5–22 / 1.3 | −.016em | — |
| Body | Inter 400 | 16–17 / 1.65 | 0 | Cap at 66ch. Never Archivo. |
| Data / code | JetBrains Mono 500 | 12.5–15 | −.01em | tabular-nums always. Every metric, every ID, every latency. |
| Label | JetBrains Mono 600 | 10.5–11 | +.18em, UPPER | Eyebrows, section indices, chip text. |
Built, not described
Button
A11y: 48px height clears the 44pt floor. The focus ring is voltage, visible on every surface in the system — which is not true of most dark themes. The arrow glyph is aria-hidden. Disabled anchors use aria-disabled, never pointer-events alone.
Card and status
Agent systems
Retrieval, tool use, evals, fallbacks, and the boring production scaffolding that decides whether it survives contact with real users.
Product engineering
The application the model lives inside. Auth, data, billing, admin — the thing your customers actually touch.
Status variant
Chips carry a dot, a word, and a colour — three redundant signals. Colour is never the only one.
Hover: the border lifts, the card rises 3px, shadow-2 lands, and a top-edge specular fades in — a 1px gradient hairline that reads as light catching an edge. It is the single detail that makes an obsidian card feel like a physical object rather than a div.
Input
Recessed to bg-void, one step below the page — an input should feel cut into the surface, not stuck on top of it. The border is border-strong at 3.56:1; the hairline would fail WCAG 1.4.11.
One curve. Everything.
cubic-bezier(.16, 1, .3, 1) — expo.out. It leaves fast and lands soft. Every entrance, hover, reveal and transition in the product uses it, which is why the whole thing feels like one object instead of forty.
| Token | Duration | Curve | Applies to |
|---|---|---|---|
--t-1 | 140ms | expo.out | Hover, focus, chip, press. Feedback must land inside 100ms perceptually. |
--t-2 | 240ms | expo.out | Card lift, border transition, state change. |
--t-3 | 420ms | expo.out | Scroll reveal — opacity + 20px rise. Fires once, never on re-entry. |
--t-4 | 680ms | expo.out | Hero char stagger, circuit advance. The long ones. |
| Stagger | 22ms/char, capped at 420ms | — | Beyond the cap the last character feels laggy. |
✓ Do
- Animate transform and opacity. Nothing else. Ever.
- Make every animation depict a cause and effect — a state resolving, a signal moving, a system responding.
- Keep animations interruptible. A tap during a transition cancels it immediately.
✗ Don't
- Animate width, height, top or left. That is a reflow every frame.
- Pin more than one section. Scroll-jacking fights native scroll and is miserable on mobile.
- Ship anything whose meaning is lost under prefers-reduced-motion. If the end state does not carry the argument, the animation was decoration.
SILICONWHIZ · SUBSTRATE v1.0.0
Rendered live by the theme it documents.