/* ============================================================
   product-details.css — Flex Europa · finaldesign
   Single product (§3.2).

   REUSE: .page-head, .breadcrumb, .tabs/.tab, .scroller, .showcase-card,
   .btn, .chip, .spec-row, .qty, .container, .section-head and .mono-label
   are all existing components — .spec-row and .qty come straight out of
   the bag drawer, and the tab row and the related-products scroller are
   the home page's own, promoted to components.css so this page can share
   them. This file adds only the two-column product layout, the gallery,
   and the mobile buy bar.
   ============================================================ */

/* ---- Breadcrumb, on a phone ----
   This page is the only one whose trail is written after load: the middle
   crumb becomes the product's category and the last one its name. On a
   narrow screen the real labels wrapped the trail onto a second (at 360px,
   a third) line, and that growth arrived 300ms after first paint — a 0.17
   layout shift, the whole of this page's CLS.

   So below 576px the trail is held to ONE line and the two variable labels
   ellipsize instead of wrapping. The height is then the same before and
   after the product lands, whatever it is called, and nothing below it
   moves. Both labels stay in the accessibility tree in full.

   Only this page needs it, and product-details.css only loads here, so the
   rule is page-scoped without a body class. Above 575px every product in
   the catalogue already fits on one line — measured, not assumed. */
@media (max-width: 575px) {
  .breadcrumb { flex-wrap: nowrap; }
  /* The ITEM may shrink; the link inside it may not go below the
     component's own 44px floor. Zeroing that floor here was a real
     regression — "HOME" fell to 33x44 and the gate caught it. */
  .breadcrumb__item { min-inline-size: 0; }
  .breadcrumb__item:first-child { flex: none; }
  .breadcrumb__link,
  .breadcrumb__current {
    display: block;
    line-height: var(--step-size);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.product { padding-block: var(--space-2xl) var(--space-3xl); }

/* Two columns: what it looks like, and what it costs.

   The earlier version put the description and the specification in a THIRD
   grid area under the gallery, because the buy column was so much taller
   than the gallery that it left ~460px of dead white beside it. That was
   treating the symptom. With the summary carrying its full complement —
   meta row, saving, second action, assurances, share — the two columns
   balance on their own, and the long-form content is better served by a
   full-width band underneath where a 60ch measure actually fits.

   1.28fr / 1fr is not a taste call, it is the ratio that makes the two
   columns finish together. The gallery's height tracks its own width
   (stage = width / 1.25, rail = a quarter of it), while the summary's is
   near-flat at any usable width — so the balance point is a gallery
   noticeably wider than the buy panel. At 1440 that leaves ~30px between
   the two column bottoms, against 460px before this page was rebuilt.

   minmax(0, …) on both tracks: an auto minimum lets a wide child push the
   column past the container and reintroduce horizontal overflow. */
.product__layout {
  display: grid;
  grid-template-columns: minmax(0, 1.28fr) minmax(0, 1fr);
  gap: var(--space-2xl);
  align-items: start;
}

/* Between 992 and 1199 the summary is narrow enough that its copy wraps and
   the columns cannot finish together whatever the ratio. The gallery sticks
   instead: the image stays with the reader while the buy panel scrolls, so
   the shorter column is never dead space on screen. `align-items: start` is
   what gives it somewhere to travel. */
@media (min-width: 992px) {
  .gallery {
    position: sticky;
    inset-block-start: var(--anchor-offset);
  }
}

/* ============================================================
   GALLERY — stage + thumbnail rail
   A large stage with a rail of thumbnails beneath it. Each frame is an
   image or a video; the rail is an ARIA tablist, so the whole gallery is
   operable with arrow keys and announces which frame is showing.

   Everything here is drawn from existing tokens: the stage sits on
   --color-surface behind a --hairline like every other panel on the site,
   the glass chips reuse the header's --glass-bg / --glass-blur, and the
   active thumb is marked with --color-text, the same ink the active nav
   item uses.
   ============================================================ */
.gallery { display: flex; flex-direction: column; gap: var(--space-md); }

/* ---- Stage ---- */
.gallery__stage {
  position: relative;
  /* 5/4 — the SAME crop the catalogue card uses (components.css derives it
     from the approved card's own 1.236 media box), so the product does not
     appear to change shape between the card that led here and this page. */
  aspect-ratio: 5 / 4;
  overflow: hidden;
  background: var(--color-surface);
  border: var(--hairline);
}
.gallery__media {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}
/* Video is never cropped — a demo clip has framing of its own — so it is
   contained on the deepest ink in the palette rather than covered. */
.gallery__media--video {
  object-fit: contain;
  background: var(--color-ink-scrim);
  transition: none;
}

/* Click-to-zoom on stills. A transform, so it can never reflow the page. */
/* Scaling the media inside a clipped 5/4 box magnified the CROP, not the
   photograph — on a phone it showed less of the product than before. The
   stage now opens .lightbox instead; this is only the affordance. */
.gallery__stage.is-enlargeable { cursor: zoom-in; }

.gallery__zoom {
  position: absolute;
  inset-block-end: var(--space-sm);
  inset-inline-end: var(--space-sm);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--step-size);
  block-size: var(--step-size);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  color: var(--color-text-inverse);
  transition: background-color var(--transition-fast);
}
.gallery__zoom:hover { background: var(--color-ink-scrim); }
.gallery__zoom:focus-visible { outline: none; box-shadow: var(--focus-ring-dark); }

/* Frame counter — mono, on the same glass as the zoom control. Anchored to
   the TOP-left: at the bottom it collided with the native video controls,
   which occupy the full width of the bottom edge of a video frame. */
.gallery__counter {
  position: absolute;
  inset-block-start: var(--space-sm);
  inset-inline-start: var(--space-sm);
  z-index: 1;
  padding: 5px var(--space-sm);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-inverse);
}

