/* ============================================================================
   SHOP
   ---------------------------------------------------------------------------
   Nothing here invents a colour, a curve or a duration. Every value is a token
   from tokens.css, so the shop inherits the surface system for free: put a
   section on data-surface="light" and the whole grid follows.

   The one rule the rest of the site sets and this file keeps: gold means
   energy or action. So gold is the price, the add-to-cart, the cart badge and
   the live-stock dot — and nothing else. Availability that is NOT good news
   (on order, sold out) is deliberately cobalt or quiet, never gold, because a
   gold "out of stock" chip would read as a feature.
   ========================================================================= */

/* The few colours that differ by theme and that no alias carries: the ink
   on a gold fill, the scrim behind the two dialogs, their drop shadow, the
   quiet ink of the compare tray, and the ground a product photo sits on.
   Photos are shot on white, so on paper the tile IS white and the photo
   bleeds into it; on the dark ground the white photo is framed by the sunk
   tile instead, which is the look the shop has always had. */
:root {
  --shop-on-accent:   #16110A;
  --shop-scrim:       rgba(3, 6, 15, 0.82);
  --shop-lift:        0 40px 100px -40px rgba(0, 0, 0, 0.9);
  --shop-tray-ink:    var(--cobalt-200);
  --shop-tray-well:   var(--cobalt-850);
}

:root[data-theme="light"],
:root:not([data-theme]) [data-surface="light"] {
  --shop-on-accent:   var(--paper-0);
  --shop-scrim:       rgba(10, 19, 48, 0.42);
  --shop-lift:        0 40px 90px -40px rgba(10, 19, 48, 0.38);
  --shop-tray-ink:    var(--slate-600);
  --shop-tray-well:   var(--paper-2);
  --shop-photo:       var(--paper-0);
}





/* ============================================================================
   SHOP HERO
   ========================================================================= */

.shophero { padding-block: clamp(6rem, 4rem + 6vw, 9rem) var(--s-8); position: relative; }
.shophero__inner { display: grid; gap: var(--s-5); max-width: 44rem; }
.shophero__title { font-size: var(--t-h1); letter-spacing: var(--track-tight); line-height: var(--lead-tight); }
.shophero__meta {
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5);
  padding-top: var(--s-4); border-top: 1px solid var(--rule);
  font-family: var(--font-data); font-size: var(--t-tiny);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
}
.shophero__meta b { color: var(--accent); font-weight: 500; }

/* The stock model, in two sentences, between the claim and the counts. Set
   at lead size but quieter than the lead above it — it is the small print
   that makes the big print believable, not a second headline. */
.shophero__model {
  max-width: 60ch;
  font-size: var(--t-small); color: var(--text-tertiary);
  line-height: var(--lead-text);
}


/* ============================================================================
   FEATURED RAIL

   A horizontal scroller on every width rather than a grid that collapses:
   eight picks are a shelf, and a shelf that reflows into a column is the
   grid again. Scroll snapping so a swipe lands on a card and not between two.
   ========================================================================= */

.featured { margin-bottom: var(--s-7); }
.featured__head {
  display: flex; align-items: baseline; gap: var(--s-4);
  margin-bottom: var(--s-4);
}
.featured__head .eyebrow { margin: 0; }
.featured__hint { margin-left: auto; font-size: var(--t-tiny); color: var(--text-quiet); }

.featured__row {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(13rem, 15rem);
  gap: var(--s-3);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* Room for the focus ring on the first and last card, which a scroller
     with no padding clips against its own edge. */
  padding: 2px;
  margin: -2px;
}
.featured__row > * { scroll-snap-align: start; }

.feat {
  display: grid; grid-template-rows: auto 1fr auto;
  background: var(--bg-panel);
  border: 1px solid var(--rule); border-radius: var(--r-md);
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .feat:hover { border-color: var(--rule-strong); }
}
.feat__media { position: relative; display: block; aspect-ratio: 1 / 1; }
.feat__media img { width: 100%; height: 100%; object-fit: cover; }
.feat__flags { position: absolute; inset: var(--s-2) var(--s-2) auto auto; display: grid; gap: 4px; justify-items: end; }

.feat__body { display: grid; gap: 2px; align-content: start; padding: var(--s-3) var(--s-3) 0; }
.feat__brand {
  margin: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-mono); text-transform: uppercase; color: var(--text-quiet);
}
.feat__name {
  font-size: var(--t-small); font-weight: 600; line-height: var(--lead-text);
  color: var(--text-primary); text-decoration: none;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
@media (hover: hover) and (pointer: fine) {
  .feat__name:hover { color: var(--accent); }
}
.feat__foot {
  display: flex; align-items: end; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-3);
}


/* ============================================================================
   FACETS — one chip, everywhere

   The same button draws a category above the bar and a brand inside the
   panel, because they do the same thing: press to add, press again to drop,
   and more than one may be on. Two blocks of cards used to stand here
   instead, each holding a single answer.

   Every chip carries its count, and a chip whose count is zero is dimmed and
   left where it is. Removing it would move every chip beside it under the
   pointer, which is how a filter row starts feeling like it is fighting back.
   ========================================================================= */

.facet__opt {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 2.25rem; padding: 0 var(--s-3);
  background: var(--bg-panel);
  border: 1px solid var(--rule); border-radius: var(--r-pill);
  color: var(--text-secondary);
  font: inherit; font-size: var(--t-small);
  cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .facet__opt:hover { border-color: var(--rule-accent); color: var(--accent); }
}
.facet__opt.is-on {
  border-color: var(--rule-accent); background: var(--bg-raised); color: var(--accent);
}
.facet__opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Nothing behind it. Dimmed, not disabled: it is still the way back to a
   wider view, and a pressed chip is never dimmed. */
.facet__opt.is-empty { opacity: 0.45; }

.facet__icon { width: 15px; height: 15px; flex: none; color: var(--accent); }
.facet__n {
  font-family: var(--font-data); font-size: var(--t-micro);
  font-variant-numeric: tabular-nums; color: var(--text-quiet);
}
.facet__opt.is-on .facet__n { color: inherit; }

@media (pointer: coarse) {
  .facet__opt { min-height: 2.75rem; }
}

/* ---- the category row, above the filter bar ---- */
.cats { display: grid; gap: var(--s-3); margin-bottom: var(--s-6); }
.cats__label {
  margin: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
}
.cats__row { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* ---- the groups inside the filter panel ---- */
.facets { display: grid; gap: var(--s-4); grid-column: 1 / -1; }
.facet { display: grid; gap: var(--s-2); }
.facet__head {
  margin: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
}
.facet__opts { display: flex; flex-wrap: wrap; gap: var(--s-2); }


/* ============================================================================
   FILTER BAR
   ========================================================================= */

.filterbar {
  position: sticky; top: var(--nav-h); z-index: var(--z-sticky);
  transition: top var(--dur-slow) var(--ease-out);
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-3) 0;
  margin-bottom: var(--s-5);
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.filterbar__search { position: relative; flex: 1 1 15rem; min-width: 11rem; }
.filterbar__search input {
  width: 100%; height: 2.5rem;
  padding: 0 var(--s-3) 0 2.25rem;
  background: var(--bg-panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  color: var(--text-primary);
  font: inherit; font-size: var(--t-small);
}
.filterbar__search input::placeholder { color: var(--text-quiet); }
.filterbar__search input:focus {
  outline: none; border-color: var(--rule-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
.filterbar__search svg {
  position: absolute; left: 0.7rem; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--text-quiet); pointer-events: none;
}

.filterbar select {
  height: 2.5rem; padding: 0 2rem 0 var(--s-3);
  background: var(--bg-panel) no-repeat right 0.6rem center;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238390A8' stroke-width='1.5'/%3E%3C/svg%3E");
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  color: var(--text-primary); font: inherit; font-size: var(--t-small);
  appearance: none; cursor: pointer;
}
/* Two of them now that brand has moved in. They share a line on a phone, which
   costs the sticky bar one 30px row — paid for many times over by the whole
   brand section that is no longer above the grid. The max-width is a guard
   against a long brand name stretching the bar on a desktop; nothing in the
   catalogue currently reaches it. */
.filterbar select { flex: 0 1 auto; min-width: 6.5rem; max-width: 11rem; text-overflow: ellipsis; }
.filterbar select:focus { outline: none; border-color: var(--rule-accent); }
.filterbar select option { background: var(--bg-raised); color: var(--text-primary); }

.filterbar__count {
  font-family: var(--font-data); font-size: var(--t-tiny);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
  margin-left: auto; white-space: nowrap;
}

.activefilters { display: flex; gap: var(--s-2); flex-wrap: wrap; margin-bottom: var(--s-5); }
.activefilters:empty { display: none; }

.filterchip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: 0.3rem 0.55rem 0.3rem 0.7rem;
  border: 1px solid var(--rule-accent); border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--accent);
  cursor: pointer; font-weight: 500;
}
@media (hover: hover) and (pointer: fine) {
  .filterchip:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
}
.filterchip svg { width: 9px; height: 9px; }


/* ============================================================================
   PRODUCT GRID
   ========================================================================= */

.products { display: grid; gap: var(--gutter); grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr)); }

