I’d been auditing captainrandom.co.uk one block at a time for three days. Block by block, page by page. The audit had caught a deprecated role="marquee" on the announce bar. Caught two fabricated marketing items that didn’t correspond to real posts. Caught the missing aria-describedby on the newsletter form. Caught the marquee scrolling without a hover-pause.

Then I opened the site on my phone for the first time.

There was no menu.

Not a wrong menu. Not a menu hidden behind a broken hamburger. Not a menu with the wrong items in it. No menu, full stop. The brand wordmark and two CTAs were visible. Every other page on the site was unreachable from a phone for three days while the audit was actively running.

What the audit was good at

Three real wins the audit had logged that week. All of them present-tense bugs: something is wrong with this thing that exists.

Announce bar. The outer element was <div role="marquee">, a WAI-ARIA role deprecated in 1.2 and gone in 2.x. The inner items were <div aria-hidden> despite the outer container being aria-label-ed “Latest updates.” The contradiction was visible to a screen reader: a labelled region advertising content that was then hidden from assistive tech. Fix in three lines: swap to <aside aria-label> outside, <ul role="list"> inside, drop the aria-hidden.

Tech marquee. Same hover-pause gap as the announce bar. Items scrolling continuously with no way for a visitor to stop the scroll to read a tech name or kanji that caught their eye. Standard marquee anti-pattern. One CSS rule: .marquee:hover .marquee-row { animation-play-state: paused; }.

Newsletter form. Disclaimer text sitting underneath the email input, visually paired but not programmatically associated. Screen readers would read the input without the context. Added aria-describedby="news-disclaimer" on the input, matching id on the disclaimer. Two attributes.

Each of these has the same shape. There is a thing. The thing has a defect. The audit, doing its job, walks the page and finds the defect. The fix is local, scoped, two-to-three lines of code.

What the audit missed

Now the mobile nav.

The desktop .nav-links had display: none at viewports of 880 pixels or below. There was no .mobile-nav element. There was no hamburger button. There was no media query block that introduced either. The page-by-page audit had walked every URL, every block, every section, but only at desktop width. The audit had no “render this page at iPhone width” pass.

The mobile nav wasn’t broken. It didn’t exist.

A visitor on a phone landed on the homepage, saw the brand and two CTAs, and had no path to /writing, /work, /now, /learning, or /contact. The chrome was trapping phone-first traffic on whatever URL it landed on. For three days. While a careful audit was running.

The fix took ninety minutes. New .nav-toggle hamburger button visible only below 880 pixels. New .mobile-menu overlay with ARIA-correct aria-hidden and inert attributes when closed. useState for open state. Three useEffects for body-scroll lock, ESC-key close, and usePathname change close. A z-index ladder to keep the close button above the drawer surface.

But the interesting thing isn’t the fix. It’s why the audit missed it.

Two kinds of bug

Audits are diff-based. They compare the thing in front of them against a standard of correctness and surface where the thing fails the standard. They are excellent at this. They will catch the deprecated ARIA role because the deprecated role is there and the standard says it shouldn’t be. They will catch the missing hover-pause because the marquee is there and best practice says it should pause. They will catch the unlinked disclaimer because the disclaimer is there and the input is there and the link between them is missing.

The mobile nav had none of this. The desktop list, at desktop width, was correct. At mobile width, the desktop list disappeared (also correct; that’s what display: none does). The expected replacement element wasn’t merely wrong; it wasn’t in the file. The audit had nothing to diff against.

This isn’t a one-off. It’s a pattern. There are two classes of bug:

  • Present-bugs. Something is here and something about it is wrong. Deprecated, broken, mis-styled, mis-labelled, factually false.
  • Absent-bugs. Something should be here and isn’t. No mobile menu. No <noscript> fallback. No empty state. No loading state. No error message. No skip link. No alt text on the hero image you forgot to set alt on.

Audits find present-bugs. They are systematically blind to absent-bugs. The mobile nav escape isn’t an audit-discipline failure; the audit was working as designed. It’s a methodology-scope failure: the audit can’t see what isn’t there.

The fix isn’t to audit harder

It’s tempting to respond “the audit was incomplete, do the audit more thoroughly next time.” That doesn’t help. A more thorough audit still has the same diff-based shape. You can audit at every viewport (and you should; I now do) and that specific class of absent-bug gets caught. But absent-bugs are infinite. You cannot enumerate everything that should be present by walking what is present.

The real fix is to add an inventory pass before the audit pass. Two different questions, asked in sequence:

  1. Inventory. What should exist on this page? Driven by the requirements doc, the design system, the device matrix, the user-journey map. The output is a list of expected surfaces: a nav menu at every viewport, a working form, a hover-pause on every marquee, a focus ring on every interactive element, a 404 path, a skip link, alt text on every image, a loading state for the gallery.
  2. Audit. For each expected surface, does the thing that exists match the standard? This is the audit you were already running. It now has a checklist of expected surfaces to walk instead of walking the rendered page and hoping you notice what’s missing.

The two passes catch different bug classes. Inventory catches absent-bugs. Audit catches present-bugs. Run only the audit and you systematically miss the absent ones, which is exactly what happened on captainrandom for three days.

Why this keeps happening with AI

The previous article on this site argued that the bugs Claude Code left behind weren’t logic errors. They were hypotheses presented as facts. Same pattern, different surface. There, the AI inferred what a person like me probably had (twelve years of experience, thirty-seven repos) and shipped those numbers as data. Here, the audit inferred completeness from the absence of warnings: no warning means nothing’s missing.

Both failures share a shape: confident, fluent, structurally correct output where the missing thing is the thing that matters. The reflex for AI-generated code was verify, don’t trust. The reflex for audits is the same: inventory before audit. Don’t trust the audit to find what shouldn’t be missing. Tell it what to look for.

What this changes

Phase 6 of the captainrandom rebuild had been a clean per-block audit. It’s now gaining a retroactive inventory pass. For every URL, two passes:

  • Inventory. Read the design system spec, the device matrix, the PRD, the user-journey doc. List every surface that should exist at every viewport: desktop nav, mobile nav, footer nav, skip link, focus ring, empty state, error state, loading state, 404 path. The list goes in the page’s audit checklist before anyone opens a browser.
  • Audit. Walk each item on the list. If it’s there, is it correct? If it’s not there, why not?

Same discipline that catches AI-generated fabrications. Don’t trust the present to tell you what was supposed to be there.

What this isn’t

This article isn’t a screed against audits. The three audit wins above (deprecated ARIA, missing hover-pause, unlinked form pairing) were real and the audit was the right tool. It’s not a screed against per-page review either. Per-page review is exactly the work that was happening when the mobile nav escape was discovered (I loaded the live URL on my phone for the first time, late in Phase 6; the audit had been progressing just fine until then).

It’s a claim about the scope of one methodology. Audits are good at one thing. Inventories are good at a different thing. Skipping the inventory because the audit feels comprehensive is the same shape of mistake as skipping verification because the AI’s output sounds right.

Where this goes

The next audit pass on this site runs inventory-first. So does the next AI-assisted feature build. The reflex is the same either way: what should be here that I haven’t checked for?

The mobile nav got fixed in ninety minutes. The methodology fix takes longer. It has to survive the next ten audits to prove it generalises. But the alternative is auditing harder and hoping the next absent-bug surfaces in time. It won’t. They never do.

All writing