/* ---- Thumbnail rail ----
   ONE COLUMN PER FRAME. --frames is set by product-details.js from
   media.length, because only the data knows how many there are; auto-fill
   guessed from a minimum width instead and left three empty tracks — a
   four-frame rail filled barely half the stage above it.

   The cap is what auto-fill was really there for: without it a two-frame
   product would show two half-stage thumbnails. With it a thumbnail is at
   most --thumb-max wide however few frames there are, and the rail simply
   ends where the frames do. */
/* The wrap exists to hang the arrows off; it is the .js-scroller root. */
.gallery__rail-wrap { position: relative; }
.gallery__rail {
  display: flex;
  gap: var(--space-sm);
  /* Thumbnails hold their size and the rail scrolls, which is what makes a
     tenth frame produce an ARROW rather than ten slivers. --frames still
     caps the rail so four or fewer never stretch to fill the column. */
  max-inline-size: calc(var(--frames, 4) * var(--thumb-max));
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.gallery__rail::-webkit-scrollbar { display: none; }
.gallery__rail .gallery__thumb {
  flex: 0 0 calc(var(--thumb-max) - var(--space-sm));
  scroll-snap-align: start;
}

/* The shared controls row, re-placed as a pair of edge arrows over the
   rail. scrollers.js still owns every behaviour — including hiding this
   whole row when the rail does not overflow, which is the "arrows only if
   there are more images" rule, measured rather than counted. */
.gallery__rail-nav {
  position: absolute;
  inset-block-start: 50%;
  inset-inline: 0;
  transform: translateY(-50%);
  margin-block-start: 0;
  pointer-events: none;          /* the rail underneath stays scrollable */
}
.gallery__rail-nav .scroller__arrow {
  pointer-events: auto;
  inline-size: var(--step-size);
  block-size: var(--step-size);
  background: var(--color-bg);
  box-shadow: var(--shadow-md);
}
/* At an end the arrow is not just dimmed, it is out of the way — a 44px
   disabled square parked over the first thumbnail is a dead zone where a
   thumbnail should be. */
.gallery__rail-nav .scroller__arrow:disabled { opacity: 0; pointer-events: none; }
.gallery__thumb {
  position: relative;
  aspect-ratio: 5 / 4;
  overflow: hidden;
  background: var(--color-surface);
  border: var(--hairline);
  cursor: pointer;
  transition: border-color var(--transition-fast), opacity var(--transition-fast);
}
.gallery__thumb img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
.gallery__thumb:hover { border-color: var(--color-border-strong); }
/* The selected frame is marked with ink and an inset ring — a border alone
   would shift the image by a pixel as it thickens. */
.gallery__thumb[aria-selected="true"] {
  border-color: var(--color-text);
  box-shadow: inset 0 0 0 2px var(--color-text);
}
.gallery__thumb:not([aria-selected="true"]) { opacity: .78; }
.gallery__thumb:not([aria-selected="true"]):hover { opacity: 1; }
.gallery__thumb:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* A video frame is flagged on its thumbnail, so nobody has to guess which
   one moves. */
.gallery__thumb-badge {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--hero-scrim);
  color: var(--color-text-inverse);
  pointer-events: none;
}
.gallery__thumb-badge .icon { inline-size: 22px; block-size: 22px; }