/* Two up on a phone. The overrides that go with it are at the END of this
   section, after the card's own rules — they are the same specificity, so
   source order is what decides, and putting them here quietly lost. */
@media (max-width: 759px) {
  .products, .skeleton { grid-template-columns: 1fr 1fr; gap: var(--s-4); }
}

.product {
  display: flex; flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  overflow: hidden;
  position: relative;

  /* All 173 cards are built once and filtered by class, deliberately, so that
     filtering never costs a rebuild. The cost of that is 173 cards laid out on
     first paint. content-visibility lets the browser skip layout and paint for
     the ones off screen while they stay in the DOM, so the instant filter
     survives untouched. contain-intrinsic-size is the placeholder height —
     roughly one card — so the scrollbar does not lurch as they resolve. */
  content-visibility: auto;
  contain-intrinsic-size: auto 25rem;
  transition: border-color var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .product:hover {
    transform: translateY(-3px);
    border-color: var(--rule-strong);
    box-shadow: var(--shadow-soft);
  }
}
/* Sold out is said with the photograph and the price, never by fading the
   whole card: opacity multiplies every text colour by the same fraction and
   drops the brand line, the summary and the flag below the ratio each was
   measured at. The name and the price step down one token, the picture goes
   grey, and the copy stays the colour it was tested in. */
.product.is-unavailable .product__name { color: var(--text-tertiary); }
.product.is-unavailable .price__now    { color: var(--text-tertiary); }
.product.is-unavailable .product__media img { filter: grayscale(1); opacity: 0.5; }
@media (hover: hover) and (pointer: fine) {
  .product.is-unavailable:hover { transform: none; }
}

/* Also carries .media, the decode-gated entrance layer from motion.css. That
   brings the loading sweep and the fade-on-decode; these three lines take back
   the parts of it that belong to a full-bleed figure and not to a product
   thumbnail sitting in a card that is already rounded. */
.product__media {
  position: relative;
  text-decoration: none;
  aspect-ratio: 4 / 3;
  background: var(--shop-photo, var(--bg-sunk));
  border-bottom: 1px solid var(--rule);
  display: grid; place-items: center;
  overflow: hidden;
  cursor: pointer;
  width: 100%; padding: 0; border-inline: 0; border-top: 0;
}
.product__media.media { border-radius: 0; }

/* A catalogue photo is a product on a white sweep, not a scene: it must fit
   the box whole. `contain` overrides the `cover` .media gives a figure.
   The sweep is painted under the content box, so the few cut-out photos
   (a black cable, black "50A" type) get the same white sheet as the rest
   instead of vanishing into the dark tile — every card one framed print. */
.product__media.media > picture > img,
.product__media img {
  width: 100%; height: 100%; object-fit: contain; padding: var(--s-5);
  background: var(--paper-0); background-clip: content-box;
  transition: transform var(--dur-slow) var(--ease-out),
              opacity var(--dur-slow) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .product:hover .product__media img { transform: scale(1.04); }
}

/* .media parks its image at scale(1.06) until the frame is revealed. A card
   inside an already-revealed grid never gets that class, so the settle has to
   resolve on decode alone or every photo sits permanently 6% overscaled. */
.product__media.media.is-loaded > picture > img,
.product__media.media.is-loaded > img { transform: scale(1); }
@media (hover: hover) and (pointer: fine) {
  .product:hover .product__media.media.is-loaded > picture > img { transform: scale(1.04); }
}

/* No photo yet? Draw the initials on the schematic grid rather than shipping a
   grey "image missing" box. An empty shop still has to look built. */
.product__nomedia {
  display: grid; place-items: center; width: 100%; height: 100%;
  font-family: var(--font-data); font-size: var(--t-h3);
  letter-spacing: var(--track-mono); color: var(--rule-strong);
  background-image:
    linear-gradient(var(--rule-faint) 1px, transparent 1px),
    linear-gradient(90deg, var(--rule-faint) 1px, transparent 1px);
  background-size: 22px 22px;
}

.product__flags {
  position: absolute; top: var(--s-3); left: var(--s-3);
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
}

.flag {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 7px; border-radius: var(--r-pill);
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; font-weight: 500;
  border: 1px solid var(--rule-strong);
  background: var(--glass);
  backdrop-filter: blur(8px);
  color: var(--text-secondary);
}
.flag--stock { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, transparent); }
.flag--low   { color: var(--text-gold); border-color: var(--rule-accent); }
.flag--out   { color: var(--text-quiet); }
.flag--save  { color: var(--text-gold); border-color: var(--rule-accent); }
.flag--fit   { color: var(--text-tertiary); }

.pulse-dot {
  width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; flex: none;
  animation: dot-pulse 2.4s var(--ease-arc) infinite;
}
@keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

.product__body { padding: var(--s-4); display: grid; gap: var(--s-2); flex: 1; align-content: start; }

.product__brand {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-mono); text-transform: uppercase; color: var(--text-quiet);
}
.product__name {
  font-family: var(--font-display); font-size: var(--t-h5);
  letter-spacing: var(--track-snug); line-height: var(--lead-snug);
  background: none; border: 0; padding: 0; margin: 0;
  color: inherit; text-align: left; cursor: pointer;
  /* .product__name and .product__media are <a> now, so the button resets
     above are inert. Two lines at most: the longest name in the catalogue is
     74 characters, which is three lines at the card's narrowest, and one
     tall card tears the whole grid row beside it. */
  text-decoration: none;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
@media (hover: hover) and (pointer: fine) {
  .product__name:hover { color: var(--accent); }
}
.product__summary {
  font-size: var(--t-small); color: var(--text-secondary);
  line-height: var(--lead-text);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}

/* When it arrives. Quiet by default — it is the same answer on almost every
   card, so it must not compete with the name — and it earns colour only in
   the one case that is genuinely different. */
.product__ship {
  font-family: var(--font-data);
  font-size: var(--t-micro);
  letter-spacing: var(--track-mono);
  color: var(--text-quiet);
}
.product__ship--now { color: var(--good); }

.product__foot {
  margin-top: auto; padding: var(--s-4);
  border-top: 1px solid var(--rule);
  /* Wraps since the foot gained a quantity stepper: on a narrow card the
     price keeps the first row and the buying controls drop to their own. */
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-3);
}

.price { display: grid; gap: 1px; }
.price__now {
  font-family: var(--font-data); font-size: var(--t-h5); font-weight: 500;
  color: var(--accent); letter-spacing: var(--track-snug); font-variant-numeric: tabular-nums;
}
.price__was {
  font-family: var(--font-data); font-size: var(--t-tiny);
  color: var(--text-quiet); text-decoration: line-through;
}
.price__vat { font-family: var(--font-data); font-size: var(--t-micro); color: var(--text-quiet); letter-spacing: var(--track-wide); }

.addbtn {
  margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  height: 2.5rem; padding: 0 var(--s-4);
  border-radius: var(--r-sm); border: 0;
  background: var(--grad-gold); color: #16110A;
  font: inherit; font-size: var(--t-tiny); font-weight: 600;
  letter-spacing: var(--track-snug);
  cursor: pointer; white-space: nowrap;
  transition: filter var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .addbtn:hover { filter: brightness(1.08); }
}
.addbtn:active { transform: scale(0.97); }
.addbtn svg { width: 14px; height: 14px; }
.addbtn:disabled {
  background: none; border: 1px solid var(--rule);
  color: var(--text-quiet); cursor: not-allowed; font-weight: 500;
}
.addbtn.is-added { background: var(--good); color: var(--on-good); }

/* ---- buying from the card ------------------------------------------------
   A stepper beside Add, because these are bought in threes, and a line under
   the foot for the one thing the card has to be able to say back: that the
   shelf has fewer than that. It stays until it is answered — the toast it
   replaces took itself away after 3.6 seconds. */

.cardbuy { margin-left: auto; display: inline-flex; align-items: center; gap: var(--s-2); }
.cardbuy .addbtn { margin-left: 0; }

.qty--sm button { width: 2rem; height: 2.25rem; }
.qty--sm input { width: 2.25rem; height: 2.25rem; font-size: var(--t-tiny); }

.cardnote {
  padding: 0 var(--s-4) var(--s-4);
  font-size: var(--t-micro); color: var(--text-gold); line-height: var(--lead-text);
}

/* One row for the stepper and the button once the card is too narrow to
   carry them beside the price. */
@media (max-width: 479px) {
  .cardbuy { width: 100%; }
  .cardbuy .addbtn { flex: 1; }
}


/* ---- the card, two up on a phone ----------------------------------------
   17rem is 272px and the shell leaves about 350px at 390px wide, so the grid
   resolved to ONE column and a card came out roughly 1100px tall. Browsing
   241 products cost about 241 swipes and the page measured 135 phone screens
   end to end.

   Two up puts six products on a screen instead of one. What it costs is the
   summary and, on the narrowest phones, the quantity stepper — three lines
   of supplier prose on a 167px card is a paragraph rather than a summary,
   and neither is what somebody scanning a grid is reading. Both are one tap
   away in the quick-look panel.

   This block sits after the card's own rules on purpose. Every selector here
   is one class, exactly like the rule it overrides, so the cascade is
   decided by which comes last in the file and nothing else.
   ------------------------------------------------------------------------ */
