/**
 * Shop layer CSS (framework core, package B1b).
 *
 * Every colour value here is a token from site/assets/css/tokens.css, never a
 * hardcoded hex value: this file inherits a fork's rebrand automatically, in
 * both themes, the same way every other core page does.
 *
 * Loaded by scripts/build-shop.mjs via a plain <link rel="stylesheet">, not
 * inlined per page: one file, one edit, both languages and every product
 * page pick it up without a rebuild of anything but the generator's own
 * output (which does not even need to change when only this file does).
 */

.shop-wrap{max-width:1180px;margin:0 auto;padding:48px 24px 72px}
.shop-head{max-width:640px;margin:0 0 34px}
.shop-head h1{font-size:clamp(1.9rem,4vw,2.6rem);letter-spacing:-.02em;line-height:1.15;margin:0 0 14px;color:var(--text)}
.shop-head p{color:var(--text-2);font-size:1.02rem;line-height:1.6;margin:0}
/* JBM-06a (shop owner feedback 24.09.2026, O-10/F-06): fixed 1/2/3-per-row
   breakpoints instead of the previous auto-fill(260px) track, which let the
   index quietly show 4 (or more, on a very wide viewport) columns of its
   own accord -- the shop owner's own request was explicitly "drei pro Reihe".
   --shop-cols is a CSS custom property, not a hardcoded `repeat(3, ...)`,
   so a fork that genuinely wants a 4-up desktop grid sets `--shop-cols: 4`
   on `:root` (or `.shop-grid` itself) rather than having to fork this rule;
   the `, 3` in var()'s second argument IS the default, no declaration
   anywhere is required to get three columns. min-width:0 on .shop-card
   below is the SAME SHRX-N3/SHR-CAT guard already used elsewhere in this
   file (.shop-cat-subcat, .shop-cat-tile): a grid item's default min-width
   is auto (its content's own intrinsic width), not 0, so an unclamped
   child could still force the whole grid track wider than intended. */
.shop-grid{display:grid;grid-template-columns:repeat(1,1fr);gap:22px}
@media (min-width:640px){.shop-grid{grid-template-columns:repeat(2,1fr)}}
@media (min-width:960px){.shop-grid{grid-template-columns:repeat(var(--shop-cols,3),1fr)}}
.shop-card{display:flex;flex-direction:column;min-width:0;background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius-lg);overflow:hidden;color:inherit;text-decoration:none;transition:transform .2s ease}
.shop-card:hover{transform:translateY(-3px)}
/* JBM-06a: object-fit was `cover` (crops top/bottom on any image whose
   aspect ratio does not match the fixed 4/3 box, exactly the "Bilder ...
   nicht oben/unten abschneiden" complaint) -- `contain` shows the whole
   photo, letterboxed on the existing --panel-a/--panel-b gradient (the
   SAME calm brand surface this box already used as its cover's loading/
   no-image placeholder, so no new token pairing is introduced). The 12px
   padding keeps a contained image off the box's own rounded corners. */
.shop-card .cover{aspect-ratio:4/3;padding:12px;box-sizing:border-box;background:linear-gradient(135deg,var(--panel-a),var(--panel-b));overflow:hidden}
.shop-card .cover img{width:100%;height:100%;object-fit:contain;display:block}
/* JBM-06a: .body's own flex:1 (grid stretches every .shop-card to the
   row's tallest card by default, `align-items: stretch` needs no explicit
   rule) plus `margin-top:auto` on whatever follows .ex (the price line,
   or the CTA itself when there is no price) is what pins the price and
   .shop-card-cta to the same bottom edge on every card in a row,
   regardless of how much title/excerpt content sits above it on that
   particular card -- no extra spacer element needed. */
.shop-card .body{padding:16px 18px 20px;display:flex;flex-direction:column;gap:8px;flex:1}
/* SHR-10: a title long enough to wrap past two lines used to push every
   sibling card in the same grid row taller to match it (no clamp at all
   before this package), so one long product name could visibly bend the
   whole row's rhythm. Same clamp mechanic site/assets/css/shop.css already
   uses for .shop-pdp-buy .lead above (display:-webkit-box is required for
   -webkit-line-clamp to take effect, not just decoration); the full,
   untruncated string still reaches assistive tech and a mouse hover via the
   element's own title="" attribute (productCardHtml()'s own docblock). */
.shop-card h2{font-size:1.05rem;line-height:1.35;margin:0;color:var(--text);display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
/* JBM-06a (shop owner feedback: "Produkttexte einheitlich kuerzen"): a
   3-line clamp, same -webkit-box/-webkit-line-clamp mechanic .shop-card h2
   above already uses, so every card's excerpt takes the same visual amount
   of room regardless of how long the underlying `excerpt` text actually
   is.
   JBM-06a R2 review: .ex must NOT carry flex:1 any more. A -webkit-line-clamp
   box that is stretched taller than its clamped lines (flex:1 in a row whose
   tallest card has a Grundpreis, a two-line title or a category chip) still
   paints lines 4, 5, ... below the ellipsis: the clamp only places the "..."
   and sizes the box's OWN intrinsic height, overflow:hidden clips at the
   stretched edge (measured in headless Chromium 141). The free space now
   goes to the element after .ex via margin-top:auto instead, so the price
   line and CTA still sit on the same bottom edge across a row. */
.shop-card .ex{color:var(--text-2);font-size:.9rem;line-height:1.5;margin:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
.shop-card .ex + *{margin-top:auto}
.shop-price{font-weight:700;color:var(--text)}
.shop-baseprice{color:var(--text-3);font-size:.78rem;margin:2px 0 0}
/* PVA-01: the doTERRA PV line under the price, existing tokens only. */
.shop-pv{color:var(--text-2);font-size:.9rem;margin:4px 0 0}
/* SHR-10 (upstreamed from a fork's own local fix, that fork's
   shop.css, design-audit finding 15.09.2026): a product card's only prior
   click affordance was its own :hover translateY lift, invisible on touch
   and to anyone not already hovering. This always-visible line closes that
   gap without a second, nested link inside the card's own outer <a>
   (invalid HTML) -- the `.ex + *` margin-top:auto above already pushes it
   to the same baseline on every card in a row, regardless of how long that card's
   excerpt happens to run.
   JBM-06a (shop owner feedback: "CTA als echter Button statt Link"): reuses
   `.btn`/`.shop-btn`'s (tokens.css, further down this file) own COLOUR
   TOKENS, not the class verbatim -- this stays its own rule (a `<span>`,
   never `<a class="btn shop-btn">`, since that would be a second, nested
   interactive element inside the card's own outer `<a>`) with its own,
   smaller sizing to fit inside a card body rather than the site-wide
   button size: background var(--accent) plus color var(--on-accent),
   background var(--accent-2) on hover/focus, box-sizing:border-box plus
   min-height:44px, the SAME 44px touch-target floor `.btn`/`.shop-btn`
   themselves use (web-design-quickstart.md's touch-target rule). Both
   colour pairings are ALREADY covered without adding a new CONTRAST_PAIRS
   entry: --on-accent/--accent at 4.5:1 ("label on a filled button",
   tools/gate/rules/contrast.mjs) and --on-accent/--accent-2 at 4.5:1
   (tests/contrast.test.mjs's own dedicated ".btn-primary/.shop-btn hover
   state" test, both themes). The card itself stays the ONLY `<a>` (this
   span is still never a second, nested interactive element, same
   constraint the removed color-only version already respected) -- it just
   now LOOKS like the solid button .btn/.shop-btn already is everywhere
   else on this site, instead of a plain accent-coloured text link. */
.shop-card-cta{display:inline-flex;align-items:center;justify-content:center;gap:6px;align-self:flex-start;box-sizing:border-box;min-height:44px;padding:10px 16px;margin-top:4px;background:var(--accent);color:var(--on-accent);font-weight:600;font-size:.88rem;border-radius:var(--radius-sm);transition:background .15s ease}
/* design-review finding (SHR-10), still true for the solid-button look:
   the whole point of this line is fixing an affordance gap for a visitor
   who does not/cannot hover (touch, mouse-less navigation) -- leaving the
   hover repaint mouse-hover-only would fix that gap for touch while still
   leaving a keyboard user with no visual change at all when Tab lands on
   the card. :focus-visible only matches a keyboard/assistive-tech focus,
   never a mouse click, so this adds no visual noise for a mouse user
   tabbing nowhere. */
.shop-card:hover .shop-card-cta,.shop-card:focus-visible .shop-card-cta{background:var(--accent-2)}
.shop-empty{background:var(--surface);border:1px dashed var(--line);border-radius:var(--radius-lg);padding:56px 30px;text-align:center;color:var(--text-2)}
/* SAU-K2: section headings on the shop index ("Kategorien"/"Neu"/"Alle
   Produkte") -- one size down from .shop-head h1 above, same --text colour
   pair, no new contrast measurement needed. */
.shop-head h2{font-size:1.3rem;letter-spacing:-.01em;line-height:1.25;margin:0 0 14px;color:var(--text)}
/**
 * SAU-K2: shop index category tiles. Reuses the exact --text/--text-2 on
 * --surface pairs .shop-cat-subcat-name/-count already use (tests/
 * contrast.test.mjs's CONTRAST_PAIRS), just a horizontal card with an
 * optional small cover thumbnail instead of a plain name+count row, since a
 * tile links to a whole category rather than a single fixed-size subcat
 * list.
 */
.shop-cat-tiles{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin:0 0 34px}
@media (min-width:640px){.shop-cat-tiles{grid-template-columns:repeat(3,1fr)}}
@media (min-width:960px){.shop-cat-tiles{grid-template-columns:repeat(4,1fr)}}
/* SHRX-05 design-review WARN (v2 subcategory-tile pass): the v2 hover state
   below (lift plus a tinted --shadow-2, never plain black per
   web-design-quickstart.md hard rule 6) is applied here too, to this tile's
   own sibling, so the two near-identical card patterns (same cover
   treatment, same name/count layout, .shop-cat-subcat below) carry the
   SAME hover weight regardless of which page they appear on. */
.shop-cat-tile{display:flex;align-items:center;gap:12px;min-height:44px;padding:10px 14px;background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius-md);color:inherit;text-decoration:none;transition:transform .2s ease,box-shadow .2s ease}
/* SHR-CAT, 16.09.2026: same SHRX-N3 bug (.shop-cat-subcat above, line 563),
   found on this tile's own sibling grid (.shop-cat-tiles, the shop index's
   TOP-LEVEL category tiles) -- a flex item's default min-width is auto (its
   content's intrinsic width), not 0. A long, un-hyphenated category name in
   .shop-cat-tile-name (white-space:nowrap) forced this flex item, and with
   it .shop-cat-tiles' whole grid track, wider than the viewport at narrow
   widths (live-verified 476px scrollWidth at a real 375px device width on
   a shared staging host's /shop/). Same fix, same shape: overflow-wrap:
   anywhere is what actually breaks the word (counts toward min-content
   width per spec, so the browser's own min-width:auto can shrink); min-width:0
   is kept as the same belt-and-braces guard SHRX-N3 added for .shop-cat-subcat. */
.shop-cat-tile{min-width:0;overflow-wrap:anywhere}
.shop-cat-tile:hover{transform:translateY(-2px);box-shadow:var(--shadow-2)}
/* JBM-06a: same object-fit:contain + padding fix as .shop-card .cover
   above, applied to this tile's own 40px thumbnail -- `cover` cropped a
   category photo's top/bottom whenever its aspect ratio did not match this
   fixed square box, the same "Bilder ... nicht abschneiden" complaint. */
.shop-cat-tile-cover{flex:0 0 auto;width:40px;height:40px;padding:4px;box-sizing:border-box;border-radius:var(--radius-sm);overflow:hidden;background:linear-gradient(135deg,var(--panel-a),var(--panel-b))}
.shop-cat-tile-cover img{width:100%;height:100%;object-fit:contain;display:block}
.shop-cat-tile-info{display:flex;flex-direction:column;gap:2px;min-width:0}
.shop-cat-tile-name{color:var(--text);font-weight:700;white-space:normal;overflow-wrap:anywhere;line-height:1.4;min-height:2.8em}
.shop-cat-tile-count{color:var(--text-2);font-size:.8rem;font-weight:600}
/**
 * SAU-K2: shop index search. .shop-search-input reuses the exact
 * --text/--surface/--line-strong treatment .mv-cart-item-qty further down
 * this file already uses (an already-measured contrast pair, no new one
 * introduced); .shop-search-result a reuses .shop-cat-subcat's own
 * --text-on-surface pairing.
 */
.shop-search{margin:0 0 30px}
.shop-search-label{display:block;font-size:.85rem;font-weight:600;color:var(--text-2);margin:0 0 6px}
.shop-search-input{width:100%;min-height:44px;padding:10px 14px;border:1px solid var(--line-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font-size:1rem}
.shop-search-input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.shop-search-results{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
.shop-search-result a{display:block;min-height:44px;padding:10px 14px;background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius-sm);color:var(--text);text-decoration:none;font-weight:600}
.shop-search-result a:hover{border-color:var(--accent)}
.shop-search-empty{color:var(--text-2);font-size:.9rem;margin:12px 0 0}
/**
 * Product detail page (PDP-03, mobile-first): the page is a single column
 * on mobile (gallery, then title/price/CTA, then the accordions below), and
 * only becomes the two-column "image left, buy-box right" layout at/above
 * 900px, where there is enough width for both without squeezing either.
 * Mobile-first here means the base (unqualified) rules below already ARE the
 * mobile layout; the @media (min-width:900px) block further down only adds
 * the wider-viewport grid, it never has to undo a desktop-first default.
 */
.shop-pdp{max-width:1080px;margin:0 auto;padding:32px 20px 72px}
.shop-crumbs{color:var(--text-2);font-size:.85rem;margin:0 0 14px}
.shop-crumbs a{color:var(--text-2);text-decoration:none}
.shop-pdp-grid{display:grid;grid-template-columns:1fr;gap:22px}
.shop-pdp-buy h1{font-size:clamp(1.6rem,5vw,2.35rem);line-height:1.18;letter-spacing:-.02em;margin:0 0 10px;color:var(--text)}
.shop-pdp-buy .lead{color:var(--text-2);font-size:1.02rem;line-height:1.55;margin:0 0 16px}
/* Mobile-only 3-line clamp (R2 review of PR #191, PDP-03): with a five-line
   excerpt AND a real product photo, the uncapped .lead pushed the CTA button
   past the 393x852 fold (measured: 855px, 3px over G-P1's limit). Above
   900px the two-column layout has its own column width and vertical room,
   so the clamp only applies below that breakpoint, same threshold as the
   gallery's mobile-only max-height below. */
@media (max-width:899px){
  .shop-pdp-buy .lead{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
}
/* SHR-05a: below 900px, .shop-pdp-gallery-main becomes a horizontally
   scrolling, scroll-snap swipe strip (one .shop-pdp-slide per screen-width
   "page"), one dot indicator, no thumbnail-click JS needed to switch
   between them on a touch device. Overridden back to the classic single-
   visible-slide layout at 900px+ in the @media(min-width:900px) block
   below, where there is room for a two-column PDP instead. Native
   scrollbars are hidden (this is a swipe gesture surface, not a text
   list); Firefox's scrollbar-width plus the WebKit pseudo-element cover
   every engine this framework targets. */
.shop-pdp-gallery-main{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.shop-pdp-gallery-main::-webkit-scrollbar{display:none}
/* Each slide is one full swipeable "page": align-items:flex-start keeps its
   height exactly the CONTENT height (the figure's own 32vh below), not
   stretched to the tallest sibling by flexbox's default align-items:stretch
   -- load-bearing for G-P1 (tests/shop-pdp-gp1-image.test.mjs measures
   .shop-pdp-figure's own height against the 852px fold). */
.shop-pdp-slide{flex:0 0 100%;scroll-snap-align:center;display:flex;align-items:flex-start;justify-content:center}
/* [hidden] normally means display:none (the UA stylesheet); overridden here
   so every slide keeps participating in the swipe strip regardless of which
   one shop.js currently marks `hidden` (its own showMain()/showIndex() keep
   toggling it, harmlessly, purely for the desktop single-visible layout
   below 900px this override replaces). Reset back to display:none at
   900px+, see the @media block below -- same footgun/fix shape
   .shop-lightbox-slide[hidden] and .shop-sticky[hidden] already use
   elsewhere in this file.
   Design-review VETO fix (PR #253): the SAU-K5 global backstop further down
   this file, `[hidden]:not([hidden="until-found"]) { display: none
   !important; }`, has the SAME specificity (0,0,2,0) as a bare
   `.shop-pdp-slide[hidden]` and carries `!important`, which this rule did
   not -- the backstop always won regardless of source order, so on mobile
   EVERY slide but the first rendered display:none and there was nothing to
   swipe (scrollWidth === clientWidth), even though the DOM's `hidden`
   attribute toggled correctly on click. The ancestor combinator
   `.shop-pdp-gallery-main .shop-pdp-slide[hidden]` raises this rule's own
   specificity to (0,0,3,0), which beats the backstop's (0,0,2,0)
   independent of `!important`/source order on either side -- verified live
   in a real Chromium: `.shop-pdp-gallery-main`'s scrollWidth now exceeds its
   clientWidth at 393px, and a video-thumbnail click visibly switches the
   main slide. A bare `!important` on the un-scoped selector would NOT have
   fixed this: equal specificity plus equal `!important` falls back to
   source order, and the backstop sits later in this same file. */
.shop-pdp-slide[hidden]{display:flex}
.shop-pdp-gallery-main .shop-pdp-slide[hidden]{display:flex !important}
/* Capped, centred product photo: the generator (build-shop.mjs) never
   constrains figure width, so a portrait product photo (bottle/tube shots
   are typically taller than wide) would otherwise balloon to 1000px+ tall on
   its own, pushing price/CTA far below the fold. Product photography aspect
   ratio is unpredictable per product, so this caps by WIDTH (proportional
   height follows), not a fixed height that would crop landscape shots via
   object-fit. On mobile it is additionally capped by height, so title and
   buy-box (G-P1: both must be visible with zero scrolling on a 393x852
   viewport) are never pushed out of the first screen by a tall photo.
   `height` (not `max-height`) is load-bearing here: img{height:100%} below
   only resolves against a parent with a definite height, not one merely
   capped by max-height, which is how a 38vh figure previously let a tall
   source image render at its own intrinsic height and get cropped to the
   top third instead of being fit inside the box (R2 review of PR #191).
   SHR-05a: renamed off the old `.shop-pdp-gallery figure` ancestor-descendant
   selector onto the class build-shop.mjs actually emits (.shop-pdp-figure),
   lower specificity, no behaviour change: .shop-pdp-gallery still contains
   nothing but a <figure> here, real <video>/embed slides carry their own
   .shop-pdp-video/.shop-pdp-video-embed classes below instead. */
.shop-pdp-figure{margin:0 auto 4px;max-width:320px;height:32vh;border-radius:var(--radius-lg);overflow:hidden}
.shop-pdp-figure img{width:100%;height:100%;object-fit:contain;display:block}
.shop-pdp-gallery-empty{aspect-ratio:4/3;max-width:320px;margin:0 auto;border-radius:var(--radius-lg);background:linear-gradient(135deg,var(--panel-a),var(--panel-b))}
/* --- SHR-05b: video slot (own file or bunny.net consent facade) --------- */
.shop-pdp-video{display:block;width:100%;max-width:320px;height:32vh;border-radius:var(--radius-lg);margin:0 auto 4px}
.shop-pdp-video-caption{margin:4px auto 0;max-width:320px;text-align:center;color:var(--text-3);font-size:.82rem}
/* Stacked card, not a text-over-image overlay: the poster (if any) stays a
   plain, opaque top image, so no new text-on-image contrast pairing has to
   be measured. embeds.js swaps this whole thing out for a real <iframe>
   once consented (its own inline styles then take over aspect-ratio/
   padding), this is only the PRE-consent facade state. */
.shop-pdp-video-embed{display:flex;flex-direction:column;align-items:center;gap:8px;width:100%;max-width:320px;margin:0 auto 4px;padding:14px;border-radius:var(--radius-lg);background:var(--surface-2);border:1px solid var(--line-soft);text-align:center}
.shop-pdp-video-embed img{width:100%;max-height:18vh;object-fit:cover;border-radius:var(--radius-sm);display:block}
.shop-pdp-video-embed .embed-title{margin:4px 0 0;font-weight:700;color:var(--text)}
.shop-pdp-video-embed .embed-text{margin:0;font-size:.82rem;color:var(--text-2)}
/* --- SHR-05a: mobile swipe-strip dot indicator --------------------------
   Non-interactive (aria-hidden, purely a scroll-position readout, see
   build-shop.mjs's renderGallery() docblock); hidden again at 900px+ below,
   where the single-visible-slide desktop layout has nothing to scroll. */
.shop-pdp-dots{display:flex;justify-content:center;gap:8px;margin:10px 0 0}
.shop-pdp-dot{width:8px;height:8px;border-radius:999px;background:var(--line-strong);flex:0 0 auto}
.shop-pdp-dot.is-active{background:var(--accent)}
/* --- PDP-05: gallery button, thumbnail strip, lightbox dialog -----------
   The <img> in build-shop.mjs's galleryMainFigureHtml() sits inside a real
   <button> (a keyboard-reachable lightbox trigger, not a second invisible
   click layer): reset every default button chrome so it behaves exactly
   like the plain <img> it replaces, position:relative only so the AI badge
   (.ai-badge, tokens.css) still has the SAME positioned ancestor it always
   had via .ai-badge-media on the figure -- the button itself never becomes
   the badge's anchor, it just must not accidentally break that inheritance
   by introducing its own stacking context quirks. */
.shop-pdp-gallery-btn{display:block;width:100%;height:100%;padding:0;border:0;background:none;appearance:none;cursor:zoom-in;font:inherit;color:inherit;text-align:inherit}
.shop-pdp-gallery-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Thumbnail strip: only rendered by renderGallery() when a product has more
   than one SLIDE total (images plus an optional video, SHR-05b), SAME
   threshold the gallery figure/empty-state rules above already use for
   their own breakpoint, see the @media(min-width:900px) block below. A
   horizontally scrollable row on mobile (no wrap: a strip of 5+ product
   photos must never push the buy-box further down the page than a
   single-image product already does), a plain wrapping row once there is
   enough width for it above 900px. */
.shop-pdp-thumbs{display:flex;gap:8px;margin:10px auto 0;max-width:320px;overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px}
.shop-pdp-thumb{flex:0 0 auto;width:52px;height:52px;padding:0;border:2px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface);appearance:none;overflow:hidden;cursor:pointer}
.shop-pdp-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.shop-pdp-thumb.is-active{border-color:var(--accent)}
.shop-pdp-thumb:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* SHR-05a: "+n" overflow tile (last visible thumbnail once a gallery has
   more than MAX_THUMBS images). Same solid --accent-soft/--accent-text pair
   .shop-review-verified above already uses, not a new colour surface. */
.shop-pdp-thumb-more{display:flex;align-items:center;justify-content:center;background:var(--accent-soft);color:var(--accent-text);font-weight:700;font-size:.9rem;border-color:var(--line-soft)}
/* SHR-05b: video thumbnail's small play badge, bottom-right corner of the
   poster. --on-accent on --accent is the SAME pairing tokens.css's own
   .btn-primary already uses (tests/contrast.test.mjs's CONTRAST_PAIRS), the
   glyph itself is fill="currentColor" (build-shop.mjs's galleryVideoThumbHtml()). */
.shop-pdp-thumb-video{position:relative}
.shop-pdp-thumb-play{position:absolute;right:3px;bottom:3px;display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:var(--accent);color:var(--on-accent)}
/* Native <dialog> (no lightbox library, PDP-05 Negativ-Regel): ::backdrop is
   only styleable on the dialog itself, not from a wrapper element, so this
   selector has to target the dialog directly. Centred via the UA's default
   fixed-position auto-margin behaviour for [open] dialogs, just capped and
   padded like every other surface in this file. */
.shop-lightbox{max-width:min(92vw,900px);max-height:88vh;margin:auto;padding:20px;border:0;border-radius:var(--radius-md);background:var(--surface);color:var(--text);box-shadow:var(--shadow-3)}
.shop-lightbox::backdrop{background:rgba(10,37,64,.6)}
.shop-lightbox-slides{position:relative;display:flex;align-items:center;justify-content:center;min-height:40vh}
/* Figure + caption of the one visible slide stack vertically (design-review
   finding on the first PDP-05 draft: without flex-direction:column here,
   the two sat side by side as row siblings of .shop-lightbox-slides, which
   both misplaced the caption text next to the image instead of below it AND
   moved the prev/next buttons' 50% vertical centre onto the caption text). */
.shop-lightbox-slide{display:flex;flex-direction:column;align-items:center;max-width:100%}
/* The `[hidden]` attribute build-shop.mjs renders on every non-active slide
   and the `display:flex` rule just above have EQUAL selector specificity
   (one class, one attribute); without this explicit override the later,
   `display:flex`-setting rule wins the cascade tie and every slide renders
   at once, `[hidden]` or not (design-review finding: all three product
   photos stacked visibly on top of each other in the dialog). */
.shop-lightbox-slide[hidden]{display:none}
.shop-lightbox-figure{margin:0;max-width:100%;max-height:72vh;text-align:center}
.shop-lightbox-figure img{max-width:100%;max-height:72vh;object-fit:contain;display:block;margin:0 auto}
.shop-lightbox .ai-disclosure{text-align:center}
/* Close/prev/next: solid, filled circular controls (web-design-quickstart.md
   hard rule 2, no ghost/outline/transparent buttons anywhere, including
   small icon-only utility controls like these). width/height 44px is the
   web-design-quickstart.md touch-target minimum (design-review VETO: 40px
   was 4px under it). z-index:1 is load-bearing, not decoration: .shop-lightbox-close
   is a DOM sibling BEFORE .shop-lightbox-slides, and .shop-lightbox-slides
   is itself position:relative with z-index:auto, so both sit in the same
   painting layer (positioned descendants, z-index:auto) and are painted in
   DOM order -- .shop-lightbox-slides (and everything inside it, including
   the image) paints on top of .shop-lightbox-close, making the close button
   unclickable on every viewport (design-review VETO, reproduced via
   elementFromPoint() and a real Playwright click). Promoting the buttons to
   z-index:1 moves them into the positive-z-index paint layer, which always
   paints after the z-index:auto layer regardless of DOM order. Prev/next
   already render inside .shop-lightbox-slides AFTER the image slides, so
   they were not affected by this bug, but get the same z-index for
   consistency and to stay correct if the markup order ever changes.
   WARNING (R2 review of PR #194, Befund 2): no OTHER selector inside
   .shop-lightbox* may ever carry a z-index >= 1. A positive z-index on
   .shop-lightbox-slides or .shop-lightbox-figure img repaints it back over
   the close/prev/next controls, reproducing this exact bug, while leaving
   the paint-order regression test above green (it only asserts that THIS
   rule has z-index >= 1, not that no other rule does). tests/shop-lightbox.
   test.mjs's "no other .shop-lightbox* rule may carry a z-index" test
   guards this direction; keep it green. */
.shop-lightbox-close,.shop-lightbox-prev,.shop-lightbox-next{position:absolute;z-index:1;display:flex;align-items:center;justify-content:center;width:44px;height:44px;min-width:44px;min-height:44px;padding:0;border:1px solid var(--line-strong);border-radius:50%;background:var(--surface);color:var(--text);font-size:1.4rem;line-height:1;cursor:pointer}
.shop-lightbox-close{top:8px;right:8px}
.shop-lightbox-prev{top:50%;left:8px;transform:translateY(-50%)}
.shop-lightbox-next{top:50%;right:8px;transform:translateY(-50%)}
.shop-lightbox-close:hover,.shop-lightbox-prev:hover,.shop-lightbox-next:hover{background:var(--surface-2)}
.shop-lightbox-close:focus-visible,.shop-lightbox-prev:focus-visible,.shop-lightbox-next:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* PDP-07 cross-sell block ("Passt dazu"): reuses .shop-cat-grid verbatim
   (SAU-K1a's own responsive grid further below, unified with .shop-grid's
   1/2/var(--shop-cols,3) schedule by JBM-06a R1), so only the heading
   needs its own rule here. */
.shop-crosssell{margin:40px 0 0}
/* Direct-child combinator on purpose: .shop-crosssell h2 (descendant) has the
   same specificity (0-1-1) as .shop-card h2 further up, and because the
   cross-sell block reuses .shop-cat-grid verbatim, every product card title
   inside it also matches .shop-crosssell h2. The later rule in source order
   then wins and blows up card titles from 16.8px to 22.4px inside this
   block. The > combinator scopes the heading style to the direct "Passt
   dazu" heading only, never to the nested card titles. */
.shop-crosssell > h2{font-size:1.4rem;letter-spacing:-.01em;line-height:1.25;margin:0 0 18px;color:var(--text)}
.shop-pdp-details{margin:28px 0 0}
.shop-pdp-details > .shop-accordions,
.shop-pdp-details > .shop-pdp-toc,
.shop-pdp-details > p,
.shop-pdp-details > h3{max-width:none}
.shop-pdp-details p{margin:0 0 16px;color:var(--text)}
.shop-pdp-details h3{font-size:1.08rem;line-height:1.35;margin:20px 0 8px;color:var(--text)}
/* SHR-07: real <ul>/<ol> lists inside a body/accordion section (markdown
   '-'/'*'/'1.' lines from build-shop.mjs's renderBodyAccordion(), no longer
   flattened into a single dash-prefixed paragraph). Indent + spacing mirror
   .shop-pdp-details p's own rhythm (16px bottom margin) so a list reads as
   just another block in the same flow, not a visually distinct component. */
.shop-accordion-body ul,
.shop-accordion-body ol{margin:0 0 16px;padding:0 0 0 1.25em;color:var(--text)}
.shop-accordion-body li{margin:0 0 6px}
.shop-accordion-body li:last-child{margin-bottom:0}
.shop-accordion-body ul:last-child,
.shop-accordion-body ol:last-child{margin-bottom:0}
/* SHR-07: sticky mini-table-of-contents, one entry per "##" body heading
   (renderBodyAccordion() only emits it once a product body has 2+ of
   them). Hidden below 900px entirely (Olli F1: mobile stays a plain
   stacked accordion, no horizontal tabs -- Baymard's own "avoid horizontal
   tabs" applies just as much to a cramped mini-ToC on a 390px screen); the
   @media(min-width:900px) block below both reveals it and gives every
   accordion its "always expanded" look. */
.shop-pdp-toc{display:none}
/* One flat list of accordions, no nested card-in-card look: the outer
   .shop-pdp-details already sits on the page background, a second bordered
   box per section would be visual noise for what is routinely a single
   "what's inside" section (see the four reference fixtures). */
.shop-accordions{display:flex;flex-direction:column;gap:2px;border-top:1px solid var(--line-soft)}
.shop-accordion{border-bottom:1px solid var(--line-soft)}
.shop-accordion summary{list-style:none;cursor:pointer;padding:16px 4px;font-weight:600;color:var(--text);display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px}
.shop-accordion summary::-webkit-details-marker{display:none}
/* A plain "+"/"-" glyph via content, not an icon font: this file has no
   dependency on one, and the glyph only needs to convey open/closed state,
   not carry meaning of its own (the summary text already says what section
   this is). */
.shop-accordion summary::after{content:"+";color:var(--text-2);font-size:1.2rem;line-height:1}
.shop-accordion[open] summary::after{content:"\2212"}
.shop-accordion-body{padding:0 4px 18px}
.shop-accordion-body p:last-child{margin-bottom:0}
/* SHR-06: flex-column, not the plain block flow it used to be, so a fork's
   own shop-fork.css can reorder the whole buy-box with nothing more than
   `.shop-buybox-<name>{order:N}` -- every renderProductPage() block below
   (badge/price/note/rating/facts/variants/cta/trust/meta) is a direct
   child `<div class="shop-buybox-<name>">`, never a bare/partial slot
   (PLAN-REVIEW "keine Partial-Slots, Rung 4"). A block with nothing to
   show is OMITTED entirely (build-shop.mjs's wrapBuyboxBlock()), never an
   empty div, so there is no stray gap to collapse. */
.shop-buybox{display:flex;flex-direction:column;background:var(--bg-2);border:1px solid var(--line-soft);border-radius:var(--radius-lg);padding:22px;margin:0}
/* A single, always-solid button: no ghost/outline variant anywhere, disabled
   is opacity only (web-design-quickstart.md hard rule 2). Contrast of
   --on-accent on --accent, and --accent-2 on hover, is re-measured per theme
   by tests/contrast.test.mjs (CONTRAST_PAIRS covers .shop-btn).
   SHR-02: `.shop-btn` is `.btn`+`.btn-primary` from tokens.css, duplicated
   verbatim (own copy, not an import) because shop pages load only shop.css,
   not tokens.css (see headCommon() in scripts/build-shop.mjs). The selector
   lists below mirror the two tokens.css rules they alias, so this stays
   visibly one button system with one definition, even though `.btn`/
   `.btn-primary` themselves never appear in shop.css's own markup.
   inline-flex + centered content (not inline-block): a button's own label
   must center itself regardless of the height a PARENT flex container
   happens to stretch it to, see tokens.css's `.btn` comment. */
.btn, .shop-btn{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;gap:8px;min-height:44px;padding:12px 24px;border:none;border-radius:var(--radius-sm);font-weight:600;text-decoration:none;text-align:center;cursor:pointer}
.btn-primary, .shop-btn{background:var(--accent);color:var(--on-accent)}
.btn-primary:hover, .shop-btn:hover{background:var(--accent-2)}
.shop-btn[disabled],.shop-btn.is-disabled{opacity:.5;pointer-events:none}
.shop-badge{display:inline-block;background:var(--accent-soft);color:var(--accent-text);font-weight:700;font-size:.74rem;letter-spacing:.08em;text-transform:uppercase;padding:5px 11px;border-radius:999px;margin:0 0 12px}
/* Package P3 (shop-pdp-design-2026-09-11.md): an external product's
   Werbe-Zeile, Grundpreis-freshness note, and the two optional trust blocks.
   .shop-price-note reuses .shop-baseprice's own font-size/colour (--text-3
   on --bg-2, same pairing already accepted for the buy-box, see
   tests/contrast.test.mjs's CONTRAST_PAIRS comment on --text-3/--surface
   only), it just needs its own selector since it can appear either next to
   the Grundpreis line or standing in for the price entirely. */
.shop-ad-label{color:var(--text-3);font-size:.78rem;margin:0 0 10px}
.shop-pv-note{color:var(--text-3);font-size:.78rem;margin:0}
/* Design-review WARN (PR #254): as the LAST buy-box block (SHR-06's own
   Dawn-order comment on renderProductPage()), the Werbe-Zeile (UWG § 5a)
   sat directly after .shop-trust-list with the exact same --text-3/.78rem/
   2px-rhythm look, reading as a fourth trust bullet rather than a
   disclosure. A visible rule + its own top padding (not a second
   margin-top stacked on the same transition, see .claude/rules/
   framework.md's own "no two spacing declarations for one transition"
   rule -- margin-top here separates the divider LINE from the block
   before it, padding-top separates the disclosure TEXT from that line, two
   different gaps either side of one border, not the same gap twice). No
   redesign, no colour/position change, just enough separation to read as
   its own thing. */
.shop-buybox-meta{margin-top:14px;border-top:1px solid var(--line-soft);padding-top:10px}
.shop-price-note{color:var(--text-3);font-size:.78rem;margin:2px 0 0}
/* SHR-06: the price-legal note (build-shop.mjs's noteBlockHtml()), EMPTY
   until a fork configures shop.labels.priceNote/shippingNote itself (see
   that function's own docblock) -- same --text-3/.78rem pairing as
   .shop-price-note above, already contrast-tested on --bg-2, no new
   colour token. Design-review WARN (PR #254): each `.shop-buybox-<name>`
   wrapper is a flex item of `.shop-buybox` (site/assets/css/shop.css's own
   comment on that rule), which BLOCKS margin collapsing between a
   wrapper's own top edge and its inner element's margin -- so a plain
   `margin-top` here (and on .shop-buybox-facts-list/.shop-buybox-chips/
   .shop-buybox-rating-link below) reliably reproduces the same rhythm
   .shop-badge's pre-existing `margin:0 0 12px` and .shop-trust-list's
   `margin:14px 0 0` already give their own neighbours, without touching
   either of those pre-existing rules (both are reused outside the buy-box
   too, e.g. .shop-badge on a product card's category chip). Chosen over a
   `gap` on `.shop-buybox` itself: a gap would ADD to, not replace, the
   badge's/trust-list's own margins (their margins stay trapped inside their
   own wrapper for the same flex-item-is-a-new-formatting-context reason),
   doubling the spacing on those two blocks specifically. */
.shop-buybox-note-text{color:var(--text-3);font-size:.78rem;margin:8px 0 0}
/* SHR-06 "auf einen Blick": a plain list (--text-2/--bg-2, already
   contrast-tested, see tests/contrast.test.mjs's CONTRAST_PAIRS comment on
   that pairing) plus the category chips, reusing .shop-cat-chip verbatim
   (categoryChipBar()'s own docblock -- no second pill/chip treatment, no
   new colour pair: .shop-cat-chip paints its own --surface background).
   `margin-top` (not `gap` on .shop-buybox, see .shop-buybox-note-text's own
   comment above): 8px whether the list is the facts block's first child
   (trapped against its flex-item wrapper's top edge) or follows the price/
   note/rating blocks above it. */
.shop-buybox-facts-list{list-style:none;margin:8px 0 0;padding:0;color:var(--text-2);font-size:.85rem;line-height:1.6}
.shop-buybox-facts-list li{margin:0 0 2px}
/* Same 8px rhythm as .shop-buybox-facts-list above, independent of whether
   the list is present: a product with only a resolvable category and no
   unit/SKU/deliveryNote renders chips as the facts block's sole child. */
.shop-buybox-chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
/* Product facts are compact links, not the catalogue's large filter pills.
   Intrinsic widths keep short labels together and let long labels wrap. */
.shop-buybox-chips .shop-cat-chip{box-sizing:border-box;min-height:28px;max-width:100%;padding:4px 6px;font-size:.8125rem;line-height:1.4;white-space:normal;overflow-wrap:anywhere}
.shop-buybox-cta .shop-btn,.shop-sticky .shop-btn{font-family:inherit;font-size:1rem}
/* SHR-08a2: the variant picker (build-shop.mjs's variantPickerHtml()) --
   solid pills, never a ghost/outline variant (web-design-quickstart.md
   hard rule 2), reusing .shop-cat-chip's own already-contrast-tested
   --surface/--line-soft (inactive) and .shop-btn's own --on-accent/--accent
   (active, here `aria-pressed="true"` instead of `.is-active`) pairings
   verbatim, no third pill colour combination to measure.
   `margin-top` lives on `.shop-variant-picker` itself, NOT on the always-
   present `.shop-buybox-variants` wrapper (SHR-06's own "empty div, no CSS
   rule of its own" contract for a non-variant product, tested by
   tests/build-shop.test.mjs's "always-present, empty direct child" case):
   a margin on the wrapper would add a visible gap on EVERY product page,
   variant-carrying or not, since a flex item's own margin is independent of
   whether it has content. */
.shop-variant-picker{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.shop-variant-btn{flex:0 0 auto;display:inline-flex;align-items:center;min-height:44px;padding:8px 18px;border-radius:999px;background:var(--surface);border:1px solid var(--line-soft);color:var(--text-2);font-weight:600;font-size:.88rem;cursor:pointer}
.shop-variant-btn:hover{border-color:var(--accent)}
.shop-variant-btn[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
/* Same opacity-only disabled treatment as .shop-btn[disabled] above (hard
   rule 2 again): a sold-out variant stays visible and legible, it never
   blends into the background or vanishes. */
.shop-variant-btn[disabled]{opacity:.5;pointer-events:none}
.shop-variant-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Same --text-3/--bg-2 pairing as .shop-price-note above (already
   contrast-tested, tools/gate/rules/contrast.mjs's CONTRAST_PAIRS), no new
   colour token for the currently-selected variant's own SKU line. */
.shop-variant-sku{color:var(--text-3);font-size:.78rem;margin:8px 0 0}
.shop-variant-sku[hidden]{display:none}
/* SHR-06: the buy-box rating-summary link (build-shop.mjs's
   ratingSummaryHtml()), linking down to the full reviews section. Reuses
   .shop-reviews-stars/-avg/-count verbatim; --accent-text on --bg-2 (the
   star glyphs, here instead of the reviews section's own background) was
   an untested pairing until this package added it to CONTRAST_PAIRS
   (tools/gate/rules/contrast.mjs), measured 5.53:1 light / 6.66:1 dark. */
.shop-buybox-rating-link{display:inline-flex;align-items:center;gap:6px;color:inherit;text-decoration:none;margin-top:8px}
.shop-buybox-rating-link:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* SAU-T1: same --text-3/.78rem pairing as .shop-price-note above (already
   contrast-tested on --bg-2, see the comment on that selector) -- no new
   colour token, no icon system, just a plain list under the CTA button. */
.shop-trust-list{list-style:none;margin:14px 0 0;padding:0;color:var(--text-3);font-size:.78rem;line-height:1.6}
.shop-trust-list li{margin:0 0 2px}
.shop-safety-note,.shop-manufacturer-block{max-width:65ch;color:var(--text-2);font-size:.88rem;line-height:1.5;margin:0 0 16px}
/* SAU-R1: consent-gated customer reviews. Same max-width/border-top rhythm
   as .shop-pdp-details/.shop-accordions above (this section sits right
   below it in the DOM); the "show all N" toggle reuses .shop-accordion
   verbatim (native <details>, no new JS toggle, no [hidden]-cascade risk,
   see this package's own briefing comment in scripts/build-shop.mjs). */
.shop-reviews{max-width:65ch;margin:28px 0 0}
.shop-reviews h2{font-size:1.4rem;letter-spacing:-.01em;line-height:1.25;margin:0 0 14px;color:var(--text)}
.shop-reviews-summary{display:flex;align-items:baseline;gap:8px;margin:0 0 16px;color:var(--text-2);font-size:.92rem}
.shop-reviews-stars,.shop-review-stars{color:var(--accent-text);letter-spacing:1px}
.shop-reviews-avg{font-weight:700;color:var(--text)}
/* SHR-05a: explicit rule for a class .shop-reviews-summary above already
   colours by inheritance (color:var(--text-2)); named here too so the class
   itself, not just its ancestor, carries the styling build-shop.mjs emits it
   for. */
.shop-reviews-count{color:var(--text-2)}
.shop-reviews-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;border-top:1px solid var(--line-soft)}
.shop-review{padding:14px 4px;border-bottom:1px solid var(--line-soft)}
.shop-review-meta{display:flex;flex-wrap:wrap;align-items:center;gap:10px;font-size:.78rem;color:var(--text-3);margin:0 0 6px}
.shop-review-author{font-weight:600;color:var(--text)}
/* SHR-05a: same reasoning as .shop-reviews-count above -- .shop-review-meta
   already colours it via inheritance, this makes the class itself carry the
   rule explicitly. */
.shop-review-date{color:var(--text-3);white-space:nowrap}
.shop-review-verified{background:var(--accent-soft);color:var(--accent-text);font-weight:700;font-size:.7rem;letter-spacing:.04em;text-transform:uppercase;padding:3px 8px;border-radius:999px}
.shop-review-text{margin:0;color:var(--text);line-height:1.55}
.shop-reviews-more{margin-top:2px}
.shop-reviews-more .shop-reviews-list{border-top:none;margin-top:0}
.shop-404{max-width:640px;margin:0 auto;padding:80px 24px;text-align:center}
.shop-404 h1{color:var(--text);font-size:1.8rem;margin:0 0 12px}
.shop-404 p{color:var(--text-2);margin:0 0 20px}

/* --- category / subcategory pages (package SAU-K1a) ---------------------
   New selectors here use a `.shop-cat-*` prefix (per the SAU-08 master
   index's Datei-Konflikt-Matrix: build-shop.mjs's PDP-03/K1a/K2/R1/T1
   packages all touch this same file, and a shared prefix keeps their own
   selectors from ever colliding), except for the page CHROME the category
   page shares verbatim with the shop overview (.shop-wrap/.shop-head/
   .shop-crumbs/.shop-empty/.shop-card/.shop-badge, all already defined
   above): those are reused as-is, not re-declared under the new prefix,
   the whole point of scripts/build-shop.mjs's own productCardHtml() /
   .shop-badge category-chip reuse being "no second card template, no
   second badge colour pair".

   Mobile-first, same convention every other block in this file already
   follows: the base (unqualified) rule below already IS the 393px layout,
   the two @media blocks only ADD wider-viewport columns, they never have
   to undo a desktop-first default.

   JBM-06a R1 review: this used to be its own, independent 2/3/4-column
   schedule (834px/1280px breakpoints), so the SAME .shop-card looked like
   a different grid depending on whether it sat on the shop index
   (.shop-grid) or a category page / the PDP-07 cross-sell block
   (.shop-cat-grid, still reused verbatim by .shop-crosssell above --
   unifying this grid changes that block's column count too, an accepted
   side effect of "one card, one grid" rather than a second exception).
   Now identical to .shop-grid's own 1/2/var(--shop-cols,3) schedule
   (below 640px / from 640px / from 960px, same --shop-cols custom
   property a fork already uses to override .shop-grid), so a fork that
   sets --shop-cols once gets the same column count everywhere .shop-card
   appears, not just on the shop index. */
.shop-cat-grid{display:grid;grid-template-columns:repeat(1,1fr);gap:22px}
@media (min-width:640px){
  .shop-cat-grid{grid-template-columns:repeat(2,1fr)}
}
@media (min-width:960px){
  .shop-cat-grid{grid-template-columns:repeat(var(--shop-cols,3),1fr)}
}
/* Subcategory tiles above the product grid: its own 2/3/4-column schedule,
   NOT .shop-cat-grid's own count (JBM-06a R1 unified .shop-cat-grid to
   .shop-grid's 1/2/var(--shop-cols,3), but a subcategory tile is a
   different, wider horizontal-row shape than a .shop-card, not the same
   component -- unifying THIS grid too would be a second, unrelated
   redesign this package never asked for). Plain solid tiles, no ghost/outline variant
   (web-design-quickstart.md hard rule 2): `--surface` fill plus a
   `--line-soft` border is the exact same treatment .shop-card already uses
   for its own container. "v2" (SHRX-05) adds an optional cover thumbnail,
   see .shop-cat-subcat-cover below. */
.shop-cat-subcats{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin:0 0 32px}
@media (min-width:834px){
  .shop-cat-subcats{grid-template-columns:repeat(3,1fr);gap:16px}
}
@media (min-width:1280px){
  .shop-cat-subcats{grid-template-columns:repeat(4,1fr);gap:18px}
}
/* SHRX-05 ("Kategorie-Kacheln v2"): box-shadow added to the transition list
   alongside the pre-existing transform (the hover rule below now animates
   both), padding trimmed from 14px/16px to 12px/14px to make room for the
   44px cover thumbnail without growing min-height past the pre-existing
   44px touch target. */
.shop-cat-subcat{display:flex;align-items:center;gap:12px;min-height:44px;padding:12px 14px;background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius-md);color:inherit;text-decoration:none;font-weight:600;transition:transform .2s ease,box-shadow .2s ease}
/* SHRX-N3, 16.09.2026: a flex item's default min-width is auto (its content's
   intrinsic width), not 0. A long compound subcategory name refused to
   wrap and forced this flex item, and with it .shop-cat-subcats' whole
   grid track, wider than the 390px viewport. overflow-wrap:anywhere is what
   fixes it: unlike break-word, it counts toward min-content width per spec,
   so the browser's own min-width:auto shrinks on its own. min-width:0 is
   kept as a belt-and-braces guard (R2 review, SHR-01b: mutation-verified
   redundant on its own, but harmless alongside overflow-wrap:anywhere).
   Still applies verbatim to the "v2" tile below: .shop-cat-subcat itself is
   still the flex item .shop-cat-subcats' grid track sizes against, the
   cover/chevron additions are new CHILDREN of it, not a second wrapper. */
.shop-cat-subcat{min-width:0;overflow-wrap:anywhere}
/* SHRX-05: transform (lift) plus a TINTED shadow token on hover, never a
   plain black shadow (web-design-quickstart.md hard rule 6) -- --shadow-2
   is the same elevation token .shop-sticky/.shop-lightbox already use,
   itself tinted navy in light mode and a near-black low-opacity black only
   in dark mode by the token's own design (tokens.css: "shadows carry almost
   no weight on dark surfaces; a border does the work" -- the existing
   .shop-cat-subcat border still does the visible work there). */
.shop-cat-subcat:hover{transform:translateY(-2px);box-shadow:var(--shadow-2)}
/* SHRX-05: optional cover thumbnail, same 1:1 aspect-ratio + gradient
   fallback + object-fit:cover treatment .shop-cat-tile-cover above already
   uses for the shop index's top-level tiles (build-shop.mjs's
   subcategoryTileHtml() renders an empty span, no <img> at all, when the
   category has no cover -- coverHtml()'s own "never a broken <img>"
   convention). aspect-ratio (not a fixed height) keeps the box square
   regardless of the 44px width ever changing. */
/* JBM-06a: same object-fit:contain + padding fix as .shop-cat-tile-cover
   above (this box's own docblock already says it shares that tile's
   treatment verbatim) -- kept in sync rather than fixing only the shop
   index's top-level tile and leaving a category page's own subcategory
   tiles still cropping. */
.shop-cat-subcat-cover{flex:0 0 auto;width:44px;aspect-ratio:1/1;padding:4px;box-sizing:border-box;border-radius:var(--radius-sm);overflow:hidden;background:linear-gradient(135deg,var(--panel-a),var(--panel-b))}
.shop-cat-subcat-cover img{width:100%;height:100%;object-fit:contain;display:block}
/* SHRX-05: name+count now stack in their own flex column (flex:1 so the
   chevron below is pushed to the tile's trailing edge without needing
   justify-content:space-between on the parent, which would fight the new
   cover/chevron siblings for space). */
.shop-cat-subcat-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.shop-cat-subcat-name{color:var(--text);white-space:normal;overflow-wrap:anywhere;line-height:1.4;min-height:2.8em}
@media (max-width:479px){
  .shop-cat-tiles,.shop-cat-subcats{grid-template-columns:minmax(0,1fr)}
  .shop-cat-tile-name,.shop-cat-subcat-name{min-height:0}
}
/* Product count badge: reuses --text-2 on --surface, an already-measured
   pair (tests/contrast.test.mjs's CONTRAST_PAIRS), same as .shop-crumbs a
   above and .ex on a .shop-card. */
.shop-cat-subcat-count{color:var(--text-2);font-size:.82rem;font-weight:600}
/* SHRX-05: decorative chevron affordance, aria-hidden in the markup (the
   whole tile is already one link, the chevron adds no information of its
   own). A CSS-drawn glyph via ::before, same convention .shop-accordion
   summary::after already uses for its own +/- indicator a few sections
   down, rather than a new inline-SVG icon this codebase has no other
   instance of. --text-3 on --surface is an already-measured 4.5:1+ pair
   (tests/contrast.test.mjs's CONTRAST_PAIRS), well past the 3.0:1 floor a
   purely decorative element would even need. Slides right on hover/focus,
   the same "controls feel responsive" micro-interaction the nav's DE/EN
   sliding toggle already follows (framework.md's segmented-toggle rule). */
.shop-cat-subcat-chevron{flex:0 0 auto;color:var(--text-3);font-size:1.15rem;line-height:1;transition:transform .2s ease}
.shop-cat-subcat-chevron::before{content:"\203a"}
.shop-cat-subcat:hover .shop-cat-subcat-chevron,
.shop-cat-subcat:focus-visible .shop-cat-subcat-chevron{transform:translateX(3px)}

/* --- category chip bar (package SAU-K1b) --------------------------------
   Horizontal, scrollable row of every top-level category, above the
   .shop-head heading on a category/subcategory page. Solid pill fill, never
   a ghost/outline variant (web-design-quickstart.md hard rule 2): inactive
   chips reuse .shop-cat-subcat's own --surface/--line-soft treatment, the
   active chip reuses .shop-btn's own --on-accent on --accent pairing
   (already contrast-tested, tests/contrast.test.mjs's CONTRAST_PAIRS), so
   this needs no new colour measurement. -webkit-overflow-scrolling:touch and
   scroll-snap mirror .shop-pdp-thumbs' own horizontally-scrolling strip
   further up in this file. */
.shop-cat-chips{display:flex;gap:10px;overflow-x:auto;-webkit-overflow-scrolling:touch;scroll-snap-type:x proximity;margin:0 0 26px;padding-bottom:4px}
.shop-cat-chip{flex:0 0 auto;scroll-snap-align:start;display:inline-flex;align-items:center;min-height:44px;padding:8px 18px;border-radius:999px;background:var(--surface);border:1px solid var(--line-soft);color:var(--text-2);font-weight:600;font-size:.88rem;text-decoration:none;white-space:nowrap}
.shop-cat-chip:hover{border-color:var(--accent)}
.shop-cat-chip.is-active{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}

/* --- "Mehr laden" pagination button (package SAU-K1b) -------------------
   Reuses the site-wide .btn/.btn-secondary classes (tokens.css) verbatim,
   same convention .mv-cart-item-remove already follows further down in this
   file, instead of a fourth button variant. site/assets/js/shop.js's
   initLoadMore() creates this element and toggles its `hidden` attribute;
   the layout rule here only centres it under whichever grid it was inserted
   after. */
.shop-loadmore-btn{display:block;margin:28px auto 0}
/* The base card rule further up and the button layout rule just above set
   `display:flex` and `display:block` respectively. Both are equal-
   specificity author-stylesheet rules, so they win the cascade over the
   browser's own `[hidden]{display:none}` user-agent rule regardless of
   source order (design-review + adversarial-review VETO on this PR:
   confirmed via a real headless-Chromium render that all cards past
   `shown` stayed visible and the button never disappeared after full
   expansion, even though `element.hidden` was correctly `true` in the DOM).
   Same footgun this file already fixes twice for other components
   (`.shop-lightbox-slide[hidden]` above, `.shop-sticky[hidden]` below).
   The card override is scoped to `[data-shop-loadmore] >` (direct children
   only, matching setupLoadMoreGrid()'s own direct-child-only card
   collection in site/assets/js/shop.js) so a card outside a paginated grid
   -- e.g. the PDP cross-sell block's plain `.shop-cat-grid`, which never
   gets `hidden` set on its cards in the first place -- is unaffected. */
[data-shop-loadmore]>.shop-card[hidden]{display:none}
.shop-loadmore-btn[hidden]{display:none}
/* SAU-K2: same footgun as the two rules just above, now on the shop
   index's own search wiring (site/assets/js/shop.js's initShopSearch()):
   .shop-grid sets display:grid unconditionally (further up in this file),
   so setting `hidden` on the "Alle Produkte" grid or the "Neu" row while a
   search is active would otherwise have NO visual effect at all, same for
   .shop-cat-tiles (its own unconditional display:grid). Verified missing
   in a real browser (Playwright) before this rule existed: the `hidden`
   DOM attribute was correctly `true`, the grid stayed fully visible. */
.shop-grid[hidden],
.shop-cat-tiles[hidden]{display:none}

@media (max-width:640px){
  .shop-wrap{padding-left:20px;padding-right:20px}
}

/* --- PDP two-column layout (package PDP-03) -----------------------------
   Image left, buy column right, body/accordions full-width below both: only
   from 900px up is there enough room for a readable image AND an unsqueezed
   buy-box side by side. Below 900px the base (unqualified) rules above
   already give the correct single-column stack, this block only adds. */
@media (min-width:900px){
  .shop-pdp-grid{grid-template-columns:minmax(0,1fr) minmax(300px,380px);align-items:start;gap:32px}
  /* SHR-05a: revert the mobile swipe-strip back to the classic single-
     visible-slide layout -- there is room for the two-column PDP now, and
     the thumbnail strip (still visible below) is the desktop way to switch
     between images. */
  .shop-pdp-gallery-main{display:block;overflow:visible}
  .shop-pdp-slide{display:block;flex:none}
  .shop-pdp-slide[hidden]{display:none}
  /* Design-review VETO fix (PR #253): the mobile swipe-strip override above
     (`.shop-pdp-gallery-main .shop-pdp-slide[hidden]{display:flex
     !important}`) is unconditional (no media guard of its own -- it has to
     beat the SAU-K5 `!important` backstop at every viewport where it
     applies), so `!important` always wins here too regardless of viewport
     width. Since `!important` rules break specificity ties by SOURCE
     ORDER, not by which one is scoped to the current media query, the
     plain `.shop-pdp-slide[hidden]{display:none}` two lines above (no
     `!important`) can never override it at 900px+. This copy matches the
     SAME selector shape and specificity (0,0,3,0) as the mobile rule, so
     the desktop reset (later in source order) wins the tie exactly at and
     above this breakpoint. */
  .shop-pdp-gallery-main .shop-pdp-slide[hidden]{display:none !important}
  .shop-pdp-dots{display:none}
  .shop-pdp-figure{max-width:none;height:auto}
  .shop-pdp-video{max-width:none;height:auto;aspect-ratio:4/3}
  .shop-pdp-video-embed,.shop-pdp-video-caption{max-width:none}
  .shop-pdp-gallery-empty{max-width:none;max-height:none}
  .shop-pdp-thumbs{max-width:none;flex-wrap:wrap;overflow-x:visible}

  /* SHR-07: reveal the mini-ToC, sticky against the viewport top while its
     accordions scroll past underneath. `top` is a small fixed offset (this
     site has no fixed/sticky header of its own above the shop pages to
     account for, unlike .shop-sticky's own bottom bar further down, which
     IS accounting for a real fixed element). --text/--bg-2 and --text-2/
     --bg-2 are both in tools/gate/rules/contrast.mjs's CONTRAST_PAIRS (see
     that file's own SHR-07 entry), so a future token edit that breaks this
     panel's legibility fails the gate, not just a one-time by-hand check. */
  .shop-pdp-toc{display:block;position:sticky;top:16px;z-index:1;margin:0 0 20px;padding:12px 18px;background:var(--bg-2);border:1px solid var(--line-soft);border-radius:var(--radius-md)}
  .shop-pdp-toc-label{display:block;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 6px}
  .shop-pdp-toc ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:4px 20px}
  .shop-pdp-toc a{color:var(--text);text-decoration:none;font-size:.92rem;font-weight:600;border-bottom:2px solid transparent}
  .shop-pdp-toc a:hover,
  .shop-pdp-toc a:focus-visible{border-bottom-color:var(--accent)}
  .shop-pdp-toc a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

  /* SHR-07 (Olli F1): every accordion stands permanently expanded on
     desktop, only the mobile layout keeps the native click-to-expand
     behaviour. The UA rule that hides a closed <details>'s body
     (`details:not([open]) > *:not(summary){display:none}`) is itself just
     author-overridable CSS, not hardwired native rendering, so the fourth
     rule below simply out-specifies it -- no JS toggle needed either way,
     the `open` attribute build-shop.mjs renders keeps controlling the
     MOBILE state exactly as PDP-03 already did.
     A modern Chromium (131+) additionally wraps every non-summary child in
     an internal `::details-content` box that ANIMATES the open/close
     transition (block-size 0 <-> auto, `content-visibility: hidden` while
     closed) -- unlike the plain UA display:none rule above, this wrapper
     is NOT overridden by unhiding `.shop-accordion-body` alone: measured
     live (Playwright, this package's own evidence run), a closed section's
     body kept a real, non-zero getBoundingClientRect() height (the CHILD
     was correctly display:block) while still painting completely empty,
     because the wrapper box around it stayed clipped to zero height. The
     second new rule below unhides that wrapper directly; a browser that
     does not know `::details-content` simply drops the one, invalid rule
     and keeps relying on the display override above, same progressive-
     enhancement shape as this file's own ::-webkit-details-marker rule
     elsewhere. `scroll-margin-top` gives the sticky ToC panel above room
     to breathe when a ToC link jumps to a section, so the heading is never
     rendered directly under the panel. */
  .shop-accordion summary{pointer-events:none;cursor:default}
  .shop-accordion summary::after{display:none}
  .shop-accordion:not([open]) > .shop-accordion-body{display:block}
  .shop-accordion::details-content{content-visibility:visible;block-size:auto;overflow:visible}
  .shop-accordion{scroll-margin-top:76px}
}

/* --- sticky buy bar (package PDP-03) -------------------------------------
   Positioned `sticky; bottom:0` as the LAST element in <body> (after the
   site footer, see build-shop.mjs's renderProductPage()): with an ordinary,
   un-positioned ancestor chain up to the viewport, that combination makes it
   behave like a "stick to the bottom once you scroll past it" footer bar for
   the whole remaining page height, and it settles into its normal, in-flow
   position only exactly when the visitor reaches the true end of the
   document -- there is no gap where it visibly "un-sticks" early. Hidden by
   default (the `hidden` attribute build-shop.mjs renders); site/assets/js/
   shop.js's IntersectionObserver removes it once the real .shop-buybox
   scrolls out of view, and never hides it again afterwards (spec). No new
   colour pair: every value here reuses tokens already covered by
   tests/contrast.test.mjs (--surface/--text/--line-strong via .shop-btn/
   .shop-price, which this bar's contents reuse verbatim; --text-3 for the
   compact Grundpreis line below is likewise already covered, it is the same
   token .shop-baseprice uses in the buy-box above the fold). */
.shop-sticky{position:fixed;right:16px;bottom:calc(16px + env(safe-area-inset-bottom,0px));width:min(380px,calc(100% - 88px));box-sizing:border-box;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-2);z-index:10}
.shop-sticky[hidden]{display:none}
.shop-sticky-inner{padding:12px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px 12px}
.shop-sticky-title{grid-column:1/-1;min-width:0;font-size:.8125rem;line-height:1.4;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.shop-sticky-inner .shop-btn{grid-column:2;grid-row:2;min-height:44px;padding:10px 12px}
.shop-sticky-inner .shop-price{white-space:nowrap}
.shop-sticky-inner .shop-btn{white-space:nowrap}
/* R3 review of PR #191: the Grundpreis (PAngV) must stay visible in the bar
   (a point of sale, it has a working add-to-cart button), but as ONE flex
   item, not two -- build-shop.mjs's priceLine({sticky:true}) wraps the main
   price and the Grundpreis line in this single <span> instead of leaving
   them as two flex-row siblings, which is what starved .shop-sticky-title
   of width in the very first place. `overflow:hidden` here (not just on the
   Grundpreis line) sets this item's own automatic minimum flex size to 0
   instead of its full un-wrapped content width -- the same mechanism that
   already lets .shop-sticky-title shrink -- so it actually yields width to
   the title under the flex-basis:0% vs auto shrink math, rather than
   forcing the title down to 0px the way an un-shrinkable sibling did.
   `max-width` caps it at just over a third of the row even when there is
   room to spare, so the title never again has to fight the price block for
   space it needs. */
.shop-sticky-price{grid-column:1;grid-row:2;display:flex;flex-direction:column;align-items:flex-start;min-width:0;line-height:1.3}
.shop-sticky-price .shop-price{overflow:hidden;text-overflow:ellipsis;max-width:100%}
.shop-sticky-baseprice{color:var(--text-3);font-size:.75rem;white-space:normal;overflow-wrap:anywhere}
/* Package P3: an external product's Stand/stale note, wrapped into the same
   .shop-sticky-price flex item as the price for the exact reason R3's own
   comment above gives for .shop-sticky-baseprice -- a second, un-shrinkable
   flex-row sibling would starve .shop-sticky-title again. */
.shop-sticky-price .shop-price-note{font-size:.75rem;white-space:normal;overflow-wrap:anywhere}

/* --- cart (package B2) -------------------------------------------------
   No new colour pairs here: every element reuses tokens already measured by
   tests/contrast.test.mjs (--text/--text-2/--surface/--line-strong), and the
   two buttons reuse the site-wide .btn-primary/.btn-secondary classes
   (tokens.css) instead of introducing a third variant. */
.shop-cart-page{margin:0}
.shop-cart-main{max-width:960px;padding-top:32px;padding-bottom:48px}
.shop-cart-main h1{margin:20px 0 32px}
.shop-cart-main p,.shop-cart-main li{overflow-wrap:anywhere}
.shop-cart-main button,.shop-cart-main input{font:inherit}
.shop-cart-main [data-mv-transfer-manual]{display:inline-block;min-height:44px;line-height:1.5}
.mv-cart-block{margin:0 0 34px}
.mv-cart-block h2{font-size:1.15rem;line-height:1.3;margin:0 0 14px;color:var(--text)}
.mv-cart-items{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}
.mv-cart-item{display:grid;grid-template-columns:1fr auto auto auto;align-items:center;gap:14px;padding:14px 16px;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--radius-md)}
/* SAU-CK2a: thumbnail + title + type-tag share the item's own first (1fr)
   grid column as a small flex row -- no change to .mv-cart-item's own
   grid-template-columns, .mv-cart-item-media just fills the same cell
   .mv-cart-item-title used to occupy alone. */
.mv-cart-item-media{display:flex;align-items:center;gap:10px;min-width:0}
.mv-cart-item-thumb{width:48px;height:48px;object-fit:cover;border-radius:var(--radius-sm);flex:0 0 auto;background:var(--surface-2)}
.mv-cart-item-info{display:flex;flex-direction:column;gap:2px;min-width:0}
.mv-cart-item-title{color:var(--text);font-weight:600}
/* SHR-08b: a resolved variant's own label, between the title and the
   type-tag, same font-size/weight step as .mv-cart-item-type just below
   but --text-2 (one shade darker) since it names the buyer's OWN choice
   (a size, a colour), not a fixed system-wide tag every line already
   carries. */
.mv-cart-item-variant{color:var(--text-2);font-size:.82rem}
.mv-cart-item-type{color:var(--text-3);font-size:.82rem}
.mv-cart-item-delivery{display:block;color:var(--text-2);font-size:.875rem;line-height:1.5;overflow-wrap:anywhere}
.mv-cart-item-qty{width:64px;min-height:44px;padding:6px 10px;border:1px solid var(--line-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--text)}
.mv-cart-item-price{color:var(--text-2);font-weight:600;white-space:nowrap}
.mv-cart-empty{color:var(--text-2);padding:24px 0}
.mv-cart-external-note{color:var(--text-3);font-size:.9rem;margin:10px 0 0}
.mv-cart-item-pv{display:block;color:var(--text-2);font-size:.875rem}
.mv-cart-pv-note{color:var(--text-3);font-size:.82rem;margin:6px 0 0}
.mv-cart-mixed-note{color:var(--text-2);font-size:.92rem;line-height:1.5;margin:0 0 24px;padding:12px 14px;background:var(--bg-2);border:1px solid var(--line-soft);border-radius:var(--radius-md)}
.mv-cart-actions{margin:30px 0 0}
.mv-transfer{margin-top:24px;padding:24px;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--radius-md)}
.mv-ext-panel:has(.mv-transfer){padding:0;border:0;background:transparent}
.mv-transfer h3{margin:0 0 10px;font-size:1.25rem;line-height:1.35;color:var(--text)}
.mv-transfer-intro{max-width:62ch;margin:0 0 20px;color:var(--text-2);line-height:1.6}
.mv-transfer-status{margin:0 0 16px;color:var(--text-2);font-size:.9375rem;line-height:1.5}
.mv-transfer-actions{display:flex;flex-wrap:wrap;gap:12px;align-items:center}
.mv-transfer-actions p{margin:0}
.mv-transfer-actions .btn{min-height:44px;box-sizing:border-box}
.mv-transfer-progress{display:block;width:100%;height:6px;margin:0 0 20px;accent-color:var(--accent)}
.mv-transfer-details{margin-top:24px;border-top:1px solid var(--line-soft);padding-top:8px;color:var(--text-2);font-size:.9375rem;line-height:1.5}
.mv-transfer-details summary{cursor:pointer;padding:12px 0;min-height:44px;box-sizing:border-box;font-weight:600}
.mv-transfer-steps{padding-left:22px;margin:16px 0}
.mv-transfer-steps li{padding:8px 0}
.mv-transfer-steps small{display:block;color:var(--text-2)}
@media(max-width:480px){.mv-transfer{padding:18px}.mv-transfer-actions{align-items:stretch}.mv-transfer-actions>.btn{width:100%;justify-content:center}}
@media(max-width:480px){.shop-cart-page #dma-consent-fab,body:has(.shop-pdp) #dma-consent-fab{position:static;margin:16px;width:44px;height:44px}}

/* --- dōTERRA hand-off panel (package E1) -------------------------------
   Reuses .btn-primary/.btn-secondary (tokens.css) for its two buttons, no
   third button variant, and every colour is a token already covered by
   tests/contrast.test.mjs elsewhere on this page. */
.mv-ext-panel{margin:16px 0 0;padding:16px 18px;background:var(--bg-2);border:1px solid var(--line-soft);border-radius:var(--radius-md)}
.mv-ext-intro{color:var(--text-2);font-size:.92rem;line-height:1.5;margin:0 0 12px}
.mv-ext-gate p{margin:0 0 12px}
.mv-ext-list-title{color:var(--text);font-weight:600;margin:0 0 8px}
.mv-ext-links{list-style:none;margin:0 0 12px;padding:0;display:flex;flex-direction:column;gap:8px}
.mv-ext-links a{display:flex;align-items:center;min-height:44px;padding:10px 14px;border-radius:var(--radius-sm);background:var(--surface);border:1px solid var(--line-strong);color:var(--accent-text);text-decoration:none;font-weight:600}
.mv-ext-links a:hover{border-color:var(--accent);background:var(--surface-2)}
/* design-reviewer BLOCKER (PR #166): --text-3 is only AA-safe (4.5:1+) against
   --surface (tokens.css), not against this panel's --bg-2 fill (measured
   4.36:1, below the 4.5:1 floor for this .82rem/13px text). --text-2 clears
   6.34:1 on --bg-2. */
.mv-ext-openall-note{color:var(--text-2);font-size:.82rem;margin:8px 0 0}

/* SAU-EX1 lead-capture form (shop.checkout.captureBeforeHandoff): design-review
   BLOCKER found this shipped with NO scoped CSS at all on the first pass,
   relying only on the global bare input/.btn rules -- same class of gap
   .mv-checkout-fields/.mv-checkout-consent/.mv-checkout-hp already solve
   for the checkout form, mirrored here 1:1 rather than invented fresh. */
.mv-ext-lead-form label{display:block;margin:0 0 .35rem;font-size:1rem;color:var(--text-2)}
.mv-ext-lead-form input[type="text"],.mv-ext-lead-form input[type="email"]{
  box-sizing:border-box;width:100%;padding:.6rem .75rem;margin:0 0 1rem;
  border:1px solid var(--line-strong);border-radius:.4rem;background:var(--surface);color:var(--text);font-size:1rem;
}
/* BLOCKER fix: the honeypot rendered fully visible (no matching CSS at all),
   so a sighted visitor who filled it in got a silent, undetectable failure
   (server returns 200, client proceeds to dōTERRA, but no lead row/mail is
   ever created). Same clip pattern .mv-checkout-hp/.freebie-hp already use. */
.mv-ext-lead-hp{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* BLOCKER fix: no wrapping flex/touch-target treatment at all, unlike the
   framework's own .mv-checkout-consent precedent for the identical control. */
.mv-ext-lead-consent{display:flex;align-items:flex-start;gap:.6rem;margin:0 0 .5rem;font-size:.95rem;color:var(--text-2)}
.mv-ext-lead-consent input{margin-top:.2rem;flex:0 0 auto}
.mv-ext-lead-consent-note{font-size:.82rem;color:var(--text-2);margin:0 0 .9rem}
/* WARN fix: error text inherited plain --text (indistinguishable from body
   copy) and had no aria-live wiring, unlike every other async-error element
   in this codebase (freebie/kuendigen/widerruf all use --error + aria-live). */
.mv-ext-lead-error{color:var(--error);font-size:.9rem;margin:0 0 .9rem}

@media (max-width:640px){
  .mv-cart-item{grid-template-columns:1fr auto;grid-template-areas:"title title" "qty price" "remove remove"}
  .mv-cart-item-media{grid-area:title}
  .mv-cart-item-qty{grid-area:qty}
  .mv-cart-item-price{grid-area:price;text-align:right}
  .mv-cart-item-remove{grid-area:remove}
}

/* Safety net for the recurring `[hidden]` vs equal-specificity author
   `display` bug (SAU-K5, four confirmed instances: PDP-03/PDP-05/SAU-K1b's
   three per-selector fixes above, plus tokens.css's `.btn[hidden]` fix for
   site/cart/index.php's checkout button). This is tokens.css's copy of the
   same backstop rule, needed here too and NOT redundant with it: a
   build-shop.mjs generated category/product page loads ONLY this file plus
   the extracted shell's inline `<style>` (headCommon(), scripts/
   build-shop.mjs) -- it never links tokens.css directly, so tokens.css's
   own copy of this rule would never reach those pages. Keep both copies in
   sync if this selector ever changes; a fork that deletes one file without
   the other silently loses this backstop on whichever page family it
   dropped. Still add the specific per-selector fix too when a new case
   turns up (see the three examples above) -- that documents WHY, this only
   guarantees WHAT. */
[hidden]:not([hidden="until-found"]) { display: none !important; }
