/**
 * Harvest — Product card (C7). The single most reused component.
 *
 * Styles BOTH surfaces so it works on custom Harvest templates AND real Magento
 * catalog output with no core edits:
 *   - `.hrv-card`             → Harvest-authored cards (home rails, quick view)
 *   - `.product-item-info`    → Magento grid / related / cross-sell / search
 *
 * Comp reference: surface panel, 1px border, 12px radius, 14px pad, aspect-1/1
 * image, accent star rating, Space-Grotesk price, add-to-cart + wishlist row,
 * absolute badge top-left. Hover: lift + reveal secondary actions.
 */

/* ---------- Card shell ----------
   T17 ADDED `.widget-product-grid` TO THIS LIST, UNSCOPED.

   WHICH MARKUP WAS ACTUALLY MISSING THE SHELL, measured rather than assumed. Home renders
   TWO product grids, enumerated from `cms_page_view_id_harvest-home.xml`: ONE CatalogWidget
   `ProductsList` (line 122, "Fresh this week") and ONE `EcommPaaS` `Bestsellers` block
   (line 147). The live page agrees — 2 `.widget-product-grid`, 8 `.product-item`.
   CatalogWidget nests its `widget-product-grid` INSIDE `div.products-grid.grid`, so
   `.products-grid .product-item-info` above already covered it and it was never broken
   (measured: white surface, 1px border, 18px radius, 14px padding, info filling its 332px
   track).
   The ONLY markup that emits a BARE `<ol class="widget-product-grid">`, with no
   `.products-grid` ancestor, is this theme's own `bestsellers.phtml`. That is the one
   surface the list missed, and it appears on Home and on the empty cart.

   THIS RULE WAS BRIEFLY SCOPED TO `.checkout-cart-index`, AND THAT WAS THE BUG.
   Home's Bestsellers row was rendering `.product-item-info` at 152px inside a 332px track
   with no surface, border, radius or padding — four bare images with 54% dead space per
   card, on the flagship page. It had simply been invisible because that row was EMPTY
   until an order was seeded; seeding it for the cart is what exposed a defect that had
   always been there, and the cart-only scope then fixed the copy nobody was looking at
   while leaving the original. A defect in shared core/widget markup belongs to the
   component, not to whichever page happened to notice it.

   A SCREENSHOT FOUND THE ORIGINAL, NOT A MEASUREMENT. Every property the task set measured
   correctly, and the card typography (14.5/700 name, Bricolage 17px price) really was
   coming from this component — which is what made "the card component is being reused"
   feel true. It was half true: the component owned the type and the photo behaviour, and
   its shell never listed this surface. */
.hrv-card,
.products-grid .product-item-info,
.products-list .product-item-info,
.block.related .product-item-info,
.block.upsell .product-item-info,
.block.crosssell .product-item-info,
.widget-product-grid .product-item-info {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--hrv-surface);
  border: 1px solid var(--hrv-border);
  border-radius: var(--hrv-radius);
  padding: 14px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
  height: 100%;
}
/* THE FOURTH LIST. The shell rule lists 7 selectors, the width rule 6, the media rule 6 — and
   this one listed 4, missing `.block.related`, `.block.upsell` and `.block.crosssell`. It was
   covered only by an accident of markup: core nests those blocks' cards inside `.products-grid`,
   so removing just that member drops hover coverage on all 8 PDP block cards to NONE. Measured,
   not reasoned. Benign today, latent tomorrow — and it is the fourth narrowing of exactly the
   family this task's own commit message is about, found by the S8 gate rather than by the fix. */
.hrv-card:hover,
.products-grid .product-item-info:hover,
.products-list .product-item-info:hover,
.widget-product-grid .product-item-info:hover,
.block.related .product-item-info:hover,
.block.upsell .product-item-info:hover,
.block.crosssell .product-item-info:hover {
  border-color: var(--hrv-border-strong);
  box-shadow: var(--hrv-shadow-card);
  transform: translateY(-2px);
}

