ColophonUpdated July 2026

Nine pages, one stylesheet.

This site has no framework, no build step, no backend and no dependencies. It's hand-written HTML and CSS on free hosting, put together with a lot of help from AI tools. If you're a filmmaker who also tinkers, this is the part you might find useful.

Pages
09
Stylesheet
22kb
Design tokens
67
Dependencies
00
Cookies
00

One stylesheet, everything

lab.css is 516 lines and governs all nine pages. It replaced eleven divergent colour blocks and nine separate navigation markups — every page had drifted into its own private version of the design. Change a value in the one token block now and it changes everywhere, which is the entire reason the rebuild was worth doing.

Sixty-seven design tokens cover colour, type, spacing and motion. Three themes — dark, light and sepia — are per-theme overrides of the same tokens, applied by an inline script in the <head> before first paint so there's no flash of the wrong colours.

What needs JavaScript, and what doesn't

The films are plain HTML. All eight, every production timeline and all 81 behind-the-scenes photographs are in the page itself, inside native <details> elements. Turn JavaScript off and the portfolio still reads completely — 1,248 words of it. Same for SkyBound.

The resources, the gear list and the story generator do not. Those three are built at runtime from data files, so with scripts disabled they fall back to a heading and a short explanation instead of 128 links, 66 items and 1,334 prompts.

That's a real trade, made deliberately. Keeping 128 resources as one data file rather than hand-written HTML is the only reason a one-person site can carry that much and stay correct — the counts, the search, the kit and the feeds all read from the same source. The cost is that those pages need scripts to draw themselves. An earlier draft of this very page claimed nothing on the site needed JavaScript. That was wrong, and it took disabling JavaScript to notice — which is the whole point of the section below.

Videos are click-to-load. A poster image stands in until you press play, and only then does a youtube-nocookie.com iframe get injected. Nothing contacts YouTube before you ask it to.

Every colour is solved, not chosen

Each text colour was calculated to WCAG AA contrast — 4.5:1 minimum — against all three theme backgrounds, not eyeballed. Where a value failed it was moved numerically until it passed. The tertiary grey landed on #857F76 because that is the first value that clears the threshold on both the page and the raised surfaces.

Photographs are grayscale until you hover them. On a touchscreen there is no hover, so under @media (hover:none) they simply arrive in colour instead — otherwise 106 photographs would have been permanently grey for every phone visitor.

Things that were quietly broken

Most of the work on this site was not building features. It was finding things that looked finished and weren't. These are the ones worth repeating, because they are all the same shape: something that appears to work, and never did.

01

The tone selector never worked. Not once.

The story generator passed the chosen tone as an array where the matching function expected a string. An array compared with .includes() never matches, so every roll silently fell through to the neutral fallback. Picking Horror returned zero horror concepts out of two thousand test rolls. One character fixed it.

02

A green nobody could see

The “free” tag used a single hardcoded green across all three themes. On the light theme it measured 1.89:1 — effectively invisible. It had been live that way the whole time. Now it's a per-theme token.

03

Badges that were never rendered

Location badges were added to the data months earlier and looked correct there. Nothing in the page template ever read that field, so they had never once appeared on screen.

04

An ignore file that ignored nothing

Source folders were excluded by name in capitals, because that is how the network drive displays them. The folders on disk are lowercase and git is case-sensitive, so not one rule matched — 868 MB of camera originals sat completely unprotected, including a single clip larger than the limit a push will accept.

05

A gear list nobody links to

One folder held 56 files where the gallery used 26. The remainder were camera originals in a format no browser but Safari renders — 69.7 MB, 63% of the site's images, loading nowhere.

What it doesn't do

There is one exception worth stating plainly: a visitor counter, GoatCounter. It is open source, sets no cookies, stores no personal data and doesn't identify you across sites — it counts that a page was opened. That's the whole thing. It's here because after building all this it seemed worth knowing whether anyone actually reads it, and it was the only option that didn't require putting a banner in front of you first. The numbers are at labmedia.goatcounter.com — whether that stays public is a decision for another day.

If you want to build one

The honest version: it is plain HTML and CSS, and the hard parts were not the code. They were deciding what to cut, checking whether things actually worked rather than assuming, and being willing to delete work. Two whole pages and four resource entries were removed during this rebuild because they weren't earning their place.

Hosting is free. The domain is the only cost. If you want to know how any specific part of it works, just ask — that offer is real.