Case study — Keel
From a 2,516-line brief to a shipped system.
Keel was built the slow way on purpose: brand first, spec second, tokens third, CSS last. This page is the record — the decisions, the numbers, and the two bugs worth more than the features they delayed.
01
Brief
Read every directive before writing a line
02
Brand
Keel — a shipping control plane, not a grid demo
03
Spec
Grid math, tokens, motion — decided on paper
04
Build
Composition, interaction, signature moment
05
Polish
Audit every claim against the actual bytes
2,516
lines of brief, digested before the build
62
design tokens governing every color and space
94
directives from the brief, each traceable in the build
0
frameworks, build steps or libraries shipped
01 / Process
The process was the product
The brief was explicit: the Bento grid is the vehicle, not the subject (§4). So before any layout existed, the project committed to a fictional company real enough to charge money — Keel, the control plane for shipping software. Every module earns its place by serving that story, and every number on the page traces to one canonical metrics table.
That produced a chain rare in portfolio work: Plan.md → BRAND.md → DESIGN-SPEC.md → build. The 2,516-line brief was distilled into a 94-line brand document (voice, naming, the amber signal color, mono numerals), then a 176-line spec (grid math, five surface tiers, motion durations scaled by tile size) — all before a single line of CSS was written.
“The Bento Grid is the vehicle, not the subject. The user should think: this is a beautiful website for this company.”
— Plan.md, §4
- BRAND.md (94 lines) — naming, voice, the one-accent rule, canonical numbers like 99.98% uptime and the 8-second rollback.
- DESIGN-SPEC.md (176 lines) — 12/6/4-column grid with real span maps, the full token table, surface hierarchy, motion tokens.
- The build — executed mechanically against the spec, so every later dispute was settled by looking it up, not re-arguing taste.
02 / System
Tokens before pixels
The spec defined 62 custom properties across two themes before composition began. Nothing in the stylesheet colors itself by hand — every value flows through a token, which is what makes dark mode a data change instead of a redesign, and what made the contrast audit recomputable by script.
- Grid — 12 columns desktop / 6 tablet / 4 mobile, with every module's span mapped per breakpoint in the spec, not improvised in the browser.
- Identity — paper-and-ink neutrals with one amber accent; JetBrains Mono carries every measurement, so numbers became the brand texture.
- Hierarchy — five surface tiers (primary, secondary, tertiary, featured, immersive) replace the equal-card soup the brief warned against.
- Motion — durations scale with tile size (160ms small / 240ms normal / 420ms large), with one spring curve reserved for the signature moment.
The signature interaction — the eight-second rollback — is the one place the system spends its spring: the page dims, the strip rises with overshoot, stages flip build→test→canary→fleet while the countdown ticks, and Escape aborts. One exceptional interaction, as the brief demanded. Everywhere else stays calm.
03 / Bugs worth keeping
Two bugs that proved the system
Both were caught by auditing the build against its own claims. Each fix is small; each lesson is not.
Bug 01 · animation
One keyword silently killed every hover on the page
Entrance reveals used animation-fill-mode: both. That sounds harmless — it even sounds thorough — but both permanently pins the animation's final keyframe at a cascade level above hover rules. Every cell that had ever revealed was stuck at translateY(0), and the hover lift had quietly stopped working everywhere at once.
.reveal { animation: rise 420ms ease-out both; } /* pins the transform forever */
.reveal { animation: rise 420ms ease-out backwards; } /* covers delays, then lets go */
The fix is one word: backwards still covers the stagger delay before the animation starts, but afterwards the element returns to its own styles — and the hover transforms come back to life. The full forensic version lives in Bug stories.
Bug 02 · tokens
A token referenced everywhere and defined nowhere
The rollback layer asked for var(--success) four times. No such token existed — in either theme. Because custom properties fail silently, the browser substituted nothing: no crash, no console warning, steady-state colors quietly absent. The honest fix wasn't to define the missing token — it was to rename the intent. The system already had a --steady family, and the rollback layer had drifted from it.
/* --success: referenced 4×, defined 0× — silent failure */
border-left: 3px solid var(--success);
border-left: 3px solid var(--steady);
The detection mattered more than the fix: a mechanical audit cross-referencing every var() against every definition. That script also recomputes WCAG contrast from the real token values — which is how it later caught two genuinely failing contrast pairs before ship.