Back to all articles
Build Log

Rebuilding This Site: Two SVG Cats, a Physics Toy Mouse, and a One-File Design System Swap

A technical build log of this site's redesign — swapping an entire dark theme for a warm editorial one without touching most components, and building interactive cats that respect accessibility, brand rules, and your click targets.

Conor Gotzens
4 min read
Educational content only. Examples and performance figures are illustrative and not guarantees. Results depend on your business context, implementation quality, and market conditions.

If you clicked anywhere on an empty part of this page, you've met Harmony and Tipper — the two cats living at the bottom of the viewport, modeled after my actual cats. This post is the build log: how the whole site went from a dark AI-consulting template to this, and how the cats work under the hood.

The one-file theme swap

The old site was a dark glassmorphism design — slate backgrounds, indigo gradients, glowing cards. The kind of theme that looks like every AI startup's landing page, which was exactly the problem.

The rebuild trick that saved days: keep every utility class name, change what they mean. The components were full of classes like glass-card, hero-gradient, elevation-high, and design tokens like text-text-secondary. Instead of touching dozens of components, I redefined the tokens and utility classes in one CSS file — cream paper values where slate used to be, warm ink instead of white, terracotta where indigo was, soft warm shadows instead of neon glows. glass-card now renders a warm paper card; no component knows the difference.

Typography did the rest of the personality work: Fraunces (a serif with genuine character, loaded via next/font so there's no layout shift or external request), Figtree for body text, IBM Plex Mono for the little uppercase kickers, and Caveat for the hand-written annotations scattered around — including the one that told you about the cats.

The cats

The cats are a single React component with a strict rule: they can never cost the visitor anything. Not a click, not a frame, not a screen-reader announcement.

  • The whole layer is pointer-events: none and aria-hidden. The cats react to clicks they observe on the window — they can't intercept anything.
  • Clicks on interactive elements (links, buttons, form fields) are ignored via a closest() check, so playing with the cats never fights the UI.
  • One requestAnimationFrame loop drives both cats and the toy physics. Positions live in refs, not React state — zero re-renders per frame. The loop pauses when the tab is hidden.
  • Animation states (sit, walk, run, pounce) are just a data-state attribute; CSS keyframes do the leg cycles, tail sway, and blinking. The JS only decides what the cat is doing, never how it looks doing it.
  • Under prefers-reduced-motion, the cats don't render at all.

Each cat is hand-built SVG — Harmony has the ear tufts, chest ruff, and plume tail of her real-life counterpart; Tipper has the white bib and the dark tail tip she's named for. Harmony ambles a little slower. Tipper usually gets to the toy first. This is also true in real life.

Why a toy mouse and not a laser pointer

The first version used a laser dot. It lasted about an hour, because my own flagship project vetoed it: MeowTested's core brand principle is that play must let a cat's hunting drive resolve — a cat can never catch a laser dot, which is why the brand refuses to sell laser toys at all. Having the MeowTested mascots chase a laser on my own site would contradict the brand on its loudest stance.

So: a toy mouse. Click, and it tosses from your cursor with real gravity — constant downward acceleration, a damped bounce or two on the floor, then it settles into a nervous wiggle while the nearest cat sprints over. If the cat beats the mouse to the landing spot, it waits crouched underneath. Then the pounce, and a little heart. The hunt resolves, every time. The physics is about fifteen lines: velocity integration in the same animation frame loop, a bounce that multiplies velocity by −0.38 until it's too slow to bounce.

The debugging appendix

Two production issues worth recording, both invisible on localhost:

The stalled screenshots. Portfolio images shipped as 1.4 MB PNGs with no sizes hint, so browsers requested the largest srcset variant — nearly 2 MB per image through the optimizer. On the deployed site those requests stalled and rendered as grey boxes. Fix: 1280px WebP (95% smaller) plus explicit sizes. Localhost hides this class of bug completely; test image weight on the real network.

The mystery grey box. A broken grey rectangle at the bottom of deploy previews turned out to be Netlify's own collaborative-review iframe being blocked by my Content-Security-Policy. The fix conditions the CSP on the build context — previews allow Netlify's drawer, production stays strict. Lesson: when something unexplainable renders on your page, enumerate the iframes before you blame your own code.

The site you're reading is the case study. View source, click around, toss the mouse — everything above is verifiable right here.