Two bugs, one symptom: when soft-scroll and orbit controls collide
I was almost done with the first post for this blog. The voice rules passed. The gate caught some things I'd missed and the fixes landed. The closing commit was queued. I scrolled the page to read it one more time before greenlight.
My mouse wheel did nothing.
The native scrollbar still dragged the page so the content was there, but the wheel was dead. The same shape of failure the post itself is about. The page hosting the proof-of-work post about wheel-dead pages was a wheel-dead page.
This is the sequel. It's shorter than the first one because there are fewer mistakes to walk you through. The methodology caught two bugs in one session and refused to ship until they were addressed. That's the whole story.
The first probe
The first hypothesis was the obvious one. The /log index has a 3D mesh visualization. Three.js orbit cameras use the scroll wheel for zoom. So probably the canvas was eating wheel events for zoom and the page couldn't scroll over it.
The agent ran a read-only diagnostic across three routes before touching anything:
Native and programmatic scroll work everywhere. Wheel works on the landing page but is dead on both /log routes including the canvas-less post route. The orbit camera is conclusively ruled out. The cause is global to /log, wheel-specific, Lenis-layer.
Claude Code, wheel-isolation diagnostic, build log
Quick note for non-coding readers. Lenis is the smooth-scroll library running underneath the landing page. The one that makes the page glide instead of step when you wheel through it.
The canvas-less post route being wheel-dead too is what ruled out the canvas. If the bug was a global /log issue rather than a canvas- specific one, the cause had to be in something that affected all /log routes the same way. Something below the canvas, deeper in the stack.
Then the agent cross-referenced its own build log:
The current LenisProvider is the pristine version from before the Phase 1 fix. The Phase 1 resilience fix was part of the fully- reverted Phase 1 bundle. BRAIN Session 007 hotfix-1 documented this exact bug. LenisProvider's one-shot useEffect reads the Lenis ref once and bails permanently on a race. The landing page only works because the Manifesto scene's reactive useLenis effect masks the race by reliably driving lenis.raf. /log has no scenes, so nothing masks the race. Lenis captures the wheel but its rAF never advances. Wheel dead, native scrollbar fine.
Claude Code, Lenis-mechanism hypothesis, build log
The Phase 1 wheel-dead bug, the one the first post is about, was always still in the code. The fix had been reverted along with everything else when Phase 1 over-scoped and got pulled back. The bug stayed latent because no sceneless route existed to expose it. Building this blog created the first sceneless route. The first surface that existed without a scene to mask the bug.
The Phase 1 ghost shipped to production months ago. I just hadn't built a page that could see it yet.
Fix one, isolated to /log
The Phase 1 fix exists in BRAIN. It's a rAF-poll replacement for the one-shot bail. But the Phase 1 retrospective explicitly ruled: any work on LenisProvider needs its own SPEC, because it's the shared runtime the landing page depends on, and a botched fix breaks more than it solves.
So the move was an opt-out instead of a fix. /log opts out of Lenis entirely. Native scroll on /log, Lenis untouched on the landing page. Document-mode reading actually prefers native scroll anyway. Smooth scroll's momentum lag is mildly worse for reading than it is for cinematic landing transitions.
The opt-out shipped, the verification matrix ran, wheel scrolled on the post route, wheel scrolled on /log mobile, wheel scrolled on /log desktop above the canvas. Landing page untouched, smoke test clean.
But /log desktop above the canvas was the qualifier that mattered.
The second probe
The agent ran the verification again and found a region where wheel was still dead:
Wheel above the canvas works. Wheel over the canvas is dead. Canvas spans y 347 to 891. OrbitControls' wheel-dolly handler preventDefaults native scroll across the canvas. The first probe couldn't see this because Lenis was masking everything. Two independent bugs, one symptom.
Claude Code, second-cause isolation, build log
The original orbit-camera hypothesis was right. It just wasn't the whole story. The first probe correctly disproved it as the sole cause because the canvas-less post route was also wheel-dead. Both routes were the Lenis bug. Once Lenis was peeled away, the orbit-camera bug emerged as the second independent cause on the route that has a canvas.
Two bugs with overlapping symptoms is the kind of layered diagnosis the methodology was built around. The post about Phase 1 walks through three sequential wrong diagnoses on one bug. This was two right diagnoses on two bugs, found in the right order, by measurement. The shape was the same, the outcome was opposite, because the rules learned from the first time were the rules used this time.
Fix two, one prop
The fix was a single prop on the OrbitControls component:
enableZoom={false}
The zoom handler is what consumes the wheel. Disable it, native scroll passes through the canvas, the page scrolls everywhere. Drag-to-orbit still works because that's a pointer-event handler, untouched. The mesh is still interactive; it just doesn't zoom from the wheel.
You also can't really tell zoom is gone unless you knew it was supposed to be there. The mesh is a navigation surface, not a 3D model viewer. Nobody comes to a blog to inspect a mesh from a different camera distance. They come to read.
The verification matrix ran one more time. Wheel works above the canvas, over the canvas, on the post route, on mobile, on the landing. Drag-to-orbit works. Native scrollbar works. CLS holds at zero on the index. Voice rules still pass. The post is finally shippable from a page that finally fully scrolls.
What this means
The first post is about three confident wrong diagnoses chasing one bug. This one is about two correct diagnoses chasing two bugs that looked like one bug. The shape rhymes; the outcome doesn't. The reason is that this time the methodology was running.
The control experiment happened first instead of fourth. The two-hotfix budget held the agent back from autonomously applying a third fix. The agent named both halts honestly instead of declaring problems solved on partial evidence. The pre-commit hook would have blocked the closing commit anyway if any of this had been hidden. Every rule in the methodology came out of a specific failure in the first arc, and every rule fired correctly the second time the same kind of problem appeared. That's what mechanical methodology accrual actually buys you.
There's no abstract lesson at the end of this one. Just the work, done twice. Once when the only gate was the person, and once when the gate was both the person and the code. The gate caught what the person would have missed first.
This post exists because the gate caught it. The first one ships now.
If your AI-built system is failing in ways that look like one bug but smell like two, I can help. Send the repo state, the failure mode, and what you've already tried. VibeKoded can scope a spec discipline install, gate configuration, or operator handoff. → Work with VibeKoded