@media (max-width: 759px) {
  .product__summary { display: none; }

  .product__body { padding: var(--s-3); gap: var(--s-1); }
  .product__foot { padding: var(--s-3); gap: var(--s-2); }
  .product__name { font-size: var(--t-body); -webkit-line-clamp: 3; }
  .price__now { font-size: var(--t-body); }

  .flag { font-size: var(--t-micro); padding: 2px var(--s-2); }
}

/* Under about 420px of card the stepper and Add cannot share a row, and
   stacking them makes the card taller than its own picture. The stepper
   goes; Add stays and adds one. */
@media (max-width: 559px) {
  .product__foot { flex-direction: column; align-items: stretch; gap: var(--s-2); }
  .cardbuy .qty--sm { display: none; }
  .cardbuy { width: 100%; margin-left: 0; }
  .cardbuy .addbtn { flex: 1; margin-left: 0; }
}


/* ============================================================================
   PRODUCT DETAIL — a panel, not a page. The catalogue is small enough that a
   route per product would be ceremony; a deep-linkable dialog is not.
   ========================================================================= */

.pdp {
  position: fixed; inset: 0; z-index: var(--z-overlay);
  display: none; align-items: flex-start; justify-content: center;
  padding: var(--s-6) var(--s-4);
  background: var(--shop-scrim);
  backdrop-filter: blur(8px);
  overflow-y: auto;
}
.pdp.is-open { display: flex; }

.pdp__card {
  width: min(100%, 60rem);
  background: var(--bg-raised);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--shop-lift);
  animation: pdp-in var(--dur-slow) var(--ease-out);
}
@keyframes pdp-in { from { opacity: 0; transform: translateY(var(--travel-y)); } to { opacity: 1; transform: none; } }

.pdp__close {
  position: absolute; top: var(--s-4); right: var(--s-4); z-index: 3;
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  border: 1px solid var(--rule-strong);
  background: var(--glass); backdrop-filter: blur(10px);
  color: var(--text-primary); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .pdp__close:hover { border-color: var(--rule-accent); color: var(--accent); }
}

.pdp__grid { display: grid; grid-template-columns: 1fr; }
@media (min-width: 760px) { .pdp__grid { grid-template-columns: 1fr 1.15fr; } }

.pdp__media {
  position: relative;
  background: var(--shop-photo, var(--bg-sunk));
  min-height: 18rem;
  display: grid; place-items: center;
  border-bottom: 1px solid var(--rule);
}
@media (min-width: 760px) { .pdp__media { border-bottom: 0; border-right: 1px solid var(--rule); } }

/* Like the card thumbnail, this frame carries .media for the decode gate and
   then takes back the two things a figure wants and a gallery does not: a
   fixed aspect box (the panel sizes itself to the layout) and a radius (the
   panel is already rounded and this sits flush against its edge). */
.pdp__media.media { aspect-ratio: auto; border-radius: 0; }

.pdp__media img,
.pdp__media.media > picture > img {
  width: 100%; height: 100%; max-height: 30rem; object-fit: contain; padding: var(--s-7);
  background: var(--paper-0); background-clip: content-box;   /* the sweep — see .product__media */
}
.pdp__media.media.is-loaded > picture > img { transform: scale(1); }

.pdp__thumb.media { border-radius: var(--r-sm); aspect-ratio: 1; }
.pdp__thumb.media::after { display: none; }

.pdp__side { padding: var(--s-6); display: grid; gap: var(--s-4); align-content: start; }
.pdp__title { font-size: var(--t-h3); letter-spacing: var(--track-tight); line-height: var(--lead-tight); }
.pdp__desc { font-size: var(--t-small); color: var(--text-secondary); line-height: var(--lead-loose); white-space: pre-line; }

/* ---- the lead, and the prose folded under the panel ----------------------
   The description used to sit here, under the title, where 900 characters of
   supplier copy pushed the price and Add to cart off the bottom of the card.
   It lives at the foot of the panel now (.pdp__fold, below), and the lead
   that stays behind is clamped to four lines.

   The clamp is gated on .js — the class the boot script sets and drops again
   if motion never arrives — so a page without script shows every word rather
   than a paragraph with no way to finish it. shop.js and product.js then
   measure whether the clamp cut anything and hang .is-ledeclipped on the
   card, which is what reveals the full lead inside the fold. A lead that
   fits is therefore never repeated. */
.js .pdp__lede {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
}

.pdp__jump {
  justify-self: start;
  margin-top: calc(var(--s-3) * -1);
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: 0; background: none; border: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--text-secondary); cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.pdp__jump svg { width: 13px; height: 13px; flex: none; color: var(--accent); }
@media (hover: hover) and (pointer: fine) {
  .pdp__jump:hover { color: var(--accent); }
}
.pdp__jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-xs); }
@media (pointer: coarse) { .pdp__jump { min-height: 2.75rem; margin-top: calc(var(--s-4) * -1); } }

.pdp__fold { border-top: 1px solid var(--rule); }
.pdp__foldhead {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-4) var(--s-6);
  cursor: pointer; list-style: none;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--text-secondary);
  transition: color var(--dur-fast) var(--ease-out);
}
.pdp__foldhead::-webkit-details-marker { display: none; }
@media (hover: hover) and (pointer: fine) {
  .pdp__foldhead:hover { color: var(--accent); }
}
.pdp__foldhead:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.pdp__fold[open] .pdp__foldhead { color: var(--text-primary); }
/* The site's accordion sign, and the site's way of turning it over. */
.pdp__fold[open] .acc__sign { color: var(--accent); transform: rotate(180deg); }
.pdp__fold[open] .acc__sign::after { transform: translate(-50%, -50%) scaleY(0); opacity: 0; }

.pdp__foldbody {
  display: grid; gap: var(--s-4);
  padding: 0 var(--s-6) var(--s-6);
  max-width: var(--measure);
}
.pdp__ledefull {
  font-size: var(--t-small); color: var(--text-secondary); line-height: var(--lead-loose);
}

/* Shown only once the measurement says the clamp cut something. */
.pdp__ledefull,
.pdp__fold--ifclipped,
.pdp__jump--ifclipped { display: none; }
.is-ledeclipped .pdp__ledefull { display: block; }
.is-ledeclipped .pdp__fold--ifclipped { display: block; }
.is-ledeclipped .pdp__jump--ifclipped { display: inline-flex; }

.pdp__features { display: grid; gap: var(--s-2); list-style: none; padding: 0; margin: 0; }
.pdp__features li {
  display: grid; grid-template-columns: 14px 1fr; gap: var(--s-3);
  font-size: var(--t-small); color: var(--text-secondary); line-height: var(--lead-text);
}
.pdp__features li::before {
  content: ''; width: 6px; height: 6px; margin-top: 0.5em;
  border-radius: 50%; background: var(--accent);
}