/* THE CARD MUST FILL ITS GRID TRACK (T17, WIDENED TO EVERY SHELL SURFACE BY basil-T10).

   MEASURED: the grid track (<li>) was 283px while `.product-item-info` inside it computed
   width: 152px — Blank sizes this card to the image role, not to its column, so four cards
   sat as narrow slabs in wide tracks.

   RETRACTED (basil-T10 S1 review, two reviewers independently), original wording kept:
   "Establishing that it was a WIDTH DECLARATION and not a shrink-to-fit layout mode took one
   experiment rather than a guess: forcing width:100% inline returned 283, so a declaration was
   winning and no !important was involved." THAT EXPERIMENT CANNOT DISTINGUISH THE TWO CASES.
   A shrink-to-fit box given `width:100%` ALSO grows — it resolves against its containing block
   — so "it moved" is true either way. And an INLINE style outranks every author rule, so the
   experiment could never show that the shipped rule wins the cascade; this repo has a recorded
   case of a proof that passed only because the injected copy carried an `!important` the
   committed rule lacked. The real evidence is the RULE ENUMERATION below.

   RETRACTED: "(0,3,0) here beats Blank's (0,1,0)/(0,2,0)." That was true of the version of this
   rule scoped to `.checkout-cart-index`, which no longer exists; the rule it sat above has been
   (0,2,0) since that scope was removed, and is now a mixed list — see the table below.

   This is the repo's recorded "a grid reskin must reset inherited float/width" lesson —
   native markup restyled into a grid collapses unless the base sizing is reset. It stayed
   invisible in the first screenshot because the images were ALSO undersized, so the card
   looked deliberately small rather than broken.

   RETRACTED: the paragraph that used to close this block read "UNSCOPED for the same reason as
   the shell above: this is bestsellers.phtml's markup, and it renders on Home as well as the
   cart. A CatalogWidget grid is unaffected — its card already fills its track — so this only
   ever repairs the surface that is broken." Every sentence is TRUE and the conclusion it invites
   is false: it repaired the one surface that was LOOKED AT. The shell rule above hands the card
   a background, a border, an 18px radius and 14px of padding to SEVEN selectors, and only ONE of
   them carried a width, so the other six fell to whatever Blank resolves.

   ===========================================================================================
   THE COMPETITORS, DERIVED FROM THE STYLESHEET TEXT — EIGHT, NOT THREE
   ===========================================================================================
   RETRACTED: an earlier version of this block said "there are exactly three width declarations
   in Blank that reach this element" and listed `.product-item-info`, `.page-products ...` and
   `.block.widget ...`. THAT WAS A SAMPLE, NOT A DENOMINATOR: it was assembled from the three
   routes that happened to be probed, which is the failure CLAUDE.md 3.4 exists to prevent.
   Re-derived by reading the SERVED bundles instead of the pages, splitting on the rule
   terminator so a minified line does not hide members:

     styles-m.css  [data-content-type=products][data-appearance=carousel] ...  width: auto   (0,3,0)
     styles-m.css  .product-item-info                          width: 152px  max-width:100%  (0,1,0)
     styles-m.css  .page-products .product-item-info                        width: 240px  (0,2,0)
     styles-m.css  .sidebar .product-items .product-item-info               width: auto   (0,3,0)
     styles-m.css  .block-wishlist-info-items .product-item-info            width: auto   (0,2,0)
     styles-m.css  .block.widget .product-item-info                         width: auto   (0,3,0)
     styles-m.css  .wishlist-index-index .product-item-info                 width: auto   (0,2,0)
     styles-l.css  .wishlist-index-index .product-item-info                 width: 240px  (0,2,0)

   So a shell member falls to Blank's 152px FLOOR exactly when none of the seven more specific
   rules matches — i.e. on a PDP, where `.block.related`, `.block.upsell` and `.block.crosssell`
   live. MEASURED on 0.1.249-local at 1440, basil:

       /oil-002-cold-pressed-olive-oil.html   .block.related  card 152px in a 332px track  x3
       /pro-advanced-spark-1001.html          .block.related  card 152px in a 332px track  x4
       /pro-advanced-spark-1001.html          .block.upsell   card 152px in a 229px track  x4

   54% dead space per card on the PDP — the highest-traffic page type in the store, and the
   IDENTICAL shape T17 recorded for Home's Bestsellers row and fixed for one selector.

   ===========================================================================================
   SPECIFICITY: A MIXED LIST, NOT A UNIFORM TIE
   ===========================================================================================
   RETRACTED: "SPECIFICITY IS A TIE, ON PURPOSE ... These are (0,2,0), exactly like
   `.page-products .product-item-info` ... Written as a tie rather than escalated with a third
   class." Three of the six selectors ALREADY carry a third class, so the paragraph described a
   design intent the code beside it did not have. In a comma list each selector carries its own
   specificity (there is no `:is()` here):

       .products-grid .product-item-info        (0,2,0)   ties  .page-products ... (0,2,0)
       .products-list .product-item-info        (0,2,0)   ties  .page-products ... (0,2,0)
       .widget-product-grid .product-item-info  (0,2,0)   LOSES to .block.widget ... (0,3,0)
       .block.related .product-item-info        (0,3,0)   no competitor above (0,1,0)
       .block.upsell .product-item-info         (0,3,0)   no competitor above (0,1,0)
       .block.crosssell .product-item-info      (0,3,0)   no competitor above (0,1,0)

   The `.widget-product-grid` row LOSING is not a regression and not new: `.block.widget` sets
   `width: auto`, which for a block-level flex child fills the track anyway, and Home measures
   332/332 with and without this rule.

   WHERE IT IS A TIE, SOURCE ORDER DECIDES. Magento groups head assets by their `media` value.
   Read off the rendered <link> list on /olive-oil-vinegar.html at 1440:

       1  styles-m.css      media=all
       3  _category.css     media=all
       4  styles-l.css      media=screen and (min-width: 768px)
       21 _product-card.css media=all

   RETRACTED: "3  _category.css  media=(none)  <- registered WITHOUT a media attribute", and with
   it the sentence that made the `media` attribute the MECHANISM of this ordering. The row was
   presented as a reading off the RENDERED link list and it is not one. The XML genuinely omits
   the attribute — `catalog_category_view.xml` has a bare `<css src="css/pages/_category.css"/>`
   — but the renderer supplies a default, so the served bytes carry `media="all"` exactly like
   `styles-m.css` and this file. Source, read from the 2.4.9 vendor tree in the running pod
   rather than inferred:

       View/Page/Config/Renderer.php:398
         return ' rel="stylesheet" type="text/css" ' . ($attributes ?: ' media="all"');

   So both files sit in the SAME media group, and the attribute cannot be what separates index 3
   from index 21. The ordering symptom is real and still worth its register row; the cause named
   here was wrong, and the prescribed remedy — "one omitted attribute away" — would have told the
   next maintainer to add an attribute that is already in the output. A no-op fix is worse than
   an open bug, because it closes the row.

   `_product-card.css` is declared `media="all"` in `Magento_Theme/layout/default.xml:54`.
   RETRACTED: this sentence used to continue "which is what lands it after Blank's bundle" —
   re-asserting, as a consequence, the very mechanism retracted forty words above. It is refuted
   by this file's own table: `_category.css` ALSO renders `media="all"` and lands at index 3,
   BEFORE `styles-l.css`. So the attribute is not what orders these files, and what does is still
   unknown. The eleventh instance in this task family of a claim corrected in one place and
   surviving as its own consequence in another — caught by the S8 gate, one layer below where
   Copilot caught the same shape in register #177's status cell. The tie is still won on source
   order; only the explanation for that order is withdrawn.

   The engine agrees on the outcome regardless: on the category page it resolves 358.656px
   against a 359px track with no `!important` anywhere in the contest.
   `catalog_category_view.xml:36` registers `_category.css` with no media attribute IN THE XML,
   and it lands at index 3, BEFORE `styles-l.css` — a real pre-existing ordering bug on that
   route. The cause is NOT the attribute (see the retraction above; the rendered link is
   `media="all"`), so the remedy is unknown and the register row must not prescribe one until it
   is measured. FILED, not fixed here: it moves a CLOSED page's whole stylesheet in the cascade.

   ===========================================================================================
   BLAST RADIUS, ENUMERATED AND MEASURED
   ===========================================================================================
   RETRACTED: "EMPTY BY CONSTRUCTION (basil-T10). Every `.product-item-info` surface the shell
   rule above lists now carries the width, so there is no list of 'which surfaces did we
   remember'." The FIRST half is true — the width list and the shell list are now the same six
   selectors, so no surface can be forgotten. The SECOND half over-claims: carrying the
   declaration is not the same as winning it, and three members are overridden by design.

   Every row below is a live counterfactual on 0.1.249-local at 1440 — the rule was deleted from
   the CSSOM, re-measured, and re-inserted, with the restored reading asserted equal to the
   original (`.rig/t10-shell-census.js`, `.rig/t10-img-fill.js`):

     SURFACE / ROUTE                      WITHOUT     WITH       VERDICT
     .block.related   PDP                 152/332     332/332    FIXED (the defect this task found)
     .block.upsell    Porto PDP           152/229     229/229    FIXED
     .block.crosssell anywhere            renders 0x  --         NEVER RENDERS: catalog_product_link
                                                                  holds 0 crosssell rows (measured)
     .products-grid   category            240/359     359/359    CHANGED - improved, T4 is CLOSED
     .products-grid   search grid         240/265     265/265    CHANGED - improved, T7 is CLOSED
     .products-grid   Home/CatalogWidget  332/332     332/332    no-op (Blank width:auto already fills)
     .products-grid   wishlist            332/332     332/332    no-op - _wishlist.css wins at (0,5,0)
     .widget-product-grid Home, empty cart 332/332    332/332    no-op (T17's rule, now folded in)
     .products-list   category list       1108/1108   1108/1108  no-op - _category.css:444 wins (0,4,0)
     .products-list   SEARCH list         240/1108    1108/1108  CHANGED - see below

   SEARCH LIST MODE IS THE ONE THAT NEEDED LOOKING AT, and it was undeclared in the first
   version of this block. `_search.css` carries no `.products-list` rules at all, so unlike the
   category route nothing overrides this member there and the card goes from Blank's 240px to
   the full 1108px row. That is the right shape for a list row and matches what `_category.css`
   already produces. It also EXPOSES a pre-existing defect rather than causing one: the product
   image on search list mode computes 0x0 BOTH WITH AND WITHOUT this rule (measured both ways in
   one run, and looked at). Filed as a register row against T7's page; not fixed from inside a
   T10 task.

   `.hrv-card` IS DELIBERATELY NOT IN THIS LIST, and the reason first written here was invented.
   RETRACTED: "it is the theme's own markup and appears in flex rails where `width:100%` is not
   the right answer." There are no flex rails. `.hrv-card` occurs in exactly ONE file in the repo
   — this one — and in ZERO `.phtml`, `.xml`, `.js` or `.php`; the theme's own carried-items
   ticket records "`.hrv-card` matches nothing on Home". It is left out because it currently
   matches no rendered markup, so widening it would be speculative rather than measured.

   BOTH INSTRUMENTS ARE COMMITTED, because this comment quotes numbers from them and a quoted
   number a reader cannot reproduce is an assertion:
       node scripts/harvest/render-rig.js <route> --width 1440 \
            --eval-file .rig/t10-shell-census.js    # every shell surface, card vs track
       node scripts/harvest/render-rig.js <route> --width 1440 \
            --eval-file .rig/t10-width-cause.js     # the matching width rules, in cascade order
   NOTE that `t10-width-cause.js` prints `332 -> 332` post-fix: its forced-width step is a
   BEFORE-state instrument, and the 152 -> 332 figure it produced is reproducible only against
   the pre-fix tree. The post-fix reproduction is the counterfactual in `.rig/t10-img-fill.js`,
   which deletes THIS rule from the live CSSOM and restores it. */
