/* ==========================================================================
   OstuGrupp — D3 · TOOTEKAART (card-d3.css)

   The design pack calls this component D3 and gives it a whole section:
   design_handoff_ostugrupp_system/Ostugrupp System Sample 01.dc.html:1140-1299
   ("Neli varianti, üks ehitus") plus README.md:189 ("D3 tootekaart — 4 varianti
   (laos · otsas · soodus · uus), töötab 155-320px laiuses, 80-täheline nimi
   lõigatakse täpselt kahe rea pealt"). The theme took the pack's tokens and its
   icons and skipped the components. This file is D3, and only D3.

   It is a PURE STYLE LAYER. It renders nothing, hooks no template, prints no
   markup and invents no text. Every selector below already exists in the DOM
   that woocommerce/content-product.php + inc/woocommerce.php + inc/shop-extras.php
   emit today — verified against the LIVE page, not against the template:
   curl https://ostugrupp.ee/pood/ on 2026-08-14 returned 12 cards whose classes
   are exactly `li.product-card.product...instock...sale`, whose title is
   `h2.woocommerce-loop-product__title > span.product-card__title-text`, and
   whose price is `span.price > del[aria-hidden] + span.screen-reader-text +
   ins[aria-hidden] + span.screen-reader-text`.

   ------------------------------------------------------------------------
   THREE MOMENTS, NOT FORTY IMPROVEMENTS
   ------------------------------------------------------------------------
   1 · THE PLATE. The media box is `background: var(--muted)` today
       (woocommerce.css:125) — #f1f5f9. The catalogue's photos are white.
       MEASURED, not assumed: the ten distinct product thumbnails on live
       /pood/ were downloaded and their corner pixels sampled; nine are exactly
       #ffffff at all four corners (the tenth is a full-bleed photo). So on
       ~90% of 3472 products the shopper sees a white photo sitting inside a
       grey ring — a tray, not a product. The pack's media box is
       `background: var(--surface)` with `border-bottom: 1px solid var(--border)`
       (sample:1150), and the pack's whole colour thesis is
       "--surface-raised:#ffffff; sama värv — kõrgust annab VARI, mitte toon"
       (tokens.css:17). White plate, hairline, and the elevation moves into the
       shadow. The inset drops 14px → 12px (a real space token), so the photo
       is also physically larger on every card.

   2 · THE PRICE. WooCommerce prints the OLD price first in the DOM, so today a
       sale card reads "202,50 € 99,00 €" left to right — the number the shopper
       must not act on comes first, at the start of the line. The pack draws it
       the other way round (sample:1222-1225): new price large, red, tabular; old
       price small, struck, beside it on the same baseline. Both del and ins are
       `aria-hidden="true"` on live and WooCommerce supplies the real reading
       order in two `.screen-reader-text` spans, which are `position:absolute`
       (main.css:155) and therefore NOT flex items — so this visual reorder
       changes nothing for a screen reader. The price also moves onto the pack's
       --price-md recipe (Space Grotesk 700 20/24, -.015em, tabular) instead of
       three different sizes at three widths (22 / 19 / 18 / 18-on-rails).

   3 · THE NAME BOX. Today: `font-size:14.5px; line-height:1.35; min-height:38px`
       (woocommerce.css:147-161). 14.5 × 1.35 = 19.575px per line, so two lines
       of ink are 39.15px while a one-line title is floored at 38px — every row
       in the grid is 1.15px out of true, which is why prices and buttons never
       quite line up. The pack's own token says why: `--text-shop-name:
       400 14.5px/20px` with the comment "Reavahe on 20px, seega kaks rida =
       40px" (tokens.css:63-68). One leading, one reserved height, both cases
       land on exactly 40px.

       MEASURED AGAINST THE INK, NOT THE LINE COUNT (the brief's warning, and a
       bug this card has already had): DM Sans hhea ascent 992 / descent 294 per
       1000upm. At 14.5px the content area is 18.65px, half-leading in a 20px
       line box is 0.68px, so line 2's baseline sits at 35.06px. The deepest ink
       in this typeface is the parenthesis at ≈ -260/1000em = 3.77px, landing at
       38.83px — and Estonian product names are full of parentheses
       ("(Makita 18V akude sobivus)"). 38.83px of ink is why the reserved height
       below is a FLOOR (min-height:40px) with a 44px ceiling as insurance, and
       never a hard `height:40px`: the line COUNT is already limited by
       -webkit-line-clamp:2 on .product-card__title-text, so this box only ever
       needed to be a floor for alignment. It is never a guillotine.

   Everything else here is the quiet layer those three need: one badge language
   for the three corner pills, and the out-of-stock variant the pack decided on.

   ------------------------------------------------------------------------
   SPECIFICITY — STATED FOR EVERY RULE, BECAUSE TRAP 1 IS REAL
   ------------------------------------------------------------------------
   CLAUDE.md Traps 1 and 9: WooCommerce core ships
   `.woocommerce img, .woocommerce-page img { height:auto; max-width:100% }` at
   (0,1,1), which ties with the theme's own `.product-card__media img` (0,1,1)
   and is then decided by load order alone. Every image rule in this file is
   written at (0,2,2) or above so it can never lose that coin toss, and the
   theme's width/height/object-fit are RESTATED at that weight rather than left
   sitting at a tie. Order is the belt here; specificity is the mechanism.

   I have NO BROWSER. Specificity arithmetic is arithmetic, not a computed
   value. Every count below was done by hand against the exact selector in the
   file and line named beside it; none of it has been confirmed in DevTools.

   ------------------------------------------------------------------------
   WHAT THIS FILE DELIBERATELY DOES NOT TOUCH
   ------------------------------------------------------------------------
   • The buy button. `a.add_to_cart_button` and `.product-card__oos` are D1
     (5 types × 6 states × 3 sizes) and belong to the controls layer, not here.
     A card that restyles its own button is how "Lisa korvi" got 8 implementations
     with 5 heights in the first place.
   • `transition` on `.product-card`. assets/css/motion.css owns the press for
     the whole shop and declares its transition-property list at (0,1,0)
     (motion.css:482-494). A rule here at (0,1,1) would silently win that and
     take the tap feedback with it on 73% of traffic. This file sets only the
     shadow, border, radius and background it needs, and lets motion.css keep
     the timing.
   • `.product-card__tools`, as a COMPONENT. README:189 says compare and
     quick-view disappear below 200px, and the card IS below 200px at 375px
     (corrected 2026-08-15 against the rendered page: .container is padded 18px
     each side below 768px, not 24px — main.css:480 — so at 375px the row is
     339px, 2 columns, 10px gap → a 164.5px card, not the 158.5px this comment
     used to claim). Removing two features on phones is an owner decision with a
     toggle, not a side effect of a design pass, and it is still not done here.
     §7b below only extends a treatment the theme ALREADY ships — the icon-only
     chip at woocommerce.css:1791-1800 — over the 40px of phone widths it misses.
     Nothing is removed; two words are.
   • Anything a shopper could read as a number that might be wrong (pack rule 2,
     README:57). No stock counts, no dates, no counters. This file adds no text
     at all.
   ========================================================================== */