/* ---------------------------------------------------------------- Summary */
.product__summary { display: flex; flex-direction: column; gap: var(--space-md); }
.product__eyebrow {
  align-self: flex-start;
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  background: var(--brand-blue);
  color: var(--color-text-inverse);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.product__title {
  font-family: var(--font-heading);
  font-size: clamp(var(--fs-2xl), 3.4vw, var(--fs-3xl));
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
}

/* SKU and availability read as one fact about the line, so they share a
   row. Stacked as two blocks they read as two unrelated announcements. */
.product__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-block-start: calc(var(--space-md) * -0.5);
}
.product__sku {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.product__meta-sep { color: var(--color-border-strong); }
.product__desc { font-size: var(--fs-md); color: var(--color-text-muted); max-inline-size: var(--measure); }

.product__price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-sm);
  /* padding: var(--space-md); */
  /* border-inline-start: 4px solid var(--color-accent-strong);
  background: var(--color-accent-soft); */
  font-family: var(--font-mono);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}

/* .product__price:has(del) {
  row-gap: var(--space-xs);
  column-gap: var(--space-sm);
}
.product__price:has(del) {
  color: var(--color-accent-deep);
} */
 
.product__price del {
  order: 2;
  font-size: var(--fs-lg);
  font-weight: var(--fw-regular);
  color: var(--color-text-faint);
  text-decoration-thickness: 1px;
  text-decoration-color: var(--color-danger);
}
.product__price ins { text-decoration: none; }
/* The selling unit — the card's own <small> treatment, at the price row's
   scale. Faint, because it qualifies the figure rather than competing
   with it. */
.product__unit {
  font-size: var(--fs-sm);
  font-family: var(--font-mono);
  font-weight: var(--fw-regular);
  color: var(--color-text-faint);
}
.product__vat {
  order: 4;
  font-size: var(--fs-xs);
  font-family: var(--font-body);
  font-weight: var(--fw-regular);
  color: var(--color-text-muted);
}
.product__price .badge-sale {
  order: 3;
  padding: 5px var(--space-sm);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius-pill);
  background: var(--color-danger-soft);
  color: var(--color-danger-deep);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wide);
  line-height: 1;
  text-transform: uppercase;
}
/* The saving, stated rather than left to be worked out from two numbers.
   On the accent tint, which is where every other "good news" chip on the
   site already lives. */
