Skip to content
Snowey Labs

11 min read

How this site was built

Astro, one token file, two themes, self-hosted fonts, one manager and five workers. The commands, what the gate caught, and what it cost.

The problem

I needed a site that is a library, not a portfolio: guides, a tools list, a channel link, a way to hire us. Four rules made it harder than a template: every sentence on it has to be true, it needs a day and a night theme that never flash the wrong one, it cannot load fonts from Google (a GDPR decision taken before the first line of code), and it ships no analytics, so there is nothing to put a cookie banner in front of.

It is also the showroom. The contact page says sites like this one are what I build, so this page has to be a fair example of what a client gets.

What I used

  • Astro 7 for the pages. It renders everything to plain HTML at build time and ships no JavaScript unless a page asks for it. This site asks for one thing: the theme switch.
  • Tailwind CSS 4, configured from CSS, not from a JavaScript file. Every colour, font, radius and spacing value comes from one @theme block that reads the design tokens.
  • A DESIGN.md that is law. Colours for both themes, type roles with exact sizes, three corner radii and nothing else, spacing, and one component table. The build reads from it; when the build disagrees with it, the build is wrong.
  • Fraunces, Archivo and JetBrains Mono, self-hosted as eight woff2 files (latin and latin-ext subsets only), copied out of the @fontsource-variable packages with their Open Font License files next to them.
  • Shiki for the code blocks, at build time, both themes emitted as CSS variables. Zero JavaScript for highlighting.
  • Playwright for the screenshots and the launch checks, because playwright-cli was not installed on this machine and installing the library was the shorter path.
npm install astro@7.3.1 tailwindcss@4.3.3 @tailwindcss/vite@4.3.3 @astrojs/sitemap@3.7.4
npm install -D playwright @axe-core/playwright lighthouse
npx playwright install chromium
npm run build      # dist/ is the whole site: 10 HTML files, one CSS file, no JS files
npm run preview    # serves dist/ on http://127.0.0.1:4321

The two themes

The order is fixed: what the visitor picked beats what the device says, and the device beats the visitor’s own clock. The clock is only read when the device expresses no preference at all, and then night runs from 19:00 to 07:00 by the visitor’s browser time. No timezone lookup, no IP lookup, no request to anyone.

The day tokens sit on bare :root. Night is redefined only under a media query guarded with :root:not([data-theme="light"]), and again under :root[data-theme="dark"], so the toggle wins in both directions. The resolver runs inline in <head>, before the stylesheet link, before first paint:

(function () {
  var t = null;
  try { t = localStorage.getItem("theme"); } catch (e) {}
  if (t !== "light" && t !== "dark") {
    t = null;
    var m = window.matchMedia;
    var device = m && (m("(prefers-color-scheme: dark)").matches || m("(prefers-color-scheme: light)").matches);
    if (!device) { var h = new Date().getHours(); t = h >= 19 || h < 7 ? "dark" : "light"; }
  }
  if (t) document.documentElement.setAttribute("data-theme", t);
})();

localStorage is wrapped in try/catch because it throws in some browsers and contexts, and the page has to render correctly with nothing stored. The toggle in the header is a real <button> whose label says what it switches to.

Night is not the day palette inverted. Every border and every muted grey was looked at in both themes, at phone and desktop width, before this page went up.

The mark

The owl is an SVG inlined into the page, stroke set to currentColor and the beak to the accent token, so it recolours with the theme. A linked <img> would not. It appears in three places and nowhere else: the header, the favicon, the contact block in the footer. It blinks once, slowly, a few seconds after the page settles. That is the only animation on the site, and it is off under prefers-reduced-motion.

How the work was split

This site was the first job run through the company’s manager, worker, red team loop, which has its own guide. The manager session built the spine alone, because everything else depends on it: tokens, layout, theme script, components, the guides collection, the home, guides, videos, privacy and 404 pages. Then five workers ran at the same time, each in its own git worktree, each owning different files:

  1. the lead pipeline guide, written from the log and the script of the real run
  2. the manager loop guide, written from the directives and the agent files
  3. the tools page, with the affiliate marker built in before there is any affiliate link
  4. the work page, with the price table and the three offers (later replaced, see the end)
  5. the launch gate script, twenty checks, automated where a check can be automated

The manager gated each result by reading it against its sources, merged the four content branches, and handed the whole wave to a red team that can read and run but cannot write.

What the gate caught

Reading each worker’s file against its sources, before the red team ever saw it:

  • A guide quoted the full path of the Python interpreter, which contained the Windows username of the machine it ran on. The company’s anonymity rule says the real name appears in no public artefact. Removed.
  • The loop guide said “three models at three prices”. The agent files define two. Fixed.
  • The tools page opened with “everything here was used for something real” and then listed two rows whose verdict line says “no verdict yet”. The intro now says which is which.
  • The work page presented one bar’s 20 percent cost target as if it were a promise the tool makes. It now says “against the target the bar sets”.
  • The lead pipeline guide skipped the upload step, which is half the pipeline’s name. Added, with the read-back check that proves the round trip.