.pdp__tags { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* The use-case chips in the panel are real filter buttons. .chip is a display
   class, so it needs the button reset the site never had to give it before. */
button.chip {
  font: inherit; font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  button.chip:hover { border-color: var(--rule-accent); color: var(--accent); }
}

.pdp__buy {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding-top: var(--s-4); border-top: 1px solid var(--rule);
}

.qty {
  display: inline-flex; align-items: center;
  border: 1px solid var(--rule); border-radius: var(--r-sm); overflow: hidden;
}
.qty button {
  width: 2.25rem; height: 2.5rem;
  background: none; border: 0; color: var(--text-secondary);
  font-size: var(--t-body); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .qty button:hover:not(:disabled) { background: var(--bg-panel); color: var(--accent); }
}
.qty button:disabled { opacity: 0.3; cursor: not-allowed; }
.qty input {
  width: 2.75rem; height: 2.5rem; text-align: center;
  background: none; border: 0; border-inline: 1px solid var(--rule);
  color: var(--text-primary);
  font-family: var(--font-data); font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
.qty input::-webkit-outer-spin-button, .qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qty input:focus { outline: none; }

.pdp__note {
  font-size: var(--t-tiny); color: var(--text-tertiary); line-height: var(--lead-text);
  padding: var(--s-3);
  border: 1px dashed var(--rule); border-radius: var(--r-sm);
}


/* ============================================================================
   CART & CHECKOUT
   ========================================================================= */

.checkout { display: grid; gap: var(--s-6); align-items: start; }
@media (min-width: 900px) { .checkout { grid-template-columns: 1.5fr 1fr; gap: var(--s-7); } }

.cartlines { display: grid; gap: var(--s-3); }

.line {
  display: grid;
  grid-template-columns: 4.5rem 1fr auto;
  gap: var(--s-4);
  align-items: center;
  padding: var(--s-4);
  background: var(--bg-panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}
.line__media {
  width: 4.5rem; height: 4.5rem;
  background: var(--shop-photo, var(--bg-sunk)) center/contain no-repeat;
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  display: grid; place-items: center;
  font-family: var(--font-data); font-size: var(--t-tiny); color: var(--text-quiet);
}
.line__name { font-weight: 600; letter-spacing: var(--track-snug); font-size: var(--t-small); }
.line__meta { font-family: var(--font-data); font-size: var(--t-micro); color: var(--text-quiet); letter-spacing: var(--track-wide); text-transform: uppercase; }
.line__ctrl { display: grid; gap: var(--s-2); justify-items: end; }
.line__price { font-family: var(--font-data); font-size: var(--t-small); color: var(--accent); font-variant-numeric: tabular-nums; }
.line__remove {
  background: none; border: 0; padding: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--text-quiet); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .line__remove:hover { color: var(--alert); }
}
.line__warn {
  grid-column: 1 / -1;
  font-size: var(--t-tiny); color: var(--text-gold); line-height: var(--lead-text);
  padding-top: var(--s-2); border-top: 1px solid var(--rule-faint);
}

@media (max-width: 559px) {
  .line { grid-template-columns: 3.5rem 1fr; }
  .line__media { width: 3.5rem; height: 3.5rem; }
  .line__ctrl { grid-column: 1 / -1; justify-items: stretch; grid-auto-flow: column; align-items: center; }
}

.summary {
  position: sticky; top: calc(var(--nav-h) + var(--s-5));
  transition: top var(--dur-slow) var(--ease-out);
  background: var(--bg-raised);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  display: grid; gap: var(--s-4);
}

.sumrow {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4);
  font-size: var(--t-small); color: var(--text-secondary);
}
.sumrow span:last-child { font-family: var(--font-data); font-variant-numeric: tabular-nums; }
.sumrow--total {
  padding-top: var(--s-3); border-top: 1px solid var(--rule);
  font-size: var(--t-h5); color: var(--text-primary); font-weight: 600;
}
.sumrow--total span:last-child { color: var(--accent); }
.sumrow--free span:last-child { color: var(--good); }

/* ---- the discount or partner code ----
   A link until it is asked for. An open empty code box on a checkout tells
   everybody who has no code that there is a price they are not getting. */
.codebox { display: grid; gap: var(--s-2); }
.codebox__row { display: flex; gap: var(--s-2); align-items: stretch; }
.codebox__row .field__input { flex: 1 1 auto; min-width: 0; text-transform: uppercase; }
.codebox__row .btn { flex: none; }

.linkbtn {
  justify-self: start;
  padding: 0; border: 0; background: none;
  font: inherit; font-size: var(--t-small);
  color: var(--accent); text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer;
}
.linkbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.codeon { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.codeon__tag {
  font-family: var(--font-data); font-size: var(--t-tiny);
  letter-spacing: var(--track-mono); text-transform: uppercase;
  padding: 3px var(--s-2);
  border: 1px dashed var(--rule-accent); border-radius: var(--r-sm);
  color: var(--accent);
}
.codenote {
  margin: 0;
  font-size: var(--t-tiny); color: var(--text-tertiary); line-height: var(--lead-text);
}

.freehint {
  font-size: var(--t-tiny); color: var(--text-tertiary); line-height: var(--lead-text);
  padding: var(--s-3); border-radius: var(--r-sm);
  background: var(--bg-panel); border: 1px solid var(--rule-faint);
}
.freehint b { color: var(--accent); }

.fulfil { display: grid; gap: var(--s-2); }
.fulfil__opt { position: relative; }
.fulfil__opt input { position: absolute; opacity: 0; pointer-events: none; }
.fulfil__opt span {
  display: grid; gap: 2px; cursor: pointer;
  padding: var(--s-3);
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  transition: border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .fulfil__opt span:hover { border-color: var(--rule-strong); }
}
.fulfil__opt input:checked + span { border-color: var(--rule-accent); background: var(--bg-panel); }
.fulfil__opt input:focus-visible + span { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
.fulfil__title { font-size: var(--t-small); font-weight: 600; display: flex; justify-content: space-between; gap: var(--s-3); }
.fulfil__title em { font-style: normal; font-family: var(--font-data); color: var(--accent); }
.fulfil__note { font-size: var(--t-tiny); color: var(--text-tertiary); line-height: var(--lead-text); }


/* ---- the three steps ----------------------------------------------------
   Cart and details share one page, so nothing on it said that paying was
   still ahead: "Place order" read like a charge. Three items, the one you
   are on lit, and the third plainly not reached. Drawn on the confirmation
   too, where it is the only thing saying this is the last screen. */

.steps {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-2) var(--s-3);
  margin: 0 0 var(--s-5); padding: 0; list-style: none;
}
.steps__step {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--text-quiet);
}
.steps__step + .steps__step::before {
  content: ''; width: 1.25rem; height: 1px;
  margin-right: var(--s-1); background: var(--rule);
}
.steps__n {
  display: grid; place-items: center;
  width: 1.375rem; height: 1.375rem; line-height: 1;
  border: 1px solid var(--rule); border-radius: 50%;
  font-variant-numeric: tabular-nums;
}
.steps__step.is-now { color: var(--text-primary); }
.steps__step.is-now .steps__n { border-color: var(--rule-accent); color: var(--accent); }
.steps__step.is-done { color: var(--text-secondary); }
.steps__step.is-done .steps__n { border-color: var(--good); color: var(--good); }

@media (max-width: 479px) {
  .steps { gap: var(--s-2); }
  .steps__step + .steps__step::before { width: 0.75rem; }
}


/* ---- the lines, folded away on a phone -----------------------------------
   Only below the two-column step: on a wide screen the lines sit beside the
   form rather than above it, and there is nothing to get out of the way. */

.cartlines__toggle {
  display: none; width: 100%;
  align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-3) var(--s-4); text-align: left;
  background: var(--bg-panel); border: 1px solid var(--rule); border-radius: var(--r-md);
  color: var(--text-secondary); font: inherit; font-size: var(--t-small);
  cursor: pointer;
}
.cartlines__act {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--accent);
}

@media (max-width: 899px) {
  .cartlines__toggle { display: flex; }
  .cartlines.is-collapsed .line { display: none; }
}


/* ---- what is true about this order, before committing to it --------------
   Delivery time, the free-delivery threshold, how the order can be paid and
   how long it is held. Four facts, all of them read off the shop's own
   settings by checkout.js rather than written here. */

.trustrow { display: grid; gap: var(--s-2); margin: 0; padding: 0; list-style: none; }
.trustrow li {
  display: grid; grid-template-columns: 14px 1fr; gap: var(--s-3);
  font-size: var(--t-tiny); color: var(--text-tertiary); line-height: var(--lead-text);
}
.trustrow li::before {
  content: ''; width: 6px; height: 6px; margin-top: 0.55em;
  border-radius: 50%; background: var(--accent);
}

.paynote {
  font-size: var(--t-tiny); color: var(--text-tertiary); line-height: var(--lead-text);
}
.paynote b { color: var(--text-secondary); }


/* ---- how far off free delivery is ----------------------------------------
   The threshold is the reason somebody adds the fourth thing, and it was a
   grey line under the total. The bar is the part that moves anybody. */

.freehint--meter { display: grid; gap: var(--s-2); }
.freehint--won b { color: var(--good); }

.meter {
  display: block; height: 4px; border-radius: 2px;
  background: var(--bg-sunk); border: 1px solid var(--rule-faint); overflow: hidden;
}
.meter__fill {
  display: block; height: 100%; border-radius: inherit;
  background: var(--grad-gold);
  transition: width var(--dur-glide) var(--ease-out);
}


/* ---- the total, pinned, on a phone ---------------------------------------
   Above 940px the summary sits beside the form and is sticky. Below it, the
   summary stacks UNDER nine fields, so the total and the button are off
   screen for the whole of the typing. This is those two facts, pinned; it
   tucks itself away whenever the real button is on screen, so there are
   never two "Place order" buttons at once. */

.paybar {
  position: fixed; inset-inline: 0; bottom: 0; z-index: var(--z-sticky);
  display: none; align-items: center; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-3) var(--s-4);
  padding-bottom: calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  background: var(--glass);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border-top: 1px solid var(--rule);
  transition: transform var(--dur-base) var(--ease-out);
}
.paybar.is-tucked { transform: translateY(120%); }
.paybar__total { display: grid; gap: 1px; }
.paybar__label {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-quiet);
}
.paybar__fig {
  font-family: var(--font-data); font-size: var(--t-h5); font-weight: 500;
  color: var(--accent); font-variant-numeric: tabular-nums;
}

@media (max-width: 899px) {
  .paybar { display: flex; }
  /* Room for it, so the last field and the privacy line are not under it. */
  .checkout { padding-bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px)); }
}
@media (prefers-reduced-motion: reduce) {
  .paybar { transition: none; }
}


/* ---- confirmation -------------------------------------------------------
   The order number is the single most important string on this page, because
   it is the payment reference. It gets display size and its own frame. */

.confirm { display: grid; gap: var(--s-5); max-width: 44rem; margin-inline: auto; }

