Colophon

How this site is built

The decisions behind the interface, and what they cost.

Stack

Next.js 16 on the App Router, TypeScript, Tailwind CSS v4, Framer Motion, next-intl, Resend for the enquiry form and Supabase for its attachments. Deployed on Vercel. No UI component library — every element here is built for this site, because a component kit brings its own visual defaults and those defaults are what make sites look alike.

The design system

Seven colour tokens, six type steps, one spacing scale. That is the whole system, and it is enforced rather than documented: the palette is defined once as CSS custom properties, and nothing in the codebase introduces a colour outside it — including hover states, which change opacity rather than reaching for a tint.

The accent, an amber taken from the vocabulary of status lamps rather than the usual neon of technology sites, appears in exactly seven places across the whole site. An accent used more often than that stops being an accent.

Type is Archivo at its expanded width axis for display sizes, Inter Tight for running text, and JetBrains Mono for readouts and labels. Archivo carries no Han glyphs, so the Chinese build drops the width axis, hands the type to whatever the reader's system provides, and opens the tracking up — Latin display sizes are set tight, which crowds square glyphs. What that choice costs is under Performance.

The status rail

The strip along the bottom edge is the one element here allowed to show off. It reports which section holds the viewport, numbered against a single source of truth shared with the page itself, and the scroll position.

The reading is measured as you scroll — whichever section covers most of the viewport, rather than one crossing a trigger line, because sections are taller than the screen. The rail lives in the layout and is never rebuilt by a navigation, so it holds no elements: an observer set up once keeps pointing at the sections of a page that has gone, and stops reporting the moment you come back. On pages that have no sections — a case study, this page — it reports nothing rather than a stale reading.

Motion

Every entrance animation on the site goes through one component. Sections do not carry their own motion configuration, which is what keeps the rhythm consistent as the page grows.

It has three rules. Large visual blocks replay when re-entered while text passages play once, so re-reading never flickers. Animations play on downward scroll only — scrolling back up still reveals content, it just arrives without motion, because an animation running backwards reads as a fault. prefers-reduced-motion replaces every displacement with a plain opacity fade and stops the status lamp pulsing.

Scrolling itself is left to the browser. A smooth-scrolling library ran here for a while, but it takes over the scroll position and fights the browser's own restoration: opening a case study from halfway down the home page landed at the bottom of it, and coming back landed somewhere approximate. Removing it took both problems with it, and anchor links and back-to-top are still eased — the browser does that on its own.

Content and language

No display string is written into a component. Interface text lives in message catalogues, and long-form content — case studies, this page — lives in MDX with one file per language. English and Chinese are written separately rather than translated, so neither reads like a translation of the other.

Switching language keeps you on the page you were reading, including individual case studies, and the choice is remembered.

Accessibility

Focus is always visible, in the accent colour, at two pixels. Every form control is bound to its label, invalid fields are announced, and error messages say how to fix the problem. The accordion carries the expanded and controls relationships it should. Body text sits at 7.3:1 against the page and the brightest text at 16.3:1, which clears the 4.5:1 requirement with room to spare.

The attachment button is drawn rather than native. A file input labels its own button in the browser's language, so a reader on the English site with a Chinese browser was shown Chinese — the control is hidden and the focus ring moved onto the button that can actually be seen.

With JavaScript disabled the page is still readable: the hidden starting state of every animation is overridden, and the FAQ answers are shown open, since without scripts they could not be opened.

Performance

Measured against the deployed site rather than a local server, whose simulated throttling produces flattering numbers that do not hold up. Mobile emulation, throttled — the configuration Lighthouse uses by default, not the kinder desktop one.

BeforeNow
Performance7194
Accessibility100100
Best practices100100
SEO100100
First contentful paint1.7 s1.2 s
Largest contentful paint8.1 s2.8 s
Cumulative layout shift00
Page weight1,434 KB422 KB

Two changes account for almost all of it.

The first was the opening headline and subheading. They were being revealed by JavaScript, so the browser could not paint them until the bundle had downloaded, parsed and run — a second of nothing on a slow phone, for text that was already in the HTML. Those entrances now run on CSS. They look identical and follow the same timing tokens, but they start at first paint.

The second was the Chinese typeface. Han fonts are cut into hundreds of unicode ranges; this site used eighteen of them, which meant eighteen files and 1,149 KB, four fifths of the page. Chinese is now set in whatever the reader's system provides: PingFang on macOS and iOS, Microsoft JhengHei on Windows, Noto Sans CJK on Android. All three are screen-tuned faces that cost nothing to download. The three font files left are Latin, 172 KB together.

The price is that Chinese does not render identically everywhere. For a site meant to be read quickly on a phone, that trade is worth taking.

The device grid in the header is CSS and component state — no image, no video, nothing to download.