.products-grid .product-item-info,
.products-list .product-item-info,
.block.related .product-item-info,
.block.upsell .product-item-info,
.block.crosssell .product-item-info,
.widget-product-grid .product-item-info {
  width: 100%;
}

/* THE MEDIA MUST GROW WITH THE CARD — AND ON ONE SURFACE IT DID NOT.

   A reviewer predicted that widening the card would letterbox its photo, because Magento puts an
   inline `style="width:NNNpx"` on `.product-image-container` and the theme only reset it for
   `.widget-product-grid` (below) and for `.hrv-page-category .products-grid` (_category.css).
   MEASURED ON FIVE SURFACES, and the stated MECHANISM is wrong everywhere — there is no inline
   width on any of them (`containerInlineWidth: null`, 5/5) — while the OUTCOME is real on
   exactly one. Card width minus padding+border is 30px, so a healthy gap is ~30:

       .block.related   PDP         card 332  img 300  gap  32   (image role is 300px; fine)
       .products-grid   PDP         card 332  img 300  gap  32
       .products-grid   category    card 359  img 329  gap  30
       .products-grid   search      card 265  img 235  gap  30
       .widget-product-grid Home    card 332  img 302  gap  30
       .block.upsell    Porto PDP   card 229  img 140  gap  89   <- LETTERBOXED, and I caused it

   `etc/view.xml` defines `related_products_list` at 300x400 but NO `upsell_products_list`, so
   Magento falls back to its 140px default and nothing stretched it. Before this task the upsell
   card was 152px, so a 140px image filled it and the gap was invisible; widening the card to its
   229px track is what exposed it. Confirmed by eye on /pro-advanced-spark-1001.html: four wide
   white boxes each with a small image in the top-left corner.

   THE SAME SIX SELECTORS AS THE SHELL AND THE WIDTH, for the same reason: a surface given the
   card treatment must be given all of it. Measured by injecting this exact text and re-reading
   (`.rig/t10-media-fix-preview.js`, restore asserted): it changes ONLY the two PDP surfaces —
   upsell 140 -> 199 and related 300x400 -> 302x302 — and is a no-op on category, search, Home
   and the widget grid, which already carry equivalent rules of their own.

   THE FAILURE MODE THIS INTRODUCES, declared rather than discovered later: the upsell image is
   now UPSCALED 1.42x from a 140px source, so it will read soft. The correct fix for the SOURCE
   is an `upsell_products_list` role in `etc/view.xml` — deliberately NOT done here, because a
   new image role 404s every image until `catalog:images:resize` has run (T14 has that exact
   incident on record) and this tenant's deploy path runs `setup:upgrade`, `di:compile` and
   `static-content:deploy` only. A soft image is worse than a sharp one and much better than a
   59px void; a 404 is worse than both. Filed.

   Specificity mirrors the existing widget block below: Magento's generated
   `.product-image-container-N` is (0,1,0), these are (0,2,0)/(0,3,0), so no `!important`. */
