Design system · v0.1

The system rendering this page

One accent, one typeface, fourteen components. Every token, scale, and rule documented below is the one styling what you are reading — the styleguide and its own acceptance test in a single file.

001 · Color

Color

One brand ramp, five inks, four surfaces, two lines. No second accent, no gradient. Contrast is a token property, not a QA step — every approved pair ships with its measured ratio, and a pair that isn’t in the table isn’t approved.

Brand ramp

5 tokens
brand-50
#F0F7FB

Tinted section background

brand-100
#CFE6F2

Borders and dividers on tint

Aa
brand-500
#2195C7

Large text ≥24px, icons, rules. Never body-size white text on it.

On white3.39:1 large
Aa
brand-600 · interactive
#197CA6

Buttons, links, focus ring

White on it4.69:1
Aa
brand-700
#146387

Hover / active; links on tint and alt

White on it6.64:1

Ink

5 tokens
Aa
ink-900
#32303D

Headings, primary body

On white12.92:1
Aa
ink-700
#4E4B5C

Secondary body, long prose

On white8.45:1
Aa
ink-500
#6B6879

Captions, supporting copy

On white5.41:1
Aa
ink-400
#757282

Meta, eyebrows — smallest approved gray, white surface only

On white4.68:1
ink-inverse
#FFFFFF

Text on ink-900, brand-600, brand-700

Surfaces & lines

6 tokens
surface
#FFFFFF

Default page

surface-alt
#F6F7F9

Alternating sections

surface-tint
#F0F7FB

Hero, callouts

surface-deep
#32303D

Footer only

line
#E3E5EA

Card borders, dividers

line-soft
#F0F1F4

Internal rules

Measured pairs

WCAG 2.2 · 4.5:1 body · 3:1 large
Contrast ratio of each text token on each light surface
Text tokensurfacesurface-altsurface-tint
ink-90012.9212.0511.93
ink-7008.457.887.81
ink-5005.415.055.00
ink-4004.684.374.33
brand-7006.646.196.13
brand-6004.694.384.34
brand-5003.393.173.14

✓ passes AA at body size · ◐ AA large text only · ✕ fails body size. ink-400 and brand-600 text are approved on surface only; on alt and tint sections, meta steps up to ink-500 and links to brand-700.

002 · Type

Type

Roboto only. One family, differentiated by weight, size, and tracking — no mono, no display face. Self-hosted, latin subset, 400 and 900 preloaded because they’re the only two faces above the fold.

text-display · h1
  • 900
  • clamp(2.5–4rem)
  • −0.03em
  • 1.05
text-h2
  • 900
  • clamp(1.875–2.75rem)
  • −0.02em
  • 1.15
text-h3
  • 700
  • clamp(1.25–1.5rem)
  • −0.015em
  • 1.25

Hero and section ledes sit between heading and body, carrying the one sentence a skimmer actually reads.

text-lede
  • 400
  • clamp(1.125–1.375rem)
  • 1.5

Body copy is set at 17px with a 1.65 line height and capped at 68 characters. The previous case-study body at 14.4px is retired — prose is for reading, and reading needs room.

text-body
  • 400
  • 1.0625rem · 17px
  • 1.65
  • 68ch max

Captions, figure notes, and footer copy.

text-sm
  • 400
  • 0.9375rem · 15px

004 · Eyebrow label

text-meta · floor
  • 500
  • 0.8125rem · 13px
  • 0.14em
  • uppercase
  • tabular

Wordmark role line, light weight

role line
  • 300
  • 15px
300 Light
Wordmark role line
400 Regular
Body, ledes
500 Medium
Buttons, nav, emphasis
700 Bold
Sub-heads, card titles
900 Black
Display, headings

Roboto has no 600. The previous site asked for it on nav links, so the browser synthesized a faux-bold and the type smeared. Restricting to real weights fixed it. Headings use text-wrap: balance, body uses text-wrap: pretty, and indices lock to tabular-nums so 001, 002, 003 stay aligned in a column without a monospace face.

003 · Space & layout

Space & layout

