Colophon
What this site is made of
Last updated 30 September 2026
What this site is made of, what it costs, and what we decided against. Versions come out of the lockfile rather than being typed here, so this page can't claim a version the site isn't running.
What it's built with
Ten packages are runtime dependencies, and three of those are the framework. Tailwind is in the table below because it produces every style on the site, but it's a build tool, and none of it ships.
| Package | Version | What it does here |
|---|---|---|
| next | 16.3.4 | The whole site. Server components by default, so the text pages ship no JavaScript of their own. What every page does carry is the shared chrome: the navigation, the smooth scrolling and the pointer effects. Every route is server-rendered per request rather than cached, which is the price of a per-request Content-Security-Policy nonce. |
| react | 19.2.4 | With react-dom at the same version. The contact form is a server action, so it works before any JavaScript arrives and gets better after. |
| tailwindcss | 4.3.0 | Every style. The scale lives in one @theme block. See below. CSS is inlined into the HTML, so there's no stylesheet request blocking the first paint. |
| framer-motion | 12.38.0 | Scroll reveals, the spine fills, and three small things in the hero: the two blurred surfaces drifting behind the headline, the breathing scale on scroll, and the magnetic pull on the buttons. The light falling in from the top-left corner isn't one of them, and neither is the hero entrance. Both are CSS, so neither can wait for JavaScript. Loaded as domAnimation rather than domMax, which is roughly half the size and the reason the nav underline is a native view transition instead. |
| lenis | 1.3.23 | Smooth scrolling. Switched off entirely when the visitor asks for reduced motion, which also hands scrolling back to the browser. |
| @base-ui/react | 1.4.1 | One component: the mobile menu. A dialog needs a focus trap, Escape, outside-click, and the right ARIA. The hand-built version here got four of six right, which is the dangerous number. |
| zod | 4.4.3 | Contact form validation, on the server. The browser never sees it. |
| clsx + tailwind-merge | 2.1.1 + 3.5.0 | Composing class names so a later utility beats an earlier one instead of both landing in the DOM. |
| class-variance-authority | 0.7.1 | One component. It stays because it's honest about being a variant table; the buttons it used to be proposed for are plain strings in lib/button.ts instead. |
There's no analytics package, no tag manager, no font service, no cookie banner and nothing to consent to. No privacy posture was bolted on afterwards to arrange that. The page makes no third-party requests at all, and a test fails the build if one appears.
What runs it
| Service | For | What happens if it's down |
|---|---|---|
| Vercel | Hosting and the server functions behind every page. | The site is down. There's no static fallback, because every response carries a fresh CSP nonce. |
| Resend | Delivering the contact form to a mailbox. | The form tells you it failed and gives you the email address directly, rather than pretending it sent. |
| Managed Redis, over HTTP | Counting contact form submissions per IP address, five an hour, so the form can't be used as a mail cannon. | The limiter falls back to a per-instance counter, says so in the logs, and the message still goes through. Losing a real enquiry is worse than letting an extra one past. |
Neither the mail service nor the rate limiter has a client library here. Both speak plain HTTP, so both are a fetch call. Adding two packages to make nine lines of code look tidier would be an odd thing to do on a site that argues about payload.
The object on the contact page
There is one moving thing on this site with any weight to it: a solid of tinted glass turning inside two silver rings, on the contact page. It is worth a section because the page you are reading says a WebGL hero object was taken out for being too expensive, and it would be a strange colophon that left out the thing built afterwards.
It is drawn with the canvas API the browser already has. Nothing was added to the table above for it, and no 3D library ships to anyone. Everything the contact page sends that the about page does not, which is this object plus the form and its validation, is 12.4 KB compressed. The hero object it learned from was 236 KB.
Nothing about it is a picture. The glass is worked out from the shape every frame: a ray from your eye is bent where it enters a face, followed through the solid, bent again or reflected back where it meets the next one, and whatever it finally points at is what that face shows. The colour is distance rather than tint, so thin edges are almost clear and a long path through the middle is deep amber. The rings are shaded by what a polished tube would reflect of one simple room.
It watches its own drawing and simplifies when it has to. Each ring is 104 pieces on a machine that can afford them and 40 on one that cannot, chosen from how long the last frames actually took, with a wait between changes so it cannot flutter. Measured: it sits at the finest level throughout on this machine, and steps down to the coarsest with the processor slowed four times, which is the only honest way to have both.
It is decoration and it is marked as such. No text, no focus stop, nothing announced. It is not rendered at all below the small breakpoint, where it would add a screenful of ornament to the shortest path on the site. Reduced motion or a touch screen gets one still lit frame and no loop at all, and the loop stops whenever it scrolls out of view or the tab goes to the background.
The three conditions this had to meet were written down when the old one was removed, and they are the reason this one is on the contact page rather than the hero. It cannot delay the headline: every one of the sixty paint runs on the deployed site recorded a single candidate, and the slowest on this page was 1.7 seconds. It cannot go to a phone without a gate: it has two. And it cannot need a caption explaining what it is, so it does not have one.
What came out
This is the more useful half of a colophon. Everything below was built, worked, and was removed anyway.
| Removed | Why | What it cost |
|---|---|---|
| A WebGL hero object | 236 KB compressed, no device gate, and the headline couldn't paint until it rendered its first frame. Largest Contentful Paint measured 9.8 seconds on a throttled phone against a 2.5 second threshold. | The hero has no moving object in it. The code is kept, with notes, rather than deleted, and what was learned from it is on the contact page instead, at a twentieth of the weight. |
| A loading screen | It held a 2,500 millisecond minimum over a page the server had already delivered, and gated the headline behind five separate conditions. | There's no branded entrance on a first visit. The page just arrives. |
| A rotating word in the headline | Every longer word became a new largest paint, and the measurement only stops when you touch the page. It also meant the sentence describing the work changed while you read it. | The headline says one thing. |
| tw-animate-css | Imported in the stylesheet, used nowhere. The one animate- class on the site is Tailwind's own. | 1,797 bytes of CSS, 247 of them compressed. |
| A component library button | It was dead code while the exact class list it would have replaced was hand-copied seventeen times. | Nothing. The classes moved to lib/button.ts, which is where they should have been. |
| Five described client projects | They weren't real. | The work page now has this site on it and the numbers it can prove, which is a smaller claim and a checkable one. |
Decisions that went the other way
GSAP was asked for and not used. The site already carries two things that move elements; a third animation runtime on a page whose measured problem was JavaScript-gated rendering would have made the number worse. If something specific needs ScrollTrigger later, that's the moment to revisit it.
Base UI was added on purpose, and it's the one dependency added rather than removed during this rebuild. Hand-building a modal is a rule worth keeping: the version it replaced moved focus correctly on open, locked scroll correctly, and set aria-expanded correctly, and then let five Tab presses walk a keyboard user into the page behind the overlay, with Escape doing nothing.
Structured data for a FAQ was considered and declined. The process and services pages read like questions and answers but aren't; marking up prose as a Q&A because it would be eligible for a rich result is the kind of thing that's technically allowed and quietly dishonest.
Every page is server-rendered on demand rather than cached at the edge. A per-request nonce and a cached file are mutually exclusive, and on a site this size a strict Content-Security-Policy is worth more than a cache hit.
That decision costs more than the cache, and it's worth naming the rest. Because every response says no-store, the browser can't keep these pages in its back/forward cache, so pressing Back re-fetches and re-renders rather than restoring the page instantly. It's the single clearest thing the security posture takes away from the reader, and it was worth knowing before choosing rather than after.
The WebGL hero was parked, not deleted. It sits in docs/parked with its dependency line, the two API changes that have landed since, and the three conditions any future home had to meet: it can't block the headline from painting, it can't ship to a phone without a device gate, and it can't need a caption explaining what it does. Those conditions are why the object on the contact page is drawn the way it is, and they were checked against it rather than assumed.
Where the decisions live
If you're reading this to work out how the site is put together, these are the files that matter.
| File | Holds |
|---|---|
| app/globals.css | Colour, type scale, tracking, radius and easing as theme tokens; the hero keyframes; one focus rule for the whole site. |
| docs/tokens.md | Generated from the stylesheet above, including a count of the values that are still not on the scale. |
| lib/button.ts | Four button sizes, with a 44 pixel floor on the tap target. |
| lib/field.ts | Every form field, label and error. |
| lib/metadata.ts | Titles, descriptions and share images, per page. |
| lib/site-metrics.ts | Every number this site publishes about itself, each one next to the test that would fail if it stopped being true. |
How to check any of this
The claims on this site are guarded by tests rather than by good intentions, because the tests are the only part that keeps working after everyone has stopped paying attention.
| Suite | Proves |
|---|---|
| no-third-party.spec.ts | No request leaves this origin. Injecting one off-site script fails it by name. |
| a11y.spec.ts | axe-core against WCAG 2.0, 2.1 and 2.2 at A and AA, on every route, at three widths. |
| no-javascript.spec.ts | Every page renders, and the contact form submits, with JavaScript switched off. |
| largest-paint.spec.ts | The headline is the first thing eligible to be the largest paint on every page that has one. |
| zoom-and-contrast.spec.ts | No sideways scrolling at 400 percent zoom, and the primary button is still distinguishable in Windows High Contrast. |
| motion.spec.ts | Reduced motion is actually honoured, and the one animation between routes actually runs. |
| published-claims.spec.ts | Every number rendered on the work page is one this repository can back up. |
Each suite also runs against the deployed site rather than a local copy, by pointing one environment variable at it. That matters more than it sounds: before this rebuild, every claim about the live site rested on a separate script that nobody ran.
What this page doesn't tell you
It's about the site, not the desk. There's no editor, keyboard or monitor list here, because that isn't what anyone is trying to find out when they land on a studio's colophon.
The numbers this site publishes, and the conditions each one was measured under, are on the work page. What has been tested for accessibility, what passed, and what hasn't been looked at yet are on the accessibility page.
Questions about this page? Email info@gbtn.studio.