.products-grid .product-image-container,
.products-list .product-image-container,
.block.related .product-image-container,
.block.upsell .product-image-container,
.block.crosssell .product-image-container,
.widget-product-grid .product-image-container {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  height: auto;
}
.products-grid .product-image-wrapper,
.products-list .product-image-wrapper,
.block.related .product-image-wrapper,
.block.upsell .product-image-wrapper,
.block.crosssell .product-image-wrapper,
.widget-product-grid .product-image-wrapper {
  padding: 0;
  height: auto;
  aspect-ratio: 1 / 1;
  display: block;
  overflow: hidden;
  border-radius: var(--hrv-radius-md);
}
.products-grid .product-image-photo,
.products-list .product-image-photo,
.block.related .product-image-photo,
.block.upsell .product-image-photo,
.block.crosssell .product-image-photo,
.widget-product-grid .product-image-photo {
  position: static;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* MEDIA SIZING FOR THE BARE WIDGET GRID (T17).

   `.product-item-photo` below already declares aspect-ratio 1/1, but the markup nests TWO
   more elements inside it — `.product-image-container` and `.product-image-wrapper` — and
   Magento generates a per-product rule sizing the container to the view.xml role (measured:
   the image rendered 152x203 inside a 283px card, letterboxed, with a white band above it).

   Specificity is deliberate, not incidental: Magento's generated
   `.product-image-container-N` is (0,1,0); these are (0,2,0), so no !important is needed.

   _home.css carries a NEARLY-equivalent block at (0,3,0), which therefore wins on Home. An
   earlier version of this comment called the two "identical declarations, so the rendering is
   the same either way". THAT IS FALSE, and a two-line grep disproves it: this rule clips the
   media with --hrv-radius-md while _home.css uses --hrv-radius-sm, and _base.css sets
   md = 14px against sm = 12px. The same component therefore clips at 14px on the cart and
   12px on Home / T2 / T3 — and the old advice to "delete Home's copy" would have moved Home
   from 12 to 14 silently, which is exactly the kind of change that should never arrive as a
   side effect.

   WHICH ONE IS CORRECT, decided against the artboards rather than by preference: every
   product-card media in the comps is drawn at 14px — 8 of 8 in Home.dc.html, and the same in
   T17's. So 14px (--hrv-radius-md) is right and is what this rule ships. _home.css, _t2.css
   and _t3.css render 12px against their own comps; correcting them is a three-page change to
   CLOSED pages needing its own verification pass, so it is RAISED here, not taken from inside
   a cart task.

   SUPERSEDED, AND THE RULES ARE DELETED RATHER THAN LEFT AS A SECOND COPY. This block used to
   re-declare `.widget-product-grid .product-image-container` / `-wrapper` / `-photo` with bodies
   BYTE-IDENTICAL to the six-selector rules above, which already list `.widget-product-grid`.
   Two mutable homes for one set of declarations is the exact class this file keeps paying for —
   change `--hrv-radius-md` in one copy and the two silently diverge. Zero behavioural change:
   the surviving rules are the same declarations at the same specificity. The reasoning above is
   kept because it is not duplicated anywhere, in particular the artboard decision (every
   product-card media is drawn at 14px, 8 of 8 in Home.dc.html) and the raised-not-taken finding
   that `_home.css`, `_t2.css` and `_t3.css` render 12px against their own comps. */

/* ---------- Image ---------- */
.hrv-card-media,
.product-item-photo {
  display: block;
  position: relative;
  border-radius: var(--hrv-radius-md);
  overflow: hidden;
  background: color-mix(in oklab, var(--hrv-text) 5%, var(--hrv-surface));
  aspect-ratio: 1 / 1;
}
.hrv-card-media img,
.product-item-photo .product-image-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.35s ease, opacity 0.25s ease;
}
.hrv-card:hover .hrv-card-media img,
.product-item-info:hover .product-item-photo .product-image-photo {
  transform: scale(1.04);
}
/* Hover image swap (Harvest cards may ship a second .hrv-card-media-hover img). */
.hrv-card-media-hover {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.hrv-card:hover .hrv-card-media-hover { opacity: 1; }

/* ---------- Badges ---------- */
.hrv-badge {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 1;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  border-radius: var(--hrv-radius-sm);
  padding: 5px 10px;
  background: var(--hrv-accent);
  color: var(--hrv-accent-text);
}
.hrv-badge--sale { background: var(--hrv-error); }
.hrv-badge--sold-out { background: var(--hrv-text-muted); }

/* ---------- Details ---------- */
.hrv-card-body,
.product-item-details {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Rating — accent stars. Works with Harvest's .hrv-rating and Magento's
   .rating-result (which uses a % width overlay of ★ glyphs). */
.hrv-rating { color: var(--hrv-accent); font-size: 13px; letter-spacing: 2px; }
.product-reviews-summary .rating-summary .rating-result > span::before,
.product-reviews-summary .rating-summary .rating-result::before { color: var(--hrv-accent); }

.hrv-card-name,
.product-item-name,
.product-item-name > a.product-item-link {
  font-size: 14px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--hrv-text);
  text-decoration: none;
}
.product-item-name > a.product-item-link:hover,
.hrv-card-name:hover { color: var(--hrv-accent); }

/* Price — Bricolage Grotesque display face, weight 600. */
.hrv-card-price,
.price-box .price {
  font-family: var(--hrv-font-display);
  font-weight: 600;
  font-size: 16px;
  color: var(--hrv-text);
}
.hrv-card-price--old,
.price-box .old-price .price,
.price-box .special-price ~ .old-price .price {
  font-family: var(--hrv-font-body);
  font-weight: 400;
  font-size: 12.5px;
  text-decoration: line-through;
  color: var(--hrv-text-subtle);
}
.price-box .special-price .price { color: var(--hrv-error); }

/* ---------- Actions row ---------- */
.hrv-card-actions,
.product-item-info .product-item-inner .actions-primary,
.product-item-info .product-item-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 4px;
}
.hrv-card-actions .hrv-btn { flex: 1; }
/* The native actions row must be allowed to WRAP. Its secondary slot holds up to
   three 40px icon buttons (wishlist, compare and the client-injected quick-view
   trigger) = 117px of `white-space: nowrap` content, which cannot sit beside the
   add-to-cart button on a ~214px-wide grid card. While this row was `nowrap`, the
   secondary slot could not shrink below min-content, so it pushed past the card
   edge and gave the whole PAGE a 25px horizontal scrollbar (measured on
   /audio.html at 1280px). Wrapping drops the icon row onto its own line instead. */
