Each card links ../colors_and_type.css and renders the real tokens, real
components and real preserved asset files — no screenshots, no redrawn marks. Start with
the dark canvas if you are new to the system; it explains why everything else looks the way
it does. Full rules live in DESIGN.md.
The foundation: what the colours, type, spacing and radii are, and the real asset files that ship with them.
The action violet, the reserved cyan, all 24 ramp steps and the error ramp, each labelled with its job.
colors-primary.html Color 2/3Four surface steps one hair apart, the foreground ramp with contrast ratios, and cards in situ. The system’s defining decision.
colors-theme-dark.html Color 3/3The three — and only three — light surfaces that exist, plus why no light theme may be derived.
colors-theme-light.html TypographyReal cuts from fonts/, the nine-step scale at true size, and the eyebrow
signature with a counter-example.
Container gutters, the asymmetric 200/120 section rhythm, bottom-heavy card padding and the two control heights.
spacing-tokens.html Spacing 2/3Eight steps at true size, the measured homepage census, and what the same card looks like at 4px.
spacing-radius.html Spacing 3/3Violet light thrown upward, side by side with the conventional drop shadow that destroys it. Plus the CTA and input halos.
spacing-shadows.html AssetsEvery wordmark, glyph, favicon and app icon loaded from the real preserved files in
build/ and assets/.
The line-and-glow layer — seven stroke-only section backgrounds, the opacity ladder that makes the falloff, and the two ramp steps they were hiding.
brand-imagery.htmlThe class layer that sits on those tokens — .btn,
.field, .checkbox, .chip, .rule and
the rest, all defined directly in ../colors_and_type.css. You write these as
plain HTML: <button class="btn btn-primary">, one stylesheet, nothing to
import and no framework. Primitives, further down this page, is the same
design delivered a second way — as Vue components. Why both
exist is explained where the two halves meet.
Eight variants with rest, hover, active, focus and disabled shown together, and the one sanctioned CTA pairing.
components-buttons.html Components 2/3Floating-label field in five states, textarea, both checkbox variants, and the assembled consultation form.
components-inputs.html Components 3/3The gradient line carried forward from the previous docs site — as a masthead accent, as a determinate bar, and three ways to break it.
components-progress.htmlEverything above is CSS. Everything below is Vue. They are not two levels of one stack and neither is built on the other — they are the same design shipped twice, for two kinds of consumer, and they meet at the tokens and nowhere above them.
| CSS components | Primitives | |
|---|---|---|
| What you write | <button class="btn btn-primary"> |
<Button variant="primary" /> |
| Where it is defined | colors_and_type.css — one stylesheet |
.vue sources, compiled into the package |
| What it needs | a <link>, and nothing else |
Vue 3, plus Tailwind told to scan the package |
| What it can express | appearance and the states CSS can reach: :hover,
:focus-visible, :disabled, :checked |
all of that, plus props, slots, v-model, the
<a>-or-<button> decision, and a glow that
follows the cursor |
| Where it works | anywhere HTML does — an email, a Rails or Django template, a CMS theme, a static page | a Vue application |
They meet at the tokens, and only there.
<Button> does not render .btn. It emits Tailwind
utilities — bg-primary-500, min-h-12,
rounded-full — which resolve through theme.css to the same
custom properties .btn reads. Change a token and both move. Change
.btn and only the CSS layer moves.
That independence is the point rather than an oversight. A Vue
component that rendered .btn would drag the whole stylesheet into every
consumer’s bundle, and a class layer that needed Vue would be useless in an
email — which is exactly where two of the six artifacts live.
The cost is drift, and it is worth knowing before you
edit either one. Because nothing connects them above the token layer, a fix
applied to one does not reach the other, and nothing fails when they disagree.
GlowButton is that showing: it renders a 44px control
against the 48px --control-height every button in the CSS layer uses.
If you change a control’s shape, check the other delivery.
Which to use. In a Vue application, the primitive —
it carries behaviour the class layer cannot. Anywhere else, the class. Do not put both
on one element: the utilities and .btn are all single-class selectors, so
which wins comes down to stylesheet order, and neither layer promises one.
Both surfaces prove parts. ../examples/ proves they
compose — a pitch deck, a contact form, two emails, a landing page and a print poster,
each a single self-contained file carrying real density.
Live components
Twelve of the thirteen stories mount the real components —
the verbatim .vue sources compiled by
tools/build-storybook.mjs and rendered by the vendored Vue and
GSAP runtimes against the site's own Tailwind theme
(docs/tailwind.css, compiled per page by Vite). The thumbnails below are those same mounts,
miniaturized. The one .astro component is a hand-translated
HTML port, and every gap between this storybook and production is written
down on its page.
The reusable controls, as Vue. Everything else on the site is built from these — and each has a counterpart in CSS components above, which is the same design without the framework. Two deliveries of one design explains why both ship.
Cards and blocks. Each takes the fields it reads as a prop, so the content can come from any CMS — or from nothing at all.
Not primitives — assemblies worth documenting because they define styling that lives nowhere else.
54 findings across the thirteen components — 28 flagged as defects, 26 as design observations — each recorded on its own story page. The four that would change behavior in production:
Two components animate against a variable they never define.
Checkbox.vue:75 and Radio.vue:61 both declare
transition: var(--default-transition-duration) transform ease-in-out;,
and --default-transition-duration appears nowhere in the
project's own code. It resolves anyway — to 150ms from Tailwind's default
theme, which the build emits because transition-colors is in
use. Lift either component out of a Tailwind build and the shorthand
collapses, snapping the indicator instead of easing it.
Button's isDisabled does not disable anything.
It sets opacity and a cursor. The disabled attribute is never
bound, so the control stays clickable and keyboard-reachable — and as a link
it still navigates.
TextField syncs its model on change, not input.
The parent's v-model only updates on blur, while
InputText in the same form updates per keystroke.
Two dead styles.
Radio's secondary hover needs a group ancestor that
nothing renders, and Button's type prop is declared
but never bound.