Making Dexter run
Every route change on this site is covered by a wipe, and riding the leading edge of that wipe is Dexter — my dog, rendered as a little line drawing, hauling the sheet of colour across the screen like he is dragging a stubborn stick. He has been the mascot of every version of this site I have ever built. The first time I animated him pulling the wipe, I reached for the obvious tool and dropped in an animated gif. It looked fine on my machine, on that load, that once.
Gifs animate on their own schedule
The problem with a gif is that you do not control its clock. It starts playing the moment it decodes, loops on its own internal timeline, and has no idea your CSS wipe exists. So Dexter’s legs and the sheet he is supposedly pulling drift out of phase — some transitions he is mid-stride, some he is frozen, some the wipe finishes before his run cycle does and he snaps off-screen mid-step. Two animations, two unrelated timelines, no way to marry them. It reads as a glitch even when nobody can say why.
What I actually wanted was frames I could drive from the same place I drive everything else: CSS. That is a sprite sheet — every frame of the run cycle laid out in one horizontal strip, and a background-position that steps across it.

One strip, seven steps
The seven frames sit in a single 2100×194 webp, 300px wide each. The element is sized to exactly one frame; the background is the whole strip; and a stepped animation walks the background one frame-width at a time:
.dexter {
width: 300px;
height: 194px;
background: url('/dexter-sprite.webp') 0 0 / 2100px 100%;
animation: dexRun 0.45s steps(7) infinite;
}
@keyframes dexRun {
to { background-position-x: -2100px; }
}steps(7) is the whole idea. Without it the browser tweens the background smoothly and you get a sad horizontal smear of overlapping frames. With it, the position snaps to each frame boundary and holds — discrete cels, exactly like hand-drawn animation. Travelling a full seven frame-widths and looping means the cycle rejoins itself seamlessly, no visible jump back to frame one.
On the site those fixed pixels are actually one custom property doing the maths — the same variable that sizes Dexter feeds the width, the 2100px strip and the -2100px travel — so the whole thing scales fluidly without touching the mechanic above. Flat numbers here just to see it plainly.
Now it obeys
Because the run is now just a CSS animation, it answers to the same controls as the wipe. There is an easter egg on this site — press "p" mid-transition and everything freezes so you can admire the artwork; that freeze is one animation-play-state toggle, and the sprite honours it for free because it is no longer an opaque gif doing its own thing. Same story for prefers-reduced-motion: the whole transition bails before Dexter ever mounts. He is not a video I am playing at you. He is a strip of drawings and a step function, and that is exactly as much control as I wanted.