.product-item-info .product-item-actions {
  flex-wrap: wrap;
}
.product-item-info .product-item-inner .actions-primary {
  min-width: 0; /* let the add-to-cart column shrink rather than overflow */
}
/* Secondary card actions (wishlist/compare/quickview) hidden until hover on
   desktop; always visible on touch (no hover) via the media query below. */
.hrv-card-actions-secondary {
  display: flex;
  gap: 6px;
}
.product-item-info .actions-secondary {
  opacity: 0;
  transition: opacity 0.15s ease;
}
.product-item-info:hover .actions-secondary { opacity: 1; }

/* Make Magento's add-to-cart fill the row like the comp. */
.product-item-info .actions-primary .action.tocart { width: 100%; }

/* ---------- Quick View trigger (T13) — DELETED BY basil-T13, register #168(a) ----------
   Six rules (~43 lines) styling `.hrv-quick-view-trigger` + `__icon` + `__label` stood here
   and matched nothing, on any route, ever.

   RETRACTED — the deleted block's own comment said the button is "Injected client-side by
   web/js/quick-view.js into each card's secondary-actions slot". BOTH HALVES ARE FALSE, and
   the second one was false even for the button that IS injected. `quick-view.js` injects
   `hrv-quickview-trigger` (no hyphen between quick and view), and it appends it to
   `.product-item-info` directly — measured, not read: on /accessories.html the card's element
   children are `product photo product-item-photo`, `product details product-item-details`,
   `hrv-quickview-trigger`. There is no `.actions-secondary` in that list.

   THE TWO NAMES MUST NOT BE REUNITED. This generation describes a 40x40 ICON-ONLY button with
   a visually-hidden label; `components/_quick-view.css` ships a 44px-minimum TEXT button. They
   are different designs, so "fixing the typo" would silently replace the shipped control.
   The live design is the one with a tap target that meets CLAUDE.md 11.4's floor.

   Verified dead before deletion by a comment-stripping emitter census across every
   `.phtml`/`.js`/`.xml`/`.html` in the theme (0 emitters for all three names), and live with
   the modal OPEN and populated: `document.querySelectorAll('.hrv-quick-view-trigger').length`
   === 0. The note above about reserving room in the actions row beside wishlist/compare is
   left alone: it is about `.actions-secondary`, which is real. */