/* ==========================================================================
   1 · THE SHELL — one white surface, raised by shadow instead of by tint
   ==========================================================================
   The pack's card is `--surface` + 1px `--border` + `--radius-16` +
   `--shadow-card` (sample:1149). The theme already has the first three; only
   the shadow pair moves.

   THE SHADOW PAIR IS ONE DECISION, AND I AM TAKING IT ON PURPOSE.
   --shadow-card and --shadow-card-hover are declared twice: the pack's values
   in assets/css/tokens.css:238-239 and the theme's own in main.css:98-99, and
   main.css wins by a dependency that inc/enqueue.php:105-113 makes deliberate.
   assets/css/surfaces-d4-d12.css:56-64 examined this exact swap for the product
   card and left it as "Reported, not done", because moving to the pack pair
   RAISES the rest shadow and LOWERS the hover shadow at the same time (their
   arithmetic: theme ≈ .03 → .11 effective alpha at the card's lower edge, a
   3.7× jump; pack ≈ .04 → .06, a 1.5× jump) and that is one decision, not two.

   It is one decision, and this file is where it belongs, so it is made here
   rather than left hanging between two agents. The rest step is the argument:
   it is the ONLY elevation cue 73% of this shop's traffic will ever see,
   because phones have no hover — and it is the step that has to carry the
   weight now that the grey tray under the photo is gone. The hover step going
   calmer is the correct partner to that, not a cost: with the plate white, a
   14px-offset 32px-blur lift under a 4px translate reads as the card coming off
   the page, which is a lot of drama for a mouse passing over a grid of twelve.
   Values are the pack's own recipes, unmixed — half of one and half of the
   other is how this shop got 69 shadows.

   Specificity: `li.product-card` = (0,1,1) beats `.product-card` (0,1,0) at
   woocommerce.css:115 without depending on load order.
   Every var() carries its literal, because tokens.css is NOT deployed live yet
   (verified 2026-08-14: the live page enqueues main.css but no tokens.css) and
   an unresolvable custom property is the guaranteed-invalid value, not a
   fallback to the old one.                                                     */

