/** Shopify CDN: Minification failed

Line 992:0 Unexpected "@media"

**/
/* theme.css — deferred (non-critical) styles. Below-the-fold + components. */

/* layout helpers */
.visually-hidden{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.85rem 1.6rem;border:1px solid var(--color-ink);
  background:var(--color-ink);color:var(--color-bg);border-radius:var(--radius);font:inherit;cursor:pointer;
  transition:transform var(--dur-med) var(--ease-spring),background var(--dur-fast) ease}
.btn:hover{transform:translateY(-1px)}
.btn--inverse{background:transparent;color:var(--color-ink);border-color:var(--color-ink)}
.btn--ghost{background:transparent;color:var(--color-ink);border-color:color-mix(in srgb,var(--color-ink) 45%,transparent)}
.btn--ghost:hover{border-color:var(--color-accent);color:var(--color-accent)}
.btn__arrow{transition:transform .2s ease}
.btn--ghost:hover .btn__arrow{transform:translateX(4px)}
.btn--add{width:100%;justify-content:center;margin-block:1rem;background:var(--color-accent);border-color:var(--color-accent);color:#100f0d}

/* section heading */
.section-head{padding-block:clamp(3rem,7vw,6rem) 1.5rem;max-width:38ch}
.section-head__eyebrow{text-transform:uppercase;letter-spacing:.22em;font-size:.74rem;color:var(--color-accent);margin:0 0 .75rem}
.section-head__title{font-size:clamp(1.8rem,3.5vw,3rem)}

/* ============================================================================
   EDITORIAL HERO SLIDESHOW (.heroslides) — sections/hero.liquid
   Copy panel on --surface left, media right (HomePage board). Replaces the
   full-bleed cinematic hero (empty-grey-block failure mode, rfc-home-page §1),
   and since 2026-09-12 holds any number of spreads. With one slide it is
   byte-for-byte the hero it has always been: no controls render at all.

   THE WHOLE CLS STRATEGY IN ONE PLACE, because a slideshow is the easiest way
   to fail the home page's CLS gate:
     1. All copy panels occupy ONE grid cell (.heroslides__copystack, every
        child at grid-area:1/1). The panel is therefore as tall as the TALLEST
        slide from first paint and does not resize when the slide changes —
        a slide with three lines of body copy and one with one line share a box.
     2. Inactive slides are hidden with opacity+visibility, never display:none.
        display:none would drop them out of the stack and the panel would
        collapse to the active slide's height, i.e. shift on every change.
        visibility:hidden also takes them out of the tab order and the
        accessibility tree, which is what we want anyway.
     3. The media panel is fixed geometry (min-height desktop, aspect-ratio
        mobile) with every image position:absolute — an image arriving, or
        being promoted from data-src, moves nothing.
     4. The controls are constant markup outside both stacks.
   Nothing here animates a property that triggers layout: opacity and transform
   only.
   ========================================================================== */
/* Sizing note (measured 2026-09-12, run 34666653568). The first build sized
   the copy panel at minmax(420px,560px) and the heading at clamp(2.4rem,
   4.5vw,4rem), inherited from the single-slide hero. Stacking four slides in
   one cell made the panel as tall as the LONGEST slide and the hero rendered
   1081px — taller than the 950px shot viewport, so the offer line and eyebrow
   of slide 3 were literally off the top of the screen, every heading broke to
   five lines, and slide 1 sat in a panel sized for slide 3 with 200px of dead
   space under its buttons. A hero that does not fit above the fold is not a
   hero. Wider column + smaller display size = three-line headings, side-by-side
   buttons, and ~610px total under a ~180px header. */
.heroslides{position:relative;display:grid;grid-template-columns:minmax(480px,640px) 1fr;
  min-height:min(620px,74svh);isolation:isolate;touch-action:pan-y pinch-zoom}
/* Swipe (hero-slideshow.js). pan-y hands VERTICAL panning to the browser
   before any script sees the touch, so a page scroll that starts on the hero
   is a page scroll; only a sideways drag reaches the handler. This is a hit-
   testing property, not a layout one: it changes no box, so the overflow gate
   measures the same page. is-dragging is set only once a MOUSE drag is
   clearly sideways, so the copy stops selecting mid-swipe but stays
   selectable for everyone else. */
.heroslides.is-dragging{user-select:none;-webkit-user-select:none}

/* Constant grounds, own grid cells, behind everything. They belong to the
   section rather than to a slide: a per-slide background would cross-fade
   through itself and the ground would visibly dip on every change. */
.heroslides__ground{grid-area:1/1/2/2;background:var(--surface);z-index:0}
.heroslides__canvas{grid-area:1/2/2/3;position:relative;overflow:hidden;z-index:0;
  background:linear-gradient(160deg,#EFECE6 0%,#E4DFD6 100%);display:grid;place-items:center}
/* A fixed 88px watermark, not a percentage. At 30% of an 880x1081 panel the
   line-mark rendered ~264px wide and read as a broken-image icon rather than
   as a considered empty state — which is the exact impression this whole PR
   exists to remove. */
.heroslides__canvas svg{width:104px;height:104px;stroke:#C6BDAE;stroke-width:1.1;fill:none}

.heroslides__copy{grid-area:1/1/2/2;z-index:1;display:flex;flex-direction:column;justify-content:center;
  gap:1.15rem;padding:clamp(2rem,4.5vw,3.25rem) clamp(1.25rem,3.2vw,3rem);min-width:0}
.heroslides__copystack{display:grid;min-width:0}
.hslide__copy{grid-area:1/1;display:flex;flex-direction:column;gap:1.15rem;min-width:0;
  opacity:0;visibility:hidden;transition:opacity .7s var(--ease-out),visibility 0s linear .7s}
.hslide__copy.is-active{opacity:1;visibility:visible;transition:opacity .7s var(--ease-out),visibility 0s}

/* The arrival. A still cross-dissolve on the photograph and a short staggered
   settle on the type — the lines come up 10px and stop. No horizontal slide
   (it reads as a widget and fights the fixed panel edge), no Ken Burns (a
   drifting zoom means the photograph is never at the crop it was composed at,
   and on cut-out product renders it looks like a fault). 520ms, --ease-out-expo:
   fast out of the gate, long tail, no overshoot. */
@keyframes heroRise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.hslide__copy.is-active>*{animation:heroRise .52s var(--ease-out-expo) both}
.hslide__copy.is-active>*:nth-child(1){animation-delay:.02s}
.hslide__copy.is-active>*:nth-child(2){animation-delay:.08s}
.hslide__copy.is-active>*:nth-child(3){animation-delay:.14s}
.hslide__copy.is-active>*:nth-child(4){animation-delay:.20s}
.hslide__copy.is-active>*:nth-child(5){animation-delay:.26s}

/* The offer line — what replaces the reference site's red circular sticker.
   --sale is #9A3B2E, a muted oxide, not pillar-box red; small caps on the
   eyebrow's rhythm; a hairline rule under it so it reads as a masthead line.
   The end date is deliberately a plain quiet fact on the same line, not a
   countdown: the urgency theatre is the thing being rejected. */
.hslide__flash{display:flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .9rem;margin:0 0 -.15rem;
  padding-bottom:.6rem;border-bottom:1px solid color-mix(in srgb,var(--sale) 28%,transparent);
  max-width:46ch}
.hslide__flashtext{font:600 .74rem var(--sans);text-transform:uppercase;letter-spacing:.14em;color:var(--sale)}
.hslide__flashnote{font:400 .72rem var(--sans);letter-spacing:.04em;color:var(--ink-3)}

.hslide__eyebrow{text-transform:uppercase;letter-spacing:.16em;font-size:.7rem;font-weight:600;color:var(--accent);margin:0}
.hslide__title{font-family:var(--serif);font-weight:300;font-size:clamp(2rem,2.6vw,2.75rem);line-height:1.06;
  letter-spacing:-.012em;margin:0;text-wrap:balance;max-width:20ch}
.hslide__text{font-size:clamp(.94rem,1.05vw,1rem);line-height:1.6;color:var(--ink-2);margin:0;max-width:46ch}
.hslide__ctas{display:flex;flex-wrap:wrap;gap:.75rem;margin-top:.35rem}

.hero-cta{display:inline-flex;align-items:center;gap:.65rem;font:500 .8rem var(--sans);letter-spacing:.06em;
  text-transform:uppercase;padding:.85rem 1.35rem;background:var(--ink);color:var(--paper);border:1px solid var(--ink);
  transition:background .2s var(--ease),border-color .2s var(--ease),color .2s var(--ease)}
.hero-cta:hover{background:var(--accent);border-color:var(--accent);color:#fff}
.hero-cta--ghost{background:transparent;color:var(--ink)}
.heroslides__trust{display:flex;flex-wrap:wrap;gap:.4rem 1.5rem;margin:.35rem 0 0;padding:0;list-style:none;
  font-size:.75rem;color:var(--ink-3)}

/* --- controls -------------------------------------------------------------
   In the copy panel, never over the photograph. That is what the split layout
   buys: no scrim to tune per image, nothing covering a product, and at 390px —
   where the media stacks above the copy — the controls stay with the words. */
.heroslides__controls{display:flex;align-items:center;justify-content:space-between;gap:1.25rem;
  flex-wrap:wrap;margin-top:1.1rem}

/* The index rail: the progress affordance, and deliberately not dots.
   This theme has no dots; its position indicator is a counter (.pgal__count,
   .plb__count), so dots would be a new idiom. A numeral beside a rule says
   which slide AND how many AND — because the active rule fills with --accent
   over exactly the rotate interval — how long is left, AND, by freezing
   mid-fill, that rotation is paused. It is also a 60px target instead of an
   8px one. */
.heroslides__ixrail{display:flex;align-items:center;gap:.55rem;flex-wrap:wrap}
.heroslides__ix{display:flex;align-items:center;gap:.5rem;padding:.5rem .1rem;border:0;background:none;
  cursor:pointer;color:var(--ink-3);transition:color .25s var(--ease)}
.heroslides__ix:hover,.heroslides__ix[aria-current="true"]{color:var(--ink)}
.heroslides__ixnum{font:500 .68rem var(--sans);letter-spacing:.1em;font-variant-numeric:tabular-nums}
.heroslides__ixrule{display:block;width:34px;height:1px;background:var(--line);position:relative;overflow:hidden;
  transition:width .35s var(--ease-out),background .25s var(--ease)}
.heroslides__ix[aria-current="true"] .heroslides__ixrule{width:52px;background:color-mix(in srgb,var(--ink) 18%,transparent)}
.heroslides__ixfill{position:absolute;inset:0;background:var(--accent);transform:scaleX(0);transform-origin:left}
.heroslides__ix[aria-current="true"] .heroslides__ixfill{transform:scaleX(1)}
.heroslides__ix:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
/* Only a slideshow that is genuinely rotating gets the travelling fill. When
   it is not (interval 0, reduced motion, or the visitor took control) the
   active rule is simply solid — the state stays legible without pretending a
   timer exists. */
@keyframes heroFill{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.heroslides.is-playing .heroslides__ix[aria-current="true"] .heroslides__ixfill{
  animation:heroFill var(--hero-interval,7s) linear both}
.heroslides.is-playing.is-paused .heroslides__ix[aria-current="true"] .heroslides__ixfill{animation-play-state:paused}

/* Arrows: .card__arrow reused verbatim (32x48 block, chevron, the accent wipe
   ::after), overridden two classes deep exactly as .plb .plb__nav does in the
   lightbox — and for the same two reasons, spelled out there: .card__arrow is
   opacity:0/pointer-events:none until an ancestor .card is hovered and there is
   no .card here, and it carries its own @media (max-width:900px){display:none}
   which has to be re-asserted rather than inherited. Equal specificity would
   leave this depending on source order inside one 198KB file. */
.heroslides__buttons{display:flex;align-items:center;gap:.4rem}
/* position:RELATIVE, never static — and this is the third trap in borrowing
   .card__arrow, after the ones the lightbox documents. .card__arrow::after is
   the accent wipe: position:absolute; inset:0; z-index:-1. `position:static`
   here removed the button as a containing block, so the pseudo-element sized
   itself against the nearest positioned ancestor — .heroslides itself — and
   on :hover/:focus-visible its transform:scaleX(1) painted the ENTIRE hero
   section in --accent behind the copy. Caught in the 390 shots of run
   34667160754 (hero-mid-transition-390, hero-slide-2-390): after a click the
   button keeps :hover, so the whole copy panel went bronze with the type
   ghosted over it. isolation:isolate on .card__arrow does NOT help — it
   creates a stacking context, which constrains paint order, not the
   containing block for absolute positioning. top/left/right are reset too,
   because .card__arrow--prev/--next set them and relative positioning would
   otherwise offset the button by half its parent's height. */
.heroslides .heroslides__nav{position:relative;top:auto;left:auto;right:auto;
  opacity:1;pointer-events:auto;transform:none;
  display:grid;place-items:center;width:40px;height:40px}
.heroslides .heroslides__nav svg{width:16px;height:16px}
.heroslides .heroslides__nav:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.heroslides .heroslides__nav--toggle .heroslides__icon-play{display:none}
.heroslides.is-stopped .heroslides__nav--toggle .heroslides__icon-play{display:block}
.heroslides.is-stopped .heroslides__nav--toggle .heroslides__icon-pause{display:none}
/* prefers-reduced-motion stops rotation outright, so the pause button would be
   a control for something that is not happening. The script hides it. */
.heroslides .heroslides__nav--toggle[hidden]{display:none}

/* --- media --------------------------------------------------------------- */
.heroslides__media{grid-area:1/2/2/3;z-index:1;position:relative;overflow:hidden;min-height:280px}
.hslide__media{position:absolute;inset:0;margin:0;opacity:0;visibility:hidden;
  transition:opacity .7s var(--ease-out),visibility 0s linear .7s}
.hslide__media.is-active{opacity:1;visibility:visible;transition:opacity .7s var(--ease-out),visibility 0s}
.hslide__media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

@media (max-width:899px){
  .heroslides{grid-template-columns:1fr;grid-template-rows:auto auto;min-height:0}
  .heroslides__canvas{grid-area:1/1/2/2}
  .heroslides__media{grid-area:1/1/2/2;aspect-ratio:4/3;min-height:0}
  .heroslides__ground{grid-area:2/1/3/2}
  .heroslides__copy{grid-area:2/1/3/2}
  .hslide__title{max-width:none}
}
@media (max-width:560px){
  /* One row of numerals plus three buttons does not fit 390px minus the
     gutters. Break it at a fixed width so the height change is a media query
     and not something that can happen mid-session. */
  .heroslides__controls{flex-direction:column;align-items:stretch;gap:1rem}
  .heroslides__buttons{justify-content:flex-end}
  .heroslides__ixrule{width:26px}
  .heroslides__ix[aria-current="true"] .heroslides__ixrule{width:40px}
}
@media (prefers-reduced-motion:reduce){
  /* critical.css clamps EVERY animation and transition to .001ms rather than
     removing them, which would leave the fill juddering rather than stopping.
     Kill it outright; the active rule falls back to its solid state, and the
     script never starts a timer for these visitors in the first place. */
  .heroslides__ixfill{animation:none!important}
  .hslide__copy.is-active>*{animation:none!important}
}

/* finishes showcase */
.materials__grid{display:grid;gap:1px;grid-template-columns:repeat(4,1fr);background:color-mix(in srgb,var(--color-ink) 8%,transparent)}
@media (max-width:820px){.materials__grid{grid-template-columns:repeat(2,1fr)}}
.material{position:relative;display:block;background:var(--color-bg);overflow:hidden}
.material__media{position:relative}
.material__swatch{position:absolute;inset:0;background:var(--swatch,#b08d57)}
.material__spot{position:absolute;inset:0;background:radial-gradient(60% 50% at 50% 18%,color-mix(in srgb,var(--color-ink) 8%,transparent),rgba(0,0,0,.55));opacity:.85;transition:opacity .4s}
.material:hover .material__spot{opacity:.55}
.material:hover img{transform:scale(1.04)}
.material img{transition:transform .6s cubic-bezier(.2,.7,.2,1)}
.material__label{position:absolute;left:1rem;bottom:1rem;display:flex;align-items:center;gap:.5rem;font-size:.92rem}
.material__dot{width:12px;height:12px;border-radius:50%;background:var(--swatch,#b08d57);box-shadow:0 0 0 1px color-mix(in srgb,var(--color-ink) 30%,transparent)}

/* lookbook scene */
.lookbook{position:relative;min-height:88vh;display:grid;align-items:center;overflow:hidden}
.lookbook__media{position:absolute;inset:0}
.lookbook__scrim{position:absolute;inset:0;background:linear-gradient(90deg,rgba(8,7,5,.72),transparent 60%)}
.lookbook__inner{position:relative;width:100%}
.lookbook__copy{max-width:34ch}
.lookbook__title{font-size:clamp(2rem,4vw,3.5rem);font-weight:300;margin-block:.5rem 1rem}
.lookbook__text{opacity:.9;margin-bottom:1.5rem}
.hotspot{position:absolute;width:28px;height:28px;display:grid;place-items:center}
.hotspot__pulse{width:14px;height:14px;border-radius:50%;background:var(--color-ink);box-shadow:0 0 0 0 color-mix(in srgb,var(--color-ink) 55%,transparent);animation:pulse 2.4s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--color-ink) 55%,transparent)}70%{box-shadow:0 0 0 14px transparent}100%{box-shadow:0 0 0 0 transparent}}
.hotspot__card{position:absolute;left:130%;white-space:nowrap;background:var(--color-surface);padding:.5rem .75rem;border-radius:var(--radius);font-size:.82rem;opacity:0;transform:translateX(-6px);transition:.2s;pointer-events:none}
.hotspot:hover .hotspot__card{opacity:1;transform:translateX(0)}

/* cart drawer */
.drawer-open{overflow:hidden}
.cart-drawer[aria-hidden="true"]{pointer-events:none}
/* invisible click-catcher — Athlete2 pattern keeps the page undimmed */
.cart-drawer__scrim{position:fixed;inset:0;background:transparent;z-index:90}
.cart-drawer[aria-hidden="true"] .cart-drawer__scrim{display:none}
.cart-drawer__panel{position:fixed;top:0;right:0;height:100dvh;width:min(640px,100%);background:var(--color-bg);
  z-index:91;display:flex;flex-direction:column;transform:translateX(100%);transition:transform var(--dur-med) var(--ease-spring);
  border-left:1px solid color-mix(in srgb,var(--color-ink) 12%,transparent);
  box-shadow:0 16px 40px color-mix(in srgb,var(--color-ink) 10%,transparent)}
.cart-drawer[aria-hidden="false"] .cart-drawer__panel{transform:none}

.cart-drawer__head{display:flex;justify-content:space-between;align-items:center;padding:1.1rem 1.6rem;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.cart-drawer__head h2{font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;margin:0;font-family:var(--font-body,inherit)}
.cart-drawer__close{background:none;border:0;color:inherit;font-size:1.6rem;cursor:pointer;line-height:1}
.cart-drawer__items{list-style:none;margin:0;padding:0;overflow:auto;flex:1}
.cart-line{display:grid;grid-template-columns:64px 1fr auto auto;gap:1rem;padding:1rem 1.6rem;align-items:start;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 6%,transparent)}
.cart-line:last-child{border-bottom:0}
.cart-line__media{border-radius:0;overflow:hidden;width:64px;height:64px;background:color-mix(in srgb,var(--color-ink) 5%,transparent)}
.cart-line__title{font-size:.86rem;line-height:1.3;margin:0 0 .15rem}
.cart-line__title a,.cart-line__info>a{color:var(--color-ink);text-decoration:none}
.cart-line__variant,.cart-line__sku,.cart-line__prop{color:var(--color-muted);font-size:.78rem;margin:0}
.cart-line__qty{display:inline-flex;align-items:center;margin-top:.35rem;border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);width:max-content}
.cart-line__qty button{width:24px;height:24px;border:0;background:none;cursor:pointer;color:var(--color-muted);border-radius:0;font:inherit;font-size:.9rem}
.cart-line__qty button:hover{background:color-mix(in srgb,var(--color-ink) 5%,transparent);color:var(--color-ink)}
.cart-line__qty span{min-width:24px;text-align:center;font-size:.82rem}
.cart-line__price{font-size:.9rem;font-weight:600}
.cart-line__remove{border:0;background:none;cursor:pointer;padding:.2rem;color:color-mix(in srgb,var(--color-ink) 40%,transparent)}
.cart-line__remove:hover{color:var(--color-ink)}
.cart-line__remove svg{width:14px;height:14px;display:block}
.cart-drawer__confirm{display:flex;align-items:center;gap:.8rem;background:var(--color-success);color:#fff;
  font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;padding:.85rem 1.6rem}
.cart-drawer__confirm[hidden]{display:none}
.cart-drawer__confirm-ic svg{width:18px;height:18px;display:block}
.cart-line__details summary{font-size:.74rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:color-mix(in srgb,var(--color-ink) 55%,transparent);cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:.3em;margin:.15rem 0}
.cart-line__details summary::-webkit-details-marker{display:none}
.cart-line__details summary::after{content:'+';font-size:1em}
.cart-line__details[open] summary::after{content:'\2212'}
.cart-drawer__subtotal--big{align-items:baseline}
.cart-drawer__subtotal--big span{font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--color-muted)}
.cart-drawer__subtotal--big b{font-size:1.7rem;font-weight:600}
.cart-drawer__ctas{display:grid;grid-template-columns:1fr 1fr;gap:.7rem;margin-top:.9rem}
/* Chat docked in the drawer (src/islands/cart/ChatDock.jsx). The grid only
   grows a third track when the button exists — no <shopify-chat>, no track. */
.cart-drawer__ctas:has(.cart-drawer__chat){grid-template-columns:auto 1fr 1fr}
.cart-drawer__chat{gap:.4rem;padding-inline:.9rem;white-space:nowrap}
.cart-drawer__chat svg{flex:none}
/* while the drawer is open and our docked Chat is live, Inbox's own launcher
   steps aside so it can never sit on top of Checkout (Luke, 2026-09-16) */
body.drawer-open.cart-chat shopify-chat::part(activator){display:none}
/* ---------- cart bands (Groups.jsx) ---------------------------------------
   Luke, 2026-09-16, choosing from the three-option mock: "full-width bands is
   what we want" — each group header is a tinted strip across the drawer with
   a rule above and below, the bulky band in a faint green so it pairs with the
   shipping promise. Inside the bulky band, tiles are sub-divided per
   supplier as "Warehouse #1", "Warehouse #2" (never the brand: #299) so the
   shopper can see exactly what each $100 pickup is for.

   ONE SCROLLER. .cart-drawer__items was the flex:1 scroller; with two lists
   the first grew to fill the drawer and left a blank band between the groups
   (Luke's #305 preview screenshot). The React tree now wraps every list in
   .cart-drawer__body, which is the only thing that grows and scrolls; the
   lists inside it are plain. The bare rule below stays for the SSR tree
   (sections/cart-drawer.liquid renders one list, no wrapper). */
.cart-drawer__scroll{flex:1;min-height:0;position:relative;display:flex;flex-direction:column}
.cart-drawer__body{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}
/* "N more items" cue (MoreBelow.jsx). Luke, 2026-09-16: Shopify's checkout pill
   is "quite awkward, let's make ours fit with our design aesthetic" — so: the
   band typography, an ink pill, a soft fade of the page colour under it, and it
   exists only while a line is below the fold. */
.cart-more{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:center;padding:1.6rem 0 .75rem;
  pointer-events:none;opacity:0;transition:opacity .18s ease;
  background:linear-gradient(to bottom,transparent,color-mix(in srgb,var(--color-bg) 92%,transparent) 55%,var(--color-bg))}
.cart-more.is-on{opacity:1}
.cart-more__btn{pointer-events:auto;display:inline-flex;align-items:center;gap:.5rem;padding:.5rem .95rem .5rem 1.05rem;border:0;border-radius:999px;
  background:var(--color-ink);color:var(--color-bg);font:inherit;font-size:.72rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  font-variant-numeric:tabular-nums;cursor:pointer;box-shadow:0 8px 22px color-mix(in srgb,var(--color-ink) 22%,transparent);transition:transform .15s ease}
.cart-more__btn:hover{transform:translateY(-1px)}
.cart-more__btn:focus-visible{outline:2px solid var(--color-bg);outline-offset:-4px}
.cart-more__btn svg{animation:cart-more-bob 1.6s ease-in-out infinite}
@keyframes cart-more-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(2px)}}
@media (prefers-reduced-motion:reduce){.cart-more__btn svg{animation:none}.cart-more{transition:none}}
.cart-drawer__body .cart-drawer__items{flex:none;overflow:visible}
.cart-group__title{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:.25rem .8rem;
  margin:0;padding:.7rem 1.6rem;font-size:.74rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-2);
  background:color-mix(in srgb,var(--color-ink) 4%,var(--color-bg));
  border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);
  border-bottom:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent)}
.cart-group__title--bulky{color:color-mix(in srgb,var(--color-success) 72%,var(--color-ink));
  background:color-mix(in srgb,var(--color-success) 9%,var(--color-bg))}
.cart-group__meta{font-weight:400;letter-spacing:0;text-transform:none;font-size:.78rem;font-variant-numeric:tabular-nums;color:inherit;opacity:.85}
.cart-group__sub{flex-basis:100%;font-weight:400;letter-spacing:0;text-transform:none;font-size:.8rem;line-height:1.4;color:inherit}
.cart-group__sub b{font-weight:600}
.cart-group__sub a{color:inherit;text-decoration:underline}
/* the warehouse / pallet sub-dividers: a quieter strip in the same green */
.cart-subgroup__title{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;margin:0;padding:.38rem 1.6rem;
  font-size:.68rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:color-mix(in srgb,var(--color-success) 72%,var(--color-ink));
  background:color-mix(in srgb,var(--color-success) 4%,var(--color-bg));
  border-bottom:1px solid color-mix(in srgb,var(--color-success) 18%,transparent)}
.cart-subgroup__meta{font-weight:400;letter-spacing:0;text-transform:none;font-size:.76rem;font-variant-numeric:tabular-nums}
.cart-group + .cart-subgroup__title,.cart-drawer__items + .cart-subgroup__title{border-top:1px solid color-mix(in srgb,var(--color-success) 18%,transparent)}
/* /cart draws its lines flush; the bands keep a small inset so the label
   does not sit on the page edge */
.cart-page .cart-group__title,.cart-page .cart-subgroup__title{padding-inline:.9rem}
/* identical geometry for both CTAs — .btn--add otherwise drags in PDP
   styles (margin-block, space-between) and the pair renders lopsided */
.cart-drawer__ctas .btn{display:inline-flex;align-items:center;justify-content:center;text-align:center;
  height:48px;margin:0;padding:0 1rem;font-size:.9rem;gap:.5rem}
.cart-drawer__ctas .btn--add{background:var(--color-accent);border-color:var(--color-accent);color:#fff}
.flyout__foot .flyout__cta{height:48px;display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0 1rem;font-size:.9rem}
.cart-drawer__foot{padding:1.2rem 1.6rem;border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent)}
.cart-drawer__subtotal{display:flex;justify-content:space-between;font-size:1.05rem}
.cart-drawer__note{color:var(--color-muted);font-size:.8rem;margin:.4rem 0 0}
.cart-drawer__empty{padding:3rem 1.5rem;text-align:center;display:grid;gap:1rem;place-items:center}

/* surface cards on dark */
.card__media{background:var(--color-surface)}

/* product grid + card */
/* content-visibility lives on the TILE, not on the grid.
   On the container it could only ever skip work when the ENTIRE grid was
   offscreen, which on a PLP is never: the grid starts under the toolbar and
   runs past the fold, so it is always "relevant to the user" and every tile
   in it renders. Measured at 1440x900 against this stylesheet and the real
   card markup, settled (the skip state resolves at the next rendering
   opportunity, so a synchronous measurement right after insertion reads the
   intrinsic size and flatters the container):

     tiles   forced restyle x20      forced reflow x10     scroll-through
      96     174ms -> 43ms           95ms -> 15ms          0 -> 0 dropped
     1000   1802ms -> 114ms        1102ms -> 62ms        212 -> 0 dropped
                                                     worst frame 392 -> 13ms

   Per tile the container form is linear (8.7ms -> 90.1ms per restyle from 96
   to 1000 tiles); the tile form is near flat (2.1ms -> 5.7ms). The toolbar
   already offers 24/48/72 per page, so 96 is a size we ship.

   THE PADDING IS NOT DECORATION. content-visibility carries paint
   containment, which clips at the padding box. .card__arrow sits at left:-8px
   and slides in from translateX(-16px), so it occupies 24px OUTSIDE the media
   box; contained at the card's border box it would be shaved off on every
   tile instead of just the first and last column. padding-inline + the
   matching negative margin push the paint box back out and cancel on track
   sizing, so the media, the body and the badges land pixel-identically --
   verified: media 314.4x393.0 and mediaLeft 48.0 before and after, arrow
   clearing the clip edge by 4px rather than being cut.

   minmax(0,1fr), not 1fr: 1fr floors at min-content, and the card's new
   padding pushed min-content past the track, blowing four 314px columns out
   to 420px and overflowing the grid. minmax(0,1fr) is the correct form here
   regardless -- it is what stops any wide child doing the same thing.

   contain-intrinsic-size is the placeholder height for a tile that has never
   rendered; the `auto` keyword replaces it with the real height once one has.
   Measured card height is tile*1.25 + ~134px of body, so ~500px across the
   desktop column scale and ~380px on the 2-column phone grid. */
.product-grid{display:grid;gap:clamp(1rem,2vw,2rem);grid-template-columns:repeat(var(--cols,4),minmax(0,1fr));padding-inline:28px;margin-inline:-28px}
/* one column scale for BOTH grid renderers (SSR + island), every width —
   the island previously ran auto-fill/minmax and disagreed with the SSR
   grid's fixed 4 anywhere past ~1600px (750px tiles snapping to 240px
   twelve-across on ultrawides) */
@media (min-width:1600px){.product-grid{--cols:5}}
@media (min-width:2000px){.product-grid{--cols:6}}
@media (min-width:2560px){.product-grid{--cols:7}}
@media (min-width:3100px){.product-grid{--cols:8}}
/* minmax(0,1fr), never bare 1fr, at every breakpoint. Bare 1fr is
   minmax(auto,1fr): a track's minimum is its items' min-content, so a card
   whose content will not shrink below 380px makes EVERY track 380px. At 390
   that laid the grid out as two 380px columns (measured 2026-09-12, run
   34672800556: grid-template-columns resolved to "380px 380px" in a 358px
   container, column two starting at x=384) and html,body{overflow-x:clip}
   quietly cut the second column off the screen. The desktop rule above
   already used minmax(0,1fr); these two, from 2026-08-10, predate it. */