/* ---------- Grid helper (Harvest-authored rails/grids) ----------
   T17 ADDED THE CART SELECTOR TO THIS RULE RATHER THAN WRITING ITS OWN.

   The empty cart's "Popular right now" row reuses Magento_Theme::home/bestsellers.phtml,
   which emits `.product-items.widget-product-grid` — a class whose every rule in _home.css
   is scoped `.cms-index-index.hrv-page-home`, so on the cart that markup would arrive with
   no grid at all. The fix could have been a fresh 4-column rule in _cart.css; it is a
   selector on THIS rule instead, so the column counts and both breakpoints keep exactly one
   definition and cannot drift apart later. What the comp asks of that section —
   repeat(4,1fr), gap 16px — is already what this helper declares.

   T8 JOINS THE SAME RULE, FOR THE SAME REASON. The search page's zero-result state renders the
   comp's "Popular right now" row from the SAME bestsellers.phtml, so it arrives as the same
   `.product-items.widget-product-grid` with the same hard-set `products_count: 4`. Adding the
   selector here rather than a third 4-column rule in `_search.css` keeps one definition of the
   column counts AND of the 4->2 tablet step below — which is the step that exists because a
   3-column stage orphans the fourth card, a property of the count being 4 and not of the cart.
   Following the decision this comment records, not just the line it produced. */
.hrv-product-grid,
.checkout-cart-index .widget-product-grid,
.hrv-page-search .widget-product-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}