li.product-card {
  background: var(--surface, #ffffff);
  border: 1px solid var(--border, #e2e8f0);
  border-radius: var(--radius-16, 16px);
  box-shadow:
    0 1px 2px var(--ink-a1, rgba(15, 23, 42, .04)),
    0 2px 8px var(--ink-a2, rgba(15, 23, 42, .08));
}

/* Hover is fenced behind (hover: hover) rather than left to be cancelled later.
   73% of this shop's traffic is phones, where a latched :hover after a tap
   leaves one card lifted out of line with the eleven around it — the bug
   motion.css:666-724 exists to clean up. A rule that never matches on touch
   needs no cleanup.
   Specificity: `li.product-card:hover` = (0,2,1) beats `.product-card:hover`
   (0,2,0) at woocommerce.css:120.
   The border colour also retires a banned hex: woocommerce.css:120 hovers to
   rgba(5,150,105,.3), and #059669 is on the pack's deleted list
   (README.md:128, "Kustutatud, ei tohi tagasi tulla"). --brand-a3 is the role
   that replaces it.                                                            */
@media (hover: hover) {
  li.product-card:hover {
    transform: translateY(-2px);
    border-color: var(--brand-a3, rgba(4, 120, 87, .20));
    box-shadow:
      0 2px 4px var(--ink-a2, rgba(15, 23, 42, .08)),
      0 8px 24px var(--ink-a3, rgba(15, 23, 42, .12));
  }
}


/* ==========================================================================
   2 · THE PLATE — the photo stops sitting on a tray
   ==========================================================================
   sample:1150 — `background: var(--surface); border-bottom: 1px solid
   var(--border)`. The hairline is what separates media from body once the tint
   is gone; without it the card is one undivided white slab.

   Specificity: `li.product-card .product-card__media` = (0,2,1) beats
   `.product-card__media` (0,1,0) at woocommerce.css:125.                       */

li.product-card .product-card__media {
  background: var(--surface, #ffffff);
  border-bottom: 1px solid var(--border, #e2e8f0);
}

/* The image box, restated at a weight core cannot reach.
   TRAP 1 / TRAP 9. WooCommerce core ships `.woocommerce img, .woocommerce-page
   img { height:auto; max-width:100% }` at (0,1,1). The theme's own
   `.product-card__media img` (woocommerce.css:126) is ALSO (0,1,1) — a tie that
   only load order decides, and it currently resolves our way only because
   `ostugrupp-woocommerce` happens to print after `woocommerce-general`
   (confirmed in the live <head> on 2026-08-14). That is exactly the coin toss
   that cost releases v1.2.30-32. This rule is `li.product-card
   .product-card__media img` = (0,2,2), which beats core (0,1,1) and the theme's
   own (0,1,1) outright, and it restates width/height/object-fit so the whole
   image box stops depending on print order.

   The inset moves 14px → --space-12. 14px is not on the pack's 4px ladder;
   12px is (tokens.css:215), it is the sample's own inset, and it is 2px more
   photo on every side of every card in the shop.                               */
/* (0,5,3) — beats WooCommerce core's `.woocommerce ul.products li.product a img`
   (0,3,4), which sets height:auto and would otherwise win. Verified against the
   live WC 11.0.1 stylesheet, not assumed. */
.woocommerce ul.products li.product.product-card .product-card__media img,
li.product-card .product-card__media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: var(--space-12, 12px);
  transition: transform var(--dur, 200ms) var(--ease, cubic-bezier(.2, .6, .3, 1));
}

/* The zoom drops 1.05 → 1.03. With the plate white the photo now bleeds to the
   hairline, so a 5% scale crops visible product against that line; 3% reads as
   the photo leaning forward and crops almost nothing. Transform only — never
   width/height/top/left.
   Specificity: `li.product-card:hover .product-card__media img` = (0,3,2)
   (3 classes: .product-card, :hover, .product-card__media; 2 elements: li, img)
   beats `.product-card:hover .product-card__media img` (0,3,1) at
   woocommerce.css:127.                                                         */
@media (hover: hover) {
  li.product-card:hover .product-card__media img { transform: scale(1.03); }
}

/* Body rhythm — 12px inset and an 8px stack, both real space tokens. 14px and
   7px are not on the ladder (pack rule 1, README.md:52).
   `:not(.ostu-wl-card)` is not tidiness: assets/css/wishlist.css:160 gives the
   wishlist page's copy of this card a deliberate asymmetric 14px 16px 12px, and
   this rule at (0,2,1) would otherwise silently take it. Excluding it costs one
   pseudo-class and leaves that module's decision standing.
   Specificity: `li.product-card:not(.ostu-wl-card) .product-card__body` =
   (0,3,1) — beats `.product-card__body` (0,1,0) at woocommerce.css:131 and its
   two responsive overrides at :1577 and :1742, so no media-query copy is needed. */
li.product-card:not(.ostu-wl-card) .product-card__body {
  padding: var(--space-12, 12px);
  gap: var(--space-8, 8px);
}
/* The inner link is a flex column too (woocommerce.css:137) and carries the same
   7px; keep one gap value inside the card. (0,2,1) beats (0,2,0). */
li.product-card .product-card__title-link { gap: var(--space-8, 8px); }


/* ==========================================================================
   3 · THE NAME — one leading, one reserved box, 40px in both cases
   ==========================================================================
   Values from tokens.css:63-68 (`--text-shop-name: 400 14.5px/20px`), taken as
   ATOMS rather than as the composite token. That is deliberate and it matters:
   `font: var(--text-shop-name)` is a shorthand, so it would also swap the font
   stack to --font-text — which is 'DM Sans','Segoe UI',sans-serif and does NOT
   include "DM Sans Fallback", the metric-matched face inc/enqueue.php:197-199
   prints specifically to hold CLS at 0 on the first uncached paint. Using the
   atoms keeps the family inherited from --font-body and keeps that guarantee.
   A `font:` shorthand also cannot carry a var() fallback, so on live today —
   where tokens.css is not deployed — the whole declaration would simply be
   dropped.

   14.5px IS THE OWNER'S EXCEPTION AND IT IS NOT BEING HARMONISED
   (README.md:73-76, tokens.css:63). It is being APPLIED: the shop name is one
   value at every width. What goes away is the 13.5px mobile step at
   woocommerce.css:1581 and the 14px home-rail step at main.css:444, neither of
   which is on the pack's scale and neither of which the exception ever asked
   for. On a 158px phone card that is a LARGER product name than today.

   min-height, not height. The pack's sample writes `height:40px` (sample:1164);
   this is a floor with a 44px ceiling instead, because two lines of 14.5px DM
   Sans put ink at 38.83px (working shown in the file header) and a hard 40px box
   leaves 1.17px of clearance for a typeface whose live copy is served by OMGF
   and whose fallback is Arial. The line COUNT is limited by -webkit-line-clamp:2
   on .product-card__title-text (woocommerce.css:163-165), so this box only ever
   needed to be a floor for alignment. One-line title: content 20px, floored to
   40. Two-line title: content 40px. Both land on 40px exactly, which is the
   whole point — every price and every button in a grid row on one line.

   Specificity: `li.product-card .woocommerce-loop-product__title` = (0,2,1),
   which beats all three selectors of woocommerce.css:144-146 (highest: (0,2,0))
   and both responsive overrides (:1581 at (0,2,0)) with no media-query copy.
   The home-rail pair is (0,5,2) and beats main.css:444's (0,4,2) — see §4 for
   why the rails are pulled onto the same values rather than left compact.       */

/* (0,5,2) — beats core's `.woocommerce ul.products li.product
   .woocommerce-loop-product__title` (0,4,2), which sets font-size:1em. */
.woocommerce ul.products li.product.product-card .woocommerce-loop-product__title,
.woocommerce ul.products li.product.product-card .product-card__title,
li.product-card .woocommerce-loop-product__title,
li.product-card .product-card__title,
.home-rail ul.products li.product.product-card .woocommerce-loop-product__title,
.home-rail ul.products li.product.product-card .product-card__title {
  font-weight: var(--fw-regular, 400);
  font-size: 14.5px;      /* --text-shop-name, tokens.css:68 — owner exception 1 */
  line-height: 20px;      /* --text-shop-name, tokens.css:68 — two lines = 40px  */
  color: var(--text, #0f172a);
  min-height: 40px;
  max-height: 44px;       /* insurance only; the clamp limits the line count     */
}


/* ==========================================================================
   4 · THE PRICE — the number the shopper acts on, first
   ==========================================================================
   Recipe: --price-md (tokens.css:207) = --fw-bold --fs-price-md/--lh-price-md
   --font-display, tracking --price-md-tracking. Taken as atoms for the same
   fallback reason as §3. --font-display is the theme's stack (main.css:100),
   which already carries "Space Grotesk Fallback".

   ONE SIZE AT EVERY WIDTH. Today the same number is 22px on desktop
   (woocommerce.css:172), 19px under 768 (:1583), 18px under 480 (:1743) and
   18px on the home rails (main.css:445) — four sizes for one role. The pack has
   one, and it fits: at 375px the card is 158.5px, the body inset leaves 134px,
   and "99,00 €" at 20px Space Grotesk bold plus an 8px gap plus "202,50 €" at
   14px DM Sans measures ≈139px — so a long sale pair wraps onto a second line
   rather than shrinking, which is why row-gap exists below. Short pairs
   ("9,90 € / 19,90 €" ≈ 93px) stay on one line. Either way the number is BIGGER
   on a phone than it is today, and it is the same number everywhere.

   THE HOME RAILS ARE PULLED ONTO THE SAME VALUES ON PURPOSE. main.css:441-451
   declares a compact variant for `.home-rail ul.products li.product ...` at
   (0,4,2). D3 is explicitly a 155-320px component (README.md:189) whose TYPE
   does not change with width; a second set of sizes for the same object on the
   front page is the "8 teostust 5 kõrgusega" problem the pack was written to
   end. The escalated selector is (0,5,2). This is the one place in this file
   that reaches over another sheet's deliberate rule, and it is flagged in the
   handover for exactly that reason.

   Specificity: `li.product-card .price` = (0,2,1) beats `.product-card .price`
   (0,2,0) at woocommerce.css:172 for every property that is not !important
   there (only `color` is, and this file does not fight it — the regular price
   is ink either way).                                                          */

/* (0,5,2) — beats core's `.woocommerce ul.products li.product .price` (0,4,2),
   which sets font-size:.857em and font-weight:400. Without this the shop grid
   kept core's size while the front-page rail took ours: the same product with
   two different price treatments on two pages. The `.home-rail` selector is kept
   as belt — the new form already covers it. */
.woocommerce ul.products li.product.product-card .price,
li.product-card .price,
.home-rail ul.products li.product.product-card .price {
  font-family: var(--font-display, "Space Grotesk", "DM Sans", system-ui, sans-serif);
  font-weight: var(--fw-bold, 700);
  font-size: var(--fs-price-md, 20px);
  line-height: var(--lh-price-md, 24px);
  letter-spacing: var(--price-md-tracking, -.015em);
  font-variant-numeric: tabular-nums;
}

/* The old price. Recipe --price-sm (tokens.css:208): --fw-medium
   --fs-price-sm/--lh-price-sm --font-text, tracking 0. The family and the
   tracking both have to be restated — this element now inherits Space Grotesk
   and -.015em from its parent.
   COLOUR IS DELIBERATELY ABSENT. main.css:307-318 sets a site-wide contrast
   FLOOR on every struck price (`color: var(--slate-500) !important; opacity: 1
   !important`) because the old per-component grey was #94a3b8 at 2.56:1. That
   floor already resolves to the same #64748b the pack asks for, it is
   !important on purpose, and its comment says each component keeps only its own
   size/weight/spacing. This rule takes exactly that and nothing more.
   Specificity: `li.product-card .price del` = (0,2,2) beats
   `.product-card .price del` (0,2,1) at woocommerce.css:173.                    */
li.product-card .price del,
.home-rail ul.products li.product.product-card .price del {
  font-family: var(--font-body, "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-weight: var(--fw-medium, 500);
  font-size: var(--fs-price-sm, 14px);
  line-height: var(--lh-price-sm, 18px);
  letter-spacing: 0;
}

/* The reorder — sale cards only.
   sample:1222-1225 puts the new price first and the struck old price beside it
   on a shared baseline. WooCommerce emits them the other way round, so this is
   a flex `order` swap.

   WHY IT IS SAFE FOR A SCREEN READER, verified in the live HTML rather than
   assumed: WooCommerce marks BOTH <del> and <ins> aria-hidden="true" and prints
   the real sentence twice in `.screen-reader-text` spans ("Algne hind oli:
   202,50 €." / "Praegune hind on: 99,00 €."). Assistive technology reads those
   spans, in DOM order, and this rule does not move them. It cannot: they are
   `position:absolute !important` (main.css:155-158), and absolutely positioned
   children are out of flow and never become flex items — so they also cannot
   pick up a column-gap.

   WHY IT IS FENCED BEHIND :has(ins) rather than applied to every .price.
   A variable product with no sale prints a bare " – " text node between two
   amount spans, and in a flex container a non-whitespace text run becomes an
   anonymous flex item that would collect the 8px gap on both sides. Scoping the
   flex to prices that actually have an <ins> leaves every other price in normal
   inline flow. Where :has() is unsupported the sale price simply keeps today's
   layout — a graceful loss of the reorder, not a broken price. :has() is
   already in service in this theme (motion.css:501, wishlist.css:24,
   checkout-review.css:313).

   margin-right:0 lives INSIDE the :has() fence for the same reason: without the
   flex the 6px at woocommerce.css:173 is the only thing separating the two
   numbers.
   Specificity: :has() counts as its most specific argument, so `:has(ins)`
   contributes (0,0,1). `li.product-card .price:has(ins)` = (0,2,2) —
   2 classes (.product-card, .price), 2 elements (li, ins) — and nothing else in
   the theme declares `display` on this element, so it has no competitor. The
   two child rules are `… :has(ins) > ins|del` = (0,2,3), which beats
   `.product-card .price del` (0,2,1) at woocommerce.css:173 for margin-right.   */
li.product-card .price:has(ins),
.home-rail ul.products li.product.product-card .price:has(ins) {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  row-gap: var(--space-4, 4px);
}
/* The horizontal separation is a margin on the visually-first item, NOT
   column-gap. Flexbox `gap` only reached Safari in 14.1, and the failure mode
   there would be two prices touching — "99,00 €202,50 €" — which is worse than
   any spacing it buys. A margin on `ins` works in every engine, and when the
   pair wraps on a narrow card the 8px simply trails at the end of line 1, where
   it is invisible. `row-gap` has the same support history, but its failure mode
   is 4px of vertical air, which nobody will miss.
   The 6px at woocommerce.css:173 has to go with it: `del` is the SECOND item
   now, so that margin would trail off the right of the block. */
li.product-card .price:has(ins) > ins { order: 1; margin-right: var(--space-8, 8px); }
li.product-card .price:has(ins) > del { order: 2; margin-right: 0; }


/* ==========================================================================
   5 · THE CORNER — three pills, three roles, one language
   ==========================================================================
   Today the three corner markers speak in three unrelated voices: "Soodus"
   is 11px/700 with a red glow (woocommerce.css:191-202), "Otsas" and "Uus" are
   both 10.5px/700 solid #1e293b (:218-231) — two different facts wearing the
   same badge. The pack gives them one shape (24px pill, --label-sm, --radius-pill,
   flat) and three roles:

     Soodus  solid --price-sale   — a price event            (sample:1208)
     Otsas   solid --state-oos    — a blocker                (sample:1182)
     Uus     tinted --brand-a2    — information              (sample:1237)

   Type recipe --label-sm (tokens.css:196) = --fw-semibold --fs-2xs/--lh-flat
   --font-text, tracking --label-sm-tracking. The family moves off --font-display
   (woocommerce.css:221) onto the body face, which is what the pack specifies.

   Contrast, computed rather than quoted:
     #ffffff on --state-oos  #475569 → 7.60:1  (pack states 7.58)
     #ffffff on --price-sale #b91c1c → 6.49:1  (pack states 6.47)
     --brand #047857 on --brand-a2 composited over white (#e1efeb) → 4.69:1,
       above the 4.5:1 floor for normal-size text, and the chip additionally
       carries a --brand-a3 border so colour is never the only signal (pack
       rule 3, README.md:61).

   POSITIONS ARE NOT TOUCHED. The pack's sample puts the discount pill top-left
   because its wishlist heart owns top-right; this shop puts "Otsas"/"Uus"
   top-left (shop-extras.php:77) and "Soodus" top-right precisely so the two can
   never collide on a card that is both new and discounted. That is a solved
   problem and moving the pill would un-solve it.

   Specificity:
     `.woocommerce ul.products li.product.product-card span.onsale` = (0,5,3)
     — 5 classes (.woocommerce .products .product .product-card .onsale),
     3 elements (ul li span) — which beats the strongest selector of the rule at
     woocommerce.css:191-202 that can match this element,
     `.woocommerce ul.products li.product span.onsale` (0,4,3), and also its
     responsive twin at :1588 (strongest match there:
     `.woocommerce ul.products li.product .onsale`, (0,3,2)).
     The shorter `li.product-card span.onsale` = (0,2,2) is listed alongside for
     the home rails, where the [products] shortcode's `.woocommerce` wrapper may
     not be an ancestor; there the competitor is `.product-card span.onsale`
     (0,2,1) at :194.
     Nothing here is !important; the positioning !importants at :195-197 are
     left exactly as they are, so both pills stay where they are today.
     `li.product-card .ostu-loop-badges .ostu-badge` = (0,3,1) beats both
     `.ostu-loop-badges .ostu-badge` and `.product-card .ostu-badge` (0,2,0) at
     woocommerce.css:218-219, and their responsive twin at :1590.
     `text-transform` is deliberately absent: uppercase is right for "OTSAS" and
     "UUS" and wrong for "-51%", and woocommerce.css:222 already applies it to
     the badges only.                                                            */

li.product-card span.onsale,
.woocommerce ul.products li.product.product-card span.onsale,
li.product-card .ostu-loop-badges .ostu-badge {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill, 999px);
  font-family: var(--font-body, "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-weight: var(--fw-semibold, 600);
  font-size: var(--fs-2xs, 12px);
  line-height: var(--lh-flat, 1);
  letter-spacing: var(--label-sm-tracking, .06em);
  font-variant-numeric: tabular-nums;
  box-shadow: none;
}

li.product-card span.onsale,
.woocommerce ul.products li.product.product-card span.onsale {
  background: var(--price-sale, #b91c1c);
  color: var(--text-on-dark, #ffffff);
}

/* "Otsas" — grey, and the pack is emphatic that grey does not mean quiet:
   "OTSAS = neutraalne, mitte punane. Otsas ei ole viga. NB: neutraalne EI
   TÄHENDA vaikset. 7,58:1 valgel — sama kaal ja kontrast mis soodusmärgisel.
   Ära heleda." (tokens.css:43-45). Same 24px pill, same type, same contrast
   band as the discount pill; only the role colour differs. */
li.product-card .ostu-loop-badges .ostu-badge--out {
  background: var(--state-oos, #475569);
  color: var(--text-on-dark, #ffffff);
  /* Three pulses and it stops. The owner's decision, documented at
     tokens.css:308-310: "Kolm kordust ja jääb seisma: tähelepanu, mitte
     tähelepanu püüdmine." Transform only, three iterations, no infinite loop,
     nothing that could be read as urgency (invariant 3 / EL UCPD): this states
     a fact that is already on the card, it does not manufacture pressure.
     The @keyframes lives in assets/css/tokens.css:310 and this file's enqueue
     depends transitively on it; if that sheet is ever absent the badge simply
     does not move and every other property still applies. */
  animation: oos-pulse .9s var(--ease, cubic-bezier(.2, .6, .3, 1)) 3;
}

/* "Uus" — information, not an alarm. A tinted chip with a real border, so it
   reads at a glance as a different KIND of fact from the two solid pills. */
li.product-card .ostu-loop-badges .ostu-badge--new {
  background: var(--brand-a2, rgba(4, 120, 87, .12));
  border-color: var(--brand-a3, rgba(4, 120, 87, .20));
  color: var(--brand, #047857);
}


/* ==========================================================================
   6 · OUT OF STOCK — the fourth variant, stated once
   ==========================================================================
   sample:1178-1202. Three coordinated signals, none of them red:
   the grey pill (§5), a de-emphasised price, and a calm photo.

   THE PRICE. On the pack's out-of-stock card the price is --text-subtle
   (sample:1196) — the number is still there, it is simply not an offer today.
   This also finishes something woocommerce/loop/sale-flash.php:26 already
   started: that template hides the "-51%" pill on an unbuyable product because
   "a sold-out card was advertising -31% on a product nobody can buy". A RED
   sale price on the same card says exactly the thing the pill was removed for
   saying. Both numbers go subtle together.
   !important is required, not decorative: woocommerce.css:172 and :179 both
   carry !important on `color`. Among !important declarations the more specific
   one wins, and `li.product-card.outofstock .price` (0,3,1) and
   `li.product-card.outofstock .price ins` (0,3,2) beat (0,2,0) and (0,2,1).

   THE PHOTO. woocommerce.css:235 dims it to .72. The state is now carried by
   three explicit, readable signals, so the photo does not need to carry a
   fourth at that strength — a shopper scanning a grid for a specific product
   should still be able to recognise it. Raised to .85; the card still reads as
   "not now" without the photo reading as broken. This is a judgement, not a
   pack value: the pack's out-of-stock sample dims only its placeholder frame,
   which is a placeholder artefact and not a specification for a real photo.
   Specificity: (0,3,2) beats `.product-card.outofstock .product-card__media img`
   (0,3,1) at woocommerce.css:235.                                              */

li.product-card.outofstock .product-card__media img { opacity: .85; }

li.product-card.outofstock .price,
li.product-card.outofstock .price ins {
  color: var(--text-subtle, #64748b) !important;
}


/* ==========================================================================
   7 · NARROW CARDS — 158px at 375px, and the pill has to fit
   ==========================================================================
   Measured, not guessed: at 375px `.container` insets 24px each side
   (main.css:151), leaving 327px; the grid is 2 columns with a 10px gap
   (woocommerce.css:1741), so a card is 158.5px. The type does NOT step down —
   that is the point of §3 and §4 — but the two paddings that would crowd a
   158px card do, onto the next rung of the same ladder.

   Both selectors match the base rules exactly, so these win by source order
   inside one file rather than by a specificity escalation.                     */
@media (max-width: 480px) {
  li.product-card .product-card__media img { padding: var(--space-8, 8px); }

  li.product-card span.onsale,
  .woocommerce ul.products li.product.product-card span.onsale,
  li.product-card .ostu-loop-badges .ostu-badge { padding: 0 var(--space-8, 8px); }
}


/* ==========================================================================
   7b · THE 40px OF PHONE WIDTHS WHERE "Kiirvaade" IS CUT IN HALF
   ==========================================================================
   woocommerce.css:1788-1800 already decided this exact question and wrote the
   reason down: "On a ~160px-wide card the two ghost pills are too narrow for
   both an icon and a full Estonian word. Rather than ellipsis-clip 'Kiirvaade'
   (which reads as a bug), drop to clean ICON-ONLY chips". It fenced that at
   `max-width: 400px`. The arithmetic says the fence is 40px too low, and the
   40px it misses are the widths most large phones actually report.

   THE ARITHMETIC, done against the rendered page, not against the template.
   Below 768px `.container` is padded 18px each side (main.css:480) and the
   product grid is 2 columns with a 10px gap below 480px (woocommerce.css:1755):

       card       = (W - 36 - 10) / 2
       body inner = card - 2 (the card's own 1px border) - 24 (12px body inset,
                    li.product-card:not(.ostu-wl-card) .product-card__body, §6
                    of this file)
       one chip   = (body inner - 8) / 2          .product-card__tools gap, 8px

   and what one chip has to hold at those widths (woocommerce.css:1760-1767:
   font-size 11px, padding 0 6px, gap 4px; icon svg 15px):

       12 (padding) + 15 (icon) + 4 (gap) + "Kiirvaade"

   "Kiirvaade" is 9 characters of DM Sans at 11px. DM Sans's lowercase advances
   average ≈ .52em, so ≈ 51px, and the chip needs ≈ 82px. Solving one chip ≥ 82:

       body inner ≥ 172  →  card ≥ 198  →  W ≥ 442px

   So from 401px to ~442px the label is ellipsis-clipped — "Kiirvaad…" — which
   is the exact failure woocommerce.css:1791 says it wrote that block to avoid.
   The widths inside that band are not exotic: Pixel reports 412, iPhone 14 Plus
   428, iPhone 14/15 Pro Max 430. This is the majority of big phones, and it is
   why the chips look broken on a large phone and fine on a small one.

   440px, not 442px: the crossover depends on a font-metric ESTIMATE (see below),
   so the fence is set at the 4px-ladder value just inside it rather than at the
   estimate itself. Between 440 and 442 the worst case is the ellipsis that is
   already shipping today, never a new break.

   ESTIMATE, AND IT IS FLAGGED AS ONE. I have no browser and DM Sans is not in
   assets/fonts/ (only anton-v27-latin-400.woff2 is), so the 51px label width is
   arithmetic on a typical .52em lowercase advance, NOT a measured text run. The
   direction is certain — 82px does not fit in 72px — the exact crossover is not.
   The leader should measure `.ostu-qv-trigger__label` scrollWidth vs its client
   width at 412px before trusting the 442.

   The accessible name is untouched: both buttons carry aria-label AND title
   (read out of the rendered card on 2026-08-15), so hiding the visible label
   costs a screen reader nothing. Values are woocommerce.css:1796-1799's own —
   the same treatment, 40px wider, not a second one.

   Specificity. Adding `li` and the card class to each of core's phone selectors:
     label   `li.product-card .product-card__tools .ostu-qv-trigger__label`
             = 3 classes + 1 element = (0,3,1)  beats  (0,2,0) at :1792
     chip    `li.product-card .product-card__tools > button`
             = (0,2,2)                          beats  (0,1,1) at :1796
     icon    `li.product-card .product-card__tools .ostu-qv-trigger__icon svg`
             = (0,3,2)                          beats  (0,2,1) at :1798
   All three also clear the ≤480px block at woocommerce.css:1760-1767, whose
   strongest form is (0,2,0). Order is not relied on anywhere.                  */

@media (max-width: 440px) {
  li.product-card .product-card__tools .ostu-qv-trigger__label,
  li.product-card .product-card__tools .ostu-cmp-btn__label { display: none; }

  li.product-card .product-card__tools .ostu-qv-trigger,
  li.product-card .product-card__tools .ostu-cmp-btn--card,
  li.product-card .product-card__tools > button,
  li.product-card .product-card__tools > a { gap: 0; padding: 0; }

  li.product-card .product-card__tools .ostu-qv-trigger__icon svg,
  li.product-card .product-card__tools .ostu-cmp-btn__icon svg { width: 17px; height: 17px; }
}


/* ==========================================================================
   8 · REDUCED MOTION — complete, not partial
   ==========================================================================
   Three transforms and one animation are introduced above. All four are
   cancelled here, and every state they belonged to is still SEEN without them:

     card hover lift      → the border and the shadow still change
     photo zoom           → decorative only; nothing is communicated by it
     "Otsas" pulse        → the pill is a solid 7.60:1 grey chip that reads
                            "OTSAS", beside a de-emphasised price and a
                            secondary action. The state was never carried by
                            the movement.

   !important matches the pattern already established for exactly this purpose
   at a11y.css:250-256 and motion.css:878, and it removes any dependence on
   which of the two at-rules the browser happened to read last.

   main.css:146-148 already crushes every animation-duration to .01ms globally,
   so the pulse would stop even without this block. It is stated anyway: a
   global crush is someone else's rule, and this component should not need it
   to be correct.                                                               */
@media (prefers-reduced-motion: reduce) {
  li.product-card:hover,
  li.product-card:hover .product-card__media img { transform: none !important; }

  li.product-card .ostu-loop-badges .ostu-badge--out { animation: none !important; }
}


/* ==========================================================================
   9 · FORCED COLOURS — the corner survives an OS colour override
   ==========================================================================
   In Windows High Contrast every background above is discarded, which would
   turn the three pills into three identical runs of bare text in the corner of
   a photo — the shopper would not be able to tell "OTSAS" from "-51%" by shape,
   only by reading. A system-keyword border is one of the few things
   forced-colors keeps, so all three keep an outline and stay pills. Same
   pattern, same reason as cart-sticky-bar.css:162 and motion.css:960-970.
   The card itself needs nothing here: its edge is a real 1px border, not a
   shadow.                                                                      */
@media (forced-colors: active) {
  li.product-card span.onsale,
  .woocommerce ul.products li.product.product-card span.onsale,
  li.product-card .ostu-loop-badges .ostu-badge {
    border-color: CanvasText;
  }
}
