Three AIs, Three Hours, Four Bugs: A WordPress Hover-State Post-Mortem
One CSS hover effect. Two AI assistants in sequence. Three hours of debugging. And in the end, four stacked bugs that none of the AI passes had untangled — because none of them had measured anything.
This is a post-mortem of a single afternoon spent on a WooCommerce product-card hover button. The intended behaviour was simple: hover a product, an “Add to cart” button slides up over the bottom of the image, the title and price below stay put. What kept happening instead was a parade of variations — button on the title, button on the price, button half-way into a beige gap nobody could explain. Every fix attempt by an AI assistant looked locally reasonable. Every attempt failed in a slightly new way.
The lesson is unglamorous and worth the read: when an AI assistant has tried three CSS approaches in a row and none of them work, stop accepting CSS suggestions. Open DevTools and measure. Cinematic frustration is not a debugging signal. Pixel offsets are.
What the design was supposed to do
The card is a standard WooCommerce product-collection block: image on top, then the category label, then the title, then the price. On hover, a dark “Add to cart” button is supposed to slide up over the bottom strip of the image. The category label and the title never move. The button never overlaps the title.
This works on the product category archive in production. It demonstrably worked. The same CSS was supposed to apply to a custom homepage section (“Signature Pieces”) on the store subsite, and the same CSS was supposed to apply to the shop archive. Three pages, one shared stylesheet, one product-collection block.
That last point matters. The CSS was already shipping correctly on the category archive. The brief was: make the homepage section match.
The first AI loop: Claude Code Sonnet 4.6
The first AI pass replaced the working CSS approach with a different one. The original used CSS Grid: turn each .wc-block-product card into a one-column grid, drop both the image and the button into row 1, and use align-self: end on the button so it hugs the bottom of the grid cell. Hide the button with clip-path: inset(100% 0 0 0). Reveal on hover with clip-path: inset(0 0 0 0). Because the image is the only other thing in row 1, the button can only ever overlay the image.
The replacement attempt switched to position: absolute; bottom: 0 on the button and position: relative on the card.
That choice is the bug — the whole post is downstream of it — but in isolation it looks fine. You see it in tutorials all the time. The problem is the structure of WooCommerce’s rendered HTML. Inside a .wc-block-product card, the button is not a child of the image figure. It is a sibling:
<li class="wc-block-product">
<figure class="wc-block-product-image"> ...image... </figure>
<div class="wp-block-woocommerce-product-button"> ...button... </div>
<div class="wp-block-post-terms">LEATHER PORTFOLIOS</div>
<h3 class="wp-block-post-title">Oxford A5 Folio</h3>
<div class="wp-block-woocommerce-product-price">$119.00</div>
</li>
The button is a sibling of the image, the terms, the title, and the price. So when you set position: relative on the card and position: absolute; bottom: 0 on the button, “bottom” resolves to the bottom of the entire card — which is the bottom of the price text. The button does exactly what you told it to: it goes to the bottom of the card. On hover it appears, neatly overlaying the price. Not the image.
This kept getting “fixed” by adding more rules. margin: 0 !important. margin-block-start: 0 !important. margin-block-end: 0 !important. opacity: 0; visibility: hidden; transform: translateY(20%). With each new rule, the button drifted to a slightly different wrong place. None of those rules addressed the root cause, because none of them changed the fact that the absolute positioning was anchored to the wrong element.
Claude Code on Sonnet 4.6 ran the session for the first two hours. By the end of it, the patch had grown into a thirty-line block of !important-laden overrides that did not, in the end, do the thing.
The second AI loop: Mistral vibe Mistral Medium 3.5
Frustrated, I switched to a different assistant — Mistral’s latest vibe using Mistral Medium 3.5. It produced a more elaborate version of the same wrong idea: more selectors, more cascade-busting, more attempts to neutralise margins that were not the problem. The model never proposed measuring. I never asked it to. The pattern of “look at the CSS, propose new CSS, hope” stayed exactly the same across both vendors.
At one point Mistral helpfully suggested I look at the Ollie theme as a reference, because Ollie has a working product-button-hover effect. I did. Ollie achieves the effect by using a custom wrapper class (.ollie-product-button-hover) and re-parenting the button into a group with the image. That is one valid solution — it changes the markup so that the button is, structurally, a child of the image container. But it required custom block markup and a new render_block filter, which is a significant scope expansion when we already had a working grid-based version sitting in git log.
By this point three hours were gone. The category archive worked. The homepage section did not. The shop archive had also been broken in the process. I had no clear theory of why any of the attempts had failed.
What finally unstuck it: measuring
I opened a fresh session — this time on Claude Code with Opus 4.7 — reverted woocommerce.css to the last committed state (a single git checkout), and asked it for one specific thing: a five-line script that printed the computed offsets of every child element inside one product card. Not a screenshot. Not a screenshot of DevTools. The actual numbers, from getBoundingClientRect and getComputedStyle, for every direct child of .wc-block-product.
To be clear: Opus 4.7 is not, by itself, the reason the bug got solved. Asked the same way the previous two sessions had been asked — “fix this CSS” — it would almost certainly have produced another plausible-looking CSS patch. What changed was the question. The fact that I happened to be on a more capable model when I finally asked the right question is incidental. Any of the three models would have produced the right diagnostic given the same prompt. I just had not asked any of them for measurements until then.
The output looked like this:
image wrapper: top: 0 bottom: 531 height: 531
button: top: 469 bottom: 519 height: 50 margin-bottom: 12px
category terms: top: 531 bottom: 562 padding-top: 16px
title: top: 568
price: top: 589 margin-bottom: 16px
That single print-out solved the whole problem. There were four distinct issues, not one. They had stacked, and looking at them as one symptom — “button is in the wrong place, also there is a gap” — is exactly what kept the AI loops looping.
The four bugs, separated
Bug 1: wrong positioning model
The replacement CSS used position: absolute on the button with position: relative on the card. Because the button is a sibling of the image (not a child), “bottom” referred to the bottom of the card, not the bottom of the image. The fix was already in git: restore the original display: grid approach that puts the image and the button into the same grid cell. git checkout -- demo/web/app/themes/elayne/assets/styles/woocommerce.css. One line.
This is the bug that consumed almost all of the wasted time. It was also the easiest to fix once it had been correctly named.
Bug 2: the button’s 12px margin-bottom
With the grid approach restored, the button now sat inside the image grid cell. But it ended 12px above the bottom of the image. The numbers from the inspection showed the button at bottom: 519 and the image wrapper at bottom: 531. Twelve pixels.
That margin came from the WordPress core stylesheet for .wp-block-button, which applies margin-bottom: 12px. The original CSS only reset margin-block-start, not margin-block-end. So 12px of image was always visible below the button on hover. The fix was to set margin: 0 on the button container in the product-collection scope:
.wp-block-woocommerce-product-collection .wp-block-woocommerce-product-button,
.wp-block-woocommerce-product-collection .wc-block-components-product-button {
/* ...existing rules... */
margin: 0;
}
Bug 3: the 16px padding above category terms
Below the image, between the image bottom and the “LEATHER PORTFOLIOS” category label, there was a visible 16px lighter band. The inspection numbers showed padding-top: 16px on .wp-block-post-terms. That was a deliberate “breathing room” rule from earlier in the theme’s life. With the rest of the spacing tightened up, this remaining 16px gap was the only thing still visible — and it read as an unintentional design gap, not breathing room.
Why was this bug invisible on the product category page the whole time? Because the category archive template does not render the wp:post-terms block. You are already on the category page; the category label below each card would be redundant, so the template omits it. Same CSS, different blocks, different visible result. That is exactly the kind of asymmetry that throws off “this works on page A but not on page B” debugging — and exactly the kind of thing that pixel measurements catch and screenshots do not.
Fix:
.wp-block-woocommerce-product-collection .wp-block-post-terms {
padding-top: 0;
/* ...existing rules... */
}
Bug 4: the aspect-ratio mismatch (a 33px ghost strip)
Even after fixing the previous three, there was still a faint lighter band visible at the bottom of the image on the homepage section only. The category archive looked perfect; the homepage did not.
The numbers explained it. The image wrapper element (.wc-block-components-product-image) had aspect-ratio: 3/4, set by a global block-style rule shared with the category templates. The actual <img> inside the wrapper had aspect-ratio: 4/5, set inline from the pattern’s block attribute "aspectRatio": "4/5". Wrapper at 398.5 × 4/3 = 531px tall. Image at 398.5 × 5/4 = 498px tall. 33 pixels of empty wrapper showing below the image.
The category templates set the block attribute to "3/4", so both the wrapper and the image came out 531px and the strip never appeared. The homepage pattern was the only place setting "4/5", which is why the strip was specific to that one section.
The fix was a single character in the pattern file:
-<!-- wp:woocommerce/product-image ... "aspectRatio":"4/5" -->
+<!-- wp:woocommerce/product-image ... "aspectRatio":"3/4" -->
After that, all three pages rendered identically.
Why this took three hours
Take a step back. The total amount of code that actually changed:
- One file reverted (
git checkout) - Two CSS properties updated (
margin: 0,padding-top: 0) - One character changed in a pattern attribute (
4/5→3/4)
That is roughly five lines of effective change. The diagnostic work to find those five lines should have taken twenty minutes with a tape measure. It took three hours because no one — me, Sonnet 4.6, Mistral vibe medium 3.5 — ever put the tape measure down on the problem. We kept proposing CSS into a void of unmeasured assumptions. Opus 4.7 closed it out in fifteen minutes once the prompt finally asked for offsets instead of overrides.
Three patterns made the loop self-reinforcing.
Symptom-stacking
Four bugs were producing one composite visual outcome — “the button is in the wrong place and there’s a gap.” That single composite symptom matched any number of plausible CSS theories. So every fix made the symptom move slightly and the next theory grew out of the slightly-different symptom. The bugs needed to be separated before any of them could be fixed. Computed offsets separate them. Screenshots do not.
The page-comparison trap
“It works on the category page but not on the homepage” sounds like a useful clue. It is, but only after you know why — because the category template omits wp:post-terms, and because the homepage pattern uses a different aspect ratio. Without that information, “works on A, breaks on B” mostly just feeds the AI’s confidence that the answer is one selector tweak away. It is not.
AI as a CSS-suggestion machine
I was using both AIs in a way that maximised their worst tendency: I was asking for code, not for diagnosis. “Fix this CSS” produces CSS. “What is the computed bottom offset of the button element, and what rule is setting its margin-bottom” produces information. The first produces another guess. The second produces facts that constrain the next move. I spent three hours asking for the first. The fifteen minutes I spent asking for the second resolved the problem.
Where manual debugging would have been faster
A reasonable workflow on this kind of bug, from the start, looks like this:
- Open DevTools, hover the broken element. Look at the box model panel for the button. Confirm where the button actually is and what its containing block actually is.
- Print computed offsets for every child of the card. Five-line script. The shape of the problem usually announces itself: which element is taller than expected, which element has an unexpected margin, which two siblings have inconsistent aspect ratios.
- Reproduce in two places that should be visually identical. If page A looks right and page B looks wrong, run the same script on both and diff the outputs. The diff is the bug.
- Only then propose CSS. And ideally the CSS should be the smallest possible change that addresses one specific number in the diff — not a thirty-line override stack.
The AI assistants are excellent at step 4 once you have done steps 1-3. They are bad at step 1-3 unless you specifically ask them to do it, because their training rewards proposing solutions and their default mode is to produce code. They will keep producing code as long as you let them. Asking for measurements has to be explicit.
A working rule of thumb: if the second CSS proposal does not fix the bug, the next request should not be a third CSS proposal. It should be “print every relevant offset and computed style and tell me the diff between the working and broken states.” That single redirection costs nothing and shortens the next two hours by approximately two hours.
What I shipped
For the record, the final state on the theme branch:
assets/styles/woocommerce.css— reverted to the original grid + clip-path approach, plusmargin: 0on the button container andpadding-top: 0on the category terms. ~4 lines of effective change versus the broken state.patterns/woocommerce/woo-signature-pieces.php— image aspect ratio set to3/4to match the wrapper.docs/elayne/store/PRODUCT-CARD-HOVER-BUTTON-FIX.md— a developer-facing post-mortem documenting the grid + clip-path approach, why the absolute-positioning alternative is wrong on this HTML structure, and guard-rails to prevent the regression.
Two commits, neither of them dramatic.
Takeaways
For the workflow
- The cost of switching from “ask AI for code” to “ask AI for measurements” is a single sentence. The savings, when the bug is anything more than a one-line typo, are measured in hours.
- “It works on page A but breaks on page B” is rarely a CSS specificity story. More often it is a content difference (different blocks rendered, different attributes set) that the shared CSS reveals. The shared CSS is the constant, not the variable.
- If you ever find yourself adding a fourth
!importantto make a CSS rule “win” against a phantom default, the rule is probably correct and the element is wrong. Stop overriding and start measuring which element you are actually targeting.
For working with AI assistants on visual bugs
- Treat AI-suggested CSS as a hypothesis, not a fix. Verify with measurements before accepting it.
- When two consecutive AI attempts do not work, the third attempt is statistically very unlikely to work either. Break the pattern. Move to inspection.
- Provide the AI with computed offsets in your next prompt. “The button has
margin-bottom: 12pxand its bottom edge is 12px above the image bottom” is the kind of grounded fact that produces a precise, working fix in one shot. “The button is in the wrong place” is the kind of vague fact that produces another guess.
The bugs in this story were not exotic. WordPress core’s default wp-block-button margin is well documented. CSS Grid stacking is a standard pattern. Aspect-ratio inheritance has been discussed in dozens of articles. Each individual bug, taken alone, would have been spotted in five minutes. What made the afternoon expensive was the layering — and the choice to keep proposing solutions when the right move was to step back and quantify the problem.
A measuring stick, not another prompt. That is the lesson.
Stuck in an AI debugging loop?
Imagewize builds and maintains custom WordPress and WooCommerce sites — including the kind of careful CSS work that AI assistants alone tend to struggle with. If you have a project that has been “almost done” for longer than it should be, get in touch.