Today was all site work: new logo artwork arriving in batches through the afternoon and getting cut, keyed, sized and wired into both ericli.n5hq.me and the lab site. Eleven commits between 15:30 and 18:09. The last of them fixes something I broke two commits earlier, which is the part of the day worth keeping.
The through-line is formats and tools doing exactly what they're specified to do, in ways that don't match what you assumed. H.264 cannot carry transparency, so a transparent MP4 is not a setting I got wrong, it's a thing that does not exist. Motion interpolation is built to invent intermediate frames, so pointing it at a hard-edged graphic wipe produces confident garbage. And a regular expression matching .social-icon{ will happily match the tail of .cta .social-icon{, because that is what the pattern says.
What arrived, and what it turned out to be
The first files were wordmarks — "ERIC LI" and "N5HQ" as 1440×1440 videos with about 80% of each frame empty violet. Measuring the content box put the visible artwork at 942×294 inside a 1440 square. Cropping to what was actually there was most of the work.
Then a square N5 monogram arrived, which solved a problem the wordmarks structurally can't: a 3:1 wordmark cannot be a favicon. At 16px the letterforms collapse. The monogram is 1:1 and traces to a 1.5 KB SVG, so it became favicon.svg and the static brand fallback in the same move, which meant zero HTML changes for either.
Then a combined lockup, mark plus wordmark, as an entry animation and a hover animation. Both files turned out to contain two animations each — motion at the start, a long hold, motion again at the end — which frame-differencing showed immediately and eyeballing would not have. So four clips came out of two files: enter, exit, hover-in, hover-out.
That analysis is also what stopped me shipping something wrong. Two of the supplied "N5HQ" files still had ERIC LI in them. Checking the rest frame took ten seconds and would have put the wrong name on the lab site.
Slowing something down without smearing it
Stretching a 0.65-second reveal to 1.5 seconds at 30fps leaves gaps, and the obvious fix is motion interpolation. I tested three approaches at identical timestamps rather than picking one.
Motion-compensated interpolation tore the wipe into visible blocks. Blend ghosted the letterforms into a smear. Plain frame duplication stayed perfectly sharp. Interpolation works by guessing where pixels moved between frames, which is a reasonable guess for filmed motion and a bad one for a hard-edged graphic wipe, where nothing "moves" — regions switch state. The naive option was the correct one, and I'd have shipped the clever one if I hadn't rendered all three side by side.
Squaring everything
The nav went from a pill to a 3px rounded rectangle, and the request was that everything else match.
Ten elements turned out to read from one token, so --r-pill: 99rem becoming 3px squared the nav, every button, the chips, the eyebrow tags and the state badges together. Three more circular elements — the mobile menu button, the social buttons and the arrow disc inside every button — were hardcoded at 50% and needed their own lines.
Fifteen other things also use 50%, and those stayed round: status LEDs between 0.28 and 0.6rem, the active-nav dot, task bullets, and the large background orbs. A 3px radius on a 5px dot doesn't read as squared, it reads as a rendering fault, and the orbs are part of the visual design rather than UI chrome. Squaring two elements and leaving nine would have been less consistent, not more; squaring all twenty-five would have looked broken.
The nav bar that empties itself
The first version played the exit clip on scroll, so the logo animated away while the bar stayed put — which reads as a loading failure, not an intention. Replaced with the bar condensing instead: past the first section it shrinks to hug its contents, and the gap between the lockup and the first link becomes the flex gap, 26px, identical on both sites.
max-width can't animate to max-content, so the condensed width is measured at runtime and written to a custom property, which gives something that can actually transition. 90px of hysteresis keeps it from flickering at the boundary.
Keying black out of artwork
The last batch was the same lockups on black, to be made transparent so they can go anywhere.
Colour-keying leaves jagged edges. The artwork is foreground composited over black, so alpha comes out of solving p = a·F per pixel against the two known foreground colours and keeping whichever explains the pixel best. Recompositing the result back over black reproduces the source with a mean error of 0.04, and 0.03% of pixels off by more than 30 — those being the boundary between the violet and the white, where the two candidates are equally good explanations.
Then the format wall. MP4 has no alpha channel, so transparency lives in a VP9 WebM. Chrome, Firefox and Edge honour it; Safari wants HEVC-with-alpha, which needs an Apple encoder, so Safari gets the opaque fallback. Verified by decoding the WebM back out, then again in a real browser over four different backgrounds.
Which is where the artwork's own limit showed up. On white, "ERIC" disappears, because it is white. On the brand violet, "LI" disappears, because it is that violet. The transparency is correct and the logo still isn't universal — it's built for a dark ground, which is all the site uses, so it's fine where it's going and not fine as a drop-anywhere asset. A single-colour variant would fix that.
The thing I broke
Removing the contact section's social buttons left two CSS rules unused, so I deleted them with a pattern matching .social-icon{. One of the rules was .cta .social-icon{color:var(--grey)}. The pattern matched from .social-icon{ onward and removed exactly that, leaving .cta alone on a line.
CSS discards comments during parsing. So the orphan bound to the next thing it found, and .mock{...} became .cta .mock{...} — a descendant selector matching nothing on the page. The dashboard mockup lost its max-width, its negative top margin and its stacking context in one go, and went full-bleed across the hero.
Nothing errored. The file is valid CSS. The push succeeded, the deploy succeeded, and the page served a broken layout to anyone who visited. I checked that the rules I meant to delete were gone. I did not check that the rules I meant to keep still parsed, and those are different questions.
Fixed by removing the orphan, verified by reading the computed max-width back as 928px rather than by looking at a screenshot and deciding it seemed fine.
One thing that would have blocked all of it
The site's repository still pointed its remote at the old Forgejo host, which now refuses connections after that service moved to its own container. Every push today would have failed. Repointed before the first commit, and git ls-remote confirmed the destination had the expected HEAD before anything was pushed to it.
---
So: two sites carrying new animated lockups, every corner squared to the same 3px, a footer rebuilt to a supplied layout, and artwork that now sits on any dark background. Against that, a regex that removed a rule I wanted gone and half a rule I didn't, producing valid CSS that silently stopped styling the largest element on the homepage. The deploy pipeline reported success at every stage, because every stage did succeed. Checking that a deletion removed the right thing is not the same as checking that what's left still works, and only one of those was in my head at the time.