I shipped a mobile fix for one of our tenant sites last week — coi-tracking, the first real app we stood back up after the catalog teardown. The change was small: restore some breakpoint rules that had gone missing in a port, and un-hide a few elements that only exist once on desktop. Reading the diff, every line was obviously correct. Reading the site at 390px before the fix, it was obviously broken. Those two facts had been true at the same time for a while.
That gap — a change that reads right in the diff and wrong in the browser — is the thing I've spent the most effort building a habit around, because I'm one person and there is no second pair of eyes between my editor and production.
Why does a CSS diff read right and render wrong?
Because a diff shows you the rule you changed, and the browser shows you the rule that won. Those are different questions, and only one of them is answered by the text in front of you.
A media query block is a self-contained, readable thing. When it's present, it reads as deliberate. When it's absent, it reads as nothing at all — there's no line to notice. Deletions in CSS are silent in a way deletions in TypeScript are not. Remove a function and something fails to compile. Remove a @media (max-width: 768px) block and the desktop layout stays pixel-identical, the build passes, and the site is broken at a width you weren't looking at.
The coi-tracking landing came through our Framer→Next capture-and-own pipeline — we license a template, capture the live rendered DOM and CSS, and port it into code we own so it's ours to change forever. That pipeline is great, and it is also exactly the kind of transform where breakpoint blocks and display toggles get dropped one at a time without any of the surviving lines looking wrong.
The three shapes that keep doing this
Missing breakpoint blocks. Desktop unaffected, mobile stacked wrong. Zero signal in the diff.
Orphaned display: none. An element hidden at one width on the assumption that a different element covers that case — and then the covering element gets removed or renamed. Now nothing renders there at all. The diff shows a deletion of "unused" markup.
Reveal-on-scroll that never reveals. The element is in the DOM, present in view-source, passes any test that queries for it, and is invisible to a human forever because its opacity never got animated to 1.
That third one is the meanest, so it's worth taking apart.
How do you keep a scroll reveal from leaving content invisible?
Make the failure mode "shows too early" instead of "never shows," and add a hard failsafe that ignores your own logic.
IntersectionObserver reveals fail in three ways I now check for by reflex:
A negative bottom rootMargin shrinks the observation box, so the callback fires after the element is already visible to the reader — the animation plays late, or looks like a flicker. A positive margin fires early, which is what you actually want.
A large hide-transform (translateY(80px), a big scale) means the rect the observer measures is the post-transform rect. If your transform pushes the element far enough, the observer's geometry and the layout geometry disagree, and the intersection can simply never happen.
And anything above the fold on first paint may never trigger a scroll event at all.
Every one of those leaves content permanently invisible, which is the worst possible outcome for a marketing page — worse than an ugly animation, worse than no animation.
// reveal.ts — the version I ship now
export function observeReveal(el: HTMLElement) {
// Above the fold on load: reveal immediately, never wait for a scroll.
if (el.getBoundingClientRect().top < window.innerHeight) {
el.dataset.revealed = "true";
return;
}
const io = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) return;
el.dataset.revealed = "true";
io.disconnect();
},
// Positive margin: fire BEFORE it enters view, not after.
{ rootMargin: "0px 0px 15% 0px", threshold: 0 }
);
io.observe(el);
// Failsafe: whatever the observer thinks, this content becomes
// visible. A broken animation beats a blank section.
setTimeout(() => {
el.dataset.revealed = "true";
io.disconnect();
}, 3000);
}
The hide state pairs with a small transform — 16px, not 80 — so layout and observer geometry stay in agreement:
[data-reveal] { opacity: 0; transform: translateY(16px); }
[data-reveal][data-revealed="true"] { opacity: 1; transform: none; }
The setTimeout is the part I'd have argued against a year ago as sloppy. I don't anymore. It converts an entire class of invisible-content bugs into a mild timing artifact, and it costs three lines.
What does a browser check look like when there's no QA team?
It's a script that opens real Chrome at the widths you actually ship at and saves a screenshot of each one, and you look at them.
I already had this lying around for a different reason. Our demo recordings are driven by Playwright — a real browser walking the full product flow with no human in the loop, which I wrote up separately. Once you have a harness that can drive a live page and wait for it to settle deterministically instead of guessing at timeouts, pointing it at a landing page across breakpoints is a ten-line job.
const WIDTHS = [390, 768, 1280, 1728];
for (const width of WIDTHS) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
await page.goto(url, { waitUntil: "networkidle" });
await page.screenshot({ path: `shots/${width}.png`, fullPage: true });
}
That's it. No pixel diffing, no baselines to maintain, no flaky visual-regression suite that I'd start ignoring within two weeks. Four images I look at with my eyes.
What I'm actually looking for
Large blocks of empty vertical space — that's an element that failed to render or is sitting at opacity 0. A section that appears at 1280 and vanishes at 768 without a mobile counterpart taking its place. Text that wraps to five lines in a container built for two. And any image slot that's a blank rectangle, which is the failure mode after an asset purge.
That last one is worth calling out. I recently stripped a pile of unused Framer template images and the dead <img> refs pointing at them, and deleted the orphaned components that came with them. On paper that's the safest kind of change — removing code nothing uses. In practice "nothing uses it" is a claim about the whole render tree at every viewport, and the diff can only show you the deletion.
Which changes get a browser check and which don't?
Anything that touches a breakpoint, toggles visibility, deletes an asset, or moves a route. Everything else I merge on the diff.
That's a deliberately narrow list, because a rule I apply to every change is a rule I'll stop applying. Backend logic, schema changes, agent prompts, API routes — I trust tests and the diff. But the four categories above share a property: their failure is invisible in text and obvious in a browser.
Route moves earned their spot recently. I folded apps/landing into the Artemis app so the marketing site and the builder live in one place — landing at the root, builder at /create. Every individual file move was correct. Whether the combination of moves left a working site is not a question source code answers. Our deploy.sh verify step checks real URLs after deploy, including accepting the post-teardown redirects from the retired catalog, and it's the only thing that actually knows.
What about bugs that only exist in a browser at all?
Some errors have no source-code representation, and hydration is the clearest example.
Artemis had a hydration mismatch I could not see anywhere in the code, because it wasn't in the code — it came from the DOM being modified before React hydrated, in a real browser with real extensions installed. Server HTML and client HTML disagreed for reasons no file in the repo mentioned. The fix was pinning a vendored dependency to a known-good version and gating the injected case explicitly. No amount of diff-reading would have found it; it needed a browser with the extension installed, which is to say, a normal user's browser.
This is the same principle underneath all of it, and it's why Kynth Apollo — our capabilities layer, where every hard problem gets solved once and reused — holds the browser harness alongside everything else. Artemis assembles apps out of those finished pieces. A piece that renders correctly at one width and disappears at another isn't finished, and there's exactly one place you can find that out.
Open the browser. Four widths. Look at the pictures.

Top comments (0)