Tailwind’s default 0.25rem scale, unchanged. The only additions are the rhythms that should breathe: section padding and gutter are both clamp(), so the page scales between breakpoints instead of jumping at them.

Layout tokens
TokenValueApplied to
--container-max1200pxPage container
--container-measure68chProse column
--spacing-gutterclamp(1.25rem, 4vw, 2.5rem)Horizontal padding
--spacing-sectionclamp(4rem, 10vw, 8rem)Vertical rhythm · py-section
--radius-sm / radius / radius-lg6 / 12 / 20pxInputs, tags / cards, buttons / media frames
Breakpointssm 40 · md 48 · lg 64 · xl 80remTailwind v4 defaults. Mobile-first. No custom breakpoints.

Spacing scale

×0.25rem

Radius

3 tokens
radius-sm · 6px
radius · 12px
radius-lg · 20px

Elevation

3 tokens
shadow-1 · resting cards
shadow-2 · hover
shadow-3 · media frames
004 · Components

Fourteen components

Small inventory on purpose. Everything else is composition. Flowbite supplies markup and accessibility wiring for five of them; every visual default is stripped and replaced with tokens. If a component still looks like Flowbite, it isn’t done.

01

Button

Flowbite base: Buttons

Primary

Get in touchDefault
Get in touchHover
Get in touchFocus-visible
Get in touchActive · 1px down
Get in touchDisabled · 45%

Secondary

View résuméDefault
View résuméHover
View résuméFocus-visible
View résuméActive
View résuméDisabled

Ghost

Read case studyDefault
Read case studyHover
Read case studyFocus-visible
Read case studyActive
Read case studyDisabled

44px minimum target · px-6 py-3 · 12px radius · 500 at 15px. Disabled uses aria-disabled="true" plus opacity — never color alone.

02

Link

Custom

Inline

Case studyDefault · 1px
Case studyHover · 2px
Case studyFocus-visible

Nav

WorkDefault
WorkHover
WorkFocus-visible
SystemCurrent · underline

Footer

Current nav state is carried by a 2px underline plus aria-current="page", not color. Prose links stay underlined at rest.

03

Header / Nav

Flowbite base: Navbar + Collapse JS

Desktop · scrolled

Mobile · closed

Live at the top of this page. Sticky, 92% white with backdrop blur, bottom border appears once scrolled. Below 48rem it collapses — Escape closes the panel and focus is trapped while open, neither of which Flowbite Collapse provides.

04

Footer

Flowbite base: Footer

Live at the bottom of this page. surface-deep with inverse ink; three columns at md, stacked below. Every social link is icon + sr-only label — the previous site had fourteen icon-only links with no accessible name.

05

Project card

Flowbite base: Card
Default
Hover · shadow-2, media 1.02, arrow +4px

Featured · focus-within

Whole card is one link. Media 16:10 → index → title → org · product → role · year. No blurb paragraph; index and meta carry it. Media shown is a placeholder frame.

06

Stat tile

Custom
18+Years across web, UX, graphic, and multimedia
5Global brands on one commerce platform
00%Outcome metric — pulled from content/home.md

Number at h3 size, weight 900, tabular. Three per row at md. At least one tile must be a real outcome, not tenure — the third slot is a placeholder until content supplies one.

07

Eyebrow

Custom
Selected workPlain
002 · ProcessWith leading index

13px · 500 · uppercase · 0.14em · ink-400 on white, ink-500 on alt and tint.

08

Tag

Flowbite base: Badge
  • UX
  • Front-end
  • Email
  • Design systems

bg-brand-50 text-brand-700 · 6.13:1 · 13px. Non-interactive unless it filters.

09

Meta row

Custom
  • Spot Pet Insurance
  • Marketing site + lifecycle email
  • UI/UX, Front-end, Email design
  • 2021

Canonical order: Org · Product · Role · Year. Separators are generated, not typed.

10

Figure

Custom

Grid-2

Before — caption at 15px in ink-500.
After — stacks below md.

Half

Half width at md and up.

20px radius · 1px line · shadow-3. Every image ships width, height, srcset at 800/1200/1600, lazy below the fold, and real alt text.