.product__save {
  padding: 4px var(--space-sm);
  border-radius: var(--radius-pill);
  background: var(--color-accent-soft);
  color: var(--color-accent-deep);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* Availability. In-stock is .chip--accent, which already exists with
   exactly this treatment — the first version reinvented it with
   --color-accent-tint, a token whose own comment reserves it for the xTool
   band. Only the pre-order state needs a modifier of its own. */
.product__stock--pre {
  border-color: var(--brand-orange);
  background: var(--brand-orange-soft);
  color: var(--color-text);
}

/* ---------------------------------------------------------------- Buy row */
.buy {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding-block: var(--space-lg);
  border-block: var(--hairline);
}

/* ---- Variant options (colour + size) ----
   They sit between the rule under the price and the quantity stepper.
   .buy's own gap is 8px, sized for the row and the hint beneath it; the
   options are a separate decision from the quantity, so they take a step
   of their own rather than sitting 8px off the stepper.

   REUSE: .buy__label for both legends, so "Colour", "Size" and
   "Quantity" are one label treatment down the whole panel. */
.buy__options {
  /* Four tiles and their gaps fit the summary column from 360px up; the
     floor stops a two-character label collapsing into a square. */
  --tile-min:   64px;
  /* The circle inside the 44px target. The target is fixed by WCAG 2.5.5
     and does not move; what changes here is how much of it the circle
     fills. At 36 the selected ring lands exactly on the target's edge —
     2px of background then 2px of ink — so the ring is drawn INSIDE the
     hit area rather than outside it, which is what lets the swatches be
     packed tightly without one chip's ring reaching its neighbour. */
  --swatch-dot: 36px;
  /* Tighter than the tiles' --space-sm. A colour range reads as one
     field of colour, and wide gaps break it into scattered dots. Nothing
     is given up for it: the gap sits between the 44px TARGETS, not
     between the circles, so the targets still never touch. */
  --swatch-gap: 4px;

  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  margin-block-end: var(--space-md);
}

/* reset.css already strips the fieldset border and padding and gives
   <legend> a predictable display, so there is nothing to undo. */
.option { display: flex; flex-direction: column; gap: var(--space-sm); }
.option__legend { padding: 0; }
/* The chosen value, named in text. A colour identified only by the
   circle showing it would be a state told by colour alone (WCAG 1.4.1);
   product-details.js keeps this in step with the group. */
.option__value {
  margin-inline-start: var(--space-sm);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}
.option__set { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
/* Colours get a grid rather than the wrapping flex row the tiles use.
   A catalogue range can run to thirty finishes or more, and auto-fill
   holds every row in the same columns instead of leaving ragged ends
   that make a long range hard to scan. The track is the target, so the
   number of columns follows the width of the summary rail — eight on a
   desktop rail, six on a phone — and a range of any length wraps into
   it without a breakpoint or a cap. justify-content:start keeps a short
   range packed to the leading edge instead of spread across the column. */
.option__set--swatch {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--step-size));
  gap: var(--swatch-gap);
  justify-content: start;
}
.option__note { font-size: var(--fs-tiny); color: var(--color-text-muted); }
.option__note a {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- Colour: circles ----
   The <input> is absolutely positioned over the whole 44px label at
   opacity 0 — still the focusable, checkable control, so the arrow keys
   walk the group with no script, but invisible, which also means its
   own :focus-visible outline cannot draw. The dot below carries the
   focus and checked indicators instead. */
.swatch {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--step-size);
  block-size: var(--step-size);
  cursor: pointer;
}
.swatch__input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.swatch__dot {
  inline-size: 36px;
  block-size: 36px;
  border-radius: 50%;
  /* --swatch is set per option in the markup; the fallback keeps an
     unpainted chip visible rather than blank. */
  background: var(--swatch, var(--color-surface));
  /* Every dot carries the hairline or a white finish would be an
     invisible circle on a white panel. */
  border: var(--hairline-strong);
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.swatch:hover .swatch__dot { transform: scale(1.08); }
/* Chosen is a ring drawn outside the dot, never a change of fill: the
   dot is already spending colour on the finish it represents and cannot
   also use colour to report its own state. Ink, so it reads against
   every finish including the palest. */
.swatch__input:checked + .swatch__dot {
  box-shadow: 0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-text);
}
.swatch__input:focus-visible + .swatch__dot {
  box-shadow: 0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-accent-strong);
}
/* Focused AND chosen. Without this the two rules tie on specificity, the
   later one wins, and arrowing onto the swatch you already have selected
   replaces its "chosen" ink ring with the focus ring — the one moment
   you most need both facts at once. So they stack: ink inside, accent
   halo outside, still inside the 44px target. */
.swatch__input:checked:focus-visible + .swatch__dot {
  /* Three rings in the space two normally take, so the combined state
     still finishes within a pixel of the target's edge and cannot reach
     across the 4px gap to the chip beside it. */
  box-shadow:
    0 0 0 1px var(--color-bg),
    0 0 0 3px var(--color-text),
    0 0 0 5px var(--color-accent-strong);
}

/* ---- Size: tiles ---- */
.tile {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tile-min);
  min-block-size: var(--step-size);
  padding-inline: var(--space-md);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  background: var(--color-bg);
  cursor: pointer;
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}
.tile__input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.tile__label {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--color-text);
}
.tile:hover { border-color: var(--color-text); }
/* Chosen is the ink-on-white inversion .btn--primary already uses, so
   "this one" looks the same here as it does everywhere else on the site. */
.tile:has(.tile__input:checked) { background: var(--color-text); border-color: var(--color-text); }
.tile:has(.tile__input:checked) .tile__label { color: var(--color-text-inverse); }
/* :has() fallback — product-details.js sets .is-selected as well, exactly
   as .choice already does in components.css. */