@media (max-width:1000px){.product-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media (max-width:680px){.product-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.card{position:relative;content-visibility:auto;contain-intrinsic-size:auto 500px;padding-inline:28px;margin-inline:-28px}
@media (max-width:680px){.card{contain-intrinsic-size:auto 380px}}
/* blocky A-2 direction: square media, badges + actions hug the image edges */
.card__media{position:relative;border-radius:0}
/* ---------- one-frame tile reveal (quick-view loadbar language) ----------
   Shopify's CDN streams WebP/AVIF and browsers paint those row-by-row as bytes
   land, so on a slow line a tile visibly WIPES downward. The A-2 reference
   (Athlete2) hides that by collapsing the img to a 44x3 sliver behind an
   indeterminate bar and snapping it back on load. Same behaviour, our idiom:
   hold the frame at opacity 0 behind the SAME loadbar the quick view uses, and
   let assets/media-ready.js add .is-ready once the bitmap has actually decoded.
   Holding OPACITY rather than geometry keeps native lazy/eager, fetchpriority
   and srcset, all of which the data-src approach throws away.

   The reveal is a CUT, not a fade — the theme's hold-then-cut view-transition
   language, and it also leaves the alt-frame crossfade choreography below
   (which owns `transition:opacity` on this same element) completely untouched.

   .js gates the whole thing so a no-JS render never hides its images, and the
   12s failsafe animation reveals the frame even if the bundle 404s. The tile
   NEVER ends up permanently blank behind a spinner. */
.js .card__media:not(.is-ready)>img:not(.card__media-alt){opacity:0;animation:card-media-failsafe 0s linear 12s forwards}
@keyframes card-media-failsafe{to{opacity:1}}
/* z-index 0 keeps the bar above the held image but below .card__flags (1) and
   .card__cta (2), so a Sold out badge still reads while the tile is loading —
   exactly as it does over the quick view's own loading state. */
.js .card__media:not(.is-ready)::after{content:'';position:absolute;top:50%;left:50%;
  transform:translate(-50%,-50%);z-index:0;width:min(56%,140px);height:4px;pointer-events:none;
  background-color:color-mix(in srgb,var(--color-ink) 10%,transparent);
  background-image:linear-gradient(90deg,var(--color-accent) 0 100%);
  background-repeat:no-repeat;background-size:40% 100%;
  animation:card-loadbar .9s ease-in-out infinite,card-loadbar-off 0s linear 12s forwards}
@keyframes card-loadbar{from{background-position:-45% 0}to{background-position:145% 0}}
@keyframes card-loadbar-off{to{opacity:0}}
/* critical.css clamps every animation-duration to .001ms under reduced motion,
   which would turn the sweep into a 1000fps flicker rather than stopping it —
   so the bar has to be explicitly stilled here. The failsafe survives (its
   duration was already 0s; only the 12s DELAY matters). */
@media (prefers-reduced-motion:reduce){
  .js .card__media:not(.is-ready)::after{background-image:none;
    animation:card-loadbar-off 0s linear 12s forwards}
}
/* the alt frame stacks ON TOP of the main one. Its own opaque ground is what
   makes it a replacement rather than an overlay: product renders are often
   cut-out PNGs (PAR, Meir) and without it the first image showed through the
   second for as long as the tile was hovered. Same token as .card__media, so
   the transparent margin reads as one continuous tile. */
.card__media-alt{position:absolute;inset:0;opacity:0;transition:opacity .35s ease;object-fit:cover;background:var(--color-surface)}
/* .is-ready (media-ready.js) because that opaque ground is exactly what makes
   an UNLOADED alt frame paint as a blank grey tile: hovering a tile whose
   second image had not arrived yet crossfaded the photo out and surface colour
   in. The swap now waits for the frame it is swapping to. */
.card:hover .card__media-alt.is-ready{opacity:1}
/* and the covered frame is switched off outright once the crossfade has
   landed — 0s duration on a .35s delay, i.e. a step at the END of the fade, so
   the fade itself is unchanged for opaque photography while the settled state
   carries a single painted layer. :has() scopes this to tiles that HAVE an alt
   frame: island tiles render one image only, and hiding it would empty them
   (browsers without :has simply keep the opaque-alt behaviour above). */
.card__media:has(.card__media-alt)>img:not(.card__media-alt){transition:opacity 0s linear,visibility 0s linear}
.card:hover .card__media:has(.card__media-alt.is-ready)>img:not(.card__media-alt){opacity:0;visibility:hidden;transition-delay:.35s}
/* while a swatch hover previews a variant image — or the hover arrows are
   paging the card's own images — the alt-image crossfade would cover it;
   keep the main frame in charge (see the carousel block in ui.js) */
.card.is-variant-preview .card__media-alt,.card.is-browsing .card__media-alt,
.card.is-variant-preview .card__media-alt.is-ready,
.card.is-browsing .card__media-alt.is-ready{opacity:0}
/* ...which means the main frame has to come straight back, with no step-out
   pending against it */
.card.is-variant-preview .card__media:has(.card__media-alt)>img:not(.card__media-alt),
.card.is-browsing .card__media:has(.card__media-alt)>img:not(.card__media-alt),
.card.is-variant-preview .card__media:has(.card__media-alt.is-ready)>img:not(.card__media-alt),
.card.is-browsing .card__media:has(.card__media-alt.is-ready)>img:not(.card__media-alt){opacity:1;visibility:visible;transition-delay:0s}
/* hover arrows page the main frame through the product's images in place.
   A-2 blocky nav, measured on athlete2's Related Products row, slimmed for our
   tile: a 32x48 block that STRADDLES the tile edge. At rest it sits 24px
   outside / 8px inside and is invisible; on tile hover it fades in and slides
   16px — half its own width — inward, landing 8px outside / 24px inside. Rest
   and revealed positions stay exact mirrors of each other (A-2's 40x56 block
   does the same at 30/10 -> 10/30), which is what makes the block read as
   hinged on the tile boundary rather than parked beside it.
   The rest fill is deliberately NOT ink: a black slab shouting off both tile
   edges made mouseout the loudest moment on the grid. This is A-2's own
   disabled-block value — their #000 at 20% over a light tile reads about #c4c4c4
   — used as the RESTING colour, so the control is quiet until it is wanted and
   only the accent wipe raises its voice. */
.card__arrow{position:absolute;top:50%;width:32px;height:48px;border-radius:0;padding:0;border:0;
  background:color-mix(in srgb,var(--ink) 14%,var(--paper));color:var(--ink);isolation:isolate;
  display:grid;place-items:center;cursor:pointer;z-index:3;opacity:0;pointer-events:none;
  transition:opacity .2s ease,transform .2s ease,color .2s ease}
.card__arrow svg{width:15px;height:15px;stroke-width:1.5}
.card__arrow--prev{left:-8px;transform:translateY(-50%) translateX(-16px)}
.card__arrow--next{right:-8px;transform:translateY(-50%) translateX(16px)}
/* the block's own hover wipes the accent across it from the INNER edge outward.
   A-2 runs this asymmetrically and it is the better half of the effect: their
   button::after is .256s coming IN and .512s going OUT, on the spring curve our
   --ease-spring already carries. Quick to answer, slow to let go.
   z-index:-1 paints the wipe over the block's own background but under the
   chevron: the block isolates its own stacking context, so the negative layer
   lands between the two. The glyph flips to paper 100ms in, once the bronze has
   actually reached it — ink on bronze is only 2.9:1, paper on bronze is 5.0:1. */
.card__arrow::after{content:'';position:absolute;inset:0;z-index:-1;background:var(--accent);
  transform:scaleX(0);transition:transform .512s var(--ease-spring)}
.card__arrow--prev::after{transform-origin:right}
.card__arrow--next::after{transform-origin:left}
.card__arrow:hover::after,.card__arrow:focus-visible::after{transform:scaleX(1);transition-duration:.256s}
.card__arrow:hover,.card__arrow:focus-visible{color:var(--paper);
  transition:opacity .2s ease,transform .2s ease,color .12s ease .1s}
/* opacity:0 alone still hit-tests — an invisible arrow would swallow taps over
   the middle of the tile on large touchscreens (same guard as .card__cta).
   Deliberately NOT visibility:hidden, A-2's reveal property of choice: that
   would drop both blocks out of the tab order. */
.card:hover .card__arrow,.card__arrow:focus-visible{opacity:1;pointer-events:auto}
.card:hover .card__arrow--prev,.card__arrow--prev:focus-visible,
.card:hover .card__arrow--next,.card__arrow--next:focus-visible{transform:translateY(-50%) translateX(0)}
/* can't page further: the same grey, faded back, with the wipe held shut.
   Styling hook only — the card pager itself cycles (see ui.js), so nothing sets
   this today; the island/PDP pagers can opt in without a restyle. */
.card__arrow[disabled],.card__arrow[aria-disabled="true"],
.card:hover .card__arrow[disabled],.card:hover .card__arrow[aria-disabled="true"]{opacity:.45;cursor:default;pointer-events:none}
.card__arrow[disabled]::after,.card__arrow[aria-disabled="true"]::after{transform:scaleX(0)}
/* the block hangs 8px into the 28.8px grid gutter, so a hovered tile has to
   paint above its neighbour or the next card's media background clips it */
.card:hover{z-index:3}
/* and the media box must stop clipping: .media>img is already object-fit:cover
   at exactly 100%/100%, so the arrows are the only thing that reaches the edge */
.card__media{overflow:visible}
@media (max-width:900px){.card__arrow{display:none}} /* touch swipes the grid */
/* fade in place rather than snapping sideways when motion is unwelcome
   (critical.css already zeroes every transition-duration globally) */
@media (prefers-reduced-motion:reduce){
  .card__arrow--prev,.card__arrow--next{transform:translateY(-50%)}
  /* the crossfade is instant here, so the covered frame must step out with it
     rather than waiting out a delay the fade no longer takes */
  .card:hover .card__media:has(.card__media-alt)>img:not(.card__media-alt){transition-delay:0s}
}
.card__flags{position:absolute;left:0;bottom:0;display:flex;flex-wrap:wrap;gap:2px;z-index:1;max-width:100%}
.card__badge{background:var(--color-ink);color:var(--color-bg);font-size:.68rem;font-weight:700;letter-spacing:.1em;padding:.3rem .55rem;text-transform:uppercase}
.card__vendor{color:var(--color-ink);font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin:.75rem 0 .1rem}
/* brand line links through to the brand-filtered PLP; underline only on hover
   so the tile keeps reading as one editorial block at rest */
.card__vendor a{color:inherit;text-decoration:none;border-bottom:1px solid transparent}
.card__vendor a:hover{border-bottom-color:currentColor}
.card__type{color:var(--color-muted);font-size:.78rem;margin:0 0 .15rem}
.card__title{font-family:var(--font-body);font-size:.98rem;font-weight:500;line-height:1.3;margin:0 0 .25rem}
.rating--card{margin:.1rem 0 .2rem;font-size:.74rem}
.rating--card .rating__stars svg{width:11px;height:11px}
.card__price{font-size:.95rem}
.card__price s{color:var(--color-muted);margin-right:.4rem}
/* B2B sessions (window.ST_CUSTOMER.b2b / customer.b2b?): the client grids
   read prices from the Typesense index, which is RETAIL — never the
   shopper's catalog price — so they label the figure "Retail" and drop the
   Save badge/strike; the trade price is on the PDP (contextual Liquid). The
   Liquid card is contextual already; its strike reads "RRP" for trade. */
.card__price--rrp{color:var(--ink-2)}
.card__rrp{font-size:.66rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;margin-right:.35rem;color:var(--ink-3,var(--color-muted))}
.card__price--trade s::before{content:'RRP ';font-size:.72em;letter-spacing:.06em}
/* swatches (snippets/swatch.liquid): --sw carries the colour or gradient the
   swatch_value metaobject configures; ui.js reads the same property for the
   hover popover, so keep the name in sync with the facet rail swatches. */
.card__swatches{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.5rem}
/* CIRCULAR chips (Luke 2026-09-08: "change the swatches from the rounded
   square swatch concept we insisted on earlier in the project, back to a
   circular swatch like most other websites do, thebluespace is one
   example"). This one rule is the shape for every surface: card strip, PDP
   picker, megamenu Colours, mobile drawer, home finish strip — do not
   re-square any of them individually. Sizes unchanged. */
/* position:relative — the swatch is the CONTAINING BLOCK for its own
   screen-reader label. snippets/swatch.liquid puts the finish name inside
   every colour chip as <span class="visually-hidden">, and .visually-hidden
   is position:absolute. Left static, the chip is not a containing block, so
   that 1px label resolved against the initial containing block at the
   chip's static position — and inside a horizontal scroller that position
   is the chip's slot in the TRACK, which the scroller's overflow-x cannot
   clip (a scroller only clips descendants whose containing block lies
   inside it). Luke, 2026-09-12, phone, home page: "a huge spillover of empty
   white space ... by scrolling right". Measured: 36 finish-strip labels
   fanned to left:2831px at a 390px viewport (2603 at 360, 2717 at 375) and
   the document was that wide; PDP, collections and cart fit. Introduced by
   #190 (a6192c3, the all-finishes scrolling strip); the label itself dates
   from #97. Gate: scripts/overflow-gate.mjs. */
.swatch{display:inline-block;position:relative;width:14px;height:14px;padding:0;border-radius:50%;
  border:1px solid color-mix(in srgb,var(--color-ink) 20%,transparent);background:var(--sw,var(--color-muted))}
button.swatch{cursor:pointer;font:inherit;color:inherit}
/* touch-action: manipulation on every option control — a tap on a chip is
   never the first half of a double-tap-to-zoom, so iOS need not hold the
   click back to see whether a second tap follows, and two quick taps on
   neighbouring chips can never be read as one double tap. Panning is
   unaffected. (fix/mobile-swatch-tap, 2026-09-12.) */
button.swatch,.product__values [data-option-value]{touch-action:manipulation}
.swatch--lg{width:30px;height:30px}
/* xl: the home shop-by-finish tile — same component, gallery scale */
.swatch--xl{width:clamp(64px,7vw,96px);height:clamp(64px,7vw,96px)}
.swatch--img{overflow:hidden;background:var(--color-bg)}
/* the global img rule forces height:auto — pin both axes so the crop fills */
.swatch--img img{width:100%;height:100%;object-fit:cover;border-radius:50%}
/* selected = a deliberate ink RING with an inner gap, not a floating accent
   outline. background-clip keeps the colour fill inside the gap (the
   content box of a circle is a smaller circle); image swatches get the same
   read via padding. */
.swatch.is-selected,.swatch--lg.is-selected{outline:none;border:2px solid var(--ink);padding:2px;background-clip:content-box}
.swatch--img.is-selected img{border-radius:50%}
/* label-type swatches: a compact text pill on cards ('lg'/'xl' use .swtext) */
.swlabel{display:inline-block;padding:.15rem .5rem;font-size:.7rem;line-height:1.5;
  border:1px solid color-mix(in srgb,var(--color-ink) 20%,transparent);border-radius:999px;color:var(--color-muted)}
.swlabel.is-selected{border-color:var(--color-accent);color:var(--color-ink)}

/* PDP */
/* minmax(0,..): a grid fr track's default min is min-content, so one huge
   source image (5000px shower-rail portrait) forces the gallery column past
   the viewport and shoves the buy box off-screen. Zero the mins. */
.product{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);gap:clamp(1.5rem,4vw,4rem);padding-block:2rem}
/* one column below 900px: minmax(0,1fr) like the desktop tracks, never bare
   1fr (= minmax(auto,1fr)) — the gallery's thumb strip (N x 64px, no
   shrink) is the column's min-content, and on a 12-thumb product the single
   column grew to 880px at a 390px viewport, clipping the buy box on the
   right (CI 390 shots since 2026-09-08). min-width:0 on the gallery item
   makes the same guarantee from the item's side. */
@media (max-width:900px){.product{grid-template-columns:minmax(0,1fr)}}
/* gallery: main stage (scroll-snap strip) + thumbnail rail. Sticky so the
   imagery rides alongside the buy box; falls back to static on mobile. */
.pgal{display:flex;flex-direction:column;gap:.75rem;align-self:start;position:sticky;top:88px;min-width:0}
@media (max-width:900px){.pgal{position:static}}
.pgal__viewport{position:relative}
.pgal__stage{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;
  scrollbar-width:none;border-radius:var(--radius);outline:none}
.pgal__stage::-webkit-scrollbar{display:none}
.pgal__slide{flex:0 0 100%;scroll-snap-align:start;scroll-snap-stop:always;position:relative;overflow:hidden;background:#fff}
/* the BOX paints the layout, the image just fills it: absolute + contain
   breaks the intrinsic-size feedback loop, so a 1:5 rail and a square tap
   occupy identical slides regardless of source pixels. White ground matches
   supplier cut-out photography, so the letterbox is invisible. */
.pgal__slide>img,.pgal__slide .zoomable__img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
.pgal__thumb img{width:100%;height:100%;object-fit:contain;background:#fff}
.pgal__arrow{position:absolute;top:50%;transform:translateY(-50%);width:44px;height:44px;border-radius:50%;
  border:1px solid var(--line);background:color-mix(in srgb,var(--paper) 90%,transparent);color:var(--ink);
  display:grid;place-items:center;cursor:pointer;z-index:2;opacity:0;transition:opacity .15s,background .15s}
.pgal__viewport:hover .pgal__arrow,.pgal__arrow:focus-visible{opacity:1}
.pgal__arrow:hover{background:var(--paper)}
.pgal__arrow--prev{left:.75rem}
.pgal__arrow--next{right:.75rem}
.pgal__arrow[disabled]{opacity:0!important;pointer-events:none}
@media (max-width:900px){.pgal__arrow{display:none}} /* touch swipes */
.pgal__count{position:absolute;right:.75rem;top:.75rem;z-index:2;font-size:.72rem;letter-spacing:.08em;
  background:color-mix(in srgb,var(--paper) 88%,transparent);color:var(--ink-2);padding:.25rem .6rem;border-radius:999px}
.pgal__thumbs{display:flex;gap:.5rem;overflow-x:auto;scrollbar-width:none;padding:2px}
.pgal__thumbs::-webkit-scrollbar{display:none}
.pgal__thumb{position:relative;flex:0 0 64px;height:64px;padding:0;border:1px solid var(--line);
  border-radius:var(--radius);background:var(--surface);cursor:pointer;overflow:hidden;opacity:.65;transition:.15s}
.pgal__thumb img{width:100%;height:100%;object-fit:cover;display:block}
.pgal__thumb:hover{opacity:1}
.pgal__thumb.is-active{opacity:1;border-color:var(--accent);outline:1px solid var(--accent)}
.pgal__thumb-badge{position:absolute;right:3px;bottom:3px;width:20px;height:20px;border-radius:50%;
  background:color-mix(in srgb,var(--ink) 70%,transparent);color:var(--paper);display:grid;place-items:center}
.product__info{position:sticky;top:88px;align-self:start}
.product__title{font-size:clamp(1.8rem,3vw,2.6rem)}
.product__sku{color:var(--color-muted);font-size:.85rem}
/* vendor is a link into the brand-facetted PLP */
.product__vendor a{color:inherit;text-decoration:none;border-bottom:1px solid transparent;transition:border-color .15s}
.product__vendor a:hover{border-bottom-color:currentColor}

/* collection */
.collection__head{padding-block:1.1rem 0}
.collection__filters{display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem}
.filter summary{cursor:pointer;font-weight:500}
.filter__value{display:block;padding:.2rem 0;color:var(--color-muted)}
.filter__value.is-active{color:var(--color-ink);font-weight:500}
.pagination{display:flex;gap:1rem;justify-content:center;padding-block:2.5rem}

/* footer */
.site-footer{margin-top:5rem}
.site-footer__news{border-top:1px solid var(--line)}
.site-footer__news-grid{display:grid;grid-template-columns:1fr auto;gap:2rem;align-items:center;padding-block:2.6rem}
.site-footer__news h3{font-family:var(--serif);font-weight:500;font-size:1.35rem;margin:0 0 .3rem}
.site-footer__news p{color:var(--ink-3);font-size:.88rem;margin:0}
.site-footer__news-row{display:flex;min-width:min(420px,90vw)}
/* min-width:0 on the input: a flex item's automatic minimum is its
   intrinsic width, and an <input>'s intrinsic width plus the Subscribe
   button came to 392px at a 390px viewport (2026-09-12, run 34673355199,
   every template) — the row could not shrink, the form overflowed its
   column, and html,body{overflow-x:clip} cut the button off the right edge
   on every page. flex:1 says "share the row"; without min-width:0 it never
   could. The column rule at the 900px breakpoint gets minmax(0,1fr) for the
   same reason. */
.site-footer__news-row input{flex:1;min-width:0;border:1px solid var(--line);border-right:0;border-radius:var(--radius) 0 0 var(--radius);padding:.8rem 1rem;font:inherit;background:var(--paper);color:var(--ink)}
.site-footer__news-row button{border:0;background:var(--ink-rich);color:#fff;padding:.8rem 1.4rem;border-radius:0 var(--radius) var(--radius) 0;font:600 .85rem var(--sans);cursor:pointer;transition:background .2s var(--ease)}
.site-footer__news-row button:hover{background:var(--accent)}
.site-footer__news-ok{color:var(--ink-2);font-size:.9rem}
.site-footer__news-err{color:var(--sale);font-size:.78rem;margin:.4rem 0 0}
.site-footer__main{border-top:1px solid var(--line)}
.site-footer__grid{display:grid;grid-template-columns:1.4fr repeat(4,1fr);gap:2.4rem;padding-block:3.2rem}
.site-footer__logo{font-family:var(--serif);font-weight:500;font-size:1.35rem;margin:0}
.site-footer__tag{color:var(--ink-3);font-size:.85rem;margin:.5rem 0 1.1rem;max-width:26ch}
.site-footer__contact{font-size:.82rem;color:var(--ink-2);line-height:1.7;margin:0}
.site-footer__contact a{color:inherit;border-bottom:1px solid var(--line)}
.site-footer__socials{display:flex;gap:.6rem;margin-top:1.1rem}
.site-footer__socials a{width:32px;height:32px;border:1px solid var(--line);border-radius:var(--radius);display:grid;place-items:center;transition:border-color .2s var(--ease)}
.site-footer__socials a:hover{border-color:var(--accent)}
.site-footer__socials svg{width:15px;height:15px;stroke:var(--ink-2);fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;display:block}
.site-footer__col h4{font:600 .72rem var(--sans);text-transform:uppercase;letter-spacing:.12em;color:var(--ink-3);margin:0 0 .9rem}
.site-footer__col a{display:block;padding:.22rem 0;font-size:.88rem;color:var(--ink-2);transition:color .2s var(--ease)}
.site-footer__col a:hover{color:var(--ink)}
.site-footer__legal{border-top:1px solid var(--line)}
.site-footer__legal-grid{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;justify-content:space-between;padding-block:1.1rem 1.3rem;font-size:.78rem;color:var(--ink-3)}
/* Copyright + the GeoNames CC BY credit stack as ONE flex child, so the legal
   row keeps its two-child space-between layout (text left, payment marks
   right) instead of the credit becoming a third item and pushing the marks
   into the middle. */
.site-footer__legal-text{display:flex;flex-direction:column;gap:.25rem}
.site-footer__pays{display:flex;gap:.4rem;flex-wrap:wrap}
.site-footer__pays svg{height:22px;width:auto;display:block}
/* ink variant: definitive bottom edge; newsletter band stays on paper */
.site-footer--ink .site-footer__main,.site-footer--ink .site-footer__legal{background:var(--ink-rich);border-color:#2a2822}
.site-footer--ink .site-footer__main{color:#BDB7AB}
.site-footer--ink .site-footer__logo{color:#fff}
.site-footer--ink .site-footer__tag,.site-footer--ink .site-footer__col h4{color:#8C857A}
.site-footer--ink .site-footer__contact{color:#BDB7AB}
.site-footer--ink .site-footer__contact a{border-color:#3a372f}
.site-footer--ink .site-footer__col a{color:#BDB7AB}
.site-footer--ink .site-footer__col a:hover{color:#fff}
.site-footer--ink .site-footer__socials a{border-color:#3a372f}
.site-footer--ink .site-footer__socials a:hover{border-color:var(--accent-2)}
.site-footer--ink .site-footer__socials svg{stroke:#BDB7AB}
.site-footer--ink .site-footer__legal-grid{color:#8C857A}
@media (max-width:900px){
  .site-footer__news-grid{grid-template-columns:minmax(0,1fr)}
  .site-footer__grid{grid-template-columns:1fr 1fr;gap:1.8rem}
  .site-footer__brand{grid-column:1/-1}
}

/* ---------- PDP buy box / zoom / aux ---------- */
.product__option{border:0;padding:0;margin:0 0 1.25rem}
.product__option legend{padding:0;margin-bottom:.5rem;font-size:.9rem;color:var(--color-muted)}
.product__option legend span{color:var(--color-ink)}
.product__values{display:flex;flex-wrap:wrap;gap:.5rem}
/* ---------- text-value option tiles (.swtext) ----------
   Size / Length / Capacity — every option whose swatch type resolves to
   'label' (snippets/swatch-type.liquid). Luke, 2026-09-14, on the Alveri Egg
   bath PDP: "in comparison to the other swatches these size swatches don't
   look good ... let's do them more like athlete2's example text label
   swatches that are being used for size here so that it looks like a cleaner
   design." They were wide rounded pills (border-radius:999px, a warm-tan
   accent fill and a brown border when selected) sitting beside 30px circular
   finish chips, so the lighter element on the row was the one carrying the
   most visual weight.

   The athlete2 (Under Armour PDP) pattern this reproduces: a compact tile in
   a flat neutral fill, bold centred label, no border at rest, tight uniform
   gaps, a thick solid black ring on the selected tile over the SAME fill, and
   a small black callout above whichever tile the pointer is on, repeating
   that tile's label.

   Fill is --surface (#F5F5F3), the warm-white palette's own neutral, and the
   ring/callout are --ink, so this stays inside the theme's tokens.

   The selected ring is a box-shadow, not `outline`, for two reasons:
   (1) neither shifts layout, but `outline` is the theme's focus affordance
   (critical.css :where(...):focus-visible) and .swtext.is-selected would have
   out-specified it — a keyboard user on the selected tile would have lost the
   focus ring entirely; (2) box-shadow rings compose, so a focused selected
   tile shows both. Nothing here changes the tile's box, so selecting cannot
   reflow the buy box and the CLS budget is untouched. */
.swtext{display:inline-flex;align-items:center;justify-content:center;text-align:center;
  min-width:44px;min-height:44px;padding:.5rem .85rem;border:0;border-radius:3px;
  background-color:var(--surface);color:var(--ink);cursor:pointer;
  font:inherit;font-size:.9rem;font-weight:600;line-height:1.2;
  transition:background-color var(--dur-fast,.18s),box-shadow var(--dur-fast,.18s),color var(--dur-fast,.18s)}
/* hover = a slightly darker fill (the callout that goes with it is further
   down). Both are inside `@media (hover:hover)` so neither can be reached by
   a touch pointer — see the callout's note. */
@media (hover:hover){.swtext:hover{background-color:color-mix(in srgb,var(--ink) 8%,var(--surface))}}
.swtext.is-selected{box-shadow:0 0 0 2px var(--ink)}
/* no purchasable variant behind this value: muted, struck through, still
   clickable (selecting it is how the shopper reaches the sold-out Add button
   and the back-in-stock form). */
.swtext.is-unavailable{color:var(--ink-3);
  background-image:linear-gradient(to top right,transparent calc(50% - .5px),currentColor calc(50% - .5px),currentColor calc(50% + .5px),transparent calc(50% + .5px))}
.swtext.is-unavailable.is-selected{box-shadow:0 0 0 2px var(--ink-3)}

/* the callout (athlete2's black bubble + pointer). Luke, 2026-09-14, on the
   first cut of this change: "you have the wrong behaviour for click to show
   the label here in your mockup, take a look at the Athlete2 theme example I
   sent to you, that popover showing the label is shown when the user
   mouseovers the label swatch." So it follows the POINTER, not the selection:
   it names whatever tile is under the cursor (selected or not) and is gone on
   mouseout. Selection is the ink ring and nothing else.

   The whole thing — bubble, pointer and the band that holds them — lives
   inside `@media (hover:hover)` and therefore does not exist on a phone. That
   is not a nicety: iOS WebKit withholds the first tap on an element whose
   hover phase makes content visible, which is the bug #237 fixed for the
   finish chips and which scripts/swatch-tap-gate.mjs exists to hold. A
   hover-revealed tooltip that a touch device could also fire is precisely the
   trap. `:focus-visible` is in the same block for the same reason: WebKit's
   focus-visible heuristics are not something to bet the buy button on.

   CSS-only and `pointer-events:none`, so the bubble can never eat the click
   for the tile underneath it.

   Clipping and overflow are handled by construction rather than by a media
   query or a JS shifter:
   - the row reserves the band up front, whether or not anything is hovered,
     so the bubble cannot clip against the legend above it AND hovering never
     moves a pixel (the CLS budget is untouched);
   - the bubble prints the SAME string as its tile at a smaller font with
     smaller padding, clamped to max-width:100% of the tile, so it can never
     reach past that tile's own left/right edges — which are already inside
     the page gutter at 360px. */
@media (hover:hover){
  .product__values--text{padding-top:2.1rem;margin-top:-.5rem}
  .product__values--text .swtext{position:relative}
  .product__values--text .swtext:hover::after,
  .product__values--text .swtext:focus-visible::after{content:attr(data-value);
    position:absolute;left:50%;bottom:calc(100% + 9px);transform:translateX(-50%);
    max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    padding:.18rem .5rem;border-radius:3px;background:var(--ink);color:var(--paper);
    font-size:.72rem;font-weight:600;line-height:1.5;letter-spacing:.01em;
    pointer-events:none}
  /* the pointer. border-width has NO bottom edge on purpose: a 4-sided
     transparent border would make the box 10px tall, so the visible
     top-border triangle would sit 5-10px above the tile — behind the bubble,
     which starts at 9px — and nothing would be drawn between the two. */
  .product__values--text .swtext:hover::before,
  .product__values--text .swtext:focus-visible::before{content:"";
    position:absolute;left:50%;bottom:calc(100% + 4px);transform:translateX(-50%);
    width:0;height:0;border-style:solid;border-width:5px 5px 0;
    border-color:var(--ink) transparent transparent;
    pointer-events:none}
}
.btn--add{display:flex;justify-content:space-between;gap:1rem}
.price-pill{opacity:.85}
/* buy row: qty stepper + Add to cart. The stepper keeps its natural width;
   the button flexes from a 220px floor and wraps to its own full-width line
   when the column cannot give it that (390px phones), so nothing squeezes.
   .btn--add's own margin-block moves to the row. */
.product__buy{display:flex;flex-wrap:wrap;align-items:stretch;gap:.6rem;margin-block:1rem}
.product__buy .btn--add{flex:1 1 220px;width:auto;margin:0}
/* the cart drawer's stepper (.cart-line__qty), scaled to the row height:
   same hairline, same borderless buttons that tint on hover, same muted
   glyph going ink; 44px buttons so a thumb lands, SVG strokes for the +/-
   (theme rule: no text-glyph icons) */
.qty{display:inline-flex;align-items:stretch;border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);border-radius:var(--radius);min-height:46px;flex:0 0 auto}
.qty__btn{width:44px;border:0;background:none;cursor:pointer;color:var(--color-muted);border-radius:0;display:inline-flex;align-items:center;justify-content:center;padding:0;font:inherit}
.qty__btn svg{width:16px;height:16px}
.qty__btn:hover{background:color-mix(in srgb,var(--color-ink) 5%,transparent);color:var(--color-ink)}
.qty__btn:disabled{opacity:.35;cursor:default;background:none;color:var(--color-muted)}
.qty__btn:focus-visible{border-radius:var(--radius)}
.qty__input{width:52px;min-width:0;border:0;border-inline:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);background:none;color:var(--color-ink);text-align:center;font:inherit;font-size:max(var(--touch-min-font,0px),.95rem);font-variant-numeric:tabular-nums;padding:0 .2rem;border-radius:0;-moz-appearance:textfield}
.qty__input:focus-visible{outline-offset:-2px;border-radius:0}
.qty__msg{margin:-.5rem 0 .75rem;font-size:.76rem;line-height:1.35;color:var(--color-accent)}
.product__aux{margin:1rem 0 .5rem;font-size:.9rem}
/* Add to project / Price match: the theme's ghost button (proto.css .btn--ghost),
   two EQUAL-WIDTH columns (grid-auto-columns:1fr — one button alone still
   fills the row). They rendered as browser-default buttons before (Luke,
   2026-09-08); the grey trade sentence that shared this row moved under the
   price (trade-prompt.css) on 2026-09-09. Icon + label; the label span is
   what project-list.js repaints ("In your project"). */
.product__aux-actions{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:.5rem}
.product__project{height:40px;padding:0 .95rem;font-size:.8rem;letter-spacing:.02em;gap:.45rem;justify-content:center;min-width:0}
.product__project svg{flex:none;width:16px;height:16px}
.product__project.is-active{background:var(--ink);color:var(--paper);border-color:var(--ink)}
/* .zoomable is now only a layout hook (see .pgal__slide .zoomable__img above)
   plus the cursor that says "this opens". The in-place scale(2.2) zoom and its
   .zoom-toggle were removed 2026-09-10 and folded into the full-screen viewer
   — assets/product.js carries the reasoning. */
.zoomable{cursor:zoom-in;overflow:hidden}

/* ---------- predictive search overlay ---------- */
.search-overlay[aria-hidden="true"]{pointer-events:none}
.search-overlay__scrim{position:fixed;inset:0;background:rgba(5,4,3,.6);opacity:0;transition:.25s;z-index:95}
.search-overlay[aria-hidden="false"] .search-overlay__scrim{opacity:1}
.search-overlay__panel{position:fixed;inset:0;background:var(--color-surface);z-index:96;padding:1.25rem clamp(16px,4vw,48px) 0;transform:translateY(-100%);transition:transform .3s cubic-bezier(.2,.7,.2,1);height:100dvh;display:flex;flex-direction:column}
.search-overlay[aria-hidden="false"] .search-overlay__panel{transform:none}
.search-overlay__form{display:flex;gap:1rem;align-items:center;max-width:var(--page-width);margin-inline:auto}
.search-overlay__input{flex:1;background:none;border:0;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 25%,transparent);color:inherit;font-size:clamp(1.2rem,3vw,2rem);padding:.5rem 0;font-family:var(--font-heading);font-weight:300}
.search-overlay__input:focus{outline:0;border-color:var(--color-accent)}
.search-overlay__close{background:none;border:0;color:inherit;font-size:1.8rem;cursor:pointer}
/* hide the browser's native search clear (×) so only our close button shows */
input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none;appearance:none}
.search-overlay__results{max-width:var(--page-width);width:100%;margin:1rem auto 0;flex:1;overflow-y:auto;overscroll-behavior:contain;padding-bottom:2rem}
.psearch__results{list-style:none;margin:0;padding:0;display:grid;gap:.25rem}
.psearch__item{display:flex;gap:1rem;align-items:center;padding:.6rem;border-radius:var(--radius)}
.psearch__item:hover{background:color-mix(in srgb,var(--color-ink) 6%,transparent)}
.psearch__thumb{width:56px;border-radius:var(--radius);overflow:hidden;flex:none}
.psearch__title{display:block}.psearch__price{color:var(--color-muted);font-size:.85rem}
.psearch__all{display:inline-block;margin-top:1rem;color:var(--color-accent)}
.psearch__empty{color:var(--color-muted);padding:1rem 0}

/* ---------- forms / auth ---------- */
.auth label,.search-page__form{display:block;margin-bottom:1rem}
.auth input,.search-page__form input{background:var(--color-surface);border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);color:inherit;border-radius:var(--radius);padding:.7rem .9rem;width:100%;font:inherit;margin-top:.3rem}
.search-page__form{display:flex;gap:.75rem;max-width:560px}
.cart-page__foot{display:flex;justify-content:space-between;align-items:flex-end;margin-top:2rem;flex-wrap:wrap;gap:1rem}
.cart-page__subtotal{font-size:1.2rem}
.rte a{color:var(--color-accent);text-decoration:underline}
.rte h2{margin-top:2rem}
/* ---------- card hover actions (quick view + wishlist) ---------- */
/* A-2 hover clusters: save-to-project wedges the image's TOP-RIGHT corner;
   Quickview + add-to-cart sit BOTTOM-LEFT and appear over the badges.
   States: card-hover = light block + ink symbol; button-hover = accent
   block + white symbol. */
/* The two renderers hang .card__actions off DIFFERENT offset parents, so one
   `right` cannot serve both: the Liquid card (snippets/product-card.liquid)
   makes it a direct child of .card, while the island tile (typesense-plp.js,
   Tile.jsx) nests it inside .card__media. Only the first resolves against
   .card's padding box, which the paint-escape padding moved out by 28px — so
   the compensation is scoped to exactly that case with a child combinator.
   Every other absolutely-positioned tile part (media-alt, arrows, flags, cta)
   sits inside .card__media in BOTH renderers and needs nothing. */
.card__actions{position:absolute;top:0;right:0;display:flex;flex-direction:column;gap:0;pointer-events:none;z-index:2}
.card > .card__actions{right:28px}
.card:hover .card__actions,.card:focus-within .card__actions{pointer-events:auto}
.card__act{width:42px;height:42px;border-radius:0;border:0;background:color-mix(in srgb,var(--color-ink) 6%,var(--color-bg));color:var(--color-ink);cursor:pointer;font-size:.95rem;display:grid;place-items:center}
.card__act:hover{background:var(--color-accent);color:#fff}
.card__act.is-active{background:var(--color-accent);color:#fff}
.card__cta{position:absolute;left:0;bottom:0;display:flex;gap:0;z-index:2;pointer-events:none}
.card:hover .card__cta,.card:focus-within .card__cta{pointer-events:auto}
.card__qview{border:0;background:var(--color-accent);color:#fff;height:42px;padding:0 1.1rem;font:inherit;font-size:.74rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;cursor:pointer}
.card__qview:hover{background:var(--color-ink);color:var(--color-bg)}

/* ---------- A-2 tile reveal, our clothes (motion only) ----------
   Measured on athlete2 category tiles (.products-grid .product-item), which is
   where their Quickview + cart controls actually live:

     rest   .quick-view / .action:is(.tocompare,.towishlist,.primary)
            { opacity:0; visibility:hidden; transform:translateY(10px);
              transition:all .3s }
     hover  { visibility:visible;
              animation:move-action-top .15s cubic-bezier(.33,.975,.245,.91) forwards }
     @keyframes move-action-top{0%{translateY(10px);opacity:0}100%{translateY(0);opacity:1}}
     delays  .action.primary 0ms | .quick-view 50ms | .towishlist 50ms | .tocompare 100ms
     overlay .product-grid-overlay rgba(0,0,0,.04), opacity .14s IN / .4s OUT

   The whole tile is built on ONE asymmetry: it answers in .15s and lets go in
   .3s — twice as slow leaving as arriving, the same 1:2 ratio their button wipe
   uses (.256s in, .512s out). That, plus the 50ms offset between the primary
   control and the ones flanking it, is the entire effect; everything rises the
   same 10px on the same spring, which our --ease-spring already is.
   Ported onto our own arrangement and colours — the bronze QUICKVIEW pill and
   the light icon square are untouched, only their timing changes. The reveal
   moves from the containers onto the controls themselves, because a stagger is
   impossible while one parent fades the pair as a single unit. */
.card__cta .card__act,.card__qview,.card__actions .card__act{
  opacity:0;transform:translateY(10px);transition:opacity .3s ease,transform .3s ease}
.card:hover .card__cta .card__act,.card:focus-within .card__cta .card__act,
.card:hover .card__qview,.card:focus-within .card__qview,
.card:hover .card__actions .card__act,.card:focus-within .card__actions .card__act{
  opacity:1;transform:none;transition:opacity .15s var(--ease-spring),transform .15s var(--ease-spring)}
/* their stagger mapped by role, not by position: .action.primary leads and the
   controls either side of it follow 50ms later */
.card:hover .card__qview,.card:focus-within .card__qview,
.card:hover .card__actions .card__act,.card:focus-within .card__actions .card__act{transition-delay:50ms}
/* a busy add-to-cart has to stay dimmed through the reveal rules above */
.card:hover .card__cta .card__act--atc[aria-busy],
.card:focus-within .card__cta .card__act--atc[aria-busy],
/* touch has no hover to reveal on — the controls sit out permanently. The
   reveal now lives on the controls, so these have to unset it there too. */
@media (hover:none){.card__cta{pointer-events:auto}
  .card__cta .card__act,.card__qview{opacity:1;transform:none}}
.card__quickview{width:auto;border-radius:999px;padding:0 .9rem;font-size:.78rem;white-space:nowrap}
.card__badge--out{background:var(--color-muted)}
@media (hover:none){.card__actions{pointer-events:auto;flex-direction:row}
  .card__actions .card__act{opacity:1;transform:none}}

/* ---------- quick-view modal ---------- */
.qv-modal[aria-hidden="true"]{pointer-events:none}
.qv-modal__scrim{position:fixed;inset:0;background:rgba(5,4,3,.6);opacity:0;transition:.25s;z-index:97}
.qv-modal[aria-hidden="false"] .qv-modal__scrim{opacity:1}
.qv-modal__panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-46%);opacity:0;width:min(880px,94vw);max-height:90vh;overflow:auto;background:var(--color-surface);border-radius:calc(var(--radius)*2);z-index:98;transition:.3s cubic-bezier(.2,.7,.2,1);box-shadow:0 30px 80px rgba(0,0,0,.5)}
.qv-modal[aria-hidden="false"] .qv-modal__panel{transform:translate(-50%,-50%);opacity:1}
/* qv close inherits .btn-close (square component); positioning below */
.qv-modal__loading{padding:4rem;text-align:center;color:var(--color-muted)}
.qv{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:1.5rem}
@media (max-width:700px){.qv{grid-template-columns:1fr}}
/* opaque ground. .media's tint is ink 5% over TRANSPARENT, so a cut-out PNG
   render lets whatever sits behind the frame read through the product; the
   same 5% composited over the panel surface looks identical and stops there. */
.qv__media{border-radius:var(--radius);overflow:hidden;background:color-mix(in srgb,var(--color-ink) 5%,var(--color-surface))}
.qv__title{font-size:1.5rem;font-weight:300}
.qv__full{display:inline-block;margin-top:1rem;color:var(--color-accent)}

/* ---------- mobile nav: drill-down drawer ---------- */
.mnav[aria-hidden="true"]{pointer-events:none}
/* The drawer opens BELOW the site header, which stays visible and interactive
   above it: the header's own left toggle is the open/close control, and it has
   to be on screen for its dots->X morph to be seen at all. --mnav-top is set
   from the header's real bounding box by ui.js on open (56px is the fallback
   for a first paint before JS). */
.mnav{--mnav-top:56px;--mnav-bottom:0px}
.mnav__scrim{position:fixed;top:var(--mnav-top);left:0;right:0;bottom:0;
  background:rgba(5,4,3,.55);opacity:0;transition:opacity .25s;z-index:92}
.mnav[aria-hidden="false"] .mnav__scrim{opacity:1}
/* the panel stops where the bottom bar starts (--mnav-bottom, also measured):
   the bar is lifted above the scrim, so a full-height panel would hide its own
   foot — the trade CTA — behind it */
.mnav__panel{position:fixed;top:var(--mnav-top);left:0;
  height:calc(100dvh - var(--mnav-top) - var(--mnav-bottom));
  width:min(400px,92vw);background:var(--color-surface);
  z-index:93;transform:translateX(-100%);transition:transform .3s cubic-bezier(.2,.7,.2,1);
  display:flex;flex-direction:column;overflow:hidden}
.mnav[aria-hidden="false"] .mnav__panel{transform:none}

/* persistent bar: back · current level title. No close button — the header
   toggle owns close. The title IS the panel heading; ui.js retitles it and
   focuses it on every drill/back. */
.mnav__bar{flex:none;display:flex;align-items:center;height:52px;padding-right:.3rem;
  border-bottom:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent)}
.mnav__iconbtn{flex:none;width:52px;height:52px;display:grid;place-items:center;padding:0;
  background:none;border:0;cursor:pointer;color:color-mix(in srgb,var(--color-ink) 72%,transparent)}
.mnav__iconbtn[hidden]{display:none}
.mnav__iconbtn svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.mnav__title{flex:1;min-width:0;margin:0;padding-left:1.15rem;font-family:var(--font-heading);font-size:1.05rem;
  font-weight:400;line-height:1.2;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mnav__back:not([hidden])+.mnav__title{padding-left:.2rem}
.mnav__title:focus{outline:none}

/* the sliding stack: current sits at 0, ancestors push left and dim, every
   other level parks off the right edge. visibility (delayed out, immediate
   in) is what takes hidden levels out of the tab order in EVERY browser —
   `inert` from ui.js is the progressive-enhancement half of the same job. */
.mnav__track{position:relative;flex:1;min-height:0}
.mnav__level{position:absolute;inset:0;padding:.4rem 0 1.4rem;background:var(--color-surface);
  overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
  transform:translateX(100%);visibility:hidden;
  transition:transform .28s cubic-bezier(.16,1,.3,1),opacity .28s cubic-bezier(.16,1,.3,1),visibility 0s .28s}
.mnav__level.is-current{transform:none;opacity:1;visibility:visible;
  transition:transform .28s cubic-bezier(.16,1,.3,1),opacity .28s cubic-bezier(.16,1,.3,1),visibility 0s}
.mnav__level.is-prev{transform:translateX(-28%);opacity:.3}

/* rows. Split row: the label navigates to the category's own collection, the
   chevron drills — two unambiguous targets rather than one that has to
   guess. A column with nothing to link to degrades to a full-width drill. */
.mnav__rows{list-style:none;margin:0;padding:0}
.mnav__row{display:flex;align-items:stretch;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 7%,transparent)}
.mnav__row-link,.mnav__row-full{display:flex;align-items:center;justify-content:space-between;gap:.75rem;
  flex:1;min-width:0;min-height:52px;width:100%;padding:.75rem 1.15rem;font:inherit;font-size:1rem;
  text-align:left;color:var(--color-ink);background:none;border:0;cursor:pointer}
.mnav__row-link{padding-right:.6rem}
.mnav__row-drill{flex:none;width:56px;display:grid;place-items:center;background:none;border:0;cursor:pointer;
  color:color-mix(in srgb,var(--color-ink) 55%,transparent);
  border-left:1px solid color-mix(in srgb,var(--color-ink) 7%,transparent)}
.mnav__chev{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.mnav__row-full .mnav__chev{color:color-mix(in srgb,var(--color-ink) 45%,transparent)}
.mnav__row-link:active,.mnav__row-full:active,.mnav__row-drill:active,
.mnav__links a:active{background:color-mix(in srgb,var(--color-ink) 5%,transparent)}

/* "View all" leads every level */
.mnav__all{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:.5rem .9rem .85rem;
  padding:.75rem 1rem;border-radius:var(--radius,4px);font-size:.94rem;font-weight:600;color:var(--color-ink);
  background:color-mix(in srgb,var(--color-accent) 13%,transparent)}
.mnav__all svg,.mnav__group-all svg{flex:none;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.mnav__all svg{width:18px;height:18px}

/* tier-3 groups */
.mnav__group{padding:.1rem 0 1rem}
.mnav__group-head{margin:0;padding:.6rem 1.15rem .35rem;font-family:var(--font-body);font-size:.7rem;
  font-weight:600;letter-spacing:.13em;text-transform:uppercase;
  color:color-mix(in srgb,var(--color-ink) 50%,transparent)}
.mnav__links{list-style:none;margin:0;padding:0}
.mnav__links a{display:block;min-height:46px;padding:.72rem 1.15rem;font-size:.95rem;
  color:color-mix(in srgb,var(--color-ink) 85%,transparent)}
.mnav__group-all{display:inline-flex;align-items:center;gap:.4rem;margin:.4rem 1.15rem 0;padding:.45rem 0;
  font-size:.9rem;font-weight:600;color:var(--color-accent)}
.mnav__group-all svg{width:16px;height:16px}

/* finish chips — the same --sw custom property snippets/swatch.liquid paints */
.mnav__chips{display:flex;flex-wrap:wrap;gap:.5rem;padding:.4rem 1.15rem .15rem}
.mnav__chip{display:inline-flex;align-items:center;gap:.45rem;min-height:38px;padding:.4rem .85rem .4rem .5rem;
  border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);border-radius:22px;
  font-size:.85rem;color:var(--color-ink);background:var(--color-bg)}
.mnav__chip:active{border-color:var(--color-accent)}
.mnav__chip-sw{width:18px;height:18px;flex:none;border-radius:50%;
  background:var(--sw,color-mix(in srgb,var(--color-ink) 18%,transparent));
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--color-ink) 20%,transparent)}

/* level-0 quick link + drawer foot */
.mnav__quick{padding:1rem 1.15rem 0}
.mnav__quick-link{display:inline-flex;align-items:center;gap:.55rem;font-size:.94rem;font-weight:600;color:var(--color-accent)}
.mnav__quick-link svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6}
.mnav__foot{flex:none;display:grid;gap:.75rem;padding:.9rem 1.15rem calc(.9rem + env(safe-area-inset-bottom));
  border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent)}
.mnav__foot-links{display:flex;gap:1.5rem;font-size:.9rem;color:color-mix(in srgb,var(--color-ink) 70%,transparent)}
.mnav__trade{justify-content:center;text-align:center}

/* ---------- the morphing toggle: 2x2 dot grid <-> X ----------
   Four half-strokes run corner -> centre with pathLength="100". Closed, each
   carries a single zero-length dash at its corner end — with round linecaps
   that dash IS the dot. The gap is 200, deliberately longer than the path: a
   pattern summing to exactly the path length restarts at the end point and
   paints a fifth dot where the four strokes meet. Opening grows the dash the
   full length, so the dots
   stretch inward and fuse into the X's two crossing strokes, staggered
   clockwise from top-left. The dasharray rides a non-overshooting expo curve
   (a negative dash length would invalidate the array and flash a solid line);
   the spring overshoot lives on the 90-degree swing and the stroke-width,
   where an overshoot cannot go invalid. Both instances (slim header + bottom
   bar) key off aria-expanded, which ui.js sets on every toggle at once. */
.mnav-toggle{--mt-size:24px;-webkit-tap-highlight-color:transparent}
.bottom-bar__item.mnav-toggle{--mt-size:22px}
.mnav-toggle .mnav-toggle__icon{display:grid;place-items:center}
.mnav-toggle .mnav-toggle__icon .mnav-toggle__svg{width:var(--mt-size);height:var(--mt-size);display:block;fill:none}
.mnav-toggle .mnav-toggle__icon .mnav-toggle__grid{transform-origin:12px 12px;transform:rotate(0deg);
  transition:transform .42s var(--ease-morph)}
.mnav-toggle .mnav-toggle__icon .mnav-toggle__svg .mnav-toggle__stroke{
  fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:5;stroke-dasharray:.01 200;
  transition:stroke-dasharray .3s var(--ease-out-expo),stroke-width .42s var(--ease-morph)}
.mnav-toggle .mnav-toggle__stroke--b{transition-delay:.045s}
.mnav-toggle .mnav-toggle__stroke--c{transition-delay:.09s}
.mnav-toggle .mnav-toggle__stroke--d{transition-delay:.135s}
.mnav-toggle[aria-expanded="true"] .mnav-toggle__icon .mnav-toggle__grid{transform:rotate(90deg)}
.mnav-toggle[aria-expanded="true"] .mnav-toggle__icon .mnav-toggle__svg .mnav-toggle__stroke{
  stroke-dasharray:100 200;stroke-width:2}

/* ---------- mobile bottom bar ---------- */
.bottom-bar{position:fixed;left:0;right:0;bottom:0;z-index:60;display:none;justify-content:space-around;
  background:color-mix(in srgb,var(--color-surface) 94%,transparent);backdrop-filter:blur(12px);
  border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);padding:.4rem .25rem env(safe-area-inset-bottom)}
.bottom-bar__item{flex:1;background:none;border:0;color:var(--color-muted);font:inherit;font-size:.62rem;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:.15rem;padding:.35rem 0;text-decoration:none}
.bottom-bar__item .bottom-bar__icon{font-size:1.15rem;line-height:1}
.bottom-bar__item i{font-style:normal;color:var(--color-accent)}
/* the scrim (z 92) would bury the bottom bar (z 60) — lift it while the drawer
   is open so its Menu item stays a real control (and keeps morphing in sync) */
.drawer-open .bottom-bar{z-index:94}
@media (max-width:900px){
  .bottom-bar{display:flex}
  body{padding-bottom:64px}
  /* .nav is hidden in critical.css, which owns the nav/toggle complement — a
     `.nav{display:none}` here loses to critical's `.site-header .nav` and only
     looks like it works. */
  .product__info{position:static}
}

/* ---------- Typesense instant search UI ---------- */
.ts{display:grid;grid-template-columns:230px 1fr;gap:2rem;margin-top:1rem}
@media (max-width:820px){.ts{grid-template-columns:1fr}.ts__facets{display:flex;flex-wrap:wrap;gap:1rem}}
.ts__facet{margin-bottom:1.25rem}
.ts__facet h4{font-size:.72rem;text-transform:uppercase;letter-spacing:.12em;color:var(--color-muted);margin:0 0 .5rem}
.ts__bar{display:flex;justify-content:space-between;align-items:center;gap:1rem;margin-bottom:1rem;color:var(--color-muted);font-size:.85rem}
.ts-hit{display:flex;flex-direction:column;gap:.55rem;border-radius:var(--radius);text-decoration:none;color:inherit}
.ts-hit:hover .ts-hit__title{text-decoration:underline;text-underline-offset:3px}
.ts-hit__media{width:100%;border-radius:var(--radius);overflow:hidden;aspect-ratio:1/1;background:color-mix(in srgb,var(--color-ink) 5%,transparent)}
.ts-hit__media img{width:100%;height:100%;object-fit:cover;display:block}
.ts-hit__meta{display:flex;flex-direction:column;gap:.15rem}
.ts-hit__vendor{font-size:.68rem;text-transform:uppercase;letter-spacing:.14em;color:var(--color-muted)}
.ts-hit__title{font-size:.92rem;line-height:1.35}
.ts-hit__price{font-size:.9rem;font-weight:600}
.ts-hit__price s{color:var(--color-muted);font-weight:400;margin-right:.35rem}
/* InstantSearch structural resets + grid */
.ts .ais-Hits-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:1.5rem 1.25rem}
.ts .ais-Hits-item{margin:0;padding:0}
.ts ol,.ts ul{list-style:none;margin:0;padding:0}
.ts .ais-RefinementList-item{margin:.3rem 0}
.ts .ais-RefinementList-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.88rem}
.ts .ais-RefinementList-count{margin-left:auto;font-size:.72rem;color:var(--color-muted);background:color-mix(in srgb,var(--color-ink) 7%,transparent);border-radius:99px;padding:.05rem .5rem}
.ts .ais-RefinementList-checkbox{accent-color:var(--color-accent)}
.ts .ais-SearchBox-form{display:flex;gap:.4rem;margin-bottom:.5rem}
.ts .ais-SearchBox-input{width:100%;padding:.4rem .6rem;border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);border-radius:var(--radius);background:transparent;color:inherit;font-size:max(var(--touch-min-font,0px),.85rem)}
.ts .ais-SearchBox-submit,.ts .ais-SearchBox-reset{background:none;border:0;color:var(--color-muted);cursor:pointer}
.ts .ais-RefinementList-showMore,.ts .ais-ClearRefinements-button,.ts .ais-InfiniteHits-loadMore{background:none;border:1px solid color-mix(in srgb,var(--color-ink) 20%,transparent);border-radius:99px;padding:.3rem .9rem;font-size:.78rem;cursor:pointer;color:inherit;margin-top:.35rem}
.ts .ais-ClearRefinements-button--disabled{display:none}
.ts .ais-RangeInput-form{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap}
.ts .ais-RangeInput-input{width:5.2rem;padding:.35rem .5rem;border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);border-radius:var(--radius);background:transparent;color:inherit;font-size:max(var(--touch-min-font,0px),.85rem)}
.ts .ais-RangeInput-submit{background:var(--color-ink);color:var(--color-surface);border:0;border-radius:var(--radius);padding:.35rem .8rem;font-size:.8rem;cursor:pointer}
.ts .ais-SortBy-select{background:transparent;color:inherit;border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);border-radius:var(--radius);padding:.35rem .6rem;font-size:max(var(--touch-min-font,0px),.82rem)}
.ts .ais-Pagination-list{display:flex;gap:.35rem;justify-content:center;margin:1.5rem 0 0}
.ts .ais-Pagination-link{display:inline-block;min-width:2rem;text-align:center;padding:.3rem .5rem;border-radius:var(--radius);text-decoration:none;color:inherit}
.ts .ais-Pagination-item--selected .ais-Pagination-link{background:var(--color-ink);color:var(--color-surface)}
.ts .ais-Pagination-item--disabled .ais-Pagination-link{opacity:.35;pointer-events:none}
.ts .ais-CurrentRefinements-list{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:.75rem}
.ts .ais-CurrentRefinements-item{display:flex;align-items:center;gap:.35rem;background:color-mix(in srgb,var(--color-ink) 7%,transparent);border-radius:99px;padding:.2rem .7rem;font-size:.78rem}
.ts .ais-CurrentRefinements-delete{background:none;border:0;cursor:pointer;color:var(--color-muted)}
.ts .ais-Stats-text{white-space:nowrap}
@media (max-width:820px){.ts .ais-Hits-list{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem .8rem}}
.ts-hit__vendor{display:block;color:var(--color-muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.06em}
.ts-hit__title{display:block}.ts-hit__price{color:var(--color-muted);font-size:.88rem}
.ts__hits.product-grid{display:grid}
/* InstantSearch widget overrides -> our dark theme */
.ais-RefinementList-list,.ais-Pagination-list{list-style:none;margin:0;padding:0}
.ais-RefinementList-item{display:flex;align-items:center;gap:.5rem;padding:.2rem 0;font-size:.9rem}
.ais-RefinementList-count{color:var(--color-muted);font-size:.78rem}
.ais-RefinementList-showMore,.ais-ClearRefinements-button{background:none;border:0;color:var(--color-accent);cursor:pointer;font:inherit;padding:.3rem 0}
.ais-SearchBox-input,.ais-RangeInput-input{background:var(--color-surface);border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);color:inherit;border-radius:var(--radius);padding:.4rem .6rem}
.ais-SortBy-select{background:var(--color-surface);color:inherit;border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);border-radius:var(--radius);padding:.35rem .6rem}
.ais-CurrentRefinements-item{display:inline-flex;gap:.4rem;background:color-mix(in srgb,var(--color-accent) 14%,transparent);padding:.2rem .5rem;border-radius:999px;font-size:.8rem;margin:.2rem}