11

Callout

Flowbite base: Alert

Tint background, 3px brand-600 left rule, right corners rounded only, p-6.

12

Prose block

@tailwindcss/typography · prose-dilan

Decisions is the section that separates a case study from a gallery. It names the tradeoff, the option not taken, and why — with links underlined at rest and a measure that never exceeds 68 characters.

  • Headings map to the h2 and h3 tokens
  • Link color maps to brand-600, hover to brand-700
  • The plugin’s defaults never ship unmodified

Case-study body order: Problem · Role · Process · Decisions · Outcome · Reflections.

13

Section

Custom
defaultsurface
altsurface-alt
tintsurface-tint

py-section. Sections alternate — two adjacent sections never share a background. This page runs tint → default → alt → default → alt → default → alt → default → tint.

14

FAQ list

Custom
What is a design system?
A shared set of tokens, components and rules that keeps every page of a product consistent without rebuilding it each time.
Why a question and answer list?
It answers the thing a reader actually typed, and it gives answer engines a clean pair to quote.
Is a card a link?
No. These carry no hover or focus state, because nothing here is clickable.

dldiv.faq__itemdt + dd. Cards are not links, so they take no hover, focus or lift state — that vocabulary belongs to the Project card. Fill surface-card on an alt section, radius 12px, no border and no shadow. The question is text-body at weight 700, one step below card__title, so the grid stays quieter than the work cards above it. One column, two from 48rem, three from 64rem with the first card spanning two. Visible answers must match the FAQPage JSON-LD on the page that uses it.

005 · Motion

Motion

One curve, one duration band, three approved behaviors. Motion signals state or connects two views; nothing moves to decorate.

Motion tokens and approved behaviors
BehaviorSpecWhy it exists
--ease-brandcubic-bezier(.22,.61,.36,1)Single curve, whole site
Duration band150 / 240 / 420msfast / default / slow — nothing outside it
Hover & focusfast · color, shadow, 1px pressConfirms the target is interactive
Scroll reveal12px rise + fade · slow · 60ms staggerDirects reading order on first pass; fires once via IntersectionObserver
Mobile navCollapse open / closeConnects the toggle to the panel it controls

Everything is gated behind prefers-reduced-motion. With it on, the site is fully static and every element is visible. Reveals only arm when JavaScript runs, so content never depends on motion — or on scripts — to be reachable.

006 · Accessibility

Accessibility contract

WCAG 2.2 AA, every page, every build. Not a checklist run at the end — most of these are decided by tokens before a page exists.

Accessibility decisions
AreaDecision
ContrastBody ink 12.9:1. Every text/background pair appears in the measured table; unlisted pairs are unapproved.
Focus3px brand-600 ring, 2px offset, on everything focusable. Never outline: none without a replacement.
Targets44×44px minimum on every interactive element.
Structurelang="en", skip link first in tab order, header › nav › main#main › footer, one h1, no skipped levels.
Images & iconsEvery image has alt or a deliberate alt="". Every icon-only control has an accessible name.
MeaningColor never carries it alone: current nav is underlined, disabled uses aria-disabled, prose links stay underlined.
KeyboardFully operable. Mobile nav closes on Escape and traps focus only while open.
Flowbite wiringaria-expanded, aria-controls, aria-labelledby kept intact. Removing them to simplify markup is a regression.
ReflowWorks at 320px and 200% zoom without horizontal scroll.

Open issues found building this page

v0.1 → v0.2
007 · Why publish this

A system you can inspect

A design system described in slides is a claim. One that renders its own documentation is evidence. This page is the styleguide, the acceptance test for the system, and a case study at the same time — if a token is wrong, it’s wrong here first, where anyone can open the inspector on it.

The swatch grid was never the interesting part. The decisions about what to leave out are: one accent instead of a palette, one typeface instead of three, fourteen components instead of a library, and contrast measured before a pair is allowed to exist.

Those limits are what keep a system applied the same way on the tenth page as on the first. Building this page is also how the two open issues above were found — which is the argument for publishing it.