This site is deliberately small. A handful of static pages, one stylesheet, nothing shipped to the browser to run. Here is how it fits together, and the design system it runs on, shown with real values.
The stack
Static HTML and CSS, and nothing client-side to run. I write the pages in Eleventy, a static site generator, and it produces plain HTML at build time. The build runs on my machine and I commit the output, so the deploy step only copies files. No build server, no surprises in CI.
A few choices keep it quick:
- One stylesheet, inlined into the head of every page. The browser never blocks on a separate CSS request.
- Fonts self-hosted as woff2: Poppins, two subsets, three weights.
- The hero image gets a high fetch priority, so the biggest thing on the screen starts loading first.
That is most of the performance story. The pages hit 100 on mobile Lighthouse, and they get there by not loading things they don't need, not by anything clever.
Why tokens
Colour and corner radius spread fast, even on a site this small. Every new element is a chance to invent one more value that's almost like the last. So they all live in one place. Every colour, every radius, every shadow is a CSS variable, defined once at the top of the stylesheet, and nothing else in the file hardcodes a value.
Change the accent in one place and the whole site follows. Building a new component becomes a pick from a fixed menu. What comes next is that menu, pulled straight from the live variables.
Colours
The accent is a cyan pair. Cyan-700 does the work that needs contrast, links, buttons, the highlighted words in headings. Cyan-600 is a step lighter and only shows up in decoration, the slashes in the logo, the shape behind the photo, the focus ring.
- --accent#0891b2Decorative: logo, blob, focus ring
- --accent-dark#0e7490Links, buttons, eyebrows
The neutrals are a slate ramp, cool greys that sit next to the cyan without fighting it. Ink for headings and the dark bands, a slightly lighter slate for body copy, and a mid grey for secondary text. All of them clear AA contrast on white.
- --ink#0f172aHeadings, dark bands
- --ink-2#1e293bBody copy
- --muted#64748bSecondary text
Surfaces stay quiet. White for the page and the cards, a barely-there slate tint for sections and chips, a soft cyan only behind the photo, and one line colour for every border.
- --white#ffffffPage and cards
- --surface#f8fafcTinted sections, chips
- --soft#ecfeffHero photo tint
- --line#e2e8f0Borders
The dark sections (the stats band, the final call to action, the footer) get their own small set, so text on dark stays readable and borders stay subtle.
- --ink-deep#020617Footer background
- --text-on-dark#cbd5e1Body text on dark
- --text-on-dark-muted#94a3b8Muted text on dark
- --line-on-darkwhite 18%Borders on dark
- --overlay-on-darkwhite 8%Hover fill on dark
Radius
Two working sizes and a pill. Small for buttons and chips, large for cards, the pill for tags. One-off signature shapes get their own tokens, marked do not reuse, so they stay out of the working scale.
- --radius-smButtons, chips, nav, faq
- --radius-lgCards, large surfaces
- --radius-pillTags, fully rounded
Shadows
Two elevations and the button glow. A soft one for cards at rest, a deeper one for hover and anything that floats. Tinting shadows with the ink colour, rather than pure black, keeps them feeling part of the page.
- --shadow-smResting cards
- --shadowHover, floating
- --shadow-btnPrimary button glow
Type
One typeface, Poppins, self-hosted. Headings at 700, body at 400, the occasional label at 600. Sizes scale with the viewport using CSS clamp, so the big headline shrinks on a phone without a stack of breakpoints behind it.
The card
The card is the workhorse block, one component reused wherever a piece of content needs a frame. White surface, a one pixel line, the large radius, the resting shadow, and 1.8rem of padding inside. On hover it lifts a little and the shadow deepens. Try it.
Component
This is a card
Built only from tokens: --white surface, a --line border, --radius-lg corners, --shadow-sm at rest, deepening to --shadow on hover.
Nothing in it is bespoke. Because the card is made from tokens, it matches everything else for free, and a new one never needs a fresh colour or a new kind of corner. That's the payoff of keeping the menu small.
Spacing and the box model
A strict spacing scale earns its keep on a bigger product. On a small site a handful of consistent values do most of the work, and reaching for one of them beats inventing a new number each time.
- 24px Page gutter, the side breathing room on every section, set on
.container. - clamp(3.25rem, 7vw, 6rem) Space above and below each section, smaller on a phone.
- 1.8rem Padding inside a card.
- ~1.4rem Gap between cards and grid items.
Two habits keep this predictable. Everything uses border-box sizing, set once in the reset, so padding and border count inside an element's width. A full width box with padding stays full width, it just gains room inside.
The reset also zeroes every default margin, then space goes back on in one direction. Headings and paragraphs push room below themselves, not above. So when two elements meet, their margins don't double up, the gap is whatever the lower element asked for.
Breakpoints
The layout is mostly fluid. Type and spacing scale with the viewport using CSS clamp, so there are only two hard breakpoints where the structure rearranges.
- ≤ 860px The hero drops to one column with the photo on top, and the nav collapses into a burger menu.
- ≤ 540px The base font eases down a touch, the stats and cards go to one column, and the hero buttons stretch to full width.
Paired buttons don't need a breakpoint to behave. Split the row into equal halves so they sit side by side while they fit, and let them wrap to full width once the row is too narrow. They stay equal at every size.
The one rule
Reuse a token, don't invent. If a value genuinely isn't there yet, add it to the top of the stylesheet first, then reference it. One small set, used everywhere. That's the whole idea.
This is the same habit I bring to other people's code. Most apps don't get messy on purpose, they grow without a system until small decisions pile up. If yours is at that point, that's the kind of thing I help untangle, with my team at Fingoweb behind the fixes.