.confirm__seal {
  display: grid; gap: var(--s-3); justify-items: center; text-align: center;
  padding: var(--s-6) var(--s-5);
  border: 1px solid var(--rule-accent); border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--accent) 4.5%, transparent);
}
.confirm__ref {
  font-family: var(--font-data); font-size: var(--t-h3);
  letter-spacing: var(--track-snug); color: var(--accent);
}
.confirm__tick {
  width: 2.75rem; height: 2.75rem; border-radius: var(--r-pill);
  display: grid; place-items: center;
  border: 1px solid var(--rule-accent); color: var(--accent);
}

.bank {
  border: 1px solid var(--rule); border-radius: var(--r-lg); overflow: hidden;
}
.bank__head {
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--rule);
  background: var(--bg-panel);
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
}
.bank__rows { display: grid; }
.bankrow {
  display: grid; grid-template-columns: 9rem 1fr auto;
  gap: var(--s-3); align-items: center;
  padding: var(--s-3) var(--s-5);
  border-bottom: 1px solid var(--rule-faint);
}
.bankrow:last-child { border-bottom: 0; }
.bankrow__k {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-quiet);
}
.bankrow__v { font-family: var(--font-data); font-size: var(--t-small); word-break: break-word; }
.bankrow--ref .bankrow__v { color: var(--accent); font-weight: 600; }

.copybtn {
  background: none; border: 1px solid var(--rule); border-radius: var(--r-xs);
  color: var(--text-tertiary); cursor: pointer;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  padding: 3px 7px;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .copybtn:hover { border-color: var(--rule-accent); color: var(--accent); }
}
.copybtn.is-done { color: var(--good); border-color: color-mix(in srgb, var(--good) 45%, transparent); }

@media (max-width: 559px) {
  .bankrow { grid-template-columns: 1fr auto; }
  .bankrow__k { grid-column: 1 / -1; }
}

.sendrow { display: flex; gap: var(--s-3); flex-wrap: wrap; }
.sendrow .btn { flex: 1 1 12rem; }

/* ---- the card option ----------------------------------------------------
   One primary button, the note under it, and the EFT details folded away
   beneath — present, because some customers will always prefer them, but
   second. */
.paynow { display: grid; gap: var(--s-3); }
.paynow__note { line-height: var(--lead-text); margin: 0; }
.payalt { border-top: 1px solid var(--rule-faint); padding-top: var(--s-3); }
.payalt__sum { cursor: pointer; list-style: none; display: flex; align-items: center; gap: var(--s-2); }
.payalt__sum::-webkit-details-marker { display: none; }
.payalt__sum::before {
  content: ''; width: 0.4rem; height: 0.4rem;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform var(--dur-fast) var(--ease-out);
}
.payalt[open] .payalt__sum::before { transform: rotate(45deg); }
.payalt__body { margin-top: var(--s-3); }

.ordercode {
  font-family: var(--font-data); font-size: var(--t-micro);
  line-height: 1.5; color: var(--text-quiet);
  background: var(--bg-sunk);
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  padding: var(--s-3);
  max-height: 8rem; overflow: auto;
  word-break: break-all; white-space: pre-wrap;
  width: 100%; resize: vertical;
}


/* ============================================================================
   EMPTY & LOADING STATES
   ========================================================================= */

.shopstate {
  display: grid; gap: var(--s-4); justify-items: center; text-align: center;
  padding: var(--s-9) var(--s-5);
  border: 1px dashed var(--rule); border-radius: var(--r-lg);
}
.shopstate h3 { font-size: var(--t-h4); letter-spacing: var(--track-snug); }
.shopstate p { max-width: 44ch; color: var(--text-secondary); font-size: var(--t-small); line-height: var(--lead-text); }

/* ============================================================================
   FILTERING — the View Transition

   Filtering used to rebuild the grid from a string. It now hides, shows and
   reorders cards that already exist, which means the browser can measure them
   before and after and animate the difference: a real FLIP, for the cost of
   wrapping the mutation in startViewTransition().

   The root is excluded while it runs. Left in, the default `root` snapshot
   would cross-fade the entire page — nav, hero and footer — every time
   somebody typed a letter into the search box.
   ========================================================================= */

html.is-gridvt { view-transition-name: none; }

::view-transition-group(*) {
  animation-duration: var(--dur-base);
  animation-timing-function: var(--ease-out);
}

/* Entries decelerate, exits accelerate — the rule the whole site runs on, and
   the reason a filter feels like cards moving rather than a list redrawing. */
::view-transition-new(*) {
  animation: vt-in var(--dur-base) var(--ease-rise) both;
}
::view-transition-old(*) {
  animation: vt-out var(--dur-fast) var(--ease-exit) both;
}

@keyframes vt-in  { from { opacity: 0; transform: translateY(var(--travel-sm)); } }
@keyframes vt-out { to   { opacity: 0; transform: translateY(calc(var(--travel-sm) * -0.5)); } }

/* The engine is not asked to run at all under reduced motion — withTransition()
   calls the mutation directly — but a browser that starts one anyway must not
   animate travel. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none; }
}


.skeleton { display: grid; gap: var(--gutter); grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr)); }
.skeleton__card {
  height: 22rem; border-radius: var(--r-lg);
  border: 1px solid var(--rule); background: var(--bg-panel);
  position: relative; overflow: hidden;
}
.skeleton__card::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--rule-faint), transparent);
  transform: translateX(-100%);
  animation: sweep 1.4s var(--ease-arc) infinite;
}
@keyframes sweep { to { transform: translateX(100%); } }


/* ============================================================================
   TOAST — the add-to-cart acknowledgement
   ========================================================================= */

.shoptoast {
  position: fixed; left: 50%; bottom: var(--s-6); z-index: var(--z-overlay);
  transform: translate(-50%, calc(100% + var(--s-8)));
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-5);
  background: var(--bg-raised);
  border: 1px solid var(--rule-accent);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-soft);
  font-size: var(--t-small); color: var(--text-primary);
  transition: transform var(--dur-base) var(--ease-out);
  pointer-events: none;
  max-width: calc(100% - 2rem);
}
.shoptoast.is-up { transform: translate(-50%, 0); }
.shoptoast svg { color: var(--accent); flex: none; }
.shoptoast a { pointer-events: auto; color: var(--text-gold); white-space: nowrap; }


/* ============================================================================
   PRODUCT GALLERY — thumbnail strip under the main image

   The strip only appears when there is more than one image. A row of one
   thumbnail looks like a control, and clicking it does nothing, which is worse
   than having no strip at all.
   ========================================================================= */

.pdp__gallery { display: flex; flex-direction: column; min-width: 0; }
.pdp__gallery .pdp__media { flex: 1; }

.pdp__thumbs {
  display: flex; gap: var(--s-2);
  padding: var(--s-3);
  border-top: 1px solid var(--rule);
  background: var(--bg-sunk);
  overflow-x: auto;
  scrollbar-width: thin;
}

.pdp__thumb {
  flex: none;
  width: 3.75rem; height: 3.75rem;
  padding: 4px;
  background: var(--shop-photo, var(--bg-panel));
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.pdp__thumb img,
.pdp__thumb picture > img { width: 100%; height: 100%; object-fit: contain; display: block; background: var(--paper-0); }
@media (hover: hover) and (pointer: fine) {
  .pdp__thumb:hover { border-color: var(--rule-strong); transform: translateY(-2px); }
}
.pdp__thumb.is-on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--rule-accent); }
.pdp__thumb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The main image swaps on click, so give it a beat rather than a hard cut. */
.pdp__media img,
.pdp__media picture > img { transition: opacity var(--dur-fast) var(--ease-out); }

@media (min-width: 760px) {
  .pdp__gallery { border-right: 1px solid var(--rule); }
  .pdp__gallery .pdp__media { border-right: 0; }
}

/* A card whose product has more than one photo says so, quietly. */
.product__count {
  position: absolute; bottom: var(--s-3); right: var(--s-3);
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 7px; border-radius: var(--r-pill);
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide);
  border: 1px solid var(--rule); background: var(--glass);
  backdrop-filter: blur(8px); color: var(--text-tertiary);
}


/* ============================================================================
   TOUCH TAP TARGETS
   Applied by input type, not by viewport width — a narrow desktop window is
   not a phone and should not grow chunky controls. The catalogue is the page
   most likely to be worked through one-handed on a phone, and every control
   on it stopped at 40px.
   ========================================================================= */