.tile.is-selected { background: var(--color-text); border-color: var(--color-text); }
.tile.is-selected .tile__label { color: var(--color-text-inverse); }
.tile:has(.tile__input:focus-visible) { box-shadow: var(--focus-ring); }

/* Carried, but not sold online. Dashed edge and a struck label, with the
   reason in text beside it — the styling alone would be a state told by
   appearance only. */
.tile--unavailable { cursor: not-allowed; border-style: dashed; }
.tile--unavailable:hover { border-color: var(--color-border-strong); }
.tile--unavailable .tile__label { color: var(--color-text-faint); text-decoration: line-through; }

.buy__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-md);
}
.buy__field { display: flex; flex-direction: column; gap: var(--space-xs); }
/* .qty__value is a <span> in the drawer; here it is a real <input>, and an
   input carries a default `size` that stretched the field to 176px — a
   three-digit box six times wider than its content. Constrained so the
   stepper reads as the same compact control the drawer uses. */
.buy .qty__value {
  inline-size: 3.5em;
  padding-inline: 0;
  background: var(--color-bg);
  border-block: none;
}
.buy__label {
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-muted);
}
/* Natural width, not `flex: 1 1 200px`. Filling the row made a 286px green
   slab — far heavier than anything in the approved design, where
   .btn--cart's floor is 148px. */
.buy__add { flex: 0 0 auto; }
/* Add to Cart is .btn--lg (52px); .fav-btn is built at --step-size (44px).
   Side by side and bottom-aligned, that read as one button sitting 8px
   proud of the other. The row is a pair, so the pair matches — and it
   matches by taking .btn--lg's own figure rather than a new number. */
.buy__fav { min-block-size: 52px; }
/* Under the row, not beside the button: alongside it the hint had nothing
   to align to and read as a detached fragment floating in the column. */
.buy__max { font-size: var(--fs-tiny); color: var(--color-text-muted); }

/* ---- Second action ----
   Equipment at this price is routinely bought against a written quotation,
   so the alternative to the bag is a real destination. */
.product__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
}
.product__quote { flex: 0 0 auto; }
.product__call {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-block-size: var(--step-size);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-muted);
  transition: color var(--transition-fast);
}
.product__call .icon { color: var(--color-accent-strong); }
.product__call:hover { color: var(--color-text); }

/* ---------------------------------------------------------------- Assurances */
/* .assurances / .assurance moved to css/shared/components.css when the
   bag summary became the second page to use them — a page stylesheet
   may never be a dependency of another page. The four-across default
   is unchanged, and its responsive collapse went with it — a component
   that only reflows correctly on one page is not a component. */

/* ---------------------------------------------------------------- Share */
.product__share {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}
.product__share-label {
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-inline-end: var(--space-xs);
}
/* Same object as .scroller__arrow — hairline square, ink fill on hover —
   at the 44px target floor. */
.product__share-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 44px;
  block-size: 44px;
  border: var(--hairline);
  border-radius: var(--radius-control);
  color: var(--color-text-muted);
  transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
.product__share-btn:hover {
  background: var(--color-text);
  border-color: var(--color-text);
  color: var(--color-text-inverse);
}
.product__share-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.product__share-said {
  font-size: var(--fs-tiny);
  color: var(--color-accent-deep);
}

/* ============================================================
   INFORMATION — one tabbed band at full container width.
   .tabs / .tab come from components.css; only the band and the panels
   are declared here.
   ============================================================ */
/* No rule of its own at the top: the assurance strip immediately above
   already closes with one, and two hairlines 64px apart is a stripe. */
.product__info { margin-block-start: var(--space-2xl); }
/* The panel is the tab row's own surface: no box, no card — the tab pills
   are already the visible edge of this block. */
.product__panel:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* Description: prose beside its highlight list, so neither runs the full
   1200px of the band. */
.product__panel--split {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: var(--space-2xl);
  align-items: start;
}
.product__prose { max-inline-size: var(--measure); }
.product__prose p { font-size: var(--fs-md); color: var(--color-text-muted); }
.product__prose p + p { margin-block-start: var(--space-md); }