/* ---------- recently viewed ---------- */
.recently-viewed:empty{display:none}
.recently-viewed{padding-block:3rem}
.recently-viewed h2{font-size:1.4rem;margin-bottom:1rem}

/* ---------- ratings ---------- */
.rating{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;margin:.25rem 0}
.rating__stars{position:relative;display:inline-flex;color:color-mix(in srgb,var(--color-ink) 22%,transparent)}
.rating__stars svg{width:13px;height:13px;flex:none;display:block}
.rating__stars-row{display:inline-flex}
.rating__stars-fill{position:absolute;left:0;top:0;display:inline-flex;width:var(--pct,0%);overflow:hidden;color:var(--color-accent)}
.star-input svg{width:18px;height:18px;display:block}
.card__act svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
/* the notepad glyph is drawn narrower in its viewBox than the cart bag —
   render it larger so the two corner buttons read the same weight */
.card__actions .card__act svg{width:21px;height:21px}
.bottom-bar__item .bottom-bar__icon svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.ship-bar__ic{width:15px;height:15px;vertical-align:-3px;display:inline-block}
.rating__count{color:var(--color-muted)}
.card__badge--new{background:var(--color-accent);color:#fff}

/* ---------- product recommendations ---------- */
.recommendations{padding-block:3rem}
.recommendations__title{font-size:1.4rem;margin-bottom:1rem}

/* ---------- shop by brand (tabs) ---------- */
.shop-brand__tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1.5rem;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);padding-bottom:.5rem}
.shop-brand__tab{background:none;border:0;color:var(--color-muted);font:inherit;cursor:pointer;padding:.4rem .2rem;position:relative}
.shop-brand__tab.is-active{color:var(--color-ink)}
.shop-brand__tab.is-active::after{content:'';position:absolute;left:0;right:0;bottom:-.55rem;height:2px;background:var(--color-accent)}

