The live visual system.
styles/globals.cssis the implementation; this file explains the reasoning so the next change doesn't quietly undo it.Supersedes Design System v3 ("Industrial Telemetry"), which described a dark scenario-driven site that no longer exists.
Surface separates sections. Nothing else needs to.
The page runs paper → ink → paper → paper-2 → paper-2 → oxblood. Changing
the ground is what tells a visitor one section ended and another began.
This is load-bearing. An earlier version gave all six sections an identical shell — same padding, same max-width, same alignment, one background — and separated them with 1px rules. Nothing bifurcated; the page read as one undifferentiated column. Every rule below follows from replacing hairlines with surface.
Not stylistic preferences. Each appeared in a previous version, and each is a documented AI-generation tell:
- Tiny uppercase tracked eyebrows above section headings. Scaffolding you reach for when the layout can't separate itself. The surface change replaced it.
- Numbered section markers (
01 ·,02 ·). Numbers earn a place only when the content genuinely is an ordered sequence. - Coloured side-stripe borders (
border-left> 1px as an accent). Decoration standing in for emphasis; use scale or ground instead. - Gradient text (
background-clip: text). - Cards, nested panels, pill/chip tags. Boxing content adds a frame nobody asked for. The only bordered shapes on the site are the capability schematic's panels, which are nodes in a real topology.
- One uniform scroll-reveal on every block. The uniform reflex is the tell, not motion itself.
- Display serif + small mono labels + ruled separators. The "editorial-typographic" lane — the second-order AI reflex, and where v1 landed.
Archivo, variable: wdth 62–125, wght 100–900. No second family, no mono.
Width is the hierarchy device, which is the concept doing real work: a type specimen shows one family across its range. Expanded for display, normal for reading, condensed for data.
| Utility | Axis | Size |
|---|---|---|
.type-statement |
wdth 118, wght 700 |
clamp(2.75rem, 7vw, 6rem) |
.type-plate |
wdth 112, wght 600 |
clamp(2rem, 5vw, 3.75rem) |
.type-sub |
wdth 100, wght 600 |
clamp(1.5rem, 3vw, 2.25rem) |
.type-body |
wdth 100, wght 400 |
1.0625rem, max 68ch |
.type-data |
wdth 78, wght 500 |
0.8125rem |
Steps are ≥1.25 apart. Display ceiling is 6rem — above that the page is
shouting. Tracking floor is -0.04em — tighter and letters touch.
Values are fixed. What matters is that they cover surface, not just accents.
| Plate | Ground | Text ramp |
|---|---|---|
| Paper | #faf9f7 |
--ink / --ink-muted / --ink-dim |
| Paper-2 | #f2f0ec |
same |
| Ink | #14151a |
--on-ink / --on-ink-muted / --accent-on-dark |
| Oxblood | #4a1811 |
--on-close / --on-close-muted |
The accent #b4331f is a redline — correction, emphasis, one mark per
plate. It is never a ground.
The closing plate is that redline's own hue taken to depth: OKLCH hue 30 against the accent's 32, darkened (L 0.29 vs 0.52) and desaturated (C 0.078 vs 0.169). Related by construction, not by repetition.
This is a correction, and the reason matters. The closing plate was
originally a full-strength --accent drench. At chroma 0.169 across a whole
ground it was the loudest thing on the site and did not sit with the rest of the
palette. It also squeezed the typography: only two tiers cleared AA (5.81 and
4.76, with a third measuring 4.39 and failing). The depth buys headroom —
--on-close-muted clears 6.81:1, so the muted tier is genuinely muted rather
than a near-white pressed against the limit.
Consequence to preserve: a dark plate's header wash must be tinted from that
plate's own ground. A neutral bg-ink/80 over oxblood composites to #1f1618,
0.075 below the plate in OKLCH lightness — it reads as a bar laid on top rather
than the surface dimmed. See DARK_PLATE_WASH in
components/layout/site-header.tsx.
Every text/ground pair is verified with culori's wcagContrast at ≥4.5:1.
Not ceremony — measuring caught two real failures that reasoning missed:
--ink-dimwas originally#8a8c96→ 3.18:1, failed. Now#6b6d79.--accenton the ink plate → 2.98:1, failed. Hence--accent-on-dark(#e0563c, 4.83:1). The paper accent must never be used on a dark plate.- On vermilion only two tiers survive:
#f2d4cemeasures 4.39:1 and fails.
Re-run the gate after any colour change.
Per-plate, never uniform:
- Hero — one choreographed staggered load. Transform-only, opacity stays 1:
the
<h1>is the LCP element and fading it in delays the largest paint. - Approach — the lattice's scroll-driven resolve.
- Work — per-project stagger on scroll.
- Credentials — none, deliberately. Something has to stay still.
- Contact — the transit band runs continuously; the closing content staggers in behind it.
prefers-reduced-motion is a hard override at the call site
(components/shared/reveal.tsx), not a tier decision.
Text-only pages are a failure mode, so the page carries two real visuals:
- The lattice (
components/approach/) — a scattered node graph resolving into an ordered lattice on scroll. Both states share one edge list, so the same graph reads as tangle, then structure. Light line-art on the ink plate. Gated bylib/capability: reduced-motion, <768px and no-WebGL2 get an SSR'd SVG of the resolved end state, andthreestays out of their bundle. - The capability arc (
components/capability/arc-diagram.tsx) — a schematic of offensive base → GRC + AI security, on an engineering substrate. Resolves entirely fromdata/skills.ts. Full SVG ≥768px; a stacked HTML variant below, so labels never scale under 12px. - The transit band (
components/contact/) — the closing plate's right 30%. Vertical channels with short segments travelling down them: data in flight. Distinct from the lattice on purpose — that one is topology, this one is flow, and repeating the node-and-edge language would weaken both.
Rule: the band is built from positioned elements and percentages. No svg,
no viewBox, no preserveAspectRatio.
This is the scar tissue from a real failure, recorded so it is not repeated. The
band originally held a guilloche — a square, radially symmetric engraving — in a
container that is 30% of the viewport wide and a full section tall. With
preserveAspectRatio="slice" the browser scales artwork to cover, so it was
rendered at section height and its width overflowed the band:
| Viewport | Band | Artwork rendered | Visible | Cropped |
|---|---|---|---|---|
| 1440 | 432px | 1300px | 33% | 868px |
| 1920 | 576px | 1400px | 41% | 824px |
A third of the motif reached the screen, and because it also rotated, lines swept in and out of that slot and read as broken. A square motif and a tall band are incompatible shapes, and no tuning reconciles them. Percentages resize with their container and cannot disagree with it, which is why the replacement is CSS-only.
Two further constraints that must survive edits:
- The band is reserved, never overlaid. The content container carries
lg:pr-[34%]so text never sits on the pattern. Belowlgthe band is not rendered at all. - Segments start mid-flight. Offsets are applied as negative
animation-delay, so the band is never seen filling from empty. Travel runs-15% → 115%so a segment fully clears the view before it loops, keeping the repeat invisible; timing islinear, because easing makes every rail pulse in unison.
Config is seeded and deterministic (transit-config.ts, the same pattern as the
lattice), so server and client markup cannot diverge.
Before shipping any visual change:
npm run type-check && npm run lint && npm run test && npm run build- Contrast gate over every pair in the colour table — ≥4.5:1, no exceptions.
- Ban sweep:
eyebrow,border-l-2,border-r-2,background-clip,01/02markers. Nothing outside comments. - Scroll the page and confirm four distinct grounds are visible. A visitor should be able to say where a section ends without reading a word.