.highlights {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-lg);
  background: var(--color-surface);
  border: var(--hairline);
}
.highlight {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--color-text);
}
.highlight .icon {
  flex: none;
  inline-size: 16px;
  block-size: 16px;
  color: var(--color-accent-strong);
  margin-block-start: 3px;
}

/* Specification. .spec-row is the drawer's summary row, reused verbatim;
   the table is held to a readable measure rather than stretched across the
   whole band, where a two-word value would sit a metre from its label. */
.specs { max-inline-size: 760px; }

/* Delivery & Returns — three policy blocks, each linking to the clause in
   terms.html rather than restating it in full. */
.policy {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2xl);
}
.policy__title {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text);
  margin-block-end: var(--space-sm);
}
.policy__item p { font-size: var(--fs-sm); color: var(--color-text-muted); }
.policy__link { margin-block-start: var(--space-md); }
.policy__link a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-accent-deep);
}
.policy__link a:hover { color: var(--color-text); }

/* ---------------------------------------------------------------- Related */
.related { border-block-start: var(--hairline); padding-block: var(--space-2xl); }
/* ---- Reserve one card row before the API answers -----------------------
   .showcase-card's own 480/734 ratio applied to the column width the
   --wide ladder gives at each step. It is derived from the same numbers
   the ladder uses rather than being a measured constant, so the two cannot
   drift apart, and it costs nothing once the cards are in: they are taller
   than nothing and the min simply stops applying. */
.related .scroller__track { min-block-size: calc(25vw * 734 / 480); }
@media (max-width: 1199px) {
  .related .scroller__track { min-block-size: calc((100vw / 3) * 734 / 480); }
}
@media (max-width: 991px) {
  .related .scroller__track { min-block-size: calc(50vw * 734 / 480); }
}
@media (max-width: 767px) {
  .related .scroller__track { min-block-size: calc(85vw * 734 / 480); }
}
/* The head is a kicker over a title, so it needs a column of its own inside
   the scroller's baseline-aligned head row. */
.scroller__heading { display: flex; flex-direction: column; gap: var(--space-xs); }

/* ============================================================
   MOBILE BUY BAR
   Below 992px the buy row scrolls out of reach long before the page ends,
   so the price and the primary action come back as fixed chrome. It is
   display:none from 992px up — there it would be covering content that is
   already on screen.
   ============================================================ */
.product-bar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-block-size: var(--product-bar-block);
  padding: var(--space-sm) var(--space-md);
  background: var(--color-bg);
  border-block-start: var(--hairline-strong);
  box-shadow: var(--shadow-lg);
  transform: translateY(100%);
  transition: transform var(--transition-base);
}
.product-bar.is-visible { transform: translateY(0); }
.product-bar__info { flex: 1 1 auto; min-inline-size: 0; }
.product-bar__name {
  font-size: var(--fs-sm);
  color: var(--color-text);
  /* One line: the bar is chrome, not a second product summary. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.product-bar__price {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}
.product-bar__add { flex: 0 0 auto; }

@media (min-width: 992px) {
  .product-bar { display: none; }
}
/* Step the back-to-top button over the bar instead of behind it. */
@media (max-width: 991px) {
  body.has-product-bar .back-to-top {
    inset-block-end: calc(var(--product-bar-block) + var(--space-md));
  }
}

/* ---------------------------------------------------------------- States */
.product__skeleton { display: grid; grid-template-columns: minmax(0, 1.28fr) minmax(0, 1fr); gap: var(--space-2xl); }
/* .skeleton-block / .skeleton-text shimmer is in components.css. */
/* Matched to the real frame by construction: same aspect-ratio token path,
   so the summary column cannot jump when the product lands. */
.skeleton-block { aspect-ratio: 5 / 4; }
.product__skeleton-col { display: flex; flex-direction: column; gap: var(--space-md); }
.skeleton-text--title { block-size: 2.4em; font-size: var(--fs-3xl); }
.skeleton-text--sku   { inline-size: 30%; font-size: var(--fs-xs); }
.skeleton-text--desc  { block-size: 3.2em; font-size: var(--fs-md); }
.skeleton-text--price { inline-size: 45%; font-size: var(--fs-2xl); block-size: 1.4em; }
.skeleton-text--buy   { block-size: 46px; }

/* ---------------------------------------------------------------- Breakpoints */
@media (max-width: 1199px) {
  .policy { gap: var(--space-xl); }
}

@media (max-width: 991px) {
  /* One column, in DOM order. */
  .product__layout,
  .product__skeleton,
  .product__panel--split,
  .policy {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xl);
  }
  .specs { max-inline-size: none; }
}