@media (pointer: coarse) {
  /* Add is the whole point of the card, and it sits in the bottom-right
     corner beside a price that is not a target — a near miss does nothing at
     all, which reads as the button being broken. */
  .addbtn { min-height: 44px; }
  .filterbar__search input,
  .filterbar select { height: 2.75rem; }
  /* The cart's quantity steppers: 36 x 40, and minus sits directly beside
     plus, so a miss does not do nothing — it does the opposite thing. */
  .qty button { width: 2.75rem; height: 2.75rem; }
  .qty input { height: 2.75rem; }

  /* The filter bar's two square buttons were 40 wide in a 44 tall bar, so
     they were short in one dimension only and looked right. */
  .filterbar__more,
  .wishbtn { min-width: 2.75rem; }

  /* The featured card's heading is the card's only link - there is no
     wrapping anchor behind it to catch a near miss. */
  .feat__name { display: inline-flex; align-items: center; min-height: 2.75rem; }

  /* The A-Z index: each brand heading is the link to that brand's page, and
     at 12px tall it was the shortest target on the site. */
  .allprod__brand a { display: inline-flex; align-items: center; min-height: 2.75rem; }

  /* Share: three buttons in a row, and the middle one is "copy link" while
     its neighbours open somebody else's app. */
  .sharebtn { min-height: 2.75rem; }
}


/* ==========================================================================
   REVIEWS IN THE PRODUCT PANEL

   Full width under the two columns rather than inside the right-hand one.
   The panel's right column is the buying column — name, price, quantity,
   Add — and a scrolling list of paragraphs pushed into it puts the button
   somebody came for below three testimonials.

   The block is EMPTY in the markup and stays empty for a product nobody has
   reviewed. It is not a heading over a "no reviews yet" message: an empty
   state here is a shop telling every visitor that nobody has bought this,
   which is a worse sentence than silence.
   ======================================================================== */
.pdp__reviews:empty { display: none; }
.pdp__reviews {
  display: grid; gap: var(--s-5);
  padding: var(--s-6);
  border-top: 1px solid var(--rule);
  background: var(--bg-sunk);
}
.pdp__revhead {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--s-3);
}
.pdp__revnote {
  font-size: var(--t-micro); color: var(--text-quiet);
  max-width: 68ch; line-height: var(--lead-loose);
}


/* ============================================================================
   ALL PRODUCTS — the crawlable index at the foot of the shop

   The 238 cards above are drawn by shop.js from catalog.json after the page
   loads, so until this block existed the raw HTML of the shop contained no
   link to any product. Google renders JavaScript, but rendering is queued
   separately from crawling and lags it — and in the plain-HTML link graph the
   product pages were receiving nothing at all, which is a ranking problem
   before it is a discovery one.

   So it is a real, visible index rather than a hidden one: honest, useful to
   anybody who knows the model number they want, and counted in full.

   Styled as an INDEX and not as content — sunk ground, small type, dense
   columns — so it reads as the back matter it is and does not compete with
   the shop above it.
   ========================================================================= */

.allprod {
  border-top: 1px solid var(--rule);
  background: var(--bg-sunk);
}

/* ---- the 241-link index, folded ------------------------------------------
   Written as a closed <details> by scripts/build-product-pages.py, which
   explains at length why the block exists at all. Closed on every width, and
   with no JavaScript involved in the decision: the list is scaffolding for
   crawlers, and a crawler reads the markup rather than the open state, so
   nothing is lost by folding it and about 110 phone screens are saved.

   Styling only below. Nothing here opens or closes it — the reader does.
   ------------------------------------------------------------------------ */

.allprod__sum {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  cursor: pointer;
  list-style: none;
  min-height: 2.75rem;
}
.allprod__sum::-webkit-details-marker { display: none; }
.allprod__sum:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r-xs); }

.allprod__count {
  font-family: var(--font-data);
  font-size: var(--t-micro);
  letter-spacing: var(--track-mono);
  text-transform: uppercase;
  color: var(--text-quiet);
}
/* The affordance. A closed <details> with no chevron reads as a heading. */
.allprod__count::after {
  content: ' \25BE';
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-out);
}
.allprod__wrap[open] .allprod__count::after { transform: rotate(180deg); }

.allprod__title {
  font-size: var(--t-h4);
  letter-spacing: var(--track-tight);
  line-height: var(--lead-tight);
}

.allprod__note {
  margin-top: var(--s-2);
  max-width: 54ch;
  font-size: var(--t-small);
  color: var(--text-secondary);
  line-height: var(--lead-text);
}

.allprod__group { margin-top: var(--s-7); }

.allprod__brand {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-wide);
  color: var(--text-tertiary);
}
.allprod__brand span {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-micro);
  color: var(--text-tertiary);
  opacity: 0.7;
}

/* Columns rather than a grid: a list of 126 names has no row relationship
   worth preserving, and `columns` reflows to whatever width there is without
   a breakpoint for every count. */
.allprod__list {
  columns: 16rem;
  column-gap: var(--s-6);
  list-style: none;
  padding: 0;
  margin: var(--s-3) 0 0;
}
.allprod__list li {
  break-inside: avoid;
  margin-bottom: var(--s-1, 0.25rem);
}
.allprod__list a {
  display: block;
  padding: 0.15rem 0;
  font-size: var(--t-micro);
  line-height: var(--lead-snug);
  color: var(--text-secondary);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .allprod__list a:hover { color: var(--accent); }
}
.allprod__list a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* A list of 238 links is exactly where a mis-tap is likely, and at 390px the
   rows above are about 18px tall — under the 24px WCAG 2.2 asks for. Widened
   for touch only: on a mouse, a dense index is easier to scan, and a pointer
   does not miss an 18px row. */
@media (pointer: coarse) {
  .allprod__list a {
    display: flex;
    align-items: center;
    min-height: 1.75rem;
    font-size: var(--t-small);
  }
  .allprod__list { columns: 13rem; }
}


/* ============================================================================
   CARD TOOLS — save and compare, over the photograph

   Two glass buttons in the top-right corner, opposite the flags. They sit
   outside the <a> that wraps the picture: a button inside a link is a click
   that does two things. Pressed is gold, because saving and comparing are
   actions and gold is the action colour — the same rule the Add button and
   the price already keep.
   ========================================================================= */

.product__tools {
  position: absolute; top: var(--s-3); right: var(--s-3); z-index: 2;
  display: flex; flex-direction: column; gap: var(--s-2);
}

.toolbtn {
  width: 2rem; height: 2rem; padding: 0;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  border: 1px solid var(--rule-strong);
  background: var(--glass); backdrop-filter: blur(8px);
  color: var(--text-secondary); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.toolbtn svg { width: 15px; height: 15px; }
@media (hover: hover) and (pointer: fine) {
  .toolbtn:hover { border-color: var(--rule-accent); color: var(--accent); }
}
.toolbtn:active { transform: scale(0.94); }
.toolbtn[aria-pressed="true"] { color: var(--accent); border-color: var(--rule-accent); }
.toolbtn--wish[aria-pressed="true"] svg { fill: currentColor; }
.toolbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* 2rem is a 32px target. Coarse takes it to the floor: the two buttons are
   stacked 8px apart, so the neighbour is nearer than the miss margin, and a
   thumb aimed at Save that lands low puts the product into a comparison
   instead. 241 cards is a lot of chances to do that. */
@media (pointer: coarse) {
  .toolbtn { width: 2.75rem; height: 2.75rem; }
}

/* Two 44px circles stacked down the corner of a 167px card cover about a
   third of the photograph, and on a two-up grid they were the loudest thing
   on screen after the price — 241 cards each shouting two secondary verbs
   over the one that matters.

   Compare is the one that goes. It is the more considered of the two, the
   table it fills is too wide to read on a phone anyway, and it survives in
   the quick-look panel and on the product page, both one tap from here.
   Save stays, at the full 44px: it is the impulse action, and it is the one
   people actually use on a phone.

   Hidden, never removed — scripts/shop.js still writes the button, still
   reads its pressed state, and a compare list built on a laptop still shows
   its saved cards as pressed when the same person opens the shop on their
   phone. */
@media (max-width: 559px) {
  .product__tools .toolbtn--cmp { display: none; }
}


/* ============================================================================
   FILTER BAR — the fold-out button and the saved heart
   ========================================================================= */

.filterbar__more,
.wishbtn {
  display: inline-flex; align-items: center; gap: var(--s-2);
  height: 2.5rem; padding: 0 var(--s-3);
  background: var(--bg-panel);
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  color: var(--text-secondary);
  font: inherit; font-size: var(--t-small);
  cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.filterbar__more svg,
.wishbtn svg { width: 14px; height: 14px; flex: none; }
@media (hover: hover) and (pointer: fine) {
  .filterbar__more:hover,
  .wishbtn:hover { border-color: var(--rule-accent); color: var(--accent); }
}
.filterbar__more[aria-expanded="true"],
.wishbtn[aria-pressed="true"] { border-color: var(--rule-accent); color: var(--accent); }
.wishbtn[aria-pressed="true"] svg { fill: currentColor; }
.filterbar__more:focus-visible,
.wishbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.wishbtn__count {
  font-family: var(--font-data); font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  padding: 1px 6px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--shop-on-accent);
}
.wishbtn__count:empty { display: none; }

/* How many of the panel's own filters are on. Same pill as the heart's count
   and, like it, gone when there is nothing to say. */
.morecount {
  font-family: var(--font-data); font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  padding: 1px 6px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--shop-on-accent);
}
.morecount:empty { display: none; }

/* On a phone the bar is two rows, as it was before these two buttons
   arrived: the search box with the two icon buttons beside it, then the two
   dropdowns with the count. The words on the buttons go (the icons are the
   heart and the filter lines), and the count keeps its number and drops the
   word "products" so it fits on the dropdown row. */