/* ---------- Responsive ---------- */
@media (max-width: 1023px) {
  .hrv-product-grid { grid-template-columns: repeat(3, 1fr); }

  /* THE WIDGET GRID GOES 4 -> 2 HERE, NOT 4 -> 3 -> 2.
     MEASURED at 768 with the 3-column step: 229.328px x3 with rows [3, 1] — one card alone
     beside two-thirds of an empty row. `products_count` is hard-set to 4, so that orphan is
     not an edge case, it is every tablet load.
     Home already takes this exact markup straight to 2 at this exact breakpoint
     (_home.css, S7-Home c4), so this matches the theme's existing answer rather than
     inventing a third one. `.hrv-product-grid` keeps its 3-column step above: it is the
     generic rail helper with other consumers, and its item counts are not fixed at 4.

     T8's row is the SAME markup at the SAME fixed count of 4, so it takes the same step. The
     orphan measured above is a property of that count, not of the cart, which is why this
     selector joins the rule rather than getting a step of its own. */
  .checkout-cart-index .widget-product-grid,
  .hrv-page-search .widget-product-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767px) {
  /* The product NAME link is its own tap target, not just a label on a tappable card: it
     measured 37px on all eight Home cards. The image link above it is comfortably large, but a
     shopper aiming at the title is aiming at the title. */
  .product-item-link { min-height: 44px; display: inline-flex; align-items: center; }
  .hrv-product-grid,
  .checkout-cart-index .widget-product-grid,
  .hrv-page-search .widget-product-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  /* No hover on touch — keep secondary actions visible. */
  .product-item-info .actions-secondary { opacity: 1; }

  /* N-10 — TAP TARGETS. Measured on the live build at 375, not taken from the note:
       .action.tocart      120 x 43   <- ONE PIXEL under
       .action.towishlist   35 x 35   <- well under
     44px is the threshold `_home.css:434` already enforces on the hero CTAs, so this is the
     theme disagreeing with itself rather than a new standard being invented here.

     THE 43px IS THE INTERESTING ONE. A button one pixel short reads as correct in every
     screenshot and in any review that eyeballs it; only measuring the box finds it. It comes
     from `padding: 12px 22px` on the 767 button rule meeting a 1.2 line-height — nobody chose
     43. `min-height` fixes it without touching the padding rhythm the comp specifies.

     `.action.tocompare` measures 0x0 here (hidden), so it has no tap target to fix — a
     zero-size control is not a small control, and padding it would CREATE a target where the
     design deliberately has none.

     NOT changed: `.product-item-link`, the product name, at 113 x 36. It is an inline text
     link, which WCAG 2.5.8 explicitly excepts from the target-size minimum. Padding it to 44
     would put a large invisible hit area over neighbouring text. Recording the decision
     because "everything under 44" is the obvious sweep and it would be wrong. */
  .product-item-info .actions-primary .action.tocart {
    min-height: 44px;
  }

  .product-item-info .actions-secondary .action.towishlist,
  .product-item-info .actions-secondary .action.tocompare:not([style*="display: none"]) {
    min-width: 44px;
    min-height: 44px;
  }
}

/* ==========================================================================
   COMP PARITY FOR THE CARD THE WHOLE THEME REPEATS  (wheat-Home / wheat-T2)
   ==========================================================================

   FOUND DURING wheat-T2's S8, AND IT REOPENED wheat-Home, WHICH I HAD CLOSED
   AN HOUR EARLIER. Home's artboard and T2's artboard ship the IDENTICAL card,
   so the gap was on the closed page all along.

   My S8 measured sections, headings, overflow at three viewports, dead
   controls, and translatable strings. All green. All true. None of them
   looking at the card. *** A GREEN BOARD IS NOT COVERAGE — the card had no
   instrument, so it had no verdict, and I read the silence as a pass. ***

   The comp's card: image 1/1 r14 - name 14.5/700 - a 12px per-unit line -
   bottom row [ price 17/700 Bricolage | 36x36 round accent "+" ].

   THE IMAGE RADIUS WAS NEVER WRONG, AND I FILED IT ANYWAY. My first probe read
   border-radius off the <img> element, got 0px, and I listed it as a defect.
   The radius lives on .product-item-photo with overflow:hidden, and
   --hrv-radius-md is already 14px - exactly the comp. *** A MEASUREMENT OF THE
   WRONG ELEMENT IS A FINDING ABOUT THE PROBE, NOT ABOUT THE PAGE. ***

   NOT FIXED, DECLARED: the "New" badge and the per-unit price line. Both need
   product data this catalogue does not carry (news_from_date is unset on every
   product; there is no per-unit attribute at all). Rendering them would mean
   inventing product facts on a storefront, which is worse than a fidelity gap
   - the same reasoning the T2 layout already records for the staples list.
   .hrv-badge is styled above and waits for a block that emits it.
   ========================================================================== */

/* Name - comp 14.5/700. The theme shipped 14/500 and its own rule WON, so this
   is a plain value change and not a cascade fight. */
.hrv-card-name,
.product-item-name,
.product-item-name > a.product-item-link {
  font-size: 14.5px;
  font-weight: 700;
}

/* Price - THE THEME'S DECLARED 16px/600 NEVER APPLIED TO A SINGLE CARD.
   Luma's styles-m.css carries `.product-item .price-box .price` at (0,3,0);
   the theme's `.price-box .price` above is (0,2,0) and loses, so every card
   has been rendering Luma's 1.4rem/700 while the theme's file says 16px/600.
   Enumerated with a cascade probe that ranks every matching rule, not guessed
   - a declaration check cannot see which rule won. Same family as N-16 and the
   Blank-beats-theme finding on the account pages.
   Answered at (0,4,0) so the fix does not depend on source order, which gets
   no vote until specificity ties. */
.product-item .product-item-info .price-box .price {
  font-family: var(--hrv-font-display);
  font-size: 17px;
  font-weight: 700;
  color: var(--hrv-text);
}