@media (max-width: 767px) {
  /* On a phone the primary action does take the full width — there is no
     room for it to sit beside the stepper. */
  .buy__row { align-items: stretch; }
  .buy__add { flex: 1 1 100%; }
  .product__quote { flex: 1 1 100%; }
  .product__info { margin-block-start: var(--space-xl); }
}

@media (prefers-reduced-motion: reduce) {
  .gallery__media { transition: none; }
  .product-bar { transition: none; }
  .skeleton-block, .skeleton-text { animation: none; }
}

/* ============================================================
   IMAGE LIGHTBOX
   The drawer's motion vocabulary, on the other axis: the same
   --scrim-backdrop, the same --dur-drawer/--ease-out, the same
   --z-overlay. It scales up from 98% instead of sliding in from an
   edge, because it has no edge to belong to.
   ============================================================ */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
}
.lightbox__scrim {
  position: absolute;
  inset: 0;
  background: var(--scrim-backdrop);
  opacity: 0;
  transition: opacity var(--dur-drawer) var(--ease-out);
}
.lightbox.is-open .lightbox__scrim { opacity: 1; }

.lightbox__panel {
  position: absolute;
  inset: 0;
  display: grid;
  /* An explicit minmax(0, 1fr) track, not just place-items. The panel has a
     definite height (inset: 0), but an auto row takes the IMAGE's height,
     and `max-block-size: 100%` on the image then resolves against that same
     height — i.e. against itself. A 1065x1600 photograph came out 1947px
     tall inside an 1100px viewport. The 1fr track is definite, so the
     percentage has something real to measure. */
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  /* Room for the close control and the counter, so neither ever sits on
     top of the photograph. */
  padding: calc(var(--step-size) + var(--space-md)) var(--space-md);
  opacity: 0;
  transform: scale(.98);
  transition: opacity var(--dur-drawer) var(--ease-out),
              transform var(--dur-drawer) var(--ease-out);
}
.lightbox.is-open .lightbox__panel { opacity: 1; transform: scale(1); }
.lightbox__panel:focus-visible { outline: none; }

.lightbox__img {
  max-inline-size: 100%;
  max-block-size: 100%;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
  background: var(--color-surface);
  box-shadow: var(--shadow-lg);
}

/* Controls sit on the scrim, not on the picture. */
.lightbox__close,
.lightbox__step {
  position: absolute;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--step-size);
  block-size: var(--step-size);
  border: var(--hairline);
  border-radius: var(--radius-control);
  background: var(--color-bg);
  color: var(--color-text);
  box-shadow: var(--shadow-md);
  transition: background-color var(--transition-fast), color var(--transition-fast);
}
.lightbox__close:hover,
.lightbox__step:hover {
  background: var(--color-text);
  color: var(--color-text-inverse);
}
.lightbox__close:focus-visible,
.lightbox__step:focus-visible { outline: 2px solid var(--color-accent-strong); outline-offset: 3px; }

.lightbox__close {
  inset-block-start: var(--space-md);
  inset-inline-end: var(--space-md);
}
.lightbox__step { inset-block-start: 50%; transform: translateY(-50%); }
.lightbox__step--prev { inset-inline-start: var(--space-md); }
.lightbox__step--next { inset-inline-end: var(--space-md); }

.lightbox__counter {
  position: absolute;
  inset-block-end: var(--space-md);
  inset-inline-start: 50%;
  transform: translateX(-50%);
  padding: 4px 10px;
  border-radius: var(--radius-control);
  background: var(--color-ink-scrim);
  color: var(--color-text-inverse);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
}

@media (max-width: 575px) {
  /* The steps move to the bottom corners: at 50% they overlay the picture
     on a phone, where the picture is the full width of the screen. */
  .lightbox__step { inset-block-start: auto; inset-block-end: var(--space-md); transform: none; }
  .lightbox__panel { padding-block-end: calc(var(--step-size) + var(--space-xl)); }
}

@media (prefers-reduced-motion: reduce) {
  .lightbox__scrim, .lightbox__panel { transition: none; }
  .lightbox__panel { transform: none; }
}