@media (max-width: 559px) {
  .filterbar__more > span:not(.morecount),
  .wishbtn > span:not(.wishbtn__count) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .filterbar__more, .wishbtn { position: relative; padding: 0 var(--s-3); }
  .filterbar__search { order: 0; flex: 1 1 8rem; min-width: 0; }
  .filterbar__more   { order: 1; }
  .wishbtn           { order: 2; }
  .filterbar select  { order: 3; }
  .filterbar__count  { order: 4; }
  .filterbar__word   { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .filterbar__count  { position: relative; }
}
@media (pointer: coarse) {
  .filterbar__more, .wishbtn { height: 2.75rem; }
}


/* ============================================================================
   PRICE AND STOCK — the fold-out row

   A dual-thumb slider is two native <input type="range"> laid over one
   track. The inputs themselves ignore the pointer; only their thumbs take
   it, so both thumbs can be grabbed even though they share a box. That is
   the whole trick, and it works in every engine that ships range inputs.
   ========================================================================= */

.filterrow {
  display: grid; gap: var(--s-5); align-items: end;
  grid-template-columns: minmax(0, 1fr) auto;
  padding: var(--s-4) var(--s-5);
  margin-bottom: var(--s-5);
  background: var(--bg-panel);
  border: 1px solid var(--rule); border-radius: var(--r-md);
}
@media (max-width: 559px) {
  .filterrow { grid-template-columns: 1fr; }
}

.pricer { display: grid; gap: var(--s-3); min-width: 0; }
.pricer__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-3); }
.pricer__label {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
}
.pricer__out {
  font-family: var(--font-data); font-size: var(--t-small);
  color: var(--accent); font-variant-numeric: tabular-nums;
}

.pricer__track { position: relative; height: 2rem; }
.pricer__track::before {
  content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 3px;
  transform: translateY(-50%);
  background: var(--rule-strong); border-radius: var(--r-pill);
}
.pricer__fill {
  position: absolute; top: 50%; left: 0; right: 0; height: 3px;
  transform: translateY(-50%);
  background: var(--accent); border-radius: var(--r-pill);
  pointer-events: none;
}
.pricer__track input[type="range"] {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  -webkit-appearance: none; appearance: none;
  background: none; pointer-events: none;
}
#priceLo { z-index: 2; }
#priceHi { z-index: 3; }
.pricer__track input[type="range"]::-webkit-slider-runnable-track { background: none; border: 0; }
.pricer__track input[type="range"]::-moz-range-track { background: none; border: 0; }
.pricer__track input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  pointer-events: auto;
  width: 1.25rem; height: 1.25rem; border-radius: 50%;
  background: var(--bg-raised); border: 2px solid var(--accent);
  box-shadow: var(--shadow-soft); cursor: grab;
  transition: transform var(--dur-fast) var(--ease-out);
}
.pricer__track input[type="range"]::-moz-range-thumb {
  pointer-events: auto;
  width: 1.25rem; height: 1.25rem; border-radius: 50%;
  background: var(--bg-raised); border: 2px solid var(--accent);
  box-shadow: var(--shadow-soft); cursor: grab;
}
.pricer__track input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.1); cursor: grabbing; }
.pricer__track input[type="range"]:focus { outline: none; }
.pricer__track input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }
.pricer__track input[type="range"]:focus-visible::-moz-range-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }

.pricer__inputs { display: flex; gap: var(--s-3); flex-wrap: wrap; }
.pricer__field {
  display: inline-flex; align-items: center; gap: var(--s-2);
  height: 2.25rem; padding: 0 var(--s-2) 0 var(--s-3);
  background: var(--bg-sunk);
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  font-family: var(--font-data); font-size: var(--t-small); color: var(--text-quiet);
}
.pricer__field > span { font-family: var(--font-text); font-size: var(--t-tiny); color: var(--text-tertiary); }
.pricer__field input {
  width: 5rem; background: none; border: 0; padding: 0;
  color: var(--text-primary); font: inherit; font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
.pricer__field input::-webkit-outer-spin-button,
.pricer__field input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.pricer__field input::placeholder { color: var(--text-quiet); }
.pricer__field input:focus { outline: none; }
.pricer__field:focus-within { border-color: var(--rule-accent); }

/* The in-stock switch. A checkbox drawn as a toggle, because "only show me
   what is on the shelf" is a switch you flip, not a box you tick. */
.switch {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--s-3);
  min-height: 2.25rem;
  font-size: var(--t-small); color: var(--text-secondary);
  cursor: pointer; user-select: none;
}
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch__track {
  position: relative; flex: none;
  width: 2.5rem; height: 1.5rem; border-radius: var(--r-pill);
  background: var(--bg-sunk); border: 1px solid var(--rule-strong);
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.switch__track::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: calc(1.5rem - 8px); height: calc(1.5rem - 8px); border-radius: 50%;
  background: var(--text-tertiary);
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.switch input:checked + .switch__track { background: var(--accent-soft); border-color: var(--rule-accent); }
.switch input:checked + .switch__track::after { transform: translateX(1rem); background: var(--accent); }
.switch input:focus-visible + .switch__track { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
.switch__count { font-family: var(--font-data); font-size: var(--t-micro); color: var(--text-quiet); }

/* Chips that are links or actions rather than filters: the brand page and
   "share this view". Quiet, so the removable filters stay the loud ones. */
.filterchip--quiet {
  border-color: var(--rule); background: none; color: var(--text-secondary);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .filterchip--quiet:hover { border-color: var(--rule-accent); color: var(--accent); background: none; }
}
.filterchip--quiet svg { width: 11px; height: 11px; }


/* ============================================================================
   SMALL CARDS — related, recently viewed, brand pages

   One card, four homes: under the product panel, under the grid, on the
   generated product pages and beside a brand. Moved here from product.css
   so the shop page can draw it too. The picture frame carries .media for
   the decode gate and takes back `contain`, like the big card does.
   ========================================================================= */

.relgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 10.5rem), 1fr));
  gap: var(--s-4);
}

.relcard {
  display: grid; gap: var(--s-2); align-content: start;
  padding: var(--s-3);
  background: var(--bg-tile);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  text-decoration: none; color: inherit; text-align: left;
  transition: border-color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .relcard:hover { border-color: var(--rule-accent); transform: translateY(-2px); }
}
.relcard:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .relcard { transition: none; }
  .relcard:hover { transform: none; }
}

.relcard__media {
  display: block; aspect-ratio: 1;
  border-radius: var(--r-sm); overflow: hidden;
  background: var(--shop-photo, var(--bg-sunk));
}
.relcard__media.media { aspect-ratio: 1; border-radius: var(--r-sm); }
.relcard__media img,
.relcard__media picture > img,
.relcard__media.media > picture > img {
  width: 100%; height: 100%; object-fit: contain; padding: var(--s-3); display: block;
  background: var(--paper-0); background-clip: content-box;   /* the sweep, as on the card */
}
.relcard__media.media.is-loaded > picture > img,
.relcard__media.media.is-loaded > img { transform: scale(1); }
.relcard__media .product__nomedia { font-size: var(--t-h5); }

.relcard__brand {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-mono); text-transform: uppercase; color: var(--text-quiet);
}
.relcard__name {
  font-size: var(--t-tiny); line-height: var(--lead-snug); color: var(--text-primary);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.relcard__price {
  font-family: var(--font-data); font-size: var(--t-small); font-weight: 500;
  color: var(--accent); font-variant-numeric: tabular-nums;
}


/* ============================================================================
   RECENTLY VIEWED — the strip under the grid
   ========================================================================= */

.recent { margin-top: var(--s-8); padding-top: var(--s-6); border-top: 1px solid var(--rule); }
.recent__head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4);
  margin-bottom: var(--s-4);
}
.recent__head .eyebrow { margin: 0; }
.recent__clear {
  background: none; border: 0; padding: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--text-quiet); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .recent__clear:hover { color: var(--accent); }
}


/* ============================================================================
   THE PANEL — related strip, share row, save and compare as words
   ========================================================================= */

.pdp__related {
  display: grid; gap: var(--s-4);
  padding: var(--s-6);
  border-top: 1px solid var(--rule);
}
.pdp__relhead {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-3); flex-wrap: wrap;
}
.pdp__relhead .eyebrow { margin: 0; }
.pdp__rellink { font-size: var(--t-tiny); color: var(--text-secondary); text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  .pdp__rellink:hover { color: var(--accent); }
}

.pdp__brandlink {
  color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .pdp__brandlink:hover { color: var(--accent); border-color: var(--rule-accent); }
}

.pdp__tools { display: flex; gap: var(--s-2); flex-wrap: wrap; }

.sharerow {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  padding-top: var(--s-3);
}
.sharerow__label {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-quiet);
  margin-right: var(--s-1);
}

/* A quiet pill button. Also the shape Save and Compare take in the panel
   and on the product page, where there is room for a word. */