Two things broke on the manager’s side. A shell heredoc choked on an apostrophe inside a CSS comment, so the stylesheet went in through a file write instead. And the repository had a zero-byte index.lock left over from the morning, which blocked the first commit until it was confirmed stale and removed.

What the red team caught

The red team got the diff, the criteria and the screenshots, and came back with seven findings. Two of them were mine, not the workers’:

  • The work page and the tools page said the receptionist had never been on a real phone line. I had read only the top of the directive that describes it. Further down it records a real inbound call on 28 August, what went wrong on it, and the fix. Both pages now say that instead.
  • The contact form posted to an address that does not exist in a static build, so a message would have vanished with no error, and the privacy page described the form as if it worked. The form now has a small server function behind it (deployed with the site on Cloudflare Pages, sending through an email API), and the privacy sentence says “if you send the form”.

The other five: the muted grey failed the 4.5:1 contrast rule in both themes, which the gate script had also found; the price table was clipped at phone width; the lead pipeline guide described the email step in the wrong order; the loop guide claimed the site had been through the loop “end to end” while the job was still running; and the monospace font’s ligatures turned >= into one glyph inside code blocks. All fixed the same evening.

The numbers from the gate

The launch gate is one script, site_gate.py, twenty-two lines of output. On the local preview, after the fixes above: 17 pass, 0 fail, 2 manual (sending the contact form for real, and the legal footer a Romanian company site needs), 3 not applicable (shop-only checks). What the passing lines say:

  • Lighthouse, mobile, simulated throttling, home page: performance 1.00, accessibility 1.00, SEO 1.00. Measured on the local preview and again on the live host, same numbers.
  • axe with the WCAG 2.2 AA tags: 0 violations on 9 pages, in both themes. Getting there cost two token changes: the muted grey and one comment colour in the code theme.
  • No horizontal overflow at 320 px on any page.
  • JavaScript: 0.9 KB compressed on the heaviest page. Stylesheet: 4.9 KB compressed.
  • Every sitemap URL answers 200 on the live host; an unknown path answers a real 404 there. The first live run showed 403 on those two lines: Cloudflare refuses the bare Python user agent, and the gate script sent none. It sends a named one now.
  • Every image has alt text, every page has one canonical, unique title and description, Open Graph tags and a 1200 by 630 image; no tracker anywhere, so no banner.

What changed the same night

After the gate passed, Dan looked at it and turned it into a personal site: no company exists yet, so a price table and a hire-us page would have been selling something that cannot be invoiced. The work page became a contact page with an email address, the contact form and its server function went, and the site speaks in the first person everywhere. The three offers survive as one sentence on the contact page, without prices. The legal footer a Romanian company site needs is no longer needed, because there is no company on the site.

Two more things from the same look. Dan did not like the night palette, so four variants were rendered from the home page and he picked a lighter slate grey over the near-black charcoal; the tokens changed in DESIGN.md first, with the reason written next to them. And the site looked empty to him, which it was: three guides and no pictures. Four line illustrations were generated on Higgsfield with Recraft, in the site’s three colours, for 10 credits in total. They came back as SVG, so they are inlined into the page and recolour with the theme: ink becomes currentColor, the paper becomes the surface they sit on, the one amber element stays amber. No filters, no extra requests, no character in them; the owl stays a mark.

Then he asked for one more thing: the theme switch should feel like weather. The design file bans snow as decoration, and it still does, but a one-second transition on the toggle is not decoration. So: to night, a gust sweeps the dark in from the right with a soft front and white wind streaks riding it; to day, the light pours from the top with a scalloped, liquid edge. It is the view transitions API with a mask sliding across the new view, plus one overlay of streaks that lives for a second. No library, instant for anyone with reduced motion on, and page navigations keep their plain cross-fade.

What it cost

No paid service. The fonts are Open Font License. The domain is not bought yet, on purpose: that is the last step and the owner does it in his own account.

Model usage, from the job’s ledger: the five workers used about 700,000 tokens between them on the cheaper model, three to fourteen minutes each, running in parallel; the gate worker hit its turn cap with both files written and the browser run left for the manager. The red team used about 156,000 tokens on the more expensive model. The manager session is not metered in the ledger. From the first line of the brief being read to the gate passing took about an hour of wall clock.

What is not done

  • Only the last mile of hosting. The domain is bought and the site is live on Cloudflare Pages (direct upload with Wrangler, 29 files, about two seconds); attaching the domain name and the email forwarding are two clicks in the owner’s dashboard.
  • The Telegram, YouTube and TikTok links and the contact email are placeholders in the source, listed by name in the job’s progress file, waiting on the owner.