/* ---------- product comparison ---------- */
.compare-bar{position:fixed;left:0;right:0;bottom:0;z-index:62;background:var(--color-surface);border-top:1px solid color-mix(in srgb,var(--color-ink) 12%,transparent);box-shadow:0 -10px 40px rgba(0,0,0,.3)}
@media (max-width:900px){.compare-bar{bottom:64px}}
.compare-bar__inner{display:flex;justify-content:space-between;align-items:center;gap:1rem;padding:.75rem 0}
.compare-bar__items{display:flex;gap:.5rem;flex-wrap:wrap;overflow:auto}
.compare-chip{display:inline-flex;align-items:center;gap:.4rem;background:color-mix(in srgb,var(--color-ink) 8%,transparent);border-radius:999px;padding:.25rem .5rem .25rem .75rem;font-size:.8rem;white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis}
.compare-chip button{background:none;border:0;color:var(--color-muted);cursor:pointer;font-size:1rem}
.compare-bar.shake{animation:shake .4s}
@keyframes shake{25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
.card__act.is-active{background:var(--color-accent);color:#100f0d}
.compare-modal[aria-hidden="true"]{pointer-events:none}
.compare-modal__scrim{position:fixed;inset:0;background:rgba(5,4,3,.65);opacity:0;transition:.25s;z-index:99}
.compare-modal[aria-hidden="false"] .compare-modal__scrim{opacity:1}
.compare-modal__panel{position:fixed;inset:4vh 4vw;background:var(--color-surface);border-radius:calc(var(--radius)*2);z-index:100;overflow:auto;opacity:0;transform:scale(.98);transition:.25s}
.compare-modal[aria-hidden="false"] .compare-modal__panel{opacity:1;transform:none}
.compare-modal__close{position:absolute;top:.75rem;right:.75rem;z-index:2;background:color-mix(in srgb,var(--color-bg) 60%,transparent);border:0;color:inherit;width:36px;height:36px;border-radius:50%;font-size:1.4rem;cursor:pointer}
.compare-table{width:100%;border-collapse:collapse;font-size:.9rem}
.compare-table th,.compare-table td{padding:.75rem 1rem;text-align:left;vertical-align:top;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.compare-table thead th{width:auto;text-align:center}
.compare-table tbody th{color:var(--color-muted);font-weight:500;width:18%}
.compare-th__vendor{display:block;color:var(--color-muted);font-size:.72rem;text-transform:uppercase}
.compare-th__title{display:block;font-weight:500}
.compare-th__remove{background:none;border:0;color:var(--color-muted);cursor:pointer;font-size:.78rem;text-decoration:underline}

/* ---------- PWA install prompt — REMOVED 2026-09-12 ----------------------
 * There is no .pwa-install rule here any more, and that is deliberate. The
 * pill it styled ("Install Sydney Taps for instant, app-like browsing") was
 * fixed to the bottom edge at z 70 and covered the sticky buy bar (z 61) and
 * the purchase block beneath it; its dismiss x did nothing, because this
 * rule set display:flex in the author origin and that beats the [hidden]
 * attribute's UA display:none outright. Luke, asked whether the prompt
 * should exist at all before launch: "yes, remove it please".
 *
 * snippets/pwa.liquid still ships the manifest, the apple-touch-icon and the
 * standalone meta tags, so the site is still installable from the browser's
 * own menu — we just stop soliciting it. The reasoning and what would bring
 * the prompt back live in docs/decisions/2026-09-12-pwa-install-prompt.md.
 *
 * The bug's general form is fenced off in critical.css:
 * [hidden]{display:none!important}.
 */

/* ---------- reviews ---------- */
.reviews{padding-block:3rem;border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);margin-top:2rem}
.reviews__head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem}
.reviews__title{font-size:1.6rem}
.reviews__summary{display:flex;align-items:center;gap:.6rem;color:var(--color-muted);font-size:.9rem}
.reviews__list{list-style:none;margin:0;padding:0;display:grid;gap:1.5rem;max-width:760px}
.review{padding-bottom:1.5rem;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.review__head{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:.4rem}
.review__author{font-weight:500}
.review__date{color:var(--color-muted);font-size:.8rem;margin-left:auto}
.review__title-row{font-size:1rem;margin:.25rem 0}
.review__body{color:color-mix(in srgb,var(--color-ink) 88%,transparent)}

/* ---------- verified-buyer badge (native Popover API + anchor positioning) ---------- */
.vbadge{display:inline-flex}
.vbadge__btn{display:inline-flex;align-items:center;gap:.3rem;background:none;border:0;cursor:pointer;font:inherit;
  font-size:.76rem;color:#2f9bf0;padding:.1rem .3rem;border-radius:999px}
.vbadge__btn:hover{background:color-mix(in srgb,#2f9bf0 14%,transparent)}
.vbadge__tick{color:#2f9bf0;flex:none}
.vbadge__label{font-weight:500}
.vbadge__pop{position:absolute;margin:0;max-width:280px;background:var(--color-surface);color:var(--color-ink);
  border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);border-radius:calc(var(--radius)*2);
  padding:.9rem 1rem;box-shadow:0 14px 40px rgba(0,0,0,.4);font-size:.84rem;line-height:1.45;inset:auto}
.vbadge__pop p{margin:.4rem 0 0;color:var(--color-muted)}
.vbadge__pop-title{display:flex;align-items:center;gap:.35rem;color:#2f9bf0}
.vbadge__pop::backdrop{background:transparent}
/* anchor positioning where supported (Chromium); falls back to centered popover otherwise */
@supports (position-area: bottom){
  .vbadge__pop{position-area:bottom span-all;margin-top:.5rem;justify-self:anchor-center}
}
.vbadge__pop{animation:vbpop .15s ease}
@keyframes vbpop{from{opacity:0;transform:translateY(-4px)}}

/* ---------- shipping-promise progress bar ---------- */
/* the block IS the progress bar: --ship-pct drives a full-height green
   fill AND the white-text clip, so the label's colour sweeps with the
   fill edge; 100% = the goal reached. The bar only exists when Settings >
   Shipping promise is on (sections/cart-drawer.liquid). */
.ship-bar{position:relative;overflow:hidden;padding:.85rem 1.6rem;background:color-mix(in srgb,var(--color-ink) 5%,var(--color-bg))}
.ship-bar__fillbg{position:absolute;inset:0 auto 0 0;width:var(--ship-pct,0%);background:var(--color-success);
  transition:width .5s cubic-bezier(.2,.7,.2,1)}
/* NORMAL TEXT FLOW, not flex. This was `display:flex;gap:.55rem` and it worked
   only while the copy was short: every text run and every <b> in the sentence
   becomes a FLEX ITEM, so the .55rem gap lands between "Your" and "tapware" and
   the line wraps item-by-item rather than as a sentence. Invisible with
   "You're $40 away from free shipping" (3 items); ragged and loosely spaced at
   390px with "Your tapware qualifies for free shipping - the rest ships at the
   normal rate" (7). The icon's spacing, which is what the gap was for, is now
   the icwrap's own margin. */
.ship-bar__label{position:relative;margin:0;font-size:.85rem;line-height:1.45}
.ship-bar__label--fill{position:absolute;inset:0;padding:.85rem 1.6rem;color:#fff;
  clip-path:inset(0 calc(100% - var(--ship-pct,0%)) 0 0);transition:clip-path .5s cubic-bezier(.2,.7,.2,1)}
.ship-bar__label--fill b{color:#fff}
.ship-bar__ic{flex:none}
.ship-bar__label .ship-bar__icwrap{margin-right:.55rem;vertical-align:-3px}
/* the restriction, under the bar in both states (Luke, 2026-09-16: "make it
   clear to the customer that the free shipping is for non-bulky items only").
   Above the fill so it stays legible at 100%, in the label's own colour. */
/* under the bar, never inside it: the fill and its white label cover the
   whole .ship-bar box, so a line inside it is unreadable at any fill level */
.ship-bar__note{margin:0;padding:.45rem 1.6rem .55rem;font-size:.74rem;line-height:1.4;color:var(--color-muted);
  background:color-mix(in srgb,var(--color-ink) 3%,var(--color-bg));border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.ship-bar__note a{color:inherit;text-decoration:underline}
.cart-page .ship-bar-wrap{margin:0 0 1rem}
.cart-page .ship-bar-wrap .ship-bar{margin:0}

/* ---------- delivery estimate ---------- */
/* delivery window line — one component across the product card, PDP buy box,
   quick view and the Typesense island tiles (which write identical markup).
   Dates land from ui.js; the tick is the only green in the theme, so it is a
   literal rather than a token. */
.delivery-eta{display:flex;align-items:center;gap:.4rem;margin:.4rem 0 .35rem;font-size:.84rem;line-height:1.35;color:var(--ink-2)}
.delivery-eta__tick{flex:0 0 14px;width:14px;height:14px;color:#2f7d4f}
.delivery-eta--preorder{color:var(--ink-3)}
.delivery-eta--compact{font-size:.76rem;margin:.25rem 0 .3rem}
/* PDP shipping promise (snippets/shipping-promise.liquid via shipping-eligible):
   renders ONLY on a product the policy actually covers, so it sits on the same
   row rhythm as the delivery estimate above it and carries the same weight as a
   fact, not a banner. */
.ship-promise{display:flex;align-items:center;gap:.4rem;margin:.4rem 0 .35rem;font-size:.84rem;line-height:1.35;color:#2f7d4f;font-weight:600}
.ship-promise__ic{flex:0 0 14px;width:14px;height:14px}
/* A bulky page is disclosing a CHARGE, not making an offer, so it must not wear
   the success green the free-shipping line does. Same row, same weight, ink
   colour: a statement of fact. */
.ship-promise--bulky{color:var(--ink-2);font-weight:500}
/* The cart's second line: the pallet fee, stated once, under the bar. */
.ship-bulky{margin:.1rem 0 .35rem;padding:0 1.6rem .6rem;font-size:.8rem;line-height:1.4;color:var(--ink-2)}
/* "Large item" / "Large order". Deliberately not the success green and not a
   warning red: a pallet delivery is neither a promotion nor a problem, it is a
   different kind of delivery, and the badge is there so a shopper registers
   that before reading the sentence. */
.ship-badge{display:inline-block;margin-right:.4rem;padding:.1rem .4rem;border-radius:2px;
  background:color-mix(in srgb,var(--color-ink) 10%,var(--color-bg));color:var(--ink-1);
  font-size:.72rem;font-weight:700;letter-spacing:.02em;text-transform:uppercase;vertical-align:1px}
/* ---------- the cart's delivery quote (.ship-quote) ----------
   Luke, 2026-09-15: "shipping needs to be calculated correctly in the cart".
   Two parts, because the model has two: a FIXED pallet row and the CHOICE for
   everything else, then the estimated total. Rows are a two-column grid rather
   than flex+space-between so the amounts line up down the right edge whatever
   the labels do at 390px -- the same reason .ship-bar__label stopped using
   display:flex when its copy grew. */
.ship-quote{margin:.6rem 0 0;border-top:1px solid var(--hairline,rgba(0,0,0,.08));padding-top:.55rem;
  font-size:.85rem;line-height:1.35}
.ship-quote__row{display:grid;grid-template-columns:1fr auto;gap:.5rem;align-items:baseline;
  padding:.18rem 0;color:var(--ink-1)}
.ship-quote__label{min-width:0}
.ship-quote__note{display:block;color:var(--color-muted);font-size:.75rem;line-height:1.3}
/* Part A is a statement, not an option: same ink as the rest, no affordance
   that suggests it can be turned off. */
.ship-quote__row--fixed b{white-space:nowrap}
.ship-quote__row--alt{color:var(--ink-2);font-size:.8rem}
.ship-quote__row--alt.is-muted{color:var(--color-muted)}
.ship-quote__row--total{margin-top:.3rem;border-top:1px solid var(--hairline,rgba(0,0,0,.08));
  padding-top:.4rem;font-size:.95rem;font-weight:600}
/* A refreshing quote keeps its numbers and dims them. Collapsing to a spinner
   would make the footer jump on every qty press, which is the one place a
   shopper is clicking repeatedly. */
.ship-quote.is-stale{opacity:.55;transition:opacity var(--dur-fast,.18s)}
.ship-quote--loading,.ship-quote--prompt{color:var(--color-muted)}
.ship-quote--blocked{color:var(--ink-1)}
.ship-quote__ask{padding:0;border:0;background:none;color:var(--ink-1);font:inherit;font-size:.85rem;
  text-decoration:underline;text-underline-offset:2px;cursor:pointer}

/* ---------- the per-line pallet tag (.shiptag) ----------
   Luke, 2026-09-15: "lines that incur +$100 in shipping need to have a tag with
   mouseover for popover description like we have in the size/label swatch
   attribute buttons on the product page." So this is the .swtext callout
   (above) reused, with its three load-bearing properties intact:
     - the whole hover path lives in @media (hover:hover), so a touch pointer is
       never shown hover-only UI (the invariant scripts/swatch-tap-gate.mjs
       holds, and the one iOS actually enforces by withholding the first tap);
     - the bubble is pointer-events:none, so it can never eat a click;
     - the row reserves the band up front, so showing it moves nothing.
   On touch, ShipTag.jsx sets .is-open on the first tap and the same bubble is
   drawn from the same data-tip -- one wording, two input models. */
/* Only a TAGGED price cell becomes a column. Applying it to every line would
   change the box of every price in the cart for the sake of the few that carry
   a pallet charge. */
.cart-line__price--tagged{display:flex;flex-direction:column;align-items:flex-end;gap:.25rem}
/* No width cap: the price cell is a max-content grid column, so the chip sets
   the cell's width and the price right-aligns to the same edge. The 11rem cap
   this had let a long tag ("+$100 delivery · warehouse pickup") overflow its
   own box (Luke, 2026-09-16: "this formatting is also broken"). Below 560px
   the chip wraps instead of widening the row. */
.shiptag{display:inline-block;padding:.08rem .35rem;border-radius:2px;
  background:color-mix(in srgb,var(--color-ink) 10%,var(--color-bg));color:var(--ink-1);
  font-size:.7rem;font-weight:700;letter-spacing:.02em;line-height:1.5;white-space:nowrap;text-align:right;
  text-decoration:none;position:relative}
@media (max-width:560px){.shiptag{white-space:normal;max-width:9rem}}
@media (hover:hover){
  /* The band the bubble occupies, reserved whether or not anything is hovered —
     and ONLY on a line that has a tag, so an untagged price cell keeps exactly
     the box it has today. The negative margin cancels the padding, so the row's
     height is unchanged and hovering moves nothing (the CLS budget is
     untouched, which is the whole point of reserving rather than shifting). */
  .cart-line__price--tagged{padding-top:1.9rem;margin-top:-1.9rem}
  .shiptag:hover{background:color-mix(in srgb,var(--color-ink) 16%,var(--color-bg))}
  .shiptag:hover::after,.shiptag:focus-visible::after{content:attr(data-tip)}
  .shiptag:hover::before,.shiptag:focus-visible::before{content:""}
}
.shiptag.is-open::after{content:attr(data-tip)}
.shiptag.is-open::before{content:""}
.shiptag::after{position:absolute;right:0;bottom:calc(100% + 9px);
  width:max-content;max-width:min(17rem,60vw);white-space:normal;text-align:left;
  padding:.3rem .5rem;border-radius:3px;background:var(--ink);color:var(--paper);
  font-size:.72rem;font-weight:500;line-height:1.4;letter-spacing:.01em;
  pointer-events:none;z-index:2}
/* the pointer: no bottom edge, so the triangle sits against the bubble rather
   than 5px below it with a gap (the .swtext note explains the arithmetic) */
.shiptag::before{position:absolute;right:.6rem;bottom:calc(100% + 4px);
  width:0;height:0;border-style:solid;border-width:5px 5px 0;
  border-color:var(--ink) transparent transparent;pointer-events:none;z-index:2}

/* ---------- delivery guidelines (/pages/delivery) ---------- */
.deliv-guide{max-width:760px;margin:0 auto;padding:2.5rem 1.25rem 4rem}
.deliv-guide h1{margin:0 0 .75rem;font-size:clamp(1.6rem,4vw,2.2rem);line-height:1.15}
.deliv-guide__lede{margin:0 0 .5rem;font-size:1rem;line-height:1.5}
.deliv-guide__quote{margin:0 0 2rem;font-size:.9rem;color:var(--ink-2)}
.deliv-guide__body h2{margin:2rem 0 .6rem;font-size:1.1rem;line-height:1.25}
.deliv-guide__body h3{margin:1.2rem 0 .4rem;font-size:.95rem}
.deliv-guide__body p,.deliv-guide__body li{font-size:.92rem;line-height:1.6;color:var(--ink-1)}
.deliv-guide__body ul{margin:.5rem 0 1rem;padding-left:1.1rem}
.deliv-guide__body li{margin:.35rem 0}
/* The staff-fill block is meant to look unfinished. A placeholder that blends
   in is a placeholder that ships. */
.deliv-guide__todo{margin:.5rem 0 1rem;padding:.75rem .9rem;border:1px dashed var(--ink-3);
.deliv-guide__reviewed{margin-top:2rem;font-size:.8rem;color:var(--color-muted)}
.page-legal-note{margin:2.5rem 0 0;padding-top:1rem;border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);font-size:.76rem;line-height:1.5;color:var(--color-muted);max-width:70ch}
  border-radius:3px;font-size:.88rem;line-height:1.5;color:var(--ink-2)}

.delivery-est{margin:1rem 0;padding:1rem;border:1px solid color-mix(in srgb,var(--color-ink) 12%,transparent);border-radius:var(--radius)}
.delivery-est__form{display:flex;gap:.5rem;align-items:center}
.delivery-est__icon{flex:0 0 20px;width:20px;height:20px;color:var(--ink-2)}
.delivery-est__form input{flex:1;background:var(--color-surface);border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);color:inherit;border-radius:var(--radius);padding:.55rem .7rem;font:inherit}
.delivery-est__result{margin:.75rem 0 0;font-size:.88rem;color:var(--color-muted)}

/* ---------- back in stock ---------- */
.bis{margin:1rem 0;padding:1rem;border:1px solid color-mix(in srgb,var(--color-accent) 35%,transparent);border-radius:var(--radius);background:color-mix(in srgb,var(--color-accent) 7%,transparent)}
.bis__title{margin:0 0 .6rem;font-weight:500}
.bis__row{display:flex;gap:.5rem}
.bis__row input{flex:1;background:var(--color-surface);border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);color:inherit;border-radius:var(--radius);padding:.6rem .7rem;font:inherit}
.bis__msg{margin:.6rem 0 0;font-size:.86rem;color:var(--color-accent)}

/* ---------- mega menu ---------- */
.nav__list{display:flex;gap:clamp(1rem,2vw,2rem);align-items:center;list-style:none;margin:0;padding:0}
.nav__item{position:static;display:flex;align-items:stretch}
/* the hover/click target is the FULL header-height rectangle around each
   label (not the text glyphs): anchors stretch to the navbar height and take
   horizontal padding; the list gap shrinks to compensate so the visual
   rhythm is unchanged. */
/* height:100%, NOT height:var(--header-h): the bar renders 64px but the
   token says 76px — a fixed height oversizes the anchor and its centred
   label sags ~6px below the bar's true centre (logo/search/icons all sit
   at the real midline). The li already stretches to the bar, so 100%
   keeps the full-height hover target AND true vertical centring. */
.nav__item>a{font-size:.94rem;color:color-mix(in srgb,var(--color-ink) 78%,transparent);transition:color .15s;display:flex;align-items:center;height:100%;padding-inline:.55rem}
.nav__item>a:hover{color:var(--color-ink)}
.megamenu{position:absolute;left:0;right:0;top:100%;background:var(--color-surface);border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);
  box-shadow:0 24px 50px rgba(0,0,0,.35);opacity:0;visibility:hidden;transform:translateY(-8px);transition:.2s;z-index:49}
.nav__item.has-mega.is-open .megamenu,.nav__item.has-mega:focus-within .megamenu{opacity:1;visibility:visible;transform:none}
/* --- metaobject megamenu panel (mega-menu-17; design/megamenu-mockup.html) --- */
.mega{position:absolute;left:0;right:0;top:100%;background:var(--paper);border-top:1px solid var(--line);
  box-shadow:0 34px 60px -12px rgba(25,24,19,.18);z-index:49;
  opacity:0;visibility:hidden;transform:translateY(-6px);transition:.18s var(--ease-out)}
.nav__item.has-mega.is-open .mega,.nav__item.has-mega:focus-within .mega{opacity:1;visibility:visible;transform:none}
html:not(.nav-armed) .mega{opacity:0!important;visibility:hidden!important;transition:none!important}
/* item-to-item switch: the controller stamps .nav--switching for the swap
   frame so panels cut over instantly (no fade-out/fade-in flash); the
   incoming panel's height morph is driven inline from theme.js. */
.nav--switching .mega,.nav--switching .megamenu{transition:none}
/* quick-view fallback when the product handle no longer resolves */
.qv__info--missing{grid-column:1/-1;text-align:center;padding:3rem 1.5rem;display:flex;flex-direction:column;gap:.9rem;align-items:center}
.mega__inner{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3rem;padding-block:2.6rem 2.2rem}
.mega__inner--nofeat{grid-template-columns:1fr}
.mega__cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,190px));gap:2.2rem 2.4rem;align-content:start}
.mega__eyebrow{font-size:.68rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3);
  padding-bottom:.55rem;border-bottom:1px solid var(--line);margin-bottom:.9rem;display:block}
.mega__link{display:block;font-size:.92rem;color:var(--ink-2);padding:.34rem 0;position:relative;width:fit-content}
.mega__link::after{content:'';position:absolute;left:0;bottom:.14rem;height:1px;width:0;background:var(--accent);transition:width .18s var(--ease-out)}
.mega__link:hover{color:var(--ink)}
.mega__link:hover::after{width:100%}
.mega__link--all{color:var(--accent);font-weight:500;margin-top:.35rem}
.mega__feats{display:grid;grid-template-columns:repeat(2,270px);gap:1.5rem;align-content:start}
.mega__feat{display:block}
.mega__feat-media{background:var(--surface);aspect-ratio:1/1;display:grid;place-items:center;overflow:hidden;margin-bottom:.8rem}
.mega__feat-media img{width:100%;height:100%;object-fit:contain;mix-blend-mode:multiply;transition:transform .35s var(--ease-out)}
.mega__feat:hover .mega__feat-media img{transform:scale(1.035)}
.mega__feat-eyebrow{font-size:.66rem;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);display:block;margin-bottom:.35rem}
.mega__feat-title{font-size:.93rem;font-weight:500;color:var(--ink);line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.mega__feat-price{font-size:.9rem;color:var(--ink-2);margin-top:.3rem;display:block}
.mega__foot{border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;padding-block:1rem 1.1rem}
.mega__viewall{font-size:.92rem;font-weight:500;border-bottom:1px solid var(--ink);padding-bottom:.1rem}
.mega__viewall:hover{border-color:var(--accent);color:var(--accent)}
.mega__news{font-size:.85rem;color:var(--ink-2)}
.mega__news b{color:var(--accent);font-weight:500}
/* --- tabbed two-tier variant (mega-menu-17): columns render as a slim
   second-tier tab bar; each tab owns a full-width panel. Same metaobjects. */
.mega--tabbed .mega__tier2{border-bottom:1px solid var(--line);background:var(--paper)}
/* tabs never wrap (two-line tabs read as cramped); if a hub ever outgrows
   the row the strip scrolls horizontally instead */
.mega__tabs{display:flex;gap:.4rem;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}
.mega__tabs::-webkit-scrollbar{display:none}
.mega__tab{appearance:none;background:none;border:0;cursor:pointer;font:inherit;font-size:.8rem;font-weight:600;white-space:nowrap;flex:none;text-decoration:none;display:inline-block;
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);padding:.85rem .9rem;position:relative}
.mega__tab::after{content:'';position:absolute;left:.9rem;right:.9rem;bottom:-1px;height:2px;background:transparent}
.mega__tab:hover{color:var(--ink)}
.mega__tab.is-active{color:var(--ink)}
.mega__tab.is-active::after{background:var(--accent)}
.mega__tabpanels{min-height:220px}
/* tiered (mega-menu-17 progressive): only the slim tab bar shows on
   first-level hover; the body (panel + footer) appears when a tab gains
   .is-active and the controller stamps .is-expanded on the .mega. */
.mega--tiered .mega__body{display:none}
.mega--tiered.is-expanded .mega__body{display:block}
.mega__tabpanel{display:none}
.mega__tabpanel.is-active{display:block}
.mega__tabtitle{font-family:var(--serif);font-weight:400;font-size:1.35rem;margin:0 0 1rem}
.mega__tablinks{columns:3;column-gap:2.4rem;max-width:640px}
.mega__tablinks .mega__link{break-inside:avoid}
/* tier-3 groups inside a tab panel: the by-type / by-finish / by-brand
   column spine, same grid as the simple style's columns */
.mega__cols--intab{grid-template-columns:repeat(auto-fit,minmax(150px,200px))}
.mega__tab-all{display:inline-block;margin-top:1.1rem}
@media (max-width: 980px){.mega{display:none}}
/* post-navigation parked-cursor guard: a fresh document re-applies :hover the
   moment the mouse twitches, so with the cursor resting on a nav item every
   arrival snapped the megamenu open (and every departure snapped it shut) —
   the "navbar repaints" report. Pages land un-armed; theme.liquid arms the
   nav 350ms after load, and clicks dis-arm it so the outgoing page's last
   frame (and its view-transition snapshot) never shows an open menu. */
html:not(.nav-armed) .megamenu{opacity:0!important;visibility:hidden!important;transition:none!important}
.megamenu__inner{padding-block:2rem}
.megamenu__cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1.5rem 2rem}
.megamenu__head{font-weight:500;margin-bottom:.5rem;display:block}
.megamenu__link{display:block;color:var(--color-muted);font-size:.9rem;padding:.2rem 0}
.megamenu__link:hover{color:var(--color-ink)}

/* ---------- AR / model-viewer ---------- */
model-viewer{background:var(--color-surface);border-radius:var(--radius)}
.product__ar{position:absolute;bottom:1rem;left:50%;transform:translateX(-50%);white-space:nowrap}

/* ---------- newsletter modal ---------- */
.nl-modal[aria-hidden="true"]{pointer-events:none}
.nl-modal__scrim{position:fixed;inset:0;background:rgba(5,4,3,.6);opacity:0;transition:.25s;z-index:96}
.nl-modal[aria-hidden="false"] .nl-modal__scrim{opacity:1}
.nl-modal__panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-46%);opacity:0;display:grid;grid-template-columns:1fr 1fr;width:min(720px,92vw);background:var(--color-surface);border-radius:calc(var(--radius)*2);z-index:97;transition:.3s cubic-bezier(.2,.7,.2,1);overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.5)}
.nl-modal[aria-hidden="false"] .nl-modal__panel{transform:translate(-50%,-50%);opacity:1}
@media (max-width:640px){.nl-modal__panel{grid-template-columns:1fr}.nl-modal__media{display:none}}
.nl-modal__media{position:relative}.nl-modal__media img{width:100%;height:100%;object-fit:cover}
.nl-modal__body{padding:2rem}
.nl-modal__close{position:absolute;top:.6rem;right:.6rem;background:color-mix(in srgb,var(--color-bg) 50%,transparent);border:0;color:inherit;width:34px;height:34px;border-radius:50%;font-size:1.3rem;cursor:pointer;z-index:2}
.nl-modal__form{display:flex;gap:.5rem;margin-top:1rem}
.nl-modal__form input{flex:1;background:var(--color-bg);border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);color:inherit;border-radius:var(--radius);padding:.7rem;font:inherit}

/* ---------- countdown ---------- */
.countdown{background:var(--bg);margin-block:2px}
.countdown__inner{display:flex;justify-content:space-between;align-items:center;gap:2rem;flex-wrap:wrap;padding-block:clamp(1.5rem,4vw,2.5rem)}
.countdown__title{font-size:clamp(1.4rem,3vw,2.2rem);font-weight:300;margin:.25rem 0 1rem}
.countdown__timer{display:flex;gap:1rem}
.countdown__unit{text-align:center}
.countdown__unit b{display:block;font-size:clamp(1.6rem,4vw,2.4rem);font-variant-numeric:tabular-nums;font-family:var(--font-heading)}
.countdown__unit i{font-style:normal;font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--color-muted)}

/* ---------- store locator ---------- */
.locator__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:2rem}
.showroom__media{border-radius:var(--radius);overflow:hidden;margin-bottom:1rem}
.showroom__name{font-size:1.2rem;margin:0 0 .25rem}
.showroom__addr,.showroom__hours{color:var(--color-muted);font-size:.9rem;margin:.15rem 0;white-space:pre-line}
.showroom__links{display:flex;gap:1rem;margin-top:.5rem}
.showroom__links a{color:var(--color-accent)}

/* ---------- review form ---------- */
.review-form-wrap{margin-top:2rem;max-width:560px}
.review-form-wrap summary{display:inline-flex;list-style:none;cursor:pointer}
.review-form-wrap summary::-webkit-details-marker{display:none}
.review-form{display:grid;gap:.75rem;margin-top:1rem}
.review-form input,.review-form textarea{background:var(--color-surface);border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);color:inherit;border-radius:var(--radius);padding:.7rem;font:inherit}
.review-form__verified{display:flex;align-items:center;gap:.4rem;font-size:.88rem}
.review-form__note{font-size:.86rem;color:var(--color-muted)}
.review-form__note a,.review-form__msg{color:var(--color-accent)}
.review-form__stars{display:inline-flex;flex-direction:row-reverse;gap:.15rem;font-size:1.5rem}
.star-input input{position:absolute;opacity:0}
.star-input span{color:color-mix(in srgb,var(--color-ink) 22%,transparent);cursor:pointer}
.review-form__stars:hover span,.star-input:hover ~ .star-input span,.star-input span{color:var(--color-accent)}
.star-input input:checked ~ span,.review-form__stars label:has(input:checked) span{color:var(--color-accent)}
.review-form__stars label{display:inline-flex}
.review-form__stars label:hover span,.review-form__stars label:hover ~ label span{color:var(--color-accent)}

/* configurator app block — skin layer: map theme tokens onto the plugin's
   neutral --cfg-* vars + cart line properties (configured choices) */
.cart-line__prop{color:var(--color-muted);font-size:.74rem;margin:0}
.cfg-root{
  --cfg-accent:var(--color-accent);
  --cfg-ink:var(--color-ink);
  --cfg-bg:var(--color-bg);
  --cfg-muted:var(--color-muted);
  --cfg-radius:var(--radius);
  --cfg-font:inherit;
  margin-block:1.25rem;
}
.cfg-root .cfg-add{background:var(--color-accent);border-color:var(--color-accent)}
/* configurator-mode PDP: no gallery grid — builder owns the page */
.product--configurator{grid-template-columns:1fr}
.product--configurator .product__info{max-width:none}

/* ---------- project drawer (trade project lists) ---------- */
.pdrawer__top{display:flex;gap:1rem;padding:1.2rem 1.6rem .3rem;align-items:center}
/* project combobox — Tailwind Plus select-menu pattern, Odoo create-on-miss */
.pcombo{position:relative;flex:1;min-width:0}
.pcombo__input{width:100%;border:0;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 16%,transparent);
  background:transparent;color:inherit;font:inherit;font-size:1.02rem;font-weight:600;padding:.35em 1.6em .35em 0;border-radius:0}
.pcombo__input:focus{outline:none;border-bottom-color:var(--color-accent)}
.pcombo__input::placeholder{color:color-mix(in srgb,var(--color-ink) 32%,transparent);font-weight:400}
.pcombo__caret{position:absolute;right:0;top:50%;width:16px;height:16px;transform:translateY(-50%);
  color:color-mix(in srgb,var(--color-ink) 45%,transparent);pointer-events:none;transition:transform var(--dur-fast) ease}
.pcombo.is-open .pcombo__caret{transform:translateY(-50%) rotate(180deg)}
.pcombo__panel{position:absolute;top:calc(100% + 6px);left:0;z-index:80;max-height:290px;overflow-y:auto;
  min-width:100%;width:max-content;max-width:min(400px,calc(100vw - 2rem));
  background:var(--color-bg);border:1px solid color-mix(in srgb,var(--color-ink) 12%,transparent);
  box-shadow:0 16px 40px color-mix(in srgb,var(--color-ink) 14%,transparent);
  animation:flyoutIn var(--dur-fast) var(--ease-spring)}
[data-pcombo="customer"] .pcombo__panel{left:auto;right:0}
.pcombo__opt{display:flex;align-items:center;gap:.7rem;width:100%;border:0;background:none;font:inherit;
  color:var(--color-ink);cursor:pointer;padding:.65rem .9rem;text-align:left}
.pcombo__opt:hover,.pcombo__opt.is-hot{background:color-mix(in srgb,var(--color-ink) 4%,transparent)}
.pcombo__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.92rem;font-weight:500}
.pcombo__meta{flex:none;font-size:.74rem;color:var(--color-muted)}
.pcombo__tick{flex:none;width:15px;height:15px;color:var(--color-accent);visibility:hidden}
.pcombo__tick svg{width:15px;height:15px;display:block}
.pcombo__opt.is-active .pcombo__tick{visibility:visible}
.pcombo__create{display:block;width:100%;border:0;border-top:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent);
  background:none;font:inherit;font-size:.88rem;color:var(--color-accent);cursor:pointer;padding:.7rem .9rem;text-align:left}
.pcombo__create:hover,.pcombo__create.is-hot{background:color-mix(in srgb,var(--color-accent) 7%,transparent)}
.pcombo__create b{color:var(--color-ink)}
.pcombo__none{margin:0;padding:.8rem .9rem;font-size:.84rem;color:var(--color-muted)}
.pcombo__row{display:flex;align-items:center}
.pcombo__row .pcombo__opt{flex:1;min-width:0}
.pcombo__icon{flex:none;display:grid;place-items:center;width:34px;align-self:stretch;cursor:pointer;
  color:color-mix(in srgb,var(--color-ink) 32%,transparent)}
.pcombo__icon svg{width:15px;height:15px}
.pcombo__icon:hover{color:var(--color-ink);background:color-mix(in srgb,var(--color-ink) 4%,transparent)}
.pcombo__row.is-armed .pcombo__delbtn{color:#b3261e;background:color-mix(in srgb,#b3261e 8%,transparent)}
.pcombo__row.is-armed{outline:1px solid color-mix(in srgb,#b3261e 35%,transparent);outline-offset:-1px}
.pcombo__rename{flex:1;margin:.35rem .6rem;border:0;border-bottom:1px solid var(--color-accent);
  background:transparent;color:inherit;font:inherit;font-size:max(var(--touch-min-font,0px),.92rem);font-weight:500;padding:.3em 0}
.pcombo__rename:focus{outline:none}
.pdrawer__meta-field--combo .pcombo__input{font-size:max(var(--touch-min-font,0px),.9rem);font-weight:400;padding:.32em 1.6em .32em 0}
.pdrawer__meta-field--wide .pcombo__input{font-size:1.02rem;font-weight:600}
.pdrawer__meta{display:grid;grid-template-columns:repeat(4,1fr);gap:.9rem 1rem;padding:.7rem 1.6rem 1.2rem;
  border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
/* project-level meta slims to 2 fields (PO/quote live per-order) */
.pdrawer__meta--project{grid-template-columns:repeat(2,1fr)}
/* ---------- orders accordion (doc v2) ---------- */
.porders{display:flex;flex-direction:column}
.porder{border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.porder__head{display:flex;align-items:center;gap:.7rem;width:100%;border:0;background:none;font:inherit;
  color:var(--color-ink);cursor:pointer;padding:.85rem 1.6rem;text-align:left}
.porder:not(.is-active) .porder__head:hover{background:color-mix(in srgb,var(--color-ink) 3%,transparent)}
.porder__seq{flex:none;display:grid;place-items:center;width:22px;height:22px;background:var(--color-ink);
  color:var(--color-bg);font-size:.72rem;font-weight:700}
.porder.is-active .porder__seq{background:var(--color-accent);color:#100f0d}
.porder__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:.92rem;font-weight:600;display:flex;align-items:baseline;gap:.5rem}
.porder__ref{font-weight:400;font-size:.74rem;color:var(--color-muted)}
.porder__here{flex:none;font-size:.64rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--color-accent-ink,#7a5a1e);background:color-mix(in srgb,var(--color-accent) 22%,transparent);
  padding:.2rem .5rem}
.porder__sum{flex:none;font-size:.76rem;color:var(--color-muted);font-variant-numeric:tabular-nums}
.porder__caret{flex:none;width:15px;height:15px;color:color-mix(in srgb,var(--color-ink) 45%,transparent);
  transition:transform var(--dur-fast) ease}
.porder.is-active .porder__caret{transform:rotate(180deg)}
.porder__meta{display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:.7rem .9rem;align-items:end;
  padding:.2rem 1.6rem .9rem}
.porder__del{align-self:end;padding-bottom:.15rem}
.porder__empty{margin:0;padding:.4rem 1.6rem 1.1rem;font-size:.84rem;color:var(--color-muted)}
.porder .pdrawer__lines{padding-top:.2rem}
.porder__new{display:block;width:100%;border:0;background:none;font:inherit;font-size:.84rem;font-weight:600;
  color:var(--color-accent-ink,#7a5a1e);cursor:pointer;padding:.85rem 1.6rem;text-align:left}
.porder__new:hover{background:color-mix(in srgb,var(--color-accent) 8%,transparent)}
.flyout__empty--slim{padding:1.4rem 1.6rem}
.psave__order{display:block;font-size:.72rem;color:var(--color-muted);font-weight:400}
/* board order sub-sections */
.pboard__order{border-top:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent);padding-top:.9rem;margin-top:.9rem}
.pboard__order-head{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:.4rem}
.pboard__order-head h3{margin:0;font-size:.95rem;font-weight:600}
.pdrawer__meta-field{display:flex;flex-direction:column;gap:.1rem;min-width:0}
.pdrawer__meta-field--wide{grid-column:1/-1}
.pdrawer__meta-field span{font-size:.64rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--color-muted)}
.pdrawer__meta-field input{border:0;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 13%,transparent);
  background:transparent;color:inherit;font:inherit;font-size:max(var(--touch-min-font,0px),.9rem);padding:.32em 0;border-radius:0;min-width:0}
.pdrawer__meta-field input:focus{outline:none;border-bottom-color:var(--color-accent)}
.pdrawer__meta-field input::placeholder{color:color-mix(in srgb,var(--color-ink) 30%,transparent)}
.pdrawer__lines{flex:1;overflow-y:auto}
.pline{display:grid;grid-template-columns:64px 1fr auto;gap:1rem;padding:1rem 1.6rem;align-items:start;
  border-bottom:1px solid color-mix(in srgb,var(--color-ink) 6%,transparent)}
.pline img,.pline__noimg{width:64px;height:64px;object-fit:cover;background:color-mix(in srgb,var(--color-ink) 5%,transparent)}
.pline__info{display:flex;flex-direction:column;gap:.15rem;min-width:0}
.pline__info a{font-size:.86rem;line-height:1.3;color:var(--color-ink);text-decoration:none}
.pline__info a:hover{text-decoration:underline}
.pline__info small{font-size:.78rem;color:var(--color-muted)}
.pline__qty{display:inline-flex;align-items:center;margin-top:.3rem;border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);width:max-content}
.pline__qty button{width:24px;height:24px;border:0;background:none;cursor:pointer;color:var(--color-muted)}
.pline__qty button:hover{background:color-mix(in srgb,var(--color-ink) 5%,transparent);color:var(--color-ink)}
.pline__qty span{min-width:24px;text-align:center;font-size:.82rem}
.pline__remove{border:0;background:none;cursor:pointer;padding:.2rem;color:color-mix(in srgb,var(--color-ink) 40%,transparent)}
.pline__remove:hover{color:var(--color-ink)}
/* island cards can't inline SVG through htm templates — masked icon instead.
   Save = the header's list icon + plus ("add to list"); tick once in the project. */
.card__act--save::before{content:'';display:block;width:21px;height:21px;background:currentColor;
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><defs><mask id='h'><rect width='24' height='24' fill='white'/><circle cx='17.6' cy='6.4' r='5.4' fill='black'/></mask><mask id='b'><rect width='24' height='24' fill='white'/><path d='M17.6 4.6v3.6M15.8 6.4h3.6' stroke='black' stroke-width='1.7'/></mask></defs><g mask='url(%23h)'><rect x='4.5' y='4.5' width='12' height='15.5' rx='1.5'/><path d='M7.5 2.8v3.4M10.25 2.8v3.4M13 2.8v3.4'/><path d='M7.5 10.5h6M7.5 13.5h6M7.5 16.5h3.5'/></g><circle cx='17.6' cy='6.4' r='4.2' fill='black' stroke='none' mask='url(%23b)'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><defs><mask id='h'><rect width='24' height='24' fill='white'/><circle cx='17.6' cy='6.4' r='5.4' fill='black'/></mask><mask id='b'><rect width='24' height='24' fill='white'/><path d='M17.6 4.6v3.6M15.8 6.4h3.6' stroke='black' stroke-width='1.7'/></mask></defs><g mask='url(%23h)'><rect x='4.5' y='4.5' width='12' height='15.5' rx='1.5'/><path d='M7.5 2.8v3.4M10.25 2.8v3.4M13 2.8v3.4'/><path d='M7.5 10.5h6M7.5 13.5h6M7.5 16.5h3.5'/></g><circle cx='17.6' cy='6.4' r='4.2' fill='black' stroke='none' mask='url(%23b)'/></svg>") center/contain no-repeat}
.card__act--save.is-active::before{
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><defs><mask id='h'><rect width='24' height='24' fill='white'/><circle cx='17.6' cy='6.4' r='5.4' fill='black'/></mask><mask id='b'><rect width='24' height='24' fill='white'/><path d='m15.7 6.5 1.4 1.4 2.4-2.7' stroke='black' stroke-width='1.7'/></mask></defs><g mask='url(%23h)'><rect x='4.5' y='4.5' width='12' height='15.5' rx='1.5'/><path d='M7.5 2.8v3.4M10.25 2.8v3.4M13 2.8v3.4'/><path d='M7.5 10.5h6M7.5 13.5h6M7.5 16.5h3.5'/></g><circle cx='17.6' cy='6.4' r='4.2' fill='black' stroke='none' mask='url(%23b)'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><defs><mask id='h'><rect width='24' height='24' fill='white'/><circle cx='17.6' cy='6.4' r='5.4' fill='black'/></mask><mask id='b'><rect width='24' height='24' fill='white'/><path d='m15.7 6.5 1.4 1.4 2.4-2.7' stroke='black' stroke-width='1.7'/></mask></defs><g mask='url(%23h)'><rect x='4.5' y='4.5' width='12' height='15.5' rx='1.5'/><path d='M7.5 2.8v3.4M10.25 2.8v3.4M13 2.8v3.4'/><path d='M7.5 10.5h6M7.5 13.5h6M7.5 16.5h3.5'/></g><circle cx='17.6' cy='6.4' r='4.2' fill='black' stroke='none' mask='url(%23b)'/></svg>") center/contain no-repeat}
/* SSR cards inline both state paths; is-active swaps plus for tick */
.card__act .ic-on{display:none}
.card__act.is-active .ic-off{display:none}
.card__act.is-active .ic-on{display:block}

/* ---------- save-to-project popout ---------- */
.psave{position:fixed;z-index:120;width:264px;background:var(--color-bg);
  border:1px solid color-mix(in srgb,var(--color-ink) 12%,transparent);
  box-shadow:0 16px 40px color-mix(in srgb,var(--color-ink) 14%,transparent);padding:.5rem 0}
.psave__title{margin:.3rem 1rem .35rem;font-size:.7rem;font-weight:600;letter-spacing:.08em;
  text-transform:uppercase;color:var(--color-muted)}
/* The row is a container, not a control: the tick is its own button (click to
   take the product out of that project) and the name/count is the add button.
   A single toggling button could not carry cart-style "add N more" AND
   "remove" on one press. Same box, same padding, same hover as before —
   .psave__row keeps the padding so the hover tint still spans the full row. */
.psave__row{display:flex;align-items:center;gap:.6rem;width:100%;
  font:inherit;font-size:.88rem;color:var(--color-ink);padding:.5rem 1rem;text-align:left}
.psave__row:hover{background:color-mix(in srgb,var(--color-ink) 4%,transparent)}
.psave__add{display:flex;align-items:center;gap:.6rem;flex:1;min-width:0;border:0;background:none;
  font:inherit;color:inherit;cursor:pointer;padding:0;text-align:left}
.psave__check{width:16px;height:16px;flex:none;border:1px solid color-mix(in srgb,var(--color-ink) 25%,transparent);display:grid;place-items:center;
  padding:0;background:none;color:inherit;font:inherit}
button.psave__check{cursor:pointer}
/* how many of THIS product are already in that project */
.psave__has{font-size:.74rem;font-weight:600;color:var(--color-ink);flex:none}
/* what the last press did, in units — "Added 3 to Smith reno — 6 there now" */
.psave__said{margin:.1rem 1rem .35rem;font-size:.74rem;line-height:1.35;color:var(--color-muted)}
.psave__check.is-on{background:var(--color-accent);border-color:var(--color-accent)}
.psave__check.is-on::before{content:'';width:9px;height:9px;background:#fff;
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'><path d='m5 12.5 4.5 4.5L19 7.5'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'><path d='m5 12.5 4.5 4.5L19 7.5'/></svg>") center/contain no-repeat}
.psave__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.psave__count{font-size:.74rem;color:var(--color-muted)}
.psave__row--new{color:var(--color-accent);font-size:.84rem}
.psave__search{display:block;width:calc(100% - 2rem);margin:.15rem 1rem .45rem;border:0;
  border-bottom:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);background:transparent;
  color:inherit;font:inherit;font-size:.88rem;padding:.35em 0}
.psave__search:focus{outline:none;border-bottom-color:var(--color-accent)}
.psave__search::placeholder{color:color-mix(in srgb,var(--color-ink) 32%,transparent)}
.psave .pcombo__create{border-top:0}
.psave__form{display:flex;gap:.4rem;padding:.35rem 1rem .55rem}
.psave__form input{flex:1;min-width:0;border:1px solid color-mix(in srgb,var(--color-ink) 15%,transparent);
  background:var(--color-bg);color:inherit;font:inherit;font-size:.85rem;padding:.35em .55em;border-radius:var(--radius)}
.psave__create{border:0;background:var(--color-accent);color:#fff;font:inherit;font-size:.8rem;
  padding:.35em .8em;cursor:pointer;border-radius:var(--radius)}
.psave__open{display:block;width:100%;border:0;border-top:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent);
  background:none;font:inherit;font-size:.8rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;
  color:color-mix(in srgb,var(--color-ink) 65%,transparent);cursor:pointer;padding:.7rem 1rem;margin-top:.3rem;text-align:center}
.psave__open:hover{color:var(--color-ink);background:color-mix(in srgb,var(--color-ink) 3%,transparent)}
/* configurable products: accent badge + sliders icon linking to the PDP */
.card__badge--cfg{background:var(--color-accent);color:#fff}
a.card__act{text-decoration:none}
.card__act--cfg::before{content:'';display:block;width:17px;height:17px;background:currentColor;
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M4 7h9M19 7h1M4 12h3M13 12h7M4 17h11M19.5 17h.5'/><circle cx='16' cy='7' r='2'/><circle cx='10' cy='12' r='2'/><circle cx='17' cy='17' r='2'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M4 7h9M19 7h1M4 12h3M13 12h7M4 17h11M19.5 17h.5'/><circle cx='16' cy='7' r='2'/><circle cx='10' cy='12' r='2'/><circle cx='17' cy='17' r='2'/></svg>") center/contain no-repeat}
/* island add-to-cart bag (masked, htm can't inline svg) */
.card__act--atc::before{content:'';display:block;width:17px;height:17px;background:currentColor;
  -webkit-mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M5 8h14l-1.2 11.2a2 2 0 0 1-2 1.8H8.2a2 2 0 0 1-2-1.8z'/><path d='M8.5 10V6.5a3.5 3.5 0 0 1 7 0V10'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M5 8h14l-1.2 11.2a2 2 0 0 1-2 1.8H8.2a2 2 0 0 1-2-1.8z'/><path d='M8.5 10V6.5a3.5 3.5 0 0 1 7 0V10'/></svg>") center/contain no-repeat}

/* ---------- project board (/pages/project) ---------- */
.pboard__top{display:flex;align-items:flex-end;justify-content:space-between;gap:1.5rem;margin-bottom:2rem;flex-wrap:wrap}
.pboard__top h1{font-family:var(--font-heading);font-weight:380;letter-spacing:-.015em;font-size:clamp(1.9rem,3.4vw,2.7rem);margin:.2rem 0 .3rem}
.pboard__sub{color:var(--color-muted);max-width:52ch;margin:0}
.pboard__proj{border:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);margin-bottom:1.6rem;background:var(--color-bg)}
.pboard__head{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;
  padding:1.1rem 1.4rem;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.pboard__head h2{margin:0;font-size:1.15rem;font-weight:600}
.pboard__chips{display:flex;gap:.45rem;flex-wrap:wrap}
.pboard__chip{font-size:.72rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;
  border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);padding:.3em .7em;color:color-mix(in srgb,var(--color-ink) 70%,transparent)}
.pboard__chip--muted{border-style:dashed;color:var(--color-muted)}
.pboard__proj .pdrawer__meta{grid-template-columns:2fr 1fr 1fr 1fr 1.2fr;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.pboard__proj .pdrawer__meta .pdrawer__meta-field--wide{grid-column:auto}
@media (max-width:820px){.pboard__proj .pdrawer__meta{grid-template-columns:1fr 1fr}}
.pboard__lines{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0 2rem;padding:.3rem .1rem}
.pboard__empty{color:var(--color-muted);padding:1.6rem 1.4rem;margin:0}
.pboard__foot{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;
  padding:1rem 1.4rem;border-top:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.pboard__est{font-size:.92rem;color:var(--color-muted)}
.pboard__est b{color:var(--color-ink);font-size:1.05rem;margin-left:.4rem}
.pboard__foot-actions{display:flex;gap:.7rem}
.pboard__del.is-armed{border-color:#b3261e;color:#b3261e}
.pdrawer__all{display:block;text-align:center;font-size:.8rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;
  color:color-mix(in srgb,var(--color-ink) 60%,transparent);margin-top:.2rem;text-decoration:none;padding:.4rem}
.pdrawer__all:hover{color:var(--color-ink)}

/* header: the project ("Lists") icon is icon-only again (Luke 2026-09-09);
   the name lives in aria-label/title and the flyout heading. */

/* auth nudge: projects are device-local until the account app lands */
.pauth{display:flex;align-items:center;gap:.8rem;background:color-mix(in srgb,var(--color-accent) 8%,transparent);
  border:1px solid color-mix(in srgb,var(--color-accent) 30%,transparent);padding:.8rem 1.1rem;font-size:.86rem;line-height:1.45}
.pauth svg{width:18px;height:18px;flex:none;stroke:var(--color-accent)}
.pauth a{color:var(--color-ink);font-weight:600}
.pdrawer__lines + .flyout__foot .pauth,.flyout__foot .pauth{margin-bottom:.4rem}
[data-project-board] > .pauth,.pboard__auth{margin-bottom:1.4rem}
/* guest banner card (drawer): "Don't lose your list" + Sign in / Create an
   account. Copy, not a gate — the device buffer keeps working underneath. */
.pauth--card{align-items:flex-start;margin:.2rem 1.6rem .6rem;padding:.9rem 1.1rem}
.pauth--card > div{display:grid;gap:.3rem;min-width:0}
.pauth--card b{font-size:.92rem}
.pauth__actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.35rem}
.pauth__actions .btn{height:36px;padding:0 .9rem;font-size:.76rem;margin:0}
/* .pauth a paints links ink; restate the button colours or the primary
   Sign in is ink-on-ink (seen on the first preview run) */
.pauth__actions .btn{color:var(--paper)}
.pauth__actions .btn--ghost{color:var(--ink)}
.pauth__actions .btn--ghost:hover{color:var(--paper)}

/* delete confirmation dialog (soft-delete: archives, recoverable) */
.pconfirm{position:fixed;inset:0;z-index:140;display:grid;place-items:center}
.pconfirm__scrim{position:absolute;inset:0;background:color-mix(in srgb,var(--color-ink) 32%,transparent)}
.pconfirm__card{position:relative;width:min(400px,calc(100vw - 2rem));background:var(--color-bg);
  border:1px solid color-mix(in srgb,var(--color-ink) 12%,transparent);
  box-shadow:0 24px 60px color-mix(in srgb,var(--color-ink) 25%,transparent);
  padding:1.4rem 1.5rem 1.3rem;animation:flyoutIn var(--dur-fast) var(--ease-spring)}
.pconfirm__title{margin:0 0 .5rem;font-size:1.05rem;font-weight:600}
.pconfirm__body{margin:0 0 1.2rem;font-size:.9rem;line-height:1.5;color:color-mix(in srgb,var(--color-ink) 70%,transparent)}
.pconfirm__body b{color:var(--color-ink)}
.pconfirm__actions{display:flex;gap:.7rem;justify-content:flex-end}
.pconfirm__actions .btn{height:44px;display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0 1.2rem;font-size:.9rem}
.pconfirm__yes{background:#b3261e;border-color:#b3261e;color:#fff}
.pconfirm__yes:hover{background:#9a1f18}
/* per-order cart CTAs (drawer + board) */
.porder__cta{display:flex;justify-content:flex-end;padding:.1rem 1.6rem 1rem}
.porder__cta .btn,.pboard__order-foot .btn{height:40px;padding:0 1.1rem;font-size:.78rem}
.pboard__order-foot{display:flex;justify-content:flex-end;margin-top:.6rem}
/* ---------- generic swatch popover (A-2): ink card above any
   [data-swatch-label], optional image via data-swatch-img ---------- */
.swpop{position:fixed;z-index:130;background:var(--color-ink);color:var(--color-bg);
  padding:.45rem .7rem;min-width:44px;text-align:center;pointer-events:none;
  opacity:0;transform:translateY(3px);transition:opacity var(--dur-fast) ease,transform var(--dur-fast) var(--ease-spring)}
.swpop.is-on{opacity:1;transform:none}
.swpop img{display:block;width:96px;height:96px;object-fit:cover;background:var(--color-bg);margin:0 auto .45rem}
.swpop img[hidden]{display:none}
.swpop__sw{display:none;width:96px;height:96px;margin:0 auto .45rem;background:var(--sw,#cfccc6)}
.swpop--colour .swpop__sw{display:block}
.swpop span{display:block;font-size:.7rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;white-space:nowrap}
.swpop::after{content:'';position:absolute;left:50%;bottom:-5px;transform:translateX(-50%);
  border:5px solid transparent;border-top-color:var(--color-ink);border-bottom:0}
.swpop--below::after{bottom:auto;top:-5px;border-top:0;border-bottom:5px solid var(--color-ink)}
/* ---------- sale pricing: discounted-first + Save chip ---------- */
.price-now{color:var(--color-accent);font-weight:600}
.card__price .price-now{color:var(--color-accent)}
.product__price.is-discounted [data-current]{color:var(--color-accent);font-weight:600}
.price-save{display:inline-block;margin-left:.55rem;background:var(--color-ink);color:var(--color-bg);
  font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:.25rem .5rem;vertical-align:middle}
.price-save[hidden]{display:none}
.qv__media .card__flags{opacity:1}
/* ---------- quick view loading bar ---------- */
.qv-modal__loading{display:grid;place-items:center;min-height:220px}
.loadbar{position:relative;display:block;width:140px;height:4px;overflow:hidden;
  background:color-mix(in srgb,var(--color-ink) 10%,transparent)}
.loadbar::after{content:'';position:absolute;top:0;left:-40%;width:40%;height:100%;background:var(--color-accent);
  animation:loadbar .9s ease-in-out infinite}
@keyframes loadbar{to{left:100%}}
/* sort dropdown in the pcombo language (PLP toolbar) */
.pcombo--sort{min-width:186px}
.pcombo--sort .pcombo__btn{display:block;width:100%;border:0;border-bottom:1px solid color-mix(in srgb,var(--color-ink) 16%,transparent);
  background:transparent;color:inherit;font:inherit;font-size:.9rem;text-align:left;cursor:pointer;
  padding:.32em 1.6em .32em 0;border-radius:0}
.pcombo--sort.is-open .pcombo__btn,.pcombo--sort .pcombo__btn:focus-visible{border-bottom-color:var(--color-accent)}
.pcombo--sort .pcombo__panel{left:auto;right:0}
/* ---------- PLP merchandising promos (plp_promo metaobjects) ----------
   One set of rules for three renderers with identical markup:
   snippets/plp-promo-card.liquid (SSR), promoTemplate() in
   assets/typesense-plp.js, and src/islands/plp/Promo.jsx. The SSR grid puts
   .promo straight into .product-grid; the islands wrap it in the hit <li>,
   hence the two selectors on the full-row rule. */
.promo{position:relative;display:flex;flex-direction:column;min-width:0;overflow:hidden;
  background:var(--surface);border:1px solid var(--line-2)}
.promo--banner,.plp-promo-cell--banner,.product-grid>*:has(>.promo--banner){grid-column:1/-1}
.promo__media{flex:none;min-height:0;background:color-mix(in srgb,var(--ink) 5%,transparent)}
.promo__media img{display:block;width:100%;height:100%;object-fit:cover}
.promo__body{display:flex;flex-direction:column;align-items:flex-start;gap:.7rem;
  padding:clamp(1.1rem,2vw,1.8rem)}
.promo__heading{margin:0;color:var(--ink);font-family:var(--serif);font-weight:380;line-height:1.06;
  letter-spacing:-.015em;font-size:clamp(1.15rem,1.6vw,1.5rem)}
.promo__text{margin:0;font-size:.88rem;line-height:1.5;color:var(--ink-2);white-space:pre-line}
.promo__cta{align-self:flex-start;margin-top:.2rem}
.promo__cta.btn{--bg:var(--accent);--fg:var(--paper);border-color:var(--accent);padding:.8em 1.4em;font-size:.8rem}
/* A promo's CTA label is STORE DATA (the plp_promo metaobject, staff-editable
   in Admin), so its length is not something the theme can know. .btn is
   inline-flex and .promo__body sets align-items:flex-start, which sizes the
   button to max-content: a label a few characters longer than the seeded one
   pushed past the 156px grid track at 360px and failed the overflow gate on
   /collections/* (2026-09-14 — changing the trade tile's label from "Apply
   for trade" to "Open a trade account" did it, on EVERY branch at once,
   because metaobjects are store-wide). Cap it at the cell and let it wrap, so
   copy length is a typography question and never a layout failure. */
.promo__cta.btn{max-width:100%;white-space:normal;text-align:left}
.promo__cta.btn>span{min-width:0;overflow-wrap:anywhere}
.promo__cta--link{max-width:100%;flex-wrap:wrap}
.promo__cta--link{position:relative;display:inline-flex;align-items:center;gap:.45em;padding-bottom:2px;
  color:var(--accent);font-family:var(--sans);font-size:.85rem;font-weight:500;letter-spacing:.03em}
.promo__cta--link::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:currentColor;
  transform:scaleX(1);transform-origin:right;transition:transform .45s var(--ease-out)}
.promo__cta--link:hover::after{transform:scaleX(0);transform-origin:left}
.promo__arrow{flex:none;width:15px;height:15px;transition:transform .4s var(--ease)}
.promo__cta:hover .promo__arrow{transform:translateX(4px)}
/* tile — fills its grid cell, on BOTH renderers (Luke 2026-09-09: "should be
   filling the full size of the card ... otherwise it looks imbalanced when
   mixed in with catalog grid search results"). This REVERSES 44a2512
   (2026-08-17), which shrank the surface to the 4:5 media zone and left the
   cell's lower third empty; PR #163 (unmerged) had the island half of this.
     SSR    .promo IS the grid item: grid stretch already fills the row.
     island .plp-hit is the item and .promo a block inside it: height:100%
            of the (definite, stretched) cell.
   Media keeps its natural 4:5 at the top but may give way (flex-shrink) so
   the copy never clips in a short row; the body takes the remainder and
   centres its copy. flex:1 on the body is safe here because no aspect-ratio
   is transferred onto it any more (the hazard #163 warned about). */
.plp-hit>.promo{height:100%}
.promo--tile .promo__media{flex:0 1 auto;aspect-ratio:4/5}
.promo--tile .promo__body{flex:1 1 auto;justify-content:center;min-width:0}
/* banner — full row: image left, copy right, stacking on narrow screens */
.promo--banner{flex-direction:row;align-items:stretch}
.promo--banner .promo__media{flex:0 0 clamp(220px,38%,520px)}
.promo--banner .promo__body{justify-content:center;gap:.85rem;max-width:56ch;padding:clamp(1.4rem,3vw,2.6rem)}
.promo--banner .promo__heading{font-size:clamp(1.4rem,2.4vw,2.1rem)}
.promo--banner .promo__text{font-size:.95rem}
@media (max-width:760px){
  .promo--banner{flex-direction:column}
  .promo--banner .promo__media{flex:none;aspect-ratio:16/9}
}
/* shipping-promise fireworks burst (A-2 success transition): 8 sticks radiate
   from the truck icon once, on crossing the threshold */
.ship-bar__icwrap{position:relative;flex:none;display:inline-flex}
.ship-burst{position:absolute;left:50%;top:50%;width:0;height:0;pointer-events:none}
.ship-burst i{position:absolute;left:-1px;top:-3px;width:2px;height:6px;background:currentColor;border-radius:1px;
  opacity:0;transform:rotate(var(--a)) translateY(-8px) scaleY(.2)}
.is-burst .ship-burst i{animation:shipburst .95s cubic-bezier(0,0,.58,1) .25s both}
@keyframes shipburst{
  0%{opacity:0;transform:rotate(var(--a)) translateY(-7px) scaleY(.2)}
  30%{opacity:1;transform:rotate(var(--a)) translateY(-13px) scaleY(1)}
  100%{opacity:0;transform:rotate(var(--a)) translateY(-17px) scaleY(.15)}}
/* ---------- chunky square close — ONE component, two parts ----------
   .btn-close  the block   (cart drawer head, project drawer head, quick view)
   .xmorph     the glyph   (those three PLUS the header search toggle's X state,
                            which keeps its own header-row chrome and takes only
                            the morph)

   Measured live on athlete2's quick-view close, #ox_quickview_wrapper
   .ox-overlay-close-btn, which is the reference the brief points at:

     block   40x40; background #000; color #fff
             transition: background-color .3s ease
     hover   background var(--a2-main-color); color #000
     glyph   span 20x20; position:relative; overflow:hidden
     bars    ::before/::after — content:''; position:absolute; height:2px;
             width:100%; top:50%; left:0; margin-top:-1px;
             background-color:currentColor; transform-origin:50% 50%;
             opacity:1; transition: transform ease .3s
             ::before rotate(45deg) · ::after rotate(-45deg)
     hover   .no-touch …:hover span:before,:after { transform: rotate(0) }

   It ROTATES AND MERGES — there is no crossfade. Both bars share one centre
   line at every frame and only their angle changes; at 0deg they land exactly
   on top of each other, and two coincident 2px bars read as one dash. Opacity
   never moves, which is why the dash arrives solid instead of fading up.

   Worth recording because it contradicts the rest of A-2: this control is
   SYMMETRIC — .3s ease both directions, on both the fill and the morph. No 1:2
   in/out ratio, no spring. Their tiles, arrows and buttons all do use those;
   the close does not. We keep their construction and run it on our house
   timing (.15s spring in, .3s out) so the close moves like the arrows and the
   tile reveal it sits beside rather than being the one control on its own clock.

   REST IS NOT THE REFERENCE'S INK BLOCK. A-2 fills theirs solid black; ours
   borrows the PDP gallery arrow's recipe instead, verbatim —

     .pgal__arrow { border:1px solid var(--line);
                    background:color-mix(in srgb,var(--paper) 90%,transparent);
                    color:var(--ink) }

   — a translucent paper block with a hairline and an ink glyph, so the control
   sits ON the surface it overlays rather than punching a hole in it. Hover is
   still the accent flip, so the morph and the colour change arrive together
   exactly as A-2's does.

   Sizing. The bars are rotated, so the visible X spans glyph x cos45 while the
   dash spans the full glyph width — the X is always 71% of the dash, and both
   scale off one number.
     quick view   32px block / 24px glyph — X 17px (53%), dash 24px (75%)
     drawer heads 64px block / 32px glyph — X 23px (35%), dash 32px (50%)
     search X     64px block / 24px glyph — X 17px (27%)
   The quick view is the one the brief resized: half the old 64px block with
   the glyph left at 24px, which doubles the X's share of the square from 27%
   to 53%. 26px was tried and rejected — a 3px margin reads cramped, and the
   dash all but touches the edges. The drawer heads keep the 64px rhythm their
   heads are built on and land on A-2's own 35/50, which is a real bump from 27
   without becoming a giant X in a header. The search X stays at 24px on
   purpose: it sits in a row of 25px icons whose drawn extent is 15-17px, and
   matching its neighbours matters more there than matching the drawers. */
.btn-close{flex:none;display:grid;place-items:center;width:64px;height:64px;cursor:pointer;padding:0;
  --xmorph-size:32px;
  /* rest is fully INVISIBLE — just the ink glyph. The translucent block
     read as an off-tone square against the modal surface (user report);
     border stays 1px transparent so the hover fill causes no shift. */
  border:1px solid transparent;background:transparent;color:var(--ink);
  transition:background .3s ease,color .3s ease,border-color .3s ease}
.btn-close:hover,.btn-close:focus-visible{background:var(--accent);border-color:var(--accent);color:var(--paper);
  transition:background .15s var(--ease-spring),color .15s var(--ease-spring),border-color .15s var(--ease-spring)}
/* the search toggle keeps its own header-row chrome and its own 24px glyph;
   only the hover fill joins the family (theme.css lands after critical.css,
   so equal specificity is enough to take the tint) */
.is-searching .icon-btn--search:hover,.is-searching .icon-btn--search:focus-visible{
  background:var(--accent);color:var(--paper)}
/* the glyph. --xmorph-size lets a host resize it without touching the bars. */
.xmorph{display:block;position:relative;overflow:hidden;width:var(--xmorph-size,24px);height:var(--xmorph-size,24px)}
.xmorph::before,.xmorph::after{content:'';position:absolute;top:50%;left:0;width:100%;height:2px;margin-top:-1px;
  background:currentColor;transform-origin:50% 50%;transition:transform .3s ease}
.xmorph::before{transform:rotate(45deg)}
.xmorph::after{transform:rotate(-45deg)}
:is(.btn-close,.icon-btn--search):is(:hover,:focus-visible) .xmorph::before,
:is(.btn-close,.icon-btn--search):is(:hover,:focus-visible) .xmorph::after{
  /* A-2's close runs symmetric .3s ease BOTH ways (user preference over
     the house spring — the 150ms spring-in read as instant) */
  transform:rotate(0)}
/* critical.css already zeroes every duration under reduce; this drops the
   rotation itself so the glyph swaps state without a spin */
@media (prefers-reduced-motion:reduce){
  .xmorph::before,.xmorph::after{transition:none}
}
/* drawer heads share one rhythm: title left, chunky close flush right */
.cart-drawer__head{min-height:64px;padding:0 0 0 1.6rem}
.flyout--panel .flyout__head{display:flex;justify-content:space-between;align-items:center;
  min-height:64px;padding:0 0 0 1.6rem}
.flyout--panel .flyout__head h2{font-family:var(--font-body,inherit);font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;margin:0}
/* in drawer heads the button stretches to the head's real height —
   a fixed 64px against the 64px+1px-border head left a 1px sliver */
.cart-drawer__head .btn-close,.flyout--panel .flyout__head .btn-close{height:auto;align-self:stretch}
/* half the drawer block, glyph left at 24px so the X takes 53% of the square
   instead of 27% — the whole point of the resize */
/* A-2's own close proportions AND size, per the user pointing at their
   quick view: 40px block, 20px glyph = visible X 35% / dash 50%, 10px margin */
.qv-modal__close{position:absolute;top:0;right:0;z-index:2;width:40px;height:40px;--xmorph-size:20px}
/* board bar: drawer-parity controls + view toggle */
.pboard__bar{display:grid;grid-template-columns:1fr auto;gap:.4rem 2rem;align-items:start;margin:0 0 1.6rem}
.pboard__bar-name{grid-column:1}
.pboard__bar .pdrawer__meta{border-bottom:0;padding:.7rem 0 0;grid-column:1}
.pboard__views{grid-column:2;grid-row:1 / span 2;display:flex;align-self:center}
.pboard__view{border:1px solid color-mix(in srgb,var(--color-ink) 18%,transparent);background:var(--color-bg);
  color:var(--color-ink);font:inherit;font-size:.8rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
  padding:.65rem 1.3rem;cursor:pointer;margin-left:-1px}
.pboard__view.is-active{background:var(--color-ink);color:var(--color-bg);border-color:var(--color-ink)}
/* customers view */
.pboard__cust{border:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);padding:1.1rem 1.4rem;margin-bottom:1rem}
.pboard__cust-head{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;margin-bottom:.5rem}
.pboard__cust-head h2{margin:0;font-size:1.05rem}
.pboard__cust-row{display:flex;align-items:center;gap:1rem;padding:.55rem 0;border-top:1px solid color-mix(in srgb,var(--color-ink) 7%,transparent)}
.pboard__cust-name{flex:1;min-width:0;font-weight:600;display:flex;align-items:baseline;gap:.6rem}
.pboard__cust-name small{font-weight:400;color:var(--color-muted)}
.pboard__cust-meta{flex:none;font-size:.82rem;color:var(--color-muted);font-variant-numeric:tabular-nums}
.pboard__cust-open{height:38px;padding:0 1rem;font-size:.78rem}
@media (max-width:760px){.pboard__bar{grid-template-columns:1fr}.pboard__views{grid-column:1;grid-row:auto}}


/* ---------- PDP quick facts (product-facts.liquid) ---------- */
.pfacts{display:flex;flex-wrap:wrap;gap:.45rem;margin:.9rem 0 .3rem}
.pfacts__chip{display:inline-flex;align-items:center;gap:.4rem;border:1px solid var(--line);background:var(--paper);border-radius:var(--radius);padding:.34rem .65rem;font:inherit;font-size:.8rem;color:var(--ink-2);cursor:pointer;transition:border-color .2s var(--ease),color .2s var(--ease)}
.pfacts__chip:hover{border-color:var(--accent);color:var(--ink)}
.pfacts__chip svg{width:14px;height:14px;stroke:var(--accent);flex:none}

/* ---------- product dossier (product-dossier.liquid + dossier.js) ----------
   Server-rendered <details> accordion is the base presentation (also no-JS
   and mobile). dossier.js upgrades to rail/tabs on desktop when the section
   setting asks for them. */
.dossier{margin-top:1.6rem}
.dossier__count{color:var(--accent);font-weight:600;margin-left:.35rem}
.dossier__acc{border-bottom:1px solid var(--line)}
.dossier__acc:first-child{border-top:1px solid var(--line)}
.dossier__acc summary{list-style:none;display:flex;justify-content:space-between;align-items:center;gap:1rem;padding:.95rem 0;font:600 .78rem var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink);cursor:pointer}
.dossier__acc summary::-webkit-details-marker{display:none}
.dossier__chev{width:16px;height:16px;color:var(--ink-3);flex:none;transition:transform .3s var(--ease)}
.dossier__acc[open] .dossier__chev{transform:rotate(180deg)}
.dossier__body{padding:0 0 1.3rem}
.dossier__ptitle{display:none;font:600 .78rem var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink);border-bottom:1px solid var(--line);padding-bottom:.55rem;margin:0 0 1.1rem}
.dossier__nav{display:none}
.dossier__prose{font-size:.92rem;color:var(--ink-2);max-width:62ch}
.dossier__micro{font-size:.72rem;text-transform:uppercase;letter-spacing:.12em;color:var(--ink-3);margin:.9rem 0 .55rem}
.dossier__micro:first-child{margin-top:0}
.dossier__spec{width:100%;border-collapse:collapse;font-size:.88rem}
.dossier__spec tr{border-bottom:1px solid var(--line)}
.dossier__spec th{font-weight:500;color:var(--ink-3);text-align:left;padding:.6rem .8rem .6rem 0;width:42%;vertical-align:top}
.dossier__spec td{padding:.6rem 0;color:var(--ink)}
.dossier__grp td{padding-top:1.3rem;font:600 .72rem var(--sans);text-transform:uppercase;letter-spacing:.12em;color:var(--accent)}
.dossier__wels{display:inline-flex;align-items:center;gap:.35rem;border:1px solid var(--line);border-radius:var(--radius);padding:.15rem .5rem;font-size:.78rem;background:var(--paper)}
.dossier__wels svg{width:13px;height:13px;fill:var(--accent);stroke:none}
.dossier__scraped{font-size:.9rem;color:var(--ink-2);margin-top:1rem}
.dossier__scraped table{width:100%;border-collapse:collapse}
.dossier__scraped td,.dossier__scraped th{border-bottom:1px solid var(--line);padding:.5rem .5rem .5rem 0;text-align:left}
.dossier__dl{display:flex;align-items:center;gap:.9rem;border:1px solid var(--line);border-radius:var(--radius);padding:.75rem .9rem;background:var(--paper);margin-bottom:.5rem;transition:border-color .2s var(--ease)}
.dossier__dl:hover{border-color:var(--accent)}
.dossier__dl-ico{width:34px;height:34px;background:var(--surface);border-radius:var(--radius);display:grid;place-items:center;flex:none}
.dossier__dl-ico svg{width:17px;height:17px;color:var(--ink-2)}
.dossier__dl-name{display:block;font-weight:500;font-size:.88rem}
.dossier__dl-meta{display:block;color:var(--ink-3);font-size:.75rem}
.dossier__dl-arr{width:16px;height:16px;color:var(--accent);margin-left:auto;flex:none}
.dossier__pict{width:100%;border-collapse:collapse;font-size:.85rem;max-width:560px}
.dossier__pict td{padding:.45rem 0;border-bottom:1px solid var(--line)}
.dossier__pict td:first-child{color:var(--ink-3);width:38%}
.dossier__pict a{border-bottom:1px solid var(--line)}
.dossier__pict a:hover{border-color:var(--accent);color:var(--accent)}
.dossier__brand{display:grid;grid-template-columns:auto 1fr;gap:1.2rem;align-items:start;max-width:640px}
.dossier__bmark{width:56px;height:56px;background:var(--ink-rich);color:#fff;border-radius:var(--radius);display:grid;place-items:center;font:500 1.3rem var(--serif)}
.dossier__blink{display:inline-flex;align-items:center;gap:.4rem;margin-top:.6rem;font-size:.82rem;font-weight:600;color:var(--accent)}
.dossier__blink svg{width:14px;height:14px}

/* rail + tabs presentations: desktop only, activated by dossier.js adding
   .dossier--js. Without JS (or under 901px) every style is the accordion. */
@media (min-width:901px){
  .dossier--rail,.dossier--tabs{grid-column:1/-1;margin-top:3rem}
  .dossier--js.dossier--rail .dossier__acc summary,
  .dossier--js.dossier--tabs .dossier__acc summary{display:none}
  .dossier--js.dossier--rail .dossier__acc,
  .dossier--js.dossier--tabs .dossier__acc{border:0}
  .dossier--js.dossier--rail .dossier__ptitle{display:block}
  .dossier--js.dossier--rail{display:grid;grid-template-columns:200px minmax(0,1fr);gap:clamp(24px,3.5vw,56px);align-items:start}
  .dossier--js.dossier--rail .dossier__nav{display:flex;flex-direction:column;position:sticky;top:calc(var(--header-h) + 12px);border-left:1px solid var(--line)}
  .dossier--js.dossier--rail .dossier__nav a{border-left:2px solid transparent;margin-left:-1px;font:600 .78rem var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3);padding:.55rem 1rem;transition:color .25s var(--ease),border-color .25s var(--ease)}
  .dossier--js.dossier--rail .dossier__nav a:hover{color:var(--ink)}
  .dossier--js.dossier--rail .dossier__nav a.is-on{color:var(--ink);border-left-color:var(--accent)}
  .dossier--js.dossier--rail .dossier__body{padding-bottom:2.4rem}
  /* scroll-margin here ADDS to the root scroll-padding-top set by the header
     block at the end of this file; keeping both landed dossier jumps 158px
     down instead of 81. The root padding supersedes it. */
  .dossier--js.dossier--rail .dossier__acc{scroll-margin-top:4px}
  .dossier--js.dossier--tabs .dossier__nav{display:flex;border-bottom:1px solid var(--line);overflow-x:auto}
  .dossier--js.dossier--tabs .dossier__nav a{position:relative;font:600 .78rem var(--sans);text-transform:uppercase;letter-spacing:.1em;color:var(--ink-3);padding:.9rem 1.1rem;white-space:nowrap;transition:color .25s var(--ease)}
  .dossier--js.dossier--tabs .dossier__nav a:hover{color:var(--ink)}
  .dossier--js.dossier--tabs .dossier__nav a.is-on{color:var(--ink)}
  .dossier--js.dossier--tabs .dossier__nav a.is-on::after{content:"";position:absolute;left:1.1rem;right:1.1rem;bottom:-1px;height:2px;background:var(--accent)}
  .dossier--js.dossier--tabs .dossier__acc{display:none}
  .dossier--js.dossier--tabs .dossier__acc.is-on{display:block;background:var(--surface);border-radius:var(--radius);padding:clamp(20px,3vw,40px);margin-top:1.2rem;animation:dossier-in .25s var(--ease)}
}
@keyframes dossier-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}


/* ---------- pairs-with strip (simple products; custom.pairs_with) ---------- */
.ppairs{margin-top:3rem}
.ppairs__label{font-size:.72rem;text-transform:uppercase;letter-spacing:.12em;color:var(--ink-3);border-bottom:1px solid var(--line);padding-bottom:.55rem;margin:0 0 1rem}
.ppairs__row{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem}
.ppairs__card{border:1px solid var(--line);border-radius:var(--radius);padding:.8rem;transition:border-color .2s var(--ease)}
.ppairs__card:hover{border-color:var(--accent)}
.ppairs__img{display:block;aspect-ratio:1/1;background:var(--surface);border-radius:var(--radius);overflow:hidden;margin-bottom:.6rem}
.ppairs__img img{width:100%;height:100%;object-fit:cover}
.ppairs__name{display:block;font-size:.82rem;font-weight:500;line-height:1.3}
.ppairs__price{display:block;font-size:.8rem;color:var(--ink-3);margin-top:.25rem}
@media (max-width:900px){.ppairs__row{grid-template-columns:1fr 1fr}}


/* configurator PDP: dossier below the builder, readable measure */
.cfg-dossier{max-width:880px;margin-top:3rem}
.cfg-dossier .dossier{margin-top:0}


/* ---------- breadcrumbs (snippets/breadcrumbs.liquid) ---------- */
.crumbs{display:flex;flex-wrap:wrap;gap:.45rem;align-items:center;margin:0 0 .9rem;font-size:.78rem;color:var(--ink-3)}
.crumbs a{color:inherit;transition:color .2s var(--ease)}
.crumbs a:hover{color:var(--ink)}
.crumbs__sep{color:var(--line);margin:0 .1rem}
.crumbs [aria-current]{color:var(--ink-2)}


/* live finish dots on tiles: hover previews (typesense-plp.js delegate) */
.swatch--live{cursor:pointer}
.swatch--live:hover{outline:1px solid var(--ink);outline-offset:1px}
.card__fin-more{font-size:.68rem;color:var(--ink-3);align-self:center;margin-left:.15rem}

/* ---------- header scroll behaviour ------------------------------------
   .site-header has carried `position:sticky;top:0` since the theme's first
   commit (critical.css:32) and has NEVER actually pinned. Shopify wraps each
   section group in its own div, and that wrapper is only as tall as the header
   inside it (measured 65px on the live preview). A sticky element can only
   travel within its parent's box, so there was zero travel: the wrapper
   scrolled away and took the header with it.

   The fix is to stick the WRAPPER, whose parent is <body> and therefore the
   whole document. Two hypotheses were tested and DISPROVED on the way, and are
   recorded so nobody re-runs them: (a) `html,body{overflow-x:clip}` in
   critical.css breaking sticky — forcing both to visible changed nothing;
   (b) sticking `.shopify-section-group-header` — that class does not exist,
   the real one is `-header-group`.

   Modes come from settings.header_scroll, stamped on <html> in theme.liquid. */
/* Scoped with :has(.site-header) because Shopify stamps
   shopify-section-group-header-group on EVERY section div in the group, not on
   one wrapper. Today the group holds one section; add an announcement bar and
   each would independently become sticky and pile on top of the other. */
:root[data-header-scroll="sticky"] .shopify-section-group-header-group:has(.site-header),
:root[data-header-scroll="reveal"] .shopify-section-group-header-group:has(.site-header){
  position:sticky;top:0;z-index:50}
/* Reveal hides by animating the STICKY OFFSET, not by transforming.
   A transform (or will-change:transform) makes this element the containing
   block for position:fixed descendants — and sections/header.liquid renders
   .flyout--panel (the project drawer, critical.css:128 `position:fixed;
   top:0;bottom:0`) INSIDE the header. Under a transform it would be laid out
   against a 65px-tall box instead of the viewport: collapsed to a strip and
   sliding off-screen with the header. critical.css:31 already carries a
   warning about backdrop-filter for exactly this reason; a transform here
   would have reintroduced it one level up. Animating `top` hides the bar with
   zero containment risk. */
:root[data-header-scroll="reveal"] .shopify-section-group-header-group:has(.site-header){
  transition:top .28s var(--ease-out,cubic-bezier(.16,1,.3,1))}
:root[data-header-scroll="reveal"].header-hidden .shopify-section-group-header-group:has(.site-header){
  top:calc(-1 * (var(--header-h) + 2px))}
/* an anchor jump must not land underneath a pinned header */
:root[data-header-scroll="sticky"],:root[data-header-scroll="reveal"]{scroll-padding-top:calc(var(--header-h) + 12px)}
@media (prefers-reduced-motion:reduce){
  :root[data-header-scroll="reveal"] .shopify-section-group-header-group:has(.site-header){transition:none}
}

/* ============ home: brand marquee ============ */
/* duplicated-track -50% loop; duration scales with brand count so 20 or 131
   brands drift at the same px/s. Pauses on hover AND focus-within. */
.brandmarq{display:flex;align-items:center;gap:clamp(1.5rem,3vw,3rem);border-block:1px solid var(--line);
  padding:1.5rem 0 1.5rem clamp(20px,5vw,72px)}
.brandmarq__eyebrow{flex:none;margin:0;font:600 .7rem var(--sans);text-transform:uppercase;letter-spacing:.16em;color:var(--ink-3)}
.brandmarq__scroller{flex:1;min-width:0;overflow:hidden;touch-action:pan-y;cursor:grab;
  mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent)}
.brandmarq.is-dragging .brandmarq__scroller{cursor:grabbing}
.brandmarq.is-dragging{user-select:none;-webkit-user-select:none}
.brandmarq__track{display:flex;width:max-content;
  animation:brandmarq calc(var(--marq-n,40) * 3.5s) linear infinite}
.brandmarq__run{display:flex;align-items:center;gap:clamp(2.5rem,4vw,4rem);padding-right:clamp(2.5rem,4vw,4rem)}
/* pause while the SCROLLER is hovered (not the whole band: resting on the
   eyebrow or the "All" link is not an intent to read the strip) or a tile has
   KEYBOARD focus. :focus-visible, not :focus-within: Chrome focuses an <a> on
   mousedown, so a press that did not end as a click left the tile focused and
   the strip paused for good after the pointer left (Luke 2026-09-13: "freezes
   up and stays stuck"; reproduced 2026-09-13, 0 px/s after leave). This is the
   no-JS rule; assets/brand-marquee.js replaces the keyframe with its own drift
   and applies the same two holds itself. */
.brandmarq__scroller:hover .brandmarq__track,.brandmarq:has(:focus-visible) .brandmarq__track{animation-play-state:paused}
@keyframes brandmarq{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.brandmarq__item{display:inline-flex;align-items:center;color:var(--ink-2);white-space:nowrap}
.brandmarq__item:hover{color:var(--ink)}
/* wordmark fallback (a brand entry with no logo file): the SAME 160x56 slot
   as an image tile, the text sized to the image marks' height. The logo files
   are normalised to a mark ~21px tall in the 144x36 artwork box (competitor-
   scrape BRAND_LOGO_NORMALISATION_2026-09-13.md); a 1.2rem serif / .78rem
   caps / .85rem tracked-caps cycle put the four treatments at 11-30px glyph
   heights in tiles of varying width (Luke 2026-09-13: "ALVERI" a large spaced
   wordmark, "Ashtone" large italic, next to 5-8px marks). Every treatment now
   lands its glyphs at ~14-15px (serif cap height at 1.3rem, sans cap height at
   1.05rem) inside the fixed slot; a long name is ellipsised, never widened.
   (.brandlogo.brandlogo--wm: two classes so this outranks .brandlogo's
   inline-flex, which sits later in the file — ellipsis needs a block.) */
.brandmarq__wm,.brandlogo.brandlogo--wm{display:block;width:160px;height:56px;line-height:56px;text-align:center;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--serif);font-size:1.3rem;letter-spacing:.03em}
.brandmarq__wm--caps{font-family:var(--sans);font-weight:600;font-size:1.05rem;letter-spacing:.14em;text-transform:uppercase}
.brandmarq__wm--ital{font-style:italic}
.brandmarq__wm--wide{font-family:var(--sans);font-weight:500;font-size:1.05rem;letter-spacing:.18em;text-transform:uppercase}
.brandmarq__all{flex:none;font:500 .8rem var(--sans);color:var(--accent);border-bottom:1px solid var(--accent);
  margin-right:clamp(20px,5vw,72px);white-space:nowrap}
@media (max-width:899px){
  /* slower drift on a narrow viewport: the same px/s reads faster when the
     window shows a sixth of the track */
  .brandmarq__track{animation-duration:calc(var(--marq-n,40) * 5s)}
  .brandmarq{flex-wrap:wrap;row-gap:1rem}
}
@media (max-width:599px){
  /* phone: label and "All" on one row, the strip full-bleed beneath them.
     Luke 2026-09-13: "in mobile view brands we carry shows one brand at a
     time, it doesn't look right" — inline, the label and the link left the
     scroller ~150px and one and a half clipped logos. The strip runs edge to
     edge (a marquee can run under the gutters; the scroller clips it, never
     the viewport — overflow-gate), the tile box hugs the artwork instead of
     holding the 160px desktop cell, and the gap tightens, so 390px shows
     three to four marks at the same 36px artwork height as desktop. */
  .brandmarq{display:grid;grid-template-columns:1fr auto;align-items:center;row-gap:.9rem;padding:1.25rem 0}
  /* explicit cells: auto-placement put the "All" link on a THIRD row, stretched
     under column 1 with a 346px underline (measured 2026-09-13, 390px) */
  .brandmarq__eyebrow{grid-column:1;grid-row:1;padding-left:20px}
  .brandmarq__all{grid-column:2;grid-row:1;justify-self:end;margin-right:20px}
  .brandmarq__scroller{grid-column:1/-1;grid-row:2;min-width:0;
    mask-image:linear-gradient(90deg,transparent,#000 4%,#000 96%,transparent);
    -webkit-mask-image:linear-gradient(90deg,transparent,#000 4%,#000 96%,transparent)}
  .brandmarq__run{gap:1.25rem;padding-right:1.25rem}
  .brandmarq .brandlogo--tile{width:auto;min-width:0}
  .brandmarq .brandlogo--dark.brandlogo--tile{padding:0 .75rem}
}
@media (prefers-reduced-motion:reduce){
  /* explicit: critical.css clamps all animation to .001ms, which would make
     an infinite marquee jitter instead of stopping */
  .brandmarq__track{animation:none}
  .brandmarq__scroller{overflow-x:auto;mask-image:none;-webkit-mask-image:none;touch-action:pan-x pan-y}
  .brandmarq__run[aria-hidden="true"]{display:none}
}

/* ============ home: category grid (sections/category-tiles.liquid) ============
   Edge-to-edge, gutterless masonry. Six columns at >= 1200px, rows as tall
   as a column is wide (container-query units so a classic scrollbar does
   not skew the square), capped at 260px so a 1920 screen gets more tiles
   per screen rather than bigger ones. grid-auto-flow:dense packs the
   1x1 / 1x2 / 2x2 tiles with no holes; every span is a class in the HTML
   and every height is CSS, so nothing is laid out by script and the images
   shift nothing (they are absolutely positioned inside a sized cell). */
.catgrid{padding-top:clamp(3rem,6vw,4.5rem);container-type:inline-size}
.catgrid__head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1.6rem}
.catgrid__lockup{display:flex;align-items:baseline;gap:1rem;flex-wrap:wrap}
.catgrid__title{font-family:var(--serif);font-weight:300;font-size:clamp(1.7rem,2.8vw,2.4rem);letter-spacing:-.01em;margin:0}
.catgrid__note{font-size:.82rem;color:var(--ink-3);margin:0}
.catgrid__all{font:500 .84rem var(--sans);color:var(--ink);border-bottom:1px solid var(--ink);white-space:nowrap;flex:none}
.catgrid__all span{display:inline-block;margin-left:.35em;transition:transform .2s var(--ease-out)}
.catgrid__all:hover{color:var(--accent);border-color:var(--accent)}
.catgrid__all:hover span{transform:translateX(3px)}
.catgrid__grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));grid-auto-rows:min(calc(100vw / 6),260px);grid-auto-rows:min(calc(100cqw / 6),260px);
  grid-auto-flow:dense;gap:0;background:var(--ink-rich)}
.cattile{position:relative;display:flex;flex-direction:column;justify-content:flex-end;overflow:hidden;min-width:0;
  background:#EFECE6;padding:clamp(.9rem,1.4vw,1.3rem) clamp(1rem,1.5vw,1.4rem);color:var(--ink);text-decoration:none;
  /* the hairline that keeps two light photographs from merging (gutterless) */
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--paper) 18%,transparent)}
.cattile--tall{grid-row:span 2}
.cattile--large{grid-column:span 2;grid-row:span 2}
.cattile__img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;
  transition:transform .7s cubic-bezier(.2,.7,.2,1)}
.cattile:hover .cattile__img,.cattile:focus-visible .cattile__img{transform:scale(1.045)}
/* the scrim never touches the top half, so the photograph is read as a
   photograph; the lower third carries the name whatever the picture is */
.cattile__scrim{position:absolute;inset:0;background:linear-gradient(180deg,rgba(25,24,19,0) 42%,rgba(25,24,19,.28) 68%,rgba(25,24,19,.68) 100%);
  transition:opacity .35s var(--ease-out)}
.cattile:hover .cattile__scrim{opacity:.92}
.cattile__initial{position:absolute;inset:0;display:grid;place-items:center;
  font-family:var(--serif);font-weight:300;font-size:clamp(3rem,6vw,5rem);color:#B8B1A6}
.cattile__t{position:relative;display:flex;flex-direction:column;gap:.15rem;min-width:0}
.cattile__eyebrow{font:600 .62rem var(--sans);text-transform:uppercase;letter-spacing:.16em;color:var(--ink-3);margin-bottom:.15rem}
.cattile__label{font-family:var(--serif);font-weight:400;font-size:clamp(1.02rem,1.35vw,1.3rem);line-height:1.15;letter-spacing:-.005em;text-wrap:balance}
.cattile--large .cattile__label{font-size:clamp(1.3rem,2vw,1.9rem)}
.cattile__n{font-size:.7rem;color:var(--ink-3);font-variant-numeric:tabular-nums;letter-spacing:.02em}
.cattile--photo{color:var(--paper);text-shadow:0 1px 2px rgba(0,0,0,.35)}
.cattile--photo .cattile__eyebrow{color:color-mix(in srgb,var(--paper) 78%,transparent)}
.cattile--photo .cattile__n{color:color-mix(in srgb,var(--paper) 72%,transparent)}
.cattile:focus-visible{outline:2px solid var(--accent-2);outline-offset:-5px}
/* promo tile: ink ground, bronze accent used once (eyebrow + rule + arrow) */
.cattile--promo{justify-content:space-between;background:var(--ink);color:var(--paper);
  background-image:radial-gradient(120% 90% at 100% 0%,rgba(168,133,79,.16),rgba(168,133,79,0) 55%);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--paper) 10%,transparent)}
.cattile__promo-body{display:flex;flex-direction:column;gap:.7rem;min-width:0}
.cattile__eyebrow--accent{color:var(--accent-2);padding-bottom:.6rem;position:relative}
.cattile__eyebrow--accent::after{content:"";position:absolute;left:0;bottom:0;width:28px;height:1px;background:var(--accent-2)}
.cattile__promo-title{font-family:var(--serif);font-weight:300;font-size:clamp(1.25rem,1.6vw,1.6rem);line-height:1.12;letter-spacing:-.01em;text-wrap:balance}
.cattile__promo-text{font-size:.86rem;line-height:1.5;color:rgba(255,255,255,.7);max-width:26ch}
.cattile__promo-lines{display:flex;flex-direction:column;margin-top:.2rem}
.cattile__promo-line{font-size:.82rem;line-height:1.35;padding:.5rem 0;border-top:1px solid rgba(255,255,255,.14);color:rgba(255,255,255,.82)}
.cattile__promo-line:last-child{border-bottom:1px solid rgba(255,255,255,.14)}
.cattile__promo-fins{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem;margin-top:.3rem}
/* the shared swatch at 18px — same chip as the finish row and the PDP, so a
   colour edited once moves all three (the .fdisc these used is deleted) */
.cattile__promo-fins .swatch{width:18px;height:18px;
  /* the tile is an ink ground: .swatch's default rim is 20% ink and vanishes
     on it, so a Gunmetal or Matte Black dot would lose its edge entirely */
  border-color:rgba(255,255,255,.35)}
.cattile__promo-fins-n{flex-basis:100%;font-size:.7rem;color:rgba(255,255,255,.6);margin-top:.15rem;letter-spacing:.02em}
.cattile__promo-cta{font:500 .84rem var(--sans);color:var(--paper);display:inline-flex;align-items:center;gap:.4rem;width:fit-content;
  border-bottom:1px solid var(--accent-2);padding-bottom:2px;margin-top:1rem}
.cattile__promo-cta span{display:inline-block;transition:transform .2s var(--ease-out)}
.cattile--promo:hover .cattile__promo-cta{color:var(--accent-2)}
.cattile--promo:hover .cattile__promo-cta span{transform:translateX(3px)}
@media (max-width:1199px){
  .catgrid__grid{grid-template-columns:repeat(4,minmax(0,1fr));grid-auto-rows:min(25vw,260px);grid-auto-rows:min(25cqw,260px)}
}
@media (max-width:699px){
  .catgrid__grid{grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:50vw;grid-auto-rows:50cqw}
  .catgrid__head{align-items:flex-end}
  .catgrid__lockup{flex-direction:column;gap:.4rem}
  .cattile{padding:.8rem .85rem}
  .cattile__label{font-size:1rem}
  .cattile--large .cattile__label{font-size:1.35rem}
  .cattile__promo-title{font-size:1.15rem}
  .cattile__promo-text{display:none}
}
@media (prefers-reduced-motion:reduce){.cattile__img{transition:none}.cattile:hover .cattile__img{transform:none}}

/* ============ home: curated product row ============ */
.prodrow{padding-top:clamp(3.5rem,7vw,5rem)}
.prodrow__head{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;margin-bottom:1.75rem}
.prodrow__lockup{display:flex;flex-direction:column;gap:.5rem}
.prodrow__eyebrow{font:600 .7rem var(--sans);text-transform:uppercase;letter-spacing:.16em;color:var(--accent);margin:0}
.prodrow__title{font-family:var(--serif);font-weight:300;font-size:clamp(1.7rem,2.8vw,2.4rem);letter-spacing:-.01em;margin:0}
.prodrow__all{font:500 .84rem var(--sans);color:var(--ink);border-bottom:1px solid var(--ink);white-space:nowrap}
.prodrow__all:hover{color:var(--accent);border-color:var(--accent)}
@media (max-width:899px){.prodrow .product-grid{--cols:2}}

/* ============ home: editorial ink band ============ */
.inkband{display:grid;grid-template-columns:1fr 1fr;min-height:420px;margin-top:clamp(3.5rem,7vw,5rem);
  background:var(--ink);color:#fff}
.inkband__copy{display:flex;flex-direction:column;justify-content:center;gap:1.35rem;
  padding:clamp(2.5rem,5vw,4rem) clamp(1.25rem,4vw,3.5rem)}
.inkband__eyebrow{font:600 .7rem var(--sans);text-transform:uppercase;letter-spacing:.16em;color:var(--accent-2);margin:0}
.inkband__title{font-family:var(--serif);font-weight:300;font-size:clamp(1.9rem,3.2vw,2.75rem);line-height:1.08;letter-spacing:-.01em;margin:0;text-wrap:balance}
.inkband__text{font-size:.97rem;line-height:1.6;color:rgba(255,255,255,.72);margin:0;max-width:46ch}
.inkband__cta{display:inline-flex;align-items:center;gap:.65rem;width:fit-content;margin-top:.35rem;
  font:500 .8rem var(--sans);letter-spacing:.06em;text-transform:uppercase;padding:1rem 1.6rem;
  background:#fff;color:var(--ink);border:1px solid #fff;
  transition:background .2s var(--ease),border-color .2s var(--ease),color .2s var(--ease)}
.inkband__cta:hover{background:var(--accent);border-color:var(--accent);color:#fff}
.inkband__media{position:relative;overflow:hidden;min-height:240px}
.inkband__media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center}
/* photograph in the panel (2026-09-08: the Alveri Deluxe back-to-wall bath
   render): a subtle ink gradient from the copy edge and the foot, so a
   white bath on a pale room sits into the dark band instead of reading as
   a pasted-on rectangle. Only paints over a real image — the SVG
   placeholder keeps its own dark gradient. */
.inkband__media:has(img)::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(90deg,rgba(25,24,19,.55) 0%,rgba(25,24,19,.14) 32%,rgba(25,24,19,0) 60%),
             linear-gradient(180deg,rgba(25,24,19,0) 62%,rgba(25,24,19,.38) 100%)}
.inkband__placeholder{position:absolute;inset:0;display:grid;place-items:center;
  background:linear-gradient(160deg,#2A2822 0%,#191813 100%)}
.inkband__placeholder svg{width:30%;height:30%;stroke:#6F6759;stroke-width:.9;fill:none}
@media (max-width:899px){.inkband{grid-template-columns:1fr}.inkband__media{order:-1;aspect-ratio:16/9;min-height:0}
  /* stacked: the copy sits below, so the fade comes up from the foot only */
  .inkband__media:has(img)::after{background:linear-gradient(180deg,rgba(25,24,19,0) 50%,rgba(25,24,19,.6) 100%)}}

/* ============ home: shop-by-finish bar ============
   One row of brushed-metal discs (sections/finish-bar.liquid). Twelve
   84px discs plus their names fit the 1344px content width at 1440; below
   ~1100px the row scrolls sideways under the finger (scroll-snap, edge
   fade, no arrows) with the discs at the SAME size — a finish disc that
   shrinks stops reading as metal. No script touches the row. */
.finbar{padding-block:clamp(3rem,6vw,4.5rem) clamp(3.5rem,7vw,5rem)}
.finbar__head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1.6rem}
.finbar__title{font-family:var(--serif);font-weight:300;font-size:clamp(1.7rem,2.8vw,2.4rem);letter-spacing:-.01em;margin:0}
.finbar__note{font-size:.82rem;color:var(--ink-3);margin:0}
.finbar__lockup{display:flex;align-items:baseline;gap:1rem;flex-wrap:wrap}
.finbar__all{font:500 .84rem var(--sans);color:var(--ink);border-bottom:1px solid var(--ink);white-space:nowrap;flex:none}
.finbar__all span{display:inline-block;margin-left:.35em;transition:transform .2s var(--ease-out)}
.finbar__all:hover{color:var(--accent);border-color:var(--accent)}
.finbar__all:hover span{transform:translateX(3px)}
.finbar__row{list-style:none;margin:0;padding:0;display:flex;justify-content:space-between;gap:.5rem;
  overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x proximity;scrollbar-width:none;-webkit-overflow-scrolling:touch;
  /* bleed so the focus ring and the hover lift are not clipped by the scroller */
  padding:6px 4px 8px;margin-inline:-4px}
.finbar__row::-webkit-scrollbar{display:none}
.finbar__item{flex:none;scroll-snap-align:start;width:var(--finbar-slot,104px)}
.finbar__link{display:flex;flex-direction:column;align-items:center;gap:.8rem;text-align:center;color:var(--ink);
  border-radius:6px;outline-offset:4px}
.finbar__link:focus-visible{outline:2px solid var(--accent)}
.finbar__label{font-size:.8rem;line-height:1.25;letter-spacing:.005em;color:var(--ink-2);text-wrap:balance;max-width:100%}
.finbar__link:hover .finbar__label{color:var(--ink)}
/* 84px, fixed at every width. .swatch--xl's own clamp(64px,7vw,96px) is the
   component's default; the row pins it because the slot maths is fixed —
   12 slots of 104px inside the 1344px content width — and because a swatch
   that shrinks on a phone stops reading as a finish. */
.finbar__link .swatch{width:84px;height:84px;transition:transform .28s var(--ease-out),box-shadow .28s var(--ease-out)}
.finbar__link:hover .swatch{transform:translateY(-3px)}
@media (max-width:1099px){
  /* the row is wider than the viewport: same discs, sideways scroll, a
     fade on the trailing edge as the only cue (no arrows, Luke 2026-09-14) */
  .finbar__row{justify-content:flex-start;gap:.25rem;
    mask-image:linear-gradient(90deg,#000 88%,transparent);-webkit-mask-image:linear-gradient(90deg,#000 88%,transparent)}
  .finbar__head{align-items:flex-end}
  .finbar__lockup{flex-direction:column;gap:.4rem}
}
@media (prefers-reduced-motion:reduce){.finbar__link:hover .swatch{transform:none}}

/* No .fdisc here, and none is coming back. The second cut of this row drew
   every swatch procedurally — a brushed-metal disc with hairline grain, a
   radial sheen and a matte/polished/brushed variant switched off the finish's
   NAME. Luke killed it on sight (2026-09-14): "the effect you've applied here
   doesn't look right for brushed metal that you've applied across everything.
   Please just have the swatch with the image from the swatch like we had
   previously." The row renders snippets/swatch.liquid — the same circular
   chip as the PDP, the card strip and the header finish strip — so one edit
   to a swatch_value metaobject moves every surface at once, and a finish that
   gains swatch photography shows the photograph with no theme change.
   docs/decisions/2026-09-14-finish-bar-brushed-discs.md. */

/* ===== compiled megamenu (docs/rfc-megamenu-dynamic.md step 3) =============
   The compiled panel reuses the whole .mega chrome — .mega, .mega--tabbed,
   .mega--tiered, .mega__tier2/__tabs/__tab, .mega__body, .mega__feats,
   .mega__foot — so assets/theme.js needed ZERO changes. What is new is the
   CONTENT of a tab panel.

   LAYOUT: THREE COLUMNS, AND A HEIGHT CAP (Luke's review, 2026-08-23).
   "we should still see the website below the megamenu like we did before."

   The first cut stacked type / finish / brand as full-width ROWS, each with its
   own heading and rule. Measured on the preview at a 1440x960 viewport that
   gave: Bathroom 859px, Kitchen 862px, Brands 1321px, against 895px of
   viewport below a 65px header — so the panel filled or overflowed the screen
   and the page vanished. The legacy panel it replaced measured 613 / 861 / 579
   / 783px. Those legacy numbers are the budget.

   The rows-not-columns rationale recorded here in the first cut ("type is the
   primary action and wants the width; stacking lets a one-group tab fill the
   panel") is OVERRULED by that review. It optimised one tab's internal balance
   and ignored the thing the shopper actually loses: sight of the page. Columns
   it is, matching the approved mockup — 520px type grid, finish column, 300px
   featured — and a cap that keeps the page in view.

   The cap is min(620px, viewport minus header minus 140px): 620 sits just
   above the legacy Bathroom panel (613) and leaves ~280px of page visible at
   960px tall, and the vh term keeps it honest on short windows. Content is
   sized to FIT that box rather than scroll — 88px thumbs, three rows at most —
   with overflow-y:auto only as a safety net for a hub that outgrows it (the
   Brands index, with 55 tiles, is the one that can). */
.mega--compiled{--mega-max:min(620px,calc(100vh - var(--header-h,65px) - 140px))}
.mega--compiled .mega__body{max-height:var(--mega-max);overflow-y:auto;overscroll-behavior:contain;
  padding-block:1.5rem .4rem}
.mega__grid{display:grid;grid-template-columns:minmax(0,520px) 240px 300px;
  gap:2.2rem;align-items:start;justify-content:start}
/* A hub with no featured column (bathroom today — its two featured products
   are drafts) keeps the SAME 520px type column rather than stretching across
   the freed width. Letting it grow put all 11 tiles on one row, which made the
   panel a thin strip with a 250px dead band under it, because the height was
   then set by the finish list. Left-weighted with calm space on the right is
   the mockup's composition and it keeps every tab the same shape. */
.mega__grid--nofeat{grid-template-columns:minmax(0,520px) 240px}
.mega__grid--nofin{grid-template-columns:minmax(0,520px) 300px}
.mega__grid--nofeat.mega__grid--nofin{grid-template-columns:minmax(0,1fr)}
/* the eyebrow carries its own trailing link, so "Shop all" and "Order samples"
   stop costing a full row of height each — that was ~90px per panel */
.mega__eyebrow{display:flex;align-items:baseline;justify-content:space-between;gap:1rem}
.mega__all{font-size:.68rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:var(--accent);white-space:nowrap}
.mega__all:hover{color:var(--ink)}
.mega__n{font-size:.66rem;font-weight:600;letter-spacing:.06em;color:var(--ink-3);
  font-variant-numeric:tabular-nums;white-space:nowrap}

/* --- category thumbnails (88px, tight) ---------------------------------- */
.mega__cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:.9rem .7rem}
.mega__cat{display:block;text-align:center;min-width:0}
.mega__cat-media{display:grid;place-items:center;aspect-ratio:1/1;background:var(--surface);
  overflow:hidden;margin-bottom:.4rem;border:1px solid transparent;transition:border-color .18s var(--ease-out)}
.mega__cat-media img{width:100%;height:100%;object-fit:contain;mix-blend-mode:multiply;
  transition:transform .35s var(--ease-out)}
.mega__cat:hover .mega__cat-media{border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
.mega__cat:hover .mega__cat-media img{transform:scale(1.06)}
/* thumb-less node: a typographic tile, not a broken-image box — NO collection
   in the store carries an image today (RFC section 1) */
.mega__cat-initial{font-family:var(--serif);font-size:1.4rem;color:var(--ink-3);line-height:1}
.mega__cat-label{display:block;font-size:.76rem;line-height:1.25;color:var(--ink-2)}
.mega__cat:hover .mega__cat-label{color:var(--ink)}
.mega__cat .mega__n{display:block;margin-top:.1rem}

/* --- finish column ------------------------------------------------------
   The chip IS snippets/swatch.liquid at its 'lg' (30px) size — the same
   circular component the PDP picker and the card strip render. Nothing here
   restyles it; the column only lays the chips out. Reusing the component means
   a colour changed in the swatch_value metaobjects moves the PDP, the card
   strip and the menu together. */
/* two columns: ten finishes in one column stood 420px tall and became the
   thing setting the panel's height. Two columns halve that and hand the
   height back to the type grid, where it belongs. */
.mega__fins{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.25rem .7rem}
.mega__fin{display:flex;align-items:center;gap:.55rem;padding:.1rem 0;color:var(--ink-2);min-width:0}
.mega__fin:hover{color:var(--ink)}
.mega__fin .swatch{flex:none;transition:border-color .18s var(--ease-out)}
.mega__fin:hover .swatch{border-color:var(--ink)}
.mega__fin-label{font-size:.8rem;flex:1 1 auto;min-width:0}

/* --- brand tiles (the Brands index) -------------------------------------
   A brand the compiler detected but staff have not given a logo yet renders as
   a TEXT tile (RFC 2.4) — the product is never orphaned from the menu, and the
   tile silently upgrades the moment a logo is attached. 55 of them today, so
   the grid is dense and the panel's cap does the rest. */
.mega__brands{display:grid;grid-template-columns:repeat(auto-fill,minmax(124px,1fr));gap:.45rem}
.mega__brand{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.2rem;
  min-height:52px;padding:.5rem .5rem;border:1px solid var(--line);background:var(--paper);
  transition:border-color .18s var(--ease-out),background .18s var(--ease-out)}
.mega__brand:hover{border-color:var(--ink);background:var(--surface)}
.mega__brand-logo{display:block;line-height:0}
.mega__brand-logo img{max-width:100px;height:auto;max-height:30px;width:auto;object-fit:contain;
  filter:grayscale(1);opacity:.82;transition:filter .2s,opacity .2s}
.mega__brand:hover .mega__brand-logo img{filter:none;opacity:1}
.mega__brand-name{font-family:var(--serif);font-size:.85rem;color:var(--ink);text-align:center;line-height:1.15}

.mega--compiled .mega__foot{padding-block:.7rem .8rem}

/* --- featured carousel (compiled panel only) -----------------------------
   The featured column crossfades one product at a time while the panel is
   open, ~5s a slide. WHY CSS keyframes rather than a JS timer: the panel is
   open for two or three seconds at a time and there may be four of them in the
   DOM; a timer per panel would either run forever in the background or need
   its own lifecycle. Keyframes cost nothing while the panel is closed
   (visibility:hidden), and theme.js does one thing only — restart the cycle on
   open, so a hover never lands on a half-faded frame.

   Slides stack in ONE grid cell rather than absolutely: the cell then sizes
   itself to the tallest slide, so a long product title cannot clip and there
   is no magic height to keep in sync with the media aspect ratio.

   Fixed keyframe sets for 2 and 3 slides rather than one parameterised set:
   the visible fraction of the cycle depends on the slide count, and Liquid
   caps the list at 3, so two named animations is the whole problem. */
.mega__carousel{display:block;width:300px;align-self:start}
/* minmax(0,1fr) + min-width:0 — a grid item's default min is MIN-CONTENT, and
   a long product title (.mega__feat-title is a -webkit-box, which does not
   shrink below its longest unbroken run) blew the slide out to 455px inside
   this 300px track, overflowing the panel and pushing body.scrollWidth to
   1548 on a 1440 viewport. Same trap .product documents further up this file.
   Caught on the deployed preview theme; CI cannot see it. */
.mega__slides{display:grid;grid-template-columns:minmax(0,1fr)}
.mega__slide{grid-area:1/1;opacity:0;min-width:0}
/* critical.css:42 sets `.nav a{white-space:nowrap}` for the top-level nav
   items, and it cascades into every anchor in an open panel — including this
   slide. That defeats .mega__feat-title's own -webkit-line-clamp:2, which only
   means anything if the text is allowed to wrap: the title laid out on one
   456px line and was clipped mid-word at the panel edge. Re-normalise inside
   the carousel. Scoped to .mega__slide, NOT .mega__feat, deliberately — the
   legacy panel has the same latent clipping, but legacy is deleted one release
   after the compiler ships and keeping it visually frozen is what makes the
   rollout safe to verify. Logged as a follow-up instead. */
/* critical.css:42 `.nav a{white-space:nowrap}` is meant for the top-level nav
   items and is inherited by EVERY anchor in an open panel. In the compiled
   panel that silently broke three things at once: category labels ran on one
   line and overlapped their neighbours ("3-Piece Basin Sets" over "Shower
   Mixers"), brand names overflowed their tiles, and the featured title
   defeated its own -webkit-line-clamp. Re-normalise on the text spans rather
   than on `.mega--compiled a`, because .mega__tab must KEEP nowrap — a
   two-line tab reads as cramped and the tab strip scrolls instead. */
.mega__cat-label,.mega__brand-name,.mega__fin-label,.mega__slide .mega__feat-title{white-space:normal}
.mega__carousel[data-n="1"] .mega__slide{opacity:1}
.mega__carousel[data-n="2"] .mega__slide{animation:megaFade2 10s var(--i-delay,0s) infinite}
.mega__carousel[data-n="3"] .mega__slide{animation:megaFade3 15s var(--i-delay,0s) infinite}
.mega__slide{--i-delay:calc(var(--i,0) * 5s)}
/* keyboard: tabbing to an off-screen slide brings it forward rather than
   sending focus somewhere invisible. !important beats the animation, which a
   normal declaration would not. */
.mega__slide:focus-visible{opacity:1!important;z-index:2}
@keyframes megaFade2{0%{opacity:1}45%{opacity:1}50%{opacity:0}95%{opacity:0}100%{opacity:1}}
@keyframes megaFade3{0%{opacity:1}30%{opacity:1}33.34%{opacity:0}96.66%{opacity:0}100%{opacity:1}}
/* progress DASHES: slim ink-line bars that fill brass over the slide's dwell,
   so the strip says how long is left. Dots can only say which slide. */
.mega__dashes{display:flex;gap:.35rem;margin-top:.6rem}
.mega__dash{flex:1 1 0;height:2px;background:var(--line);overflow:hidden}
.mega__dash-fill{display:block;height:100%;background:var(--accent);transform:scaleX(0);transform-origin:left center;
  --i-delay:calc(var(--i,0) * 5s)}
.mega__carousel[data-n="2"] .mega__dash-fill{animation:megaDash2 10s var(--i-delay,0s) infinite}
.mega__carousel[data-n="3"] .mega__dash-fill{animation:megaDash3 15s var(--i-delay,0s) infinite}
@keyframes megaDash2{0%{transform:scaleX(0)}50%{transform:scaleX(1)}50.01%{transform:scaleX(0)}100%{transform:scaleX(0)}}
@keyframes megaDash3{0%{transform:scaleX(0)}33.33%{transform:scaleX(1)}33.34%{transform:scaleX(0)}100%{transform:scaleX(0)}}
/* reading beats cycling: any pointer or keyboard attention freezes the strip */
.mega__carousel:hover .mega__slide,.mega__carousel:hover .mega__dash-fill,
.mega__carousel:focus-within .mega__slide,.mega__carousel:focus-within .mega__dash-fill{animation-play-state:paused}
@media (prefers-reduced-motion:reduce){
  .mega__slide,.mega__dash-fill{animation:none!important}
  .mega__slide{opacity:0}
  .mega__slide[style*="--i:0"]{opacity:1}
  .mega__dash:first-child .mega__dash-fill{transform:scaleX(1)}
}

/* --- top-level nav in compiled mode -------------------------------------
   Luke, 2026-08-23: the hub links must carry the same treatment the tier-2 tab
   bar has — letter-spaced uppercase at 600 — so the two tiers of the menu read
   as one system instead of a sentence-case row sitting above a shouty one.

   Scoped to .nav--caps, which sections/header.liquid adds ONLY when
   megamenu_source is not `legacy`. Styling `.nav__list a` directly would have
   been one line, but it would also restyle the legacy menu, and legacy's
   rendered output has to stay byte-identical to main while both paths are
   live — that identity is the whole verification story for this rollout. */
/* The two tiers were INVERTED: tier-1 shipped at .78rem/600 and the tier-2 tab
   bar at .8rem/600, so the hub row read as subordinate to the tabs beneath it
   (Luke, 2026-08-23: "the 2nd level menu items are larger than the 1st level").
   Tier 1 is now clearly dominant — 15.2px at 700 with wider tracking — and
   tier 2 drops slightly below its old size so the step is unambiguous.

   The tier-2 rule is scoped to .mega--compiled, NOT to bare .mega__tab: the
   hand-built panel shares that class, and legacy has to stay pixel-frozen as
   well as byte-identical while both paths are live. */
.nav--caps .nav__item>a{font-size:.88rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
  padding-inline:.8rem;color:var(--ink-2)}
.nav--caps .nav__item>a:hover,.nav--caps .nav__item.is-open>a{color:var(--ink)}
/* Luke 2026-08-23: "no indicator which section you're in when you're moused
   over the top level". A colour shift alone is invisible at 700-weight caps,
   so the open/hovered hub gets the same 2px ink underline the tab bar uses,
   and the hub owning the current page (aria-current, set by Shopify's
   link.active) keeps it at rest — the mockup's .navlink.on. */
.nav--caps .nav__item>a{position:relative}
.nav--caps .nav__item>a::after{content:"";position:absolute;left:.8rem;right:.8rem;bottom:0;height:2px;
  background:var(--ink);transform:scaleX(0);transform-origin:left;transition:transform .18s var(--ease-out)}
.nav--caps .nav__item>a:hover::after,.nav--caps .nav__item.is-open>a::after,.nav--caps .nav__item>a[aria-current="page"]::after{transform:scaleX(1)}
.nav--caps .nav__item>a[aria-current="page"]{color:var(--ink)}
/* padding trimmed before size: the Bathroom hub's 11 tabs (TAPS…LIGHTING) have
   to sit on one row at 1440 without the strip scrolling, and losing tracking or
   size to fit would have undone the hierarchy this commit is here to fix. */
.mega--compiled .mega__tab{font-size:.82rem;letter-spacing:.11em;padding:.8rem .72rem}
/* Luke 2026-08-23 (round 4): logo slightly larger in compiled mode. The width
   is an editor setting (logo_width, default 170) and the img carries an inline
   width, so scale the box rather than fight the setting; legacy untouched. */
.nav--caps~.site-header__actions{flex:none}
.site-header:has(.nav--caps) .site-header__logo-img{width:190px!important;height:auto}
/* Breathing room above the foot strip on every tab: the last row of the type
   grid / finish list was sitting on the foot's top rule (Luke, round 4). The
   foot lives INSIDE .mega__body after the grid, so body padding lands BELOW
   the foot (round-4 miss, Luke's red line 20:57) — the gap is the grid's
   bottom margin, between the content and the foot's rule. */
.mega--compiled .mega__grid{margin-bottom:1.75rem}

/* ============ availability states (custom.availability) ============
   online (absent) | call_for_price | component | in_store_only. Badges share
   the sale / sold-out idiom on tiles (.card__badge); the PDP buy box swaps
   the price + form for a call block (sections/main-product.liquid,
   sections/quick-view.liquid). Components never render a card. */
.card__badge--cfp{background:var(--paper);color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink)}
.card__badge--store{background:var(--ink-2);color:#fff}
.card__price--cfp,.product__price--cfp{color:var(--ink-2);font-weight:500}
.card--cfp .card__act--atc,.card--store .card__act--atc{display:none}
.product__cfp{border:1px solid var(--line);background:var(--surface);padding:1.1rem 1.25rem;margin:1rem 0}
.product__cfp-title{font-family:var(--serif);font-weight:400;font-size:1.35rem;line-height:1.2;margin:0 0 .3rem}
.product__cfp-text{font-size:.9rem;line-height:1.5;color:var(--ink-2);margin:0 0 .9rem}
.product__cfp-actions{display:flex;flex-wrap:wrap;gap:.6rem}
.product__cfp-actions .btn{width:auto;margin:0;display:inline-flex;align-items:center;justify-content:center;gap:.5rem}
.product__cfp-call svg{flex:none}
.product--component{display:block}
.product__component{max-width:60ch;padding:2rem 0 3rem}
.product__component p{color:var(--ink-2);line-height:1.5}
.product__component .btn{width:auto;display:inline-flex}
.sd-tile__price--cfp{color:var(--ink-2);font-weight:500}


/* ---------- project rooms (doc v3: room/area groups inside an order) ---------- */
.pgroup{border-top:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.pgroup--unassigned{border-top:0}
.pgroup__head{display:flex;align-items:center;gap:.5rem;padding:.55rem 1.6rem .35rem;min-height:40px}
.pboard__lines--grouped .pgroup__head{padding-inline:.1rem}
.pgroup__head[draggable]{cursor:grab}
.pgroup__head.is-dragging{opacity:.5}
.pgroup__grip{flex:none;width:16px;height:16px;color:color-mix(in srgb,var(--color-ink) 35%,transparent)}
.pgroup__grip svg{width:16px;height:16px;display:block}
.pgroup__label{flex:1;min-width:0;border:0;border-bottom:1px solid transparent;background:transparent;color:inherit;font:inherit;
  font-size:.9rem;font-weight:600;padding:.25em 0;border-radius:0}
.pgroup__label:hover{border-bottom-color:color-mix(in srgb,var(--color-ink) 14%,transparent)}
.pgroup__label:focus{outline:none;border-bottom-color:var(--color-accent)}
.pgroup__label::placeholder{color:color-mix(in srgb,var(--color-ink) 38%,transparent);font-weight:500}
.pgroup__name{flex:1;font-size:.72rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--color-muted)}
.pgroup__sum{flex:none;font-size:.76rem;color:var(--color-muted);font-variant-numeric:tabular-nums}
.pgroup__toggle{flex:none;width:28px;height:28px;border:0;background:none;cursor:pointer;display:grid;place-items:center;
  color:color-mix(in srgb,var(--color-ink) 45%,transparent)}
.pgroup__toggle svg{width:15px;height:15px;transition:transform var(--dur-fast) ease}
.pgroup.is-collapsed .pgroup__toggle svg{transform:rotate(-90deg)}
.pgroup__del{width:28px}
.pgroup.is-collapsed .pgroup__lines{display:none}
.pgroup__lines{min-height:8px;transition:background var(--dur-fast) ease}
.pboard__lines--grouped{display:block;padding:0}
.pboard__lines--grouped .pgroup__lines{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0 2rem;padding:.2rem .1rem}
.pgroup__lines.is-over,.pgroup.is-over{background:color-mix(in srgb,var(--color-accent) 10%,transparent);outline:1px dashed var(--color-accent);outline-offset:-1px}
.pgroup__empty{grid-column:1/-1;margin:0;padding:.6rem 1.6rem .8rem;font-size:.8rem;color:var(--color-muted)}
.pboard__lines--grouped .pgroup__empty{padding-inline:.1rem}
.pline[draggable]{cursor:grab}
.pline.is-dragging{opacity:.45}
.pline__ctl{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-top:.3rem}
.pline__ctl .pline__qty{margin-top:0}
.pline__move select{border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);background:var(--color-bg);color:inherit;
  font:inherit;font-size:.76rem;padding:.2em .4em;max-width:160px;border-radius:0}
.pgroup__add{display:flex;align-items:baseline;gap:.8rem;flex-wrap:wrap;padding:.5rem 1.6rem .9rem}
.pboard__lines--grouped + .pgroup__add,.pboard__order .pgroup__add{padding-inline:.1rem}
.pgroup__addbtn{border:0;background:none;font:inherit;font-size:.84rem;font-weight:600;cursor:pointer;padding:.2rem 0;
  color:var(--color-accent-ink,#7a5a1e)}
.pgroup__addbtn:hover{text-decoration:underline}
.pgroup__hint{font-size:.76rem;color:var(--color-muted)}
/* guest notice inside the save popout + the board's sign-in prompt */
.psave__gate{display:flex;align-items:center;gap:.5rem;margin:0 1rem .45rem;padding:.4rem .6rem;font-size:.78rem;font-weight:600;
  color:var(--color-ink);background:color-mix(in srgb,var(--color-accent) 12%,transparent);text-decoration:none}
.psave__gate svg{width:15px;height:15px;flex:none;stroke:var(--color-accent)}
.pauth b{font-weight:600}
.pboard__gate{display:flex;gap:1.2rem;align-items:flex-start;border:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);
  background:color-mix(in srgb,var(--color-accent) 7%,transparent);padding:1.4rem 1.6rem;margin-bottom:1.6rem}
.pboard__gate-icon{flex:none;width:36px;height:36px;display:grid;place-items:center;background:var(--color-bg);border-radius:50%}
.pboard__gate-icon svg{width:20px;height:20px;stroke:var(--color-accent)}
.pboard__gate h2{margin:0 0 .3rem;font-size:1.15rem;font-weight:600}
.pboard__gate p{margin:0 0 .9rem;color:color-mix(in srgb,var(--color-ink) 72%,transparent);max-width:60ch;line-height:1.5}
.pboard__gate-actions{display:flex;gap:.6rem;flex-wrap:wrap;margin:0!important}
.pboard__gate-actions .btn{height:42px;display:inline-flex;align-items:center;padding:0 1.2rem;font-size:.86rem;margin:0}
@media (max-width:640px){.pboard__gate{flex-direction:column;gap:.8rem;padding:1.1rem 1.2rem}.pgroup__head{padding-inline:1.2rem}}
/* ---------- brand logo marks (snippets/brand-logo.liquid) ----------
   Standardised sizes, the same everywhere a logo appears: line = 28px high
   (PDP vendor line), tile = a fixed 160x56 box with the artwork at most
   144x36 (About-brand dossier header, home marquee). The <img> carries the
   computed width/height so nothing shifts on load. White artwork
   (brand.logo_on_dark) sits on an ink tile. .brandlogo--wm is the text
   fallback when the entry or its logo is absent. */
.brandlogo{display:inline-flex;align-items:center;justify-content:center;flex:none;vertical-align:middle}
.brandlogo__img{display:block;object-fit:contain}
.brandlogo--line{height:28px;justify-content:flex-start}
.brandlogo--tile{width:160px;height:56px}
.brandlogo--dark{background:var(--ink-rich);border-radius:var(--radius)}
.brandlogo--dark.brandlogo--line{height:36px;padding:0 .6rem}
/* .brandlogo--wm sizing lives with .brandmarq__wm above (one fallback box);
   the line variant keeps the PDP vendor line's 28px height */
.brandlogo.brandlogo--wm.brandlogo--line{width:auto;max-width:150px;height:28px;line-height:28px;text-align:left;font-size:1.15rem}
.dossier__brand .brandlogo--wm{text-align:left}
/* PDP vendor line: the logo replaces the vendor text; the link's hover rule
   underlines text, not artwork */
.product__vendor--logo{margin:0 0 .6rem}
.product__vendor--logo a{display:inline-flex;border:0}
.product__vendor--logo a:hover{opacity:.75}
/* dossier header: the tile sits where the lettered mark did, left-aligned
   against the prose */
.dossier__brand .brandlogo--tile{justify-content:flex-start}

/* ---------- optimistic cart (assets/island-cart.js) ---------- */
/* rollback notes: rendered only while a failed/capped change has something
   to say (6s), so the normal path adds no box and the CLS gate sees nothing */
.cart-line__msg{margin:.35rem 0 0;font-size:.76rem;line-height:1.35;color:var(--color-accent)}
.cart-drawer__msg{margin:0;padding:.7rem 1.6rem;font-size:.82rem;line-height:1.35;color:var(--color-accent);
  background:color-mix(in srgb,var(--color-accent) 7%,var(--color-bg));border-bottom:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}
.cart-page .cart-drawer__msg{margin-top:1rem;border:1px solid color-mix(in srgb,var(--color-accent) 35%,transparent);border-radius:var(--radius)}
/* cart page line = the drawer line (media / info + stepper / price / remove).
   The list drops the UA's ul padding and the line its drawer inline padding
   (the page-width gutters are the margins here); from 640px the media is a
   96px square. The stepper's middle is the updates[] number input on the
   page (the no-JS Update form) — drawn exactly like the drawer's span. */
.cart-page__items{list-style:none;margin:0;padding:0}
.cart-page .cart-line{padding-inline:0}
.cart-page .cart-line__remove{align-self:start;margin-top:.2rem}
.cart-line__qty input{width:36px;height:24px;border:0;background:none;padding:0;margin:0;text-align:center;font:inherit;font-size:max(var(--touch-min-font,0px),.82rem);color:inherit;border-radius:0;-moz-appearance:textfield;appearance:textfield}
.cart-line__qty input::-webkit-outer-spin-button,.cart-line__qty input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
@media (min-width:640px){.cart-page .cart-line{grid-template-columns:96px 1fr auto auto}.cart-page .cart-line__media{width:96px;height:96px}}
/* cart page frame (fix/cart-page-layout-shipbar, 2026-09-15): the section had
   no top padding, so "Cart" sat flush under the 216px sticky header — it now
   wears the same .section-head as static pages. Its width is capped so the
   line's price and remove control stop drifting 1400px from the media. The
   shipping-promise bar and the Large-order note are the drawer's own markup,
   framed as a card above the lines. */
.cart-page{--page-width:1080px;padding-bottom:clamp(3rem,6vw,5rem)}
.cart-page__head{padding-bottom:1.25rem}
.cart-page .ship-bar{border-radius:var(--radius);margin:0 0 1rem}
.cart-page .ship-bulky{padding-inline:0;margin:0 0 1.25rem}
.cart-page .cart-page__items{border-top:1px solid color-mix(in srgb,var(--color-ink) 8%,transparent)}

/* ---------- PDP rating row + hero price (Luke 2026-09-10) ----------
   Nothing here is above the fold on mobile in the CLS sense: both blocks are
   server-rendered inside the buy box, so they paint with the first frame.
   They live in theme.css, not critical.css — that file is at ~14.5KB against
   the 15KB inline_asset_content cap that SILENTLY drops the whole file. */

/* the rating row renders only when there IS a rating (rating-data.liquid),
   so there is no empty-state height to reserve and no shift either way. */
.prating{display:flex;flex-wrap:wrap;align-items:center;gap:.3rem 1rem;margin:.6rem 0 0}
.prating__link,.prating__write{display:inline-flex;align-items:center;gap:.45rem;
  color:var(--ink-2);font-size:.82rem;line-height:1.4;text-decoration:none;
  border-bottom:1px solid transparent;transition:color .15s,border-color .15s}
.prating__link:hover,.prating__write:hover,
.prating__link:focus-visible,.prating__write:focus-visible{color:var(--ink);border-bottom-color:currentColor}
.prating__value{font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
.rating__stars--lg svg{width:16px;height:16px}
.prating__write{color:var(--ink-3)}

/* hero price: the loudest line in the buy box. baseline alignment keeps the
   strike and the Save chip sitting on the price's baseline, and because both
   are SMALLER than the price the row's height is set by the price alone —
   so a variant switch between a discounted and an undiscounted variant
   changes no height. tabular-nums stops the digits jittering when ui.js
   repaints the text on a variant change. */
.product__price--hero{display:flex;flex-wrap:wrap;align-items:baseline;gap:.1rem .7rem;margin:.75rem 0 0}
.product__price--hero [data-current]{font-size:clamp(1.8rem,1.05rem + 2.4vw,2.5rem);font-weight:600;
  line-height:1.05;letter-spacing:-.015em;font-variant-numeric:tabular-nums;color:var(--ink)}
/* the discounted colour stays var(--color-accent) — the same brass the PLP
   card sale price uses (.price-now) — set by .product__price.is-discounted
   above, which outranks this block's selector on purpose. */
.product__price--hero s{font-size:1.05rem;color:var(--ink-3);font-variant-numeric:tabular-nums;
  text-decoration-thickness:1px;text-decoration-color:color-mix(in srgb,var(--ink-3) 70%,transparent)}
.product__price--hero .price-save{margin-left:0;align-self:center}

/* reviews section: the jump target. scroll-margin clears the sticky header in
   every header mode (the root scroll-padding-top only exists for the sticky /
   reveal modes); the section takes focus programmatically from the rating row
   so a screen reader lands INSIDE it, and an outline on a full-width region
   is noise, so the visible cue is the scroll itself. */
.reviews{scroll-margin-top:calc(var(--header-h,76px) + 16px)}
.reviews:focus{outline:none}
.reviews__note{color:var(--ink-2);font-size:.9rem;max-width:60ch}
/* ---------- PDP full-screen image viewer (snippets/pdp-lightbox.liquid,
   assets/pdp-lightbox.js) ----------
   Luke 2026-09-10, from the Athlete demo: a control on the main image opens
   the photo full page. Ours is a magnifier, not the reference's [+].

   Lives in theme.css, NOT critical.css: critical.css is 14.5KB against
   Shopify's ~15KB inline_asset_content cliff, which silently swallows the
   WHOLE file when crossed (CI fails the build at 14800B). A section-loaded
   pdp-lightbox.css was considered and rejected — every other .pgal rule is in
   this file and splitting the gallery's styles across two stylesheets to save
   ~700 bytes gzipped on non-PDP pages is a bad trade for the next reader.

   ZERO LAYOUT SHIFT: .plb is `hidden` until opened and position:fixed when it
   is, so it never takes part in the document's layout. The CLS gate measures
   the PDP on every PR. */

/* the trigger, on the main image. Recipe is the .pgal__arrow one verbatim
   (translucent paper block, hairline, ink glyph) so the two controls on the
   same image read as one family. */
.pgal__expand{position:absolute;right:.75rem;bottom:.75rem;z-index:2;
  width:40px;height:40px;display:grid;place-items:center;border-radius:50%;
  border:1px solid var(--line);background:color-mix(in srgb,var(--paper) 90%,transparent);
  color:var(--ink);cursor:zoom-in;padding:0;
  opacity:1;transition:opacity .18s var(--ease),background .18s var(--ease),color .18s var(--ease)}
.pgal__expand svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.pgal__expand:hover{background:var(--accent);border-color:var(--accent);color:#fff}
/* hover-to-reveal ONLY where there is a real hover (a phone has none and would
   be left with an invisible control). Keyboard focus always brings it back —
   the trigger must not be hover-only. */
@media (hover:hover) and (pointer:fine){
  .pgal__expand{opacity:0}
  .pgal__slide:hover .pgal__expand,
  .pgal__expand:focus-visible{opacity:1}
}

/* --- the scroll lock ---
   .drawer-open puts overflow:hidden on <body>, which is the theme's existing
   convention and is still applied — but on the PDP <html> is the scrolling
   element, and body overflow does nothing to it. Measured on the ci preview
   2026-09-10: with the viewer open and body's computed overflow reading
   `hidden`, a wheel still took the page from scrollY 31 to 1217 at 1440.
   Locking the root is what holds. Removing the scrollbar would widen the
   content by its width and score a layout shift, so pdp-lightbox.js measures
   it into --plb-sbw and it is handed straight back as padding on the same
   element — net zero, and 0 on every device with overlay scrollbars. */
html.plb-locked{overflow:hidden;padding-right:var(--plb-sbw,0px)}

/* --- the overlay --- */
.plb{position:fixed;inset:0;z-index:120;opacity:0;transition:opacity .22s var(--ease-out)}
.plb.is-open{opacity:1}
.plb[hidden]{display:none}
.plb__dialog{position:absolute;inset:0;background:var(--paper);display:block;outline:none}
.plb__dialog:focus-visible{outline:2px solid var(--accent);outline-offset:-4px}

/* --- the picture, and the frame it scrolls and pans inside ---
   Luke, 2026-09-14, on the live theme, desktop, a very wide short window:
   "when in this lightbox view, if the width of the screen makes the full
   height of the image not visible we need to allow the user to scroll up/down
   around the image in the way you'd expect, keeping [X] and [magnifying glass]
   controls fixed at the top corners of the screen as they are while allowing
   to scroll or mouse-drag around the image appropriately as you'd expect."

   WHAT WAS ACTUALLY WRONG (and it was not only his aspect).
   The frame was `display:grid; place-items:center` with `overflow:hidden`, and
   the <img> was `width:100%;height:100%;object-fit:contain`, clamped by
   pdp-lightbox.js to `max-width:min(100%,1.8x)` / `max-height:min(100%,1.8x)`
   — the "clamp the image box to the frame as well as to 1.8x" commit
   (8f19740). That 100% term never bit. The frame's single implicit grid row is
   `auto`, so it sized itself to the image's own aspect-driven height, and the
   percentage then resolved against THAT row rather than against the frame's
   445px. The clamp was circular: the box was its own ceiling.

   Measured on the live theme 2026-09-14, Studio Bagno Synthesis semi recessed
   basin (natural 1356x1172), reading the frame's own scrollHeight:

     frame 2000x445  ->  img box 2000 x 1728  (1283px = 74% of the picture clipped)
     frame 1440x900  ->  img box 1440 x 1244  ( 344px = 28% of the picture clipped)

   `overflow:hidden` then threw the rest away with nothing to scroll. So any
   photo taller than the viewport's aspect was cropped on an ordinary 1440x900
   desktop too; a 4.5:1 window just made three quarters of it disappear.

   WHAT IT IS NOW.
   The box is sized in PIXELS by pdp-lightbox.js (--plb-w / --plb-h = the
   natural pixels times one scale factor). No percentages, no object-fit,
   nothing that can resolve against a track which sized itself to the content
   — that indirection is the whole bug. FIT is the default and is a true
   contain: scale = min(frameW/naturalW, frameH/naturalH, 1.8), so the picture
   still GROWS to fill the viewport (the 540px Caroma renders still reach 900px
   at 1440x900, which is what "a full page photo" asked for) but can never
   exceed the frame. The 1.8x upscale cap of D2 is unchanged.

   The frame is a real scroll container in BOTH axes for the state that does
   overflow — zoomed. `overscroll-behavior:contain` so a wheel that runs out
   of picture chains nowhere (the root is locked too; belt and braces).

   .plb__canvas exists for one reason: an item centred inside a grid area
   SMALLER than itself overflows equally in both directions, and the top/left
   half of that overflow sits at negative scroll offsets no scrollbar can
   reach. Sizing the canvas to max(frame, picture) means the picture is
   centred when it is small and flush at 0,0 when it is large, so every pixel
   of it is reachable. */
.plb__frame{position:absolute;inset:0;overflow:auto;overscroll-behavior:contain;
  cursor:zoom-in;touch-action:pan-x pan-y;scroll-behavior:auto;
  /* no scroll gutter. The controls are flush to the viewport edges by
     instruction ("< > both flush with the sides", Luke 2026-09-10) and a
     classic 15px bar would run underneath the next-arrow and the counter, and
     would shift the picture the moment zoom made it appear. macOS, iOS and
     Android have no persistent bar to lose; on Windows the grab cursor, the
     wheel and the drag are the affordance. */
  scrollbar-width:none}
.plb__frame::-webkit-scrollbar{width:0;height:0}
.plb__canvas{width:max(100%,var(--plb-w,0px));height:max(100%,var(--plb-h,0px));
  display:grid;place-items:center}
/* explicit pixels, set per image by pdp-lightbox.js. `auto` covers the one
   frame before the first load resolves; the canvas is >= the frame either way. */
.plb__img{display:block;width:var(--plb-w,auto);height:var(--plb-h,auto);
  user-select:none;-webkit-user-drag:none}
/* pannable = the picture is genuinely bigger than the frame. Grab rather than
   zoom-out, because the drag is the thing that needs advertising and the
   magnifier (pressed, top left) is the obvious way back. */
.plb.is-pannable .plb__frame{cursor:grab}
.plb.is-pannable.is-panning .plb__frame{cursor:grabbing}
.plb.is-zoomed:not(.is-pannable) .plb__frame{cursor:zoom-out}

/* --- controls: the theme's EXISTING components, positioned. Nothing here
   redraws a close button or an arrow.
   Luke on the first build (2026-09-10, on the preview): "current lightbox
   isn't following the same styling like [X] close button with morph (see quick
   view on product for an example), also [<] [>] buttons are also not styled
   like ours - see product grid card left/right buttons for cycling thorugh
   images and replicate."

   Close + zoom: `.btn-close` — the chunky square block this file already
   defines as one component in two parts (block + glyph), so the zoom taking a
   magnifier instead of `.xmorph` is the documented way to use it, not a hack.
   The close's glyph is the shared `.xmorph`: two bars at +-45deg that rotate
   to 0 on hover, the same morph as the cart drawer, the lists flyout and the
   quick-view modal. Both inherit the accent hover fill for free.
   Prev/next: `.card__arrow` — the product card's image-cycling arrows,
   including the accent wipe that scales in from the inner edge.
   These two surfaces are now COUPLED ON PURPOSE: restyling the quick-view
   close or the card arrow restyles this viewer, which is the point. */
/* --- flush to the corners, and legible over ANY image ---
   Luke, 2026-09-10, second pass on the preview: "the [X] on and other controls
   on the lightbox are not visible when the image takes up the full screen,
   they need to be clearly visible, and also pulled entirely to the edge
   (search icon button top left corner, [X] close button top right corner,
   < > both flush with the sides."

   PLACEMENT: hard to the edges, offset only by the device's safe-area insets
   so a notch or a home indicator cannot clip them. Blocks stay >= 44px
   (64px square here, 48x76 / 44x64 for the arrows).

   CONTRAST: .btn-close rests TRANSPARENT — correct in the cart drawer and the
   quick view, where it sits on a known paper surface, and wrong here, where it
   sits on an arbitrary photograph and a bare ink glyph disappears into a dark
   one. So the two corner controls take the SAME opaque ground the borrowed
   .card__arrow already carries (color-mix(ink 14%, paper)), which makes all
   four controls one family and gives every glyph its own surface regardless of
   what is behind it. A hairline is added to all four: the chip alone reads
   against a dark image, the hairline is what defines it against a white
   product on white. The overlay itself is NOT tinted — that would darken the
   product, which is the thing the shopper opened this to look at.
   Hover/pressed states still come from the shared components untouched. */
.plb__zoom,.plb__close{position:absolute;z-index:3;
  top:env(safe-area-inset-top,0px);
  background:color-mix(in srgb,var(--ink) 14%,var(--paper));
  border:1px solid color-mix(in srgb,var(--ink) 22%,transparent)}
.plb__zoom{left:env(safe-area-inset-left,0px)}
.plb__close{right:env(safe-area-inset-right,0px)}
.plb__zoom svg{width:32px;height:32px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.plb .plb__nav{border:1px solid color-mix(in srgb,var(--ink) 22%,transparent)}
/* the focus ring has the same problem as the glyph: an accent ring on a dark
   photograph is not much of a ring. The paper halo outside it guarantees the
   accent is always seen against something light. */
.plb__zoom:focus-visible,.plb__close:focus-visible,.plb .plb__nav:focus-visible{
  outline:3px solid var(--accent);outline-offset:2px;
  box-shadow:0 0 0 6px color-mix(in srgb,var(--paper) 88%,transparent)}
/* pressed = already zoomed: drop the magnifier's vertical stroke so the plus
   reads as a minus, and hold the accent fill so the state is visible without
   hovering. No icon swapping in JS. */
.plb__zoom[aria-pressed="true"] .plb__zoom-v{display:none}
.plb__zoom[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--paper)}

/* prev/next: the card arrow's look, a full-screen context's size and position.
   Two classes deep (.plb .plb__nav) on purpose — .card__arrow--prev sets its
   own left/transform and hides itself until an ancestor .card is hovered, and
   there is no .card here. Equal specificity would leave this depending on
   source order within one 177KB file. */
.plb .plb__nav{position:absolute;top:50%;width:48px;height:76px;
  opacity:1;pointer-events:auto;z-index:3;transform:translateY(-50%);
  /* display is re-asserted, not inherited: borrowing .card__arrow also borrows
     `@media (max-width:900px){.card__arrow{display:none}}` — the GRID hides its
     arrows below 900 because a tile is swiped there. A full-screen viewer is
     not a tile: it swipes AND keeps its arrows, which the brief requires and
     which a keyboard user needs. Measured before this line existed: at 390 the
     arrows were display:none, so they left the focus trap (the Tab ring held
     only the two .btn-close controls) and a click on [data-plb-next] timed out.
     .card__arrow's own display:grid + place-items:center come back with it. */
  display:grid;place-items:center}
.plb .plb__nav svg{width:22px;height:22px;stroke-width:1.5}
.plb .plb__nav--prev{left:env(safe-area-inset-left,0px);right:auto}
.plb .plb__nav--next{right:env(safe-area-inset-right,0px);left:auto}
.plb .plb__nav[hidden]{display:none}

/* n / N — the gallery's own counter chrome (.pgal__count) verbatim: same size,
   same tracking, same translucent paper pill, same --ink-2. Only the placement
   differs, because the reference puts it centred at the foot of the picture
   rather than in a corner of a tile. */
.plb__count{position:absolute;left:50%;transform:translateX(-50%);
  bottom:calc(env(safe-area-inset-bottom,0px) + clamp(.75rem,3vw,1.5rem));z-index:3;margin:0;
  font-size:.72rem;letter-spacing:.08em;color:var(--ink-2);
  /* opaque, unlike .pgal__count's 88% paper: that one sits on a white gallery
     slide, this one sits on the photograph itself. Same size, tracking, colour
     and pill shape — only the transparency changes, for the same reason the
     corner controls gained a ground. */
  background:var(--paper);border:1px solid color-mix(in srgb,var(--ink) 22%,transparent);
  padding:.25rem .6rem;border-radius:999px}
.plb__count[hidden]{display:none}

@media (max-width:600px){
  /* thumb-reachable on a phone: the arrows drop to the bottom corners rather
     than sitting mid-edge where a one-handed grip cannot get to them, and the
     counter moves up to clear them. Swipe still works either way. */
  /* the arrows stay FLUSH WITH THE SIDES here too — "< > both flush with the
     sides", Luke 2026-09-10. An earlier build moved them to the bottom corners
     for thumb reach; that is a better reach and it is not what was asked, and
     mid-edge is also where the gallery's own .pgal__arrow sits. Slightly
     narrower so they take less of a 390px picture. */
  .plb .plb__nav{width:44px;height:64px}
  .plb .plb__nav svg{width:20px;height:20px}
  .plb__count{bottom:calc(env(safe-area-inset-bottom,0px) + .75rem)}
}

@media (prefers-reduced-motion:reduce){
  /* no animated pan: the keyboard pan asks for smooth scrolling, and this is
     where it is refused. The size change itself is never animated either —
     animating width/height on a photograph is jank, not polish. */
  .plb,.pgal__expand{transition:none}
  .plb__frame{scroll-behavior:auto}
}

/* ---------- variant-scoped gallery media (sections/main-product.liquid,
   assets/product.js) ----------
   A slide/thumb belonging to a finish other than the selected one is `hidden`.
   [hidden] already means display:none, but .pgal__slide sets `flex:0 0 100%`
   and .pgal__thumb `flex:0 0 64px`, and a stray future rule that gave either a
   display would silently put a hidden finish back in the scroll-snap strip and
   in the thumb strip's min-content width — the exact measurement that clipped
   the buy box at 390px in 9fedc7e. Nail it down with !important so the filter
   cannot be undone by specificity from somewhere else in this file.
   No layout consequence: the stage is a snap strip of equal 1/1 slides and the
   thumb row a fixed-height flex line, so removing members changes neither the
   gallery's height nor the page's — the CLS gate measures this page. */
.pgal__slide[hidden],.pgal__thumb[hidden],.pgal__thumbs[hidden],
.pgal__arrow[hidden],.pgal__count[hidden]{display:none!important}
/* ---------- PDP sticky buy bar (snippets/sticky-buy-bar.liquid) ----------
   Luke 2026-09-10: a full-width bar across the bottom carrying the image,
   name, price, Add to cart and Add to project once the buy box has scrolled
   away. Reference was the Athlete theme's; the skin is ours — paper ground,
   ink hairline, proto.css buttons, SVG strokes.

   HEIGHT IS A CONSTANT, not a measurement. --sbb-h is declared per
   breakpoint and BOTH the bar and the body reserve read it, so the reserved
   space and the painted bar can never disagree (a JS-measured height would
   reserve last frame's number and shift the footer). The 48px thumbnail has
   explicit width/height and the row is a fixed min-height, so nothing inside
   can grow the bar either.

   STACKING (theme scale: bottom-bar 60, compare-bar 62, cart drawer 90/91,
   nav drawer 92/93, search 95/96, price-match modal 96/97, .psave 120):
   61 — above the mobile bottom nav so an overlap resolves in favour of the
   purchase action, and BELOW the cart drawer, the nav drawer, the search
   dialog and the Price Match modal, every one of which must bury it. It is
   deliberately NOT lifted the way .drawer-open .bottom-bar is: the bottom
   nav's Menu item has to stay live under the scrim, this bar does not.
   The compare tray (62) wins over it on the rare page where both show —
   that tray is explicitly invoked and transient, this bar is ambient.

   MOBILE: sits ON TOP OF the bottom nav (bottom:64px), the same offset
   .compare-bar already uses, rather than replacing it — the nav carries
   Menu/Search/Lists/Cart/Account and removing navigation to sell harder is
   the wrong trade. Cost is 64+56px of chrome at 390, which is why the row
   sheds its optional parts (see the breakpoints below). */
:root{--sbb-h:76px;--sbb-bottom:0px}
.sbb{position:fixed;left:0;right:0;bottom:0;z-index:61;
  background:color-mix(in srgb,var(--paper) 96%,transparent);backdrop-filter:blur(12px);
  border-top:1px solid var(--line);box-shadow:0 -8px 30px rgba(25,24,19,.10);
  transform:translateY(100%);visibility:hidden;
  transition:transform var(--dur-med) var(--ease-spring),visibility 0s linear var(--dur-med)}
.sbb.is-on{transform:none;visibility:visible;transition:transform var(--dur-med) var(--ease-spring),visibility 0s}
.sbb__inner{display:flex;align-items:center;gap:1rem;min-height:var(--sbb-h);
  padding-block:.6rem calc(.6rem + env(safe-area-inset-bottom))}
.sbb__media{flex:none;width:48px;height:48px;border:1px solid var(--line);border-radius:var(--radius);
  overflow:hidden;background:var(--surface)}
.sbb__media img{width:100%;height:100%;object-fit:cover;display:block}
/* the identity block is the ONLY squeezable item — min-width:0 lets the title
   ellipsis instead of forcing the row wider than the viewport */
.sbb__id{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:.1rem}
.sbb__title{font-family:var(--serif);font-size:.98rem;line-height:1.25;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sbb__meta{display:flex;align-items:baseline;gap:.5rem;flex-wrap:nowrap;min-width:0}
.sbb__price{font-size:1.02rem;font-weight:600;color:var(--ink);white-space:nowrap;font-variant-numeric:tabular-nums}
.sbb__price--muted{font-weight:500;color:var(--ink-2)}
.sbb__from{font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3)}
.sbb__was{font-size:.82rem;color:var(--ink-3);white-space:nowrap}
.sbb__qtychip{font-size:.76rem;font-weight:600;color:var(--ink-2);background:var(--surface);
  border:1px solid var(--line);border-radius:999px;padding:.05rem .45rem;white-space:nowrap;
  font-variant-numeric:tabular-nums;display:none}
.sbb__actions{flex:none;display:flex;align-items:stretch;gap:.5rem}
/* Button order in the bar, Luke 2026-09-12: "add to cart should be the farthest
   right in this widget, with add to [project] to the left of it, and the
   quantity picker to the left of that." Done with flex `order` rather than by
   moving the markup, because the project button is rendered once after the
   mode branches (call-for-price, configurator, standard) — reordering the
   Liquid would mean repeating it in each branch and keeping three copies in
   step. The rule generalises the instruction: the primary action is always
   rightmost, whatever it is for this product's mode. */
.sbb__qty,.sbb__ghost{order:1}
.sbb__project{order:2}
.sbb__cta{order:3}
.sbb__chat{order:4}
/* the bar's stepper: the buy box's .qty geometry at the bar's row height. The
   readout is an <output>, not an input — the form's `quantity` field stays the
   only count on the page (assets/sticky-buy-bar.js forwards -/+ to the real
   stepper so product.js keeps owning the cap). */
.sbb__qty{display:inline-flex;align-items:stretch;border:1px solid var(--line);
  border-radius:var(--radius);height:44px;flex:none}
.sbb__qty .qty__btn{width:38px}
.sbb__qty-out{width:38px;display:grid;place-items:center;border-inline:1px solid var(--line);
  font:inherit;font-size:.9rem;font-variant-numeric:tabular-nums;color:var(--ink)}
.sbb__cta{height:44px;margin:0;padding:0 1.4rem;flex:none;width:auto;justify-content:center;
  min-width:170px;white-space:nowrap}
.sbb__cta:disabled{opacity:.45;cursor:default}
.sbb__ghost,.sbb__project{height:44px;margin:0;padding:0 1rem;font-size:.8rem;gap:.45rem;
  flex:none;width:auto;justify-content:center;white-space:nowrap}
.sbb__project svg{flex:none}
.sbb__project.is-active{background:var(--ink);color:var(--paper);border-color:var(--ink)}
/* ---------- Chat, at the end of the bar (2026-09-15) -------------------
   Luke, desktop PDP: "chat button widget currently sits on top of the sticky
   add to cart/project bar, we need a better solution, or have chat become part
   of the bar, perhaps a separator on the end and then chat: {rest of the
   current sticky bar} | {Chat}."

   Shopify's chat app embed renders <shopify-chat> (the storefront web
   component, NOT the old inbox-online-store-chat iframe): a position:fixed,
   inset:0, pointer-events:none host at z-index 2147483000, whose activator
   button is absolutely positioned bottom-right at
   --shopify-chat-activator-offset. Measured on the live theme at 1440 with the
   bar shown: activator 1335.25..1420 x 834..880, .sbb__cta 1222..1392 x
   840..884 — overlapping, which is the report.

   TWO independent defences, because one of them depends on a shadow part name
   Shopify owns and could rename without telling anyone:

   1. body.sbb-on raises --shopify-chat-activator-offset to clear the bar. The
      property is registered by chat.js with inherits:true (CSS.registerProperty,
      syntax <length>, initial 20px), so declaring it on body — specificity
      (0,1,1), beating the app embed's own :root (0,1,0) regardless of source
      order — reaches the component. Verified: 140px moved the activator from
      y 834 to y 714 in a 900px viewport. Its panel is positioned off the same
      variable, so opening chat over a shown bar also clears it.
   2. When our in-bar Chat item is actually live (body.sbb-chat, set by
      sticky-buy-bar.js only after it has a real <shopify-chat> wired), Inbox's
      activator is hidden outright, so there is exactly one chat affordance on
      the screen. shopify-chat exposes part="activator" and the runtime element
      re-exports it (exportparts="activator, dialog"), so ::part reaches it from
      the theme; verified display:none, zero-area box.

   Defence 1 alone is the fallback this change would have shipped had
   programmatic open not been reliable; keeping it underneath 2 costs one
   declaration and means a renamed part degrades to "launcher above the bar"
   rather than "launcher back on top of Add to cart". Neither rule exists off a
   PDP or above the fold: both are under body.sbb-on, and sbb-on is only set
   while the bar is shown. */
body.sbb-on{--shopify-chat-activator-offset:var(--sbb-chat-offset,calc(var(--sbb-bottom) + var(--sbb-h) + 20px))}
body.sbb-on.sbb-chat shopify-chat::part(activator){display:none}
/* ---------- the panel pops out of the bar (2026-09-16) -------------------
   Luke, PDP, bar shown, panel open over the footer: "can we have this appear
   as if it pops out of the sticky bar in cases where it's triggered from
   here?"

   What Inbox's panel is (chat.js, read 2026-09-16): <section part="dialog">,
   position:absolute inside the fixed host; in the desktop layout (host
   --shopify-chat-breakpoint, 768px by default) its bottom sits at
   --shopify-chat-activator-offset, its right at 20px, and it opens by scaling
   from `right bottom` (@starting-style scale:.2;opacity:0 → scale:1 on a
   spring). It already grows out of a bottom-right point, so "pops out of the
   bar" is two corrections, not a re-animation:

   1. The bottom edge lands ON the bar's top edge. The clearance above adds a
      20px resting gap for the activator; when the panel was opened from the
      bar there is no activator to rest, so the gap goes. Same variable, same
      registered property, so every height the panel derives from the offset
      (optimal height, max-height) stays consistent.
   2. The right edge lands on the Chat button's right edge. sticky-buy-bar.js
      measures --sbb-chat-right (viewport right minus the button's right) on
      open and on resize; 20px is the fallback for the frames before it runs.
      left:auto and margin:0 undo the merchant's "left" position if Inbox is
      set that way — the launcher the shopper pressed is at the right end of
      the bar, and the panel must come from where they pressed.

   The bottom corners are squared where the panel meets the bar, and the
   shadow is cast upward only, so the panel reads as one piece with the bar
   rather than a card hovering above it. The bar's own top border stays.

   body.sbb-chat-from is set by the bar's button only and removed when the
   panel closes (see the JS), and every rule is also under body.sbb-on: scroll
   back up so the bar hides and the panel returns to its stock position rather
   than being pinned to a bar that is no longer there. Below 768px Inbox
   switches to its mobile layout (edge-to-edge sheet, own bottom offset) and
   there is nothing to anchor; the media query keeps this out of it. */
body.sbb-on.sbb-chat-from{--shopify-chat-activator-offset:calc(var(--sbb-chat-offset,calc(var(--sbb-bottom) + var(--sbb-h) + 20px)) - 20px)}
@media (min-width:768px){
  body.sbb-on.sbb-chat-from shopify-chat::part(dialog){
    left:auto;right:var(--sbb-chat-right,20px);margin:0;
    transform-origin:right bottom;
    border-bottom-left-radius:0;border-bottom-right-radius:0;
    box-shadow:0 -14px 36px rgba(25,24,19,.14)}
}
/* the separator is a border, not a pseudo-element: .sbb__chat needs both of its
   own pseudos for the callout below, and a border is exactly a 1px rule at the
   control's height with nothing to position. */
.sbb__chat{position:relative;display:flex;align-items:center;align-self:center;
  height:44px;padding-left:.6rem;border-left:1px solid var(--line)}
/* .sbb .sbb__chat-btn, not .sbb__chat-btn: proto.css loads AFTER theme.css, so
   its .btn{overflow:hidden} ties on specificity and wins on order — and it
   would clip the callout to the button's own box. Same trap the .sbb__project
   display rule below documents. */
.sbb .sbb__chat-btn{overflow:visible}
.sbb__chat-btn{height:44px;margin:0;padding:0 1rem;font-size:.8rem;gap:.45rem;
  flex:none;width:auto;justify-content:center;white-space:nowrap}
.sbb__chat-btn svg{flex:none}
/* open state: the same ink fill .sbb__project uses for is-active. The VISIBLE
   label stays "Chat" in both states on purpose — swapping it for "Close" would
   resize the button and shove the whole actions row sideways every time the
   panel opens. The state the assistive tech reads is carried by aria-expanded
   and the aria-label ("Chat" / "Close chat"), which cost no pixels. */
.sbb__chat-btn[aria-expanded="true"]{background:var(--ink);color:var(--paper);border-color:var(--ink)}
/* The callout: the athlete2 bubble the text-option tiles use (.swtext above),
   with one difference forced by where it lives. The tiles reserve a band on
   their row so the bubble cannot clip; the bar cannot reserve a band without
   getting taller, so this one is absolutely positioned ABOVE the bar, out of
   flow, over the page. Nothing moves when it appears, so the CLS budget is
   untouched either way.

   It is anchored to the button's RIGHT edge (right:0, not left:50%), because
   this is the rightmost control on the row: a centred bubble wide enough to
   hold a sentence would run off the viewport at 1440. Growing leftwards from an
   edge that is already inside the page gutter, it cannot.

   Everything is inside @media (hover:hover) — bubble, pointer and focus — for
   the reason spelled out on .swtext: iOS WebKit withholds the first tap on an
   element whose hover phase reveals content (#237), and scripts/swatch-tap-gate
   .mjs exists because of it. Under touch this tooltip does not exist at all. */
@media (hover:hover){
  .sbb__chat-btn:hover::after,
  .sbb__chat-btn:focus-visible::after{content:attr(data-tip);
    position:absolute;right:0;bottom:calc(100% + 11px);
    width:max-content;max-width:min(272px,calc(100vw - 32px));
    padding:.34rem .6rem;border-radius:3px;background:var(--ink);color:var(--paper);
    font-size:.72rem;font-weight:600;line-height:1.45;letter-spacing:.01em;
    white-space:normal;text-align:left;pointer-events:none;z-index:2}
  /* the pointer. border-width has NO bottom edge, for the reason .swtext's
     pointer documents: a 4-sided transparent border would make the box 10px
     tall and leave a gap between the triangle and the bubble. */
  .sbb__chat-btn:hover::before,
  .sbb__chat-btn:focus-visible::before{content:"";
    position:absolute;left:50%;bottom:calc(100% + 6px);transform:translateX(-50%);
    width:0;height:0;border-style:solid;border-width:5px 5px 0;
    border-color:var(--ink) transparent transparent;
    pointer-events:none;z-index:2}
  /* once the panel is open the callout has said its piece, and the pointer is
     still over the button that opened it: leaving it up would print a sentence
     explaining chat across the bottom edge of the open chat panel. */
  .sbb__chat-btn[aria-expanded="true"]::after,
  .sbb__chat-btn[aria-expanded="true"]::before{content:none}
}
/* body reserve: the bar is fixed, so the last section and the footer would sit
   under it. Padding on <body> adds space BELOW everything and moves nothing
   above it, so this costs zero CLS; it is applied only while the bar is on,
   and the bar only comes on well below the fold. */
body.sbb-on{padding-bottom:var(--sbb-h)}
/* The PWA install prompt used to paint OVER this bar (z 70 vs 61) — CI run
   34441313727 photographed it covering the product title and half the Add to
   cart button at 390, and Luke saw the same thing on the preview with the bar
   nowhere to be seen. The prompt was removed outright on 2026-09-12
   (fix/pwa-install-stacking), so nothing competes for this edge any more
   except the mobile nav and the compare tray, both handled below.

   The configurator's .toast is bottom-centred too but keeps its own transform
   (it animates on it) and is left alone: transient, z 90, and covering the bar
   for a moment is not worth breaking its slide-in. */
@media (prefers-reduced-motion:reduce){
  /* critical.css already collapses every transition to .001ms; this also drops
     the slide itself so the bar appears in place rather than shooting up */
  .sbb{transform:none;opacity:0}
  .sbb.is-on{opacity:1}
}
@media (max-width:900px){
  /* --sbb-bottom is how far the bar's own bottom edge sits above the viewport's;
     the chat clearance below is the only thing that reads it, and it must track
     .sbb{bottom} exactly. */
  :root{--sbb-h:64px;--sbb-bottom:64px}
  /* clear the mobile bottom nav (.bottom-bar, 64px) — the offset .compare-bar uses */
  .sbb{bottom:64px}
  .sbb__inner{padding-block:.5rem}
  body.sbb-on{padding-bottom:calc(64px + var(--sbb-h))}
}
/* WHAT DROPS FIRST, AND WHY. Below 720px the Add to project button goes: it is
   a save-for-later, it is still one scroll away in the buy box, and the bar
   exists to close the sale. Below 560px the quantity stepper goes too — at
   390 the row is thumbnail + title + price + a real 44px tap target and there
   is no honest way to fit six controls; the ×N chip appears in its place so
   the shopper can still SEE that the bar will add 3, and the Add still submits
   the form's quantity either way. The title is the only element allowed to
   shrink, and it ellipsises rather than wrapping the bar to two rows. */
@media (max-width:720px){
  /* .sbb .sbb__project, not .sbb__project: proto.css loads AFTER theme.css and
     its .btn{display:inline-flex} ties on specificity and wins on order. CI
     run 34440106068 measured the button still painted at 390 with the bare
     class. Same reason for .sbb__qty below. */
  .sbb .sbb__project{display:none}
}
@media (max-width:560px){
  /* Chat survives every width the bar does — it is the point of this row's
     right-hand end — but below 560 it goes icon-only beside the shortened Add
     to cart. The accessible name is on the button's aria-label, so dropping the
     span costs nothing to a screen reader. */
  .sbb .sbb__chat-label{display:none}
  .sbb__chat-btn{padding:0 .8rem}
  .sbb__chat{padding-left:.5rem}
  .sbb .sbb__qty{display:none}
  .sbb .sbb__qtychip{display:inline-block}
  .sbb__cta{min-width:0;padding:0 1.1rem}
  .sbb__inner{gap:.7rem;padding-inline:16px}
  .sbb__title{font-size:.9rem}
  .sbb__price{font-size:.95rem}
}
/* ---------- product badges ----------------------------------------------
 * Rules live in assets/product-badges.js (the resolver both client grids call)
 * and snippets/product-badges.liquid (its SSR mirror). This is presentation
 * only.
 *
 * These rules are in theme.css, not critical.css, ON PURPOSE. The PDP badge row
 * is above the fold on a phone, which would normally argue for critical.css —
 * but critical.css is 14,486 bytes against a CI guard at 14,800 and Shopify's
 * inline_asset_content silently REPLACES the file with an HTML comment at about
 * 15,360, taking the whole page's base styling with it. The base .card__badge
 * rule has always lived here and badges have never shown an unstyled flash, so
 * the row follows the chip it is made of.
 *
 * Meaning is carried by the LABEL, never by the colour alone: every badge is
 * real text inside a <span>, so a screen reader reads "Clearance" and a
 * colour-blind shopper reads it too. The colour tokens are emphasis, not
 * information. */

/* curated colour tokens — staff pick one per product_badge metaobject entry.
 * --sale is the palette's existing sale red (theme.liquid :root), not a new
 * colour invented for badges. */
.card__badge--ink{background:var(--ink);color:var(--paper)}
.card__badge--accent{background:var(--accent);color:#fff}
.card__badge--sale{background:var(--sale);color:#fff}
.card__badge--muted{background:var(--ink-3);color:#fff}
.card__badge--outline{background:var(--paper);color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink)}

/* PDP badge row — sits directly above the price, tile idiom, buy-box scale.
 * min-height holds the row's height when assets/ui.js hides the saving chip on
 * a variant change, so a variant swap never moves the price under the
 * shopper's cursor. It is a min-height on a row that EXISTS: a product with no
 * badges renders no row at all (the snippet emits nothing), so nothing is
 * reserved on the ~97% of products with no compare_at_price. Both halves of
 * that are measured by the CLS gate. */
.product__badges{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin:0 0 .5rem;min-height:1.45rem}
.product__badges .card__badge{display:inline-block}
.product__badges .card__badge[hidden]{display:none}

/* ---------- Touch font-size floor (Luke 2026-09-12, iPhone, live theme) ----------
   "When I place the cursor in the field it zooms in, this should not happen."
   His screenshot: the password page, after focusing the password field, zoomed
   so that "Opening soon" and the Enter button were cut off at the right and
   the bottom nav with them. iOS Safari zooms the page on focus of ANY input,
   select or textarea whose computed font-size is under 16px. Measured on the
   live theme at 390: the password input 11px on iOS WebKit (13.3px in
   Chromium) - no theme rule ever touched it, so it had the UA default; the
   header search .95rem = 15.2px; the buy-box qty .95rem; the cart qty .82rem =
   13.1px; the project drawer fields .9rem = 14.4px; the PDP postcode .9rem;
   price match .92rem; contact .95rem; account .92rem; the PLP price/sort
   .88rem. One condition, many instances.

   NOT the viewport meta: maximum-scale=1 / user-scalable=no would stop this
   by taking pinch-zoom away from everyone (WCAG 1.4.4), and iOS 10+ ignores
   both anyway.

   Mechanism: --touch-min-font is 0px on :root and 16px under
   (hover:none) and (pointer:coarse) (critical.css, so it is inlined first in
   both layouts). Every rule that sizes a text control below 16px wraps its
   size: font-size:max(var(--touch-min-font,0px), .9rem). Desktop computes
   max(0px,.9rem) = .9rem, exactly as before; a touch screen gets 16px. Sizes
   that are already >= 16px (.deliver__pop 1rem, .usearch 1.05rem, the search
   overlay's clamp) are not wrapped and do not move. A single global rule with
   !important was rejected: font-size:max(16px,1em) cannot see the cascaded
   value, so it would also drag the >= 16px controls down to 16px on touch.
   The rule below is the floor for a control no component styles (the
   password field): :where() keeps it at zero specificity so every component
   rule, including one at 1em, wins over it.
   CI: scripts/overflow-gate.mjs asserts every text-entry control computes to
   >= 16px under iPhone emulation (360/375/390), dialogs opened, on every page
   it measures plus /password - a future .9rem input fails the build. */
@media (hover:none) and (pointer:coarse){
  :where(input:not([type=checkbox],[type=radio],[type=range],[type=hidden],[type=submit],[type=button],[type=reset],[type=image],[type=file],[type=color]),select,textarea){font-size:max(16px,1em)}
}

/* ---------- add-on products in the buy box (snippets/product-addons.liquid)
   Ranya, 2026-09-15: the TH colour sink waste and the Pietra Bianca stone
   bath waste are "add on options" to the purchase. A quiet checklist above the
   buy row, in the option typography: checkbox, thumbnail, name, "+ $75". */
.product__addons{border:0;margin:0 0 1rem;padding:0;min-width:0}
.product__addons-title{padding:0;margin:0 0 .5rem;font-size:.74rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-2)}
.product__addon{display:flex;align-items:center;gap:.75rem;padding:.6rem .75rem;border:1px solid color-mix(in srgb,var(--color-ink) 14%,transparent);border-radius:var(--radius);cursor:pointer;background:var(--color-bg)}
.product__addon+.product__addon{margin-top:.5rem}
.product__addon:has(.product__addon-check:checked){border-color:var(--color-ink);background:color-mix(in srgb,var(--color-ink) 4%,var(--color-bg))}
.product__addon.is-unavailable{opacity:.55;cursor:not-allowed}
.product__addon-check{width:1.1rem;height:1.1rem;margin:0;accent-color:var(--color-ink);flex:none}
.product__addon-img{width:3.4rem;height:3.4rem;object-fit:contain;border-radius:calc(var(--radius) - 2px);background:color-mix(in srgb,var(--color-ink) 5%,var(--color-bg));flex:none}
.product__addon-body{display:flex;flex-direction:column;gap:.15rem;min-width:0;flex:1}
.product__addon-name{font-size:.92rem;line-height:1.3}
.product__addon-note{font-size:.8rem;color:var(--ink-2)}
.product__addon-price{font-size:.85rem;font-weight:600;font-variant-numeric:tabular-nums}
.product__addon-link{font-size:.78rem;color:var(--ink-2);text-decoration:underline;flex:none}
@media (max-width:560px){.product__addon-link{display:none}}

/* ---------- cart drawer: compact footer (header and promise bar are the
   original arrangement — two header variants were tried on 2026-09-17 and
   Luke found the original "less confusing"). Luke, 2026-09-16: "the footer of
   the cart is far too chunky ... maintain the same amount of info". */
.cart-drawer__foot--compact{padding:.8rem 1.6rem .95rem}
.cart-sum__row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;padding:.18rem 0;font-size:.92rem}
.cart-sum__row>span{min-width:0}
.cart-sum__row b{font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.cart-sum__crumb{display:block;font-size:.74rem;line-height:1.35;color:var(--color-muted)}
.cart-sum__row--total{border-top:1px solid color-mix(in srgb,var(--color-ink) 10%,transparent);margin-top:.35rem;padding-top:.5rem;font-size:1rem}
.cart-sum__row--total b{font-size:1.25rem}
.cart-sum__row--total small{display:block;font-size:.72rem;font-weight:400;color:var(--color-muted)}
.cart-drawer__foot--compact .cart-drawer__note{margin:.25rem 0 0;font-size:.74rem}
.cart-drawer__foot--compact .ship-quote{margin:0;border:0;padding:0}
.cart-drawer__foot--compact .ship-quote__ask{font-size:.85rem}
.cart-drawer__foot--compact .cart-drawer__ctas{margin-top:.7rem}
.cart-drawer__foot--compact .cart-drawer__ctas .btn{padding-top:.7rem;padding-bottom:.7rem}

/* ---------- cart drawer rows, trimmed (Luke, 2026-09-17: "trim down
   top/bottom padding in each product row, and potentially thumbnail size to
   reduce the height of each row while maintaining all of the features").
   Same title, See details, stepper, price, tag and remove; ~25px less per row. */
.cart-drawer__items .cart-line{padding:.6rem 1.6rem;gap:.8rem;grid-template-columns:56px 1fr auto auto}
.cart-drawer__items .cart-line__media{width:56px;height:56px}
.cart-drawer__items .cart-line__title{font-size:.88rem;line-height:1.25;margin:0}
.cart-drawer__items .cart-line__details summary{font-size:.7rem;margin-top:.15rem}
.cart-drawer__items .cart-line__qty{margin-top:.3rem}
.cart-drawer__items .cart-line__qty button{width:22px;height:22px;font-size:.85rem}
.cart-drawer__items .cart-line__qty input{height:22px}
.cart-drawer__items .cart-line__price{font-size:.88rem}
.cart-drawer__items .cart-line__remove{margin-top:-.1rem}
.cart-drawer__items .cart-group__title{padding:.5rem 1.6rem}
.cart-drawer__items .cart-subgroup__title{padding:.35rem 1.6rem}