.sharebtn {
  display: inline-flex; align-items: center; gap: var(--s-2);
  height: 2rem; padding: 0 var(--s-3);
  border: 1px solid var(--rule); border-radius: var(--r-pill);
  background: none; color: var(--text-secondary);
  font: inherit; font-size: var(--t-tiny);
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.sharebtn svg { width: 13px; height: 13px; flex: none; }
@media (hover: hover) and (pointer: fine) {
  .sharebtn:hover { border-color: var(--rule-accent); color: var(--accent); }
}
.sharebtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sharebtn[aria-pressed="true"] { border-color: var(--rule-accent); color: var(--accent); }
.sharebtn[data-wish][aria-pressed="true"] svg { fill: currentColor; }
.sharebtn.is-done { color: var(--good); border-color: color-mix(in srgb, var(--good) 45%, transparent); }
@media (pointer: coarse) {
  .sharebtn { height: 2.5rem; padding: 0 var(--s-4); }
}


/* ============================================================================
   COMPARE TRAY — pinned to the foot of the page while something is ticked

   Drawn by scripts/lists.js into <body>, on every page that loads it. It
   takes the raised ground of the theme, like the toast, and the gold rule
   along its top is what lifts it off the page. The toast moves up while the
   tray is showing so the two do not stack.
   ========================================================================= */

.cmptray {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-sticky);
  background: var(--bg-raised); color: var(--text-primary);
  border-top: 1px solid var(--rule-accent);
  box-shadow: var(--shadow-soft);
  transform: translateY(100%);
  transition: transform var(--dur-base) var(--ease-out);
}
.cmptray.is-on { transform: none; }
.cmptray__inner {
  display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap;
  padding-block: var(--s-3);
}
.cmptray__items { display: flex; gap: var(--s-3); flex: 1 1 auto; }
.cmptray__item,
.cmptray__slot {
  position: relative; flex: none;
  width: 3rem; height: 3rem; border-radius: var(--r-sm);
  border: 1px solid var(--rule-strong); background: var(--shop-tray-well);
  display: grid; place-items: center;
}
.cmptray__slot { border-style: dashed; background: none; }
.cmptray__item img { width: 100%; height: 100%; object-fit: contain; padding: 4px; border-radius: inherit; background: #F4F7FC; }
.cmptray__initials {
  font-family: var(--font-data); font-size: var(--t-tiny); letter-spacing: var(--track-mono); color: var(--shop-tray-ink);
}
.cmptray__x {
  position: absolute; top: -7px; right: -7px;
  width: 18px; height: 18px; padding: 0; border-radius: 50%;
  border: 1px solid var(--rule-strong); background: var(--bg-raised); color: var(--text-primary);
  display: grid; place-items: center; cursor: pointer;
}
.cmptray__x svg { width: 8px; height: 8px; }
@media (hover: hover) and (pointer: fine) {
  .cmptray__x:hover { border-color: var(--rule-accent); color: var(--text-gold); }
}
.cmptray__actions { display: flex; align-items: center; gap: var(--s-4); margin-left: auto; }
.cmptray__count {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--shop-tray-ink);
}
.cmptray__go.is-disabled { opacity: 0.45; pointer-events: none; }
.cmptray__clear {
  background: none; border: 0; padding: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--shop-tray-ink); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .cmptray__clear:hover { color: var(--text-gold); }
}
@media (max-width: 559px) {
  .cmptray__count { display: none; }
  .cmptray__actions { gap: var(--s-3); }
}

/* Room at the foot for the tray, and the toast climbs over it. The parked
   toast is translated just below the viewport; lifted by the tray's height
   its top edge would peek out above the tray, so it is also hidden while
   down — after the slide, not before, or it would vanish mid-exit. */
body.has-cmptray { padding-bottom: 5rem; }
body.has-cmptray .shoptoast { bottom: calc(var(--s-6) + 4.5rem); }
.shoptoast:not(.is-up) { visibility: hidden; transition: transform var(--dur-base) var(--ease-out), visibility 0s linear var(--dur-base); }
.shoptoast.is-up { visibility: visible; }
@media (max-width: 559px) {
  /* The tray wraps to two rows here. */
  body.has-cmptray { padding-bottom: 8rem; }
  body.has-cmptray .shoptoast { bottom: calc(var(--s-6) + 7.5rem); }
}
@media (pointer: coarse) {
  .cmptray__x { width: 24px; height: 24px; top: -9px; right: -9px; }
  .cmptray__x svg { width: 10px; height: 10px; }
}


/* ============================================================================
   COMPARE TABLE — the dialog
   ========================================================================= */

.cmp {
  position: fixed; inset: 0; z-index: var(--z-overlay);
  display: none; align-items: flex-start; justify-content: center;
  padding: var(--s-6) var(--s-4);
  background: var(--shop-scrim);
  backdrop-filter: blur(8px);
  overflow-y: auto;
}
.cmp.is-open { display: flex; }

.cmp__card {
  position: relative;
  width: min(100%, 72rem);
  background: var(--bg-raised);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-xl);
  box-shadow: var(--shop-lift);
  animation: pdp-in var(--dur-slow) var(--ease-out);
  overflow: hidden;
}
.cmp__head {
  display: flex; align-items: baseline; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-5) 4rem var(--s-4) var(--s-6);
  border-bottom: 1px solid var(--rule);
}
.cmp__title { font-size: var(--t-h4); letter-spacing: var(--track-tight); }
.cmp__sub { font-size: var(--t-tiny); color: var(--text-tertiary); }
.cmp__empty { margin: var(--s-6); }
.cmp__foot {
  display: flex; justify-content: flex-end; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-4) var(--s-6);
  border-top: 1px solid var(--rule);
}
.cmp__clear { color: var(--text-tertiary); }
@media (hover: hover) and (pointer: fine) {
  .cmp__clear:hover { color: var(--accent); }
}

.cmp__scroll { overflow-x: auto; scrollbar-width: thin; }

.cmptable { border-collapse: collapse; width: 100%; min-width: 36rem; font-size: var(--t-small); }
.cmptable th,
.cmptable td {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--rule-faint);
  vertical-align: top; text-align: left;
}
.cmptable thead th { vertical-align: bottom; border-bottom: 1px solid var(--rule); }
.cmptable tbody tr:last-child th,
.cmptable tbody tr:last-child td { border-bottom: 0; }
.cmptable th[scope="row"],
.cmptable__corner {
  position: sticky; left: 0; z-index: 1;
  background: var(--bg-raised);
  width: 8rem; min-width: 7rem;
  font-family: var(--font-data); font-size: var(--t-micro); font-weight: 500;
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--text-tertiary);
  border-right: 1px solid var(--rule-faint);
}
.cmptable td { min-width: 11rem; color: var(--text-secondary); line-height: var(--lead-text); }
.cmptable td.is-best .price__now { color: var(--good); }
.cmp__best {
  display: block; margin-top: 2px;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--good);
}
.cmprow--diff td { color: var(--text-primary); }
.cmprow__dot {
  display: inline-block; width: 5px; height: 5px; margin-left: var(--s-2);
  border-radius: 50%; background: var(--accent); vertical-align: middle;
}
.cmptable ul { margin: 0; padding-left: 1.1em; display: grid; gap: 2px; }
.cmptable .addbtn { margin-left: 0; }

.cmphead { display: grid; gap: var(--s-2); }
.cmphead__media {
  display: block; width: 100%; max-width: 9rem; aspect-ratio: 1;
  background: var(--shop-photo, var(--bg-sunk));
  border: 1px solid var(--rule); border-radius: var(--r-sm);
  overflow: hidden;
}
.cmphead__media img,
.cmphead__media picture > img { width: 100%; height: 100%; object-fit: contain; padding: var(--s-2); display: block; background: var(--paper-0); background-clip: content-box; }
.cmphead__brand {
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-mono); text-transform: uppercase; color: var(--text-quiet);
}
.cmphead__name {
  font-family: var(--font-display); font-size: var(--t-small); font-weight: 600;
  line-height: var(--lead-snug); color: var(--text-primary); text-decoration: none;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
@media (hover: hover) and (pointer: fine) {
  .cmphead__name:hover { color: var(--accent); }
}
.cmphead__remove {
  justify-self: start;
  background: none; border: 0; padding: 0;
  font-family: var(--font-data); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--text-quiet); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .cmphead__remove:hover { color: var(--alert); }
}

/* The brand headings in the index are the brand pages. */
.allprod__brand a { color: inherit; text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  .allprod__brand a:hover { color: var(--accent); }
}


/* ============================================================================
   SHOW MORE — the rest of the result
   ---------------------------------------------------------------------------
   Centred under the grid, with the running count beneath the button rather
   than inside it: the label is the action and must not change length as the
   number climbs, or the button moves under a thumb that is already on its
   way down.
   ========================================================================= */

.gridmore {
  margin-top: var(--s-7);
  display: grid;
  justify-items: center;
  gap: var(--s-3);
}

.gridmore__at {
  font-family: var(--font-data);
  font-size: var(--t-micro);
  letter-spacing: var(--track-mono);
  color: var(--text-quiet);
}