/* Bottom row - the comp puts price and add control on ONE line, price left and
   the round button right. Core stacks them: .price-box, then .product-item-inner
   wrapping .product-item-actions wrapping .actions-primary / .actions-secondary.

   `display: contents` on the two wrappers lets the real controls join the
   details grid, so the row can be composed WITHOUT a template override. The
   secondary slot (wishlist / compare) is deliberately NOT pulled onto the price
   row: it holds up to three 40px icon buttons and would blow out the auto
   column. It spans its own row underneath, where the existing wrap rule left it. */
.product-item-info .product-item-details {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  column-gap: 10px;
  row-gap: 6px;
}
.product-item-info .product-item-inner,
.product-item-info .product-item-details .product-item-actions {
  display: contents;
}
.product-item-info .product-item-details > .product-item-name,
.product-item-info .product-item-details .actions-secondary {
  grid-column: 1 / -1;
}
.product-item-info .product-item-details > .price-box {
  grid-column: 1;
  grid-row: 2;
  margin: 0;
}
.product-item-info .product-item-details .actions-primary {
  grid-column: 2;
  grid-row: 2;
}

/* The add control itself. THE LABEL STAYS IN THE ACCESSIBILITY TREE - clipped,
   not removed, not font-size:0 - because an icon-only button with no accessible
   name is a worse defect than the one being fixed. */
.product-item-info .actions-primary .action.tocart {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  min-width: 0;
  padding: 0;
  border: none;
  border-radius: var(--hrv-radius-circle);
  background: var(--hrv-accent);
  color: var(--hrv-accent-text);
}
.product-item-info .actions-primary .action.tocart > span {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}
.product-item-info .actions-primary .action.tocart::after {
  content: '+';
  font-size: 19px;
  font-weight: 600;
  line-height: 1;
}
.product-item-info .actions-primary .action.tocart:hover {
  background: var(--hrv-accent-hover);
}

/* N-10 IS NOT BEING REVERSED. That fix raised the add button to 44 and was
   measured AT 375 - a touch viewport. The comp's 36 is a DESKTOP figure, and
   36x36 still clears WCAG 2.2 AA (2.5.8 asks 24x24). So: comp size on desktop,
   44 on touch. The existing max-width:767 block below already sets
   min-height:44px; width must be pinned there too, or the button would render
   36 wide and 44 tall. Stated here because silently shrinking a closed
   accessibility fix to chase a comp is exactly the kind of change that should
   never happen without a sentence explaining it. */
@media (max-width: 767px) {
  .product-item-info .actions-primary .action.tocart {
    width: 44px;
    height: 44px;
  }
}

/* A FIX THAT REPAIRED ONE PROPERTY BROKE A SECOND, AND THE RASTER IS WHAT SHOWED IT.
   Every number above was right - name 14.5/700, price 17/700, button 36x36 two pixels off
   the price row, comp-matching at 1440, 768 and 375 - and the card still looked wrong:
   roughly 50px of empty space below the price row.

   MEASURED RATHER THAN GUESSED: .actions-secondary is opacity:0 and position:static, so it
   is INVISIBLE AND IN FLOW - 35px of reserved height it never shows, plus the card's 14px
   padding. That condition predates this change; what changed is that the add button used to
   sit on that row and now sits beside the price, so reserved space became VISIBLE dead space.
   Pre-existing cause, defect introduced here, mine to fix.

   Pinned to the card's top-right, over the image, which is where a hover-revealed wishlist
   normally lives and is closer to the comp than an empty strip: the artboard's card has no
   secondary actions at all and ends immediately after the price row. On touch the block
   above deliberately sets opacity:1, and those controls are real reachable targets that
   SHOULD hold their own row rather than sit unrevealable on top of the image.

   THE QUERY IS `hover: hover` AND `min-width: 768px`, AND THE SECOND HALF IS THERE BECAUSE
   OF A MEASUREMENT PROBLEM, NOT A DESIGN ONE. `hover: hover` alone is the semantically
   correct test - but HEADLESS CHROME REPORTS `hover: hover` AT EVERY WIDTH, so the rig
   measured the overlay at 375 while a real phone would have got the in-flow version. The
   evidence and the shipped behaviour would have disagreed and no probe available here could
   have caught it. Adding the width test makes the two agree, matches the 767 breakpoint the
   rest of this file already uses, and is strictly better than `hover: hover` alone for the
   case BOTH miss: a TOUCH TABLET at 1024 has no hover, so under the old query it would have
   received an overlay it can never reveal. Now it keeps the in-flow row.
   *** A RULE I CANNOT MEASURE IS A RULE I CANNOT VERIFY, AND AN UNVERIFIABLE RULE IN A
   COMPONENT SHARED BY 434 PAGES IS WORSE THAN A BLUNTER ONE THAT I CAN. *** */
@media (hover: hover) and (min-width: 768px) {
  .product-item-info .product-item-details .actions-secondary {
    position: absolute;
    top: 26px;
    right: 26px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
}
