There is a category of bug that no build catches. It type checks. The linter is happy. The tests are green. The page renders. And the thing is still broken, because what broke is not the code — it is the relationship between two pieces of code that no tool was asked to compare.
Three of these came out of one project in one week. None is exotic. All three are the kind you will meet again.
1. The second list
The site is trilingual. English lives at the root, Romanian under /ro, Russian
under /ru. A helper decides whether a path exists in all three, and a link
builder uses it to keep the visitor in their language:
export const TRANSLATED_PATHS = [
"/", "/work", "/services", "/faq", "/contact",
"/work/erp", "/work/vision", "/work/commerce", "/work/trading",
] as const;
export function useLangPath() {
const lang = useLang();
return (path: string) =>
hasTranslation(path) ? withLang(lang, path) : path;
}
Read the fallback in that last line. A path the helper does not recognise is returned unchanged — which is correct for the two legal pages, deliberately English-only, and silently wrong for everything else.
That list was written by hand and fell behind three times: when three case studies were added, when seven service pages were added, and when an About page was added. Nothing complained. The routes existed, they were prerendered, they were in the sitemap, they returned 200. A Romanian visitor clicking a service simply landed in English.
Ninety-eight links across the site. Counted afterwards, by crawling the prerendered output and looking for hrefs without a language prefix on pages that had one:
for (const file of prerenderedPages) {
if (!/^(ro|ru)_/.test(basename(file))) continue;
for (const href of hrefsIn(file)) {
if (!href.startsWith('/ro/') && !href.startsWith('/ru/')) suspicious.push(href);
}
}
The same helper also decides whether a page gets hreflang alternates, and
where the language switcher sends you. So the same missing entry produced three
separate defects: visitors bounced out of their language, pages published with no
alternates, and a switcher that dropped you on the homepage instead of the page
you were reading.
The fix is not a longer list. It is not having a second one:
export const TRANSLATED_PATHS = [
"/", "/work", "/services", "/faq", "/contact", "/about",
...CASE_SLUGS.map((s) => `/work/${s}`),
...SERVICE_SLUGS.map((s) => `/services/${s}`),
] as const;
Those slug arrays already feed the router and the prerender step. Deriving from them means a new route cannot exist in one place and not the other.
The general shape: any time you write a list that must agree with another list, you have created a bug with a delay fuse. The question is not whether it drifts, but when.
2. The button that could not be clicked
The hero section draws an animated material behind the text on a canvas. The canvas needs the pointer, so the layer above it gives it up:
.hero .herostage { pointer-events: none; }
.hero .herostage a,
.hero .figs { pointer-events: auto; }
Read the second rule. It re-enables anchors. The site's primary call to
action — the one that opens the project form — is a <button>, because it opens
something in the page rather than going to an address.
So it inherited pointer-events: none and could not be clicked. Not by mouse,
not by finger. It rendered correctly, it took a visible focus ring from the
keyboard, it had a working onClick handler, and clicking it did nothing. The
neighbouring "See the work" link worked, because it is an anchor.
This survived a full mobile design audit — measuring tap target sizes, contrast, heading order — because every one of those checks reads geometry and computed style, and geometry was fine. It was found by a script that tried to tap the thing:
- attempting tap action
- <section class="hero">…</section> intercepts pointer events
- retrying tap action
TimeoutError: tap: Timeout 30000ms exceeded.
That is the whole detection method: not "is the button there", but "does pressing it do anything".
.hero .herostage a,
.hero .herostage button,
.hero .figs { pointer-events: auto; }
The general shape: pointer-events: none on a container is a promise that
you will remember every kind of interactive descendant, forever. Element
selectors quietly encode an assumption about what your markup contains. When the
markup gains a <button>, <summary>, <input> or <label>, the promise
breaks and nothing tells you.
3. The rule that lost to one written earlier
A fixed bar sits at the bottom of the case-study pages, showing the phase of the 3D scene. On phones the scene does not load at all — it is drawn behind the text at very low contrast, so it costs battery to render something nobody can see — and the bar has nothing left to describe:
@media (max-width: 900px) {
#gl, #glfallback, .phasebar { display: none; }
}
The bar stayed visible. Overlapping the text.
Ninety lines earlier in the same stylesheet:
.phasebar { position: fixed; /* … */ }
.phasebar.on { display: block; }
.phasebar.on has specificity (0,2,0). .phasebar has (0,1,0). Source order
only decides between rules of equal specificity — and a media query adds
nothing to specificity at all. The later rule lost to the earlier one, exactly as
the cascade specifies.
The fix is to match the specificity you are overriding:
@media (max-width: 900px) {
#gl, #glfallback, .phasebar, .phasebar.on { display: none; }
}
Not !important. That wins the argument and loses the file: every later
override then needs its own !important, and within a year the stylesheet is a
stack of them with no way to reason about what applies.
The general shape: it is easy to believe a media query is stronger than a plain rule. It is not. It is a condition, not a weight. Any rule you intend to override inside one has to be beaten on its own terms.
What they have in common
None of these is a mistake in the sense of a typo. Each is a correct decision that stopped being correct when something else changed around it.
A hand-written list is correct until the thing it mirrors grows. An element selector is correct until the markup gains an element it does not name. A specificity is correct until you try to override it from a place that felt stronger.
What actually catches them is boringly specific:
- Derive lists, do not write them twice. If two lists must agree, one of them should be computed from the other.
- Test the interaction, not the render. "The button exists and is 44 pixels tall" and "the button does something when pressed" are different assertions, and only the second one would have caught bug 2.
- Read the computed style, not the stylesheet. For bug 3 the stylesheet said
display: noneand the element saiddisplay: block. OnegetComputedStylecall ends the argument in a second.
The theme underneath all three: the build verifies each file. It never verifies that two files still agree with each other. That gap is where this category lives.