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.

Role Design + build

Stack HTML · CSS · vanilla JS

Brief 2,516 lines · 94 directives

Output 2,567 lines · ~92KB

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
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.

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.