/* ============================================================
   components.css — Flex Europa · finaldesign
   Universal components used across sections. Scaffold needs the
   button system, icon helper, and the mono spec chip; the product
   card / tabs / scroller components arrive in their own turns.
   ============================================================ */

/* ---- Icon helper ---- */
.icon {
  inline-size: 1.25em;
  block-size: 1.25em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

.icon--sm {
  inline-size: 1em;
  block-size: 1em;
}

.icon--lg {
  inline-size: 1.5em;
  block-size: 1.5em;
}

/* Some marks are solid (social glyphs) — opt into fill */
.icon--filled {
  fill: currentColor;
  stroke: none;
}

/* ============================================================
   Buttons — sharp→soft industrial controls
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  padding-inline: var(--space-lg);
  min-block-size: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-wide);
  line-height: 1;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
  transition: background-color var(--transition-fast),
    color var(--transition-fast),
    border-color var(--transition-fast),
    box-shadow var(--transition-fast),
    transform var(--transition-fast);
}

.btn:focus-visible {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: 3px;
}

.btn:disabled,
.btn.is-loading {
  opacity: .6;
  cursor: not-allowed;
  pointer-events: none;
}

.btn .icon {
  inline-size: 1.15em;
  block-size: 1.15em;
}

.btn--lg {
  min-block-size: 52px;
  padding-inline: var(--space-xl);
  font-size: var(--fs-md);
}

.btn--sm {
  min-block-size: 40px;
  padding-inline: var(--space-md);
  font-size: var(--fs-xs);
}

/* Primary = ink (near-black) — the confident industrial default */
.btn--primary {
  background: var(--color-text);
  color: var(--color-text-inverse);
  border-color: var(--color-text);
}

.btn--primary:hover {
  background: var(--color-ink-scrim);
  border-color: var(--color-ink-scrim);
  transform: translateY(-1px);
}

/* Accent = teal-strong filled — the single accent action */
.btn--accent {
  background: var(--color-accent-strong);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-strong);
}

.btn--accent:hover {
  background: var(--color-accent-deep);
  border-color: var(--color-accent-deep);
  transform: translateY(-1px);
}

/* Brand = green filled, WHITE text (per the on-green rule). Used for the
   green CTAs, e.g. "Talk to a specialist".
   Same correction as .btn--cart below, and for the same reason: white on
   the logo teal --color-accent (#00A888) is 3.02:1, which fails AA for
   normal text. --color-accent-strong (#0B7D6E) is 5.03:1 and was already
   this button's hover fill. Hover moves down to --color-accent-deep. The
   logo teal keeps every one of its other jobs — it is still the focus
   ring, the rule accents and the on-dark text colour, where it is used
   against surfaces it actually passes on. */
.btn--brand {
  background: var(--color-accent-strong);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-strong);
}

.btn--brand:hover {
  background: var(--color-accent-deep);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-deep);
  transform: translateY(-1px);
}

/* Cart action — brand GREEN fill with white text (per the on-colour rule). Drives a
   click → spinner → "Item Added" → reset flow (home.js). min-width keeps the button
   from jittering as the label swaps between states. */
/* --brand-green-deep at rest, not --brand-green. White on the logo teal
   #00A888 measures 3.02:1 — it is the one surface on the site that failed
   AA for normal text, and the only audit Lighthouse still reported against
   this page. #0B7D6E is 5.03:1, is already in the palette, and was already
   this button's own hover colour, so the button stays white-on-green as
   the brand requires; it is a shade darker at rest. Hover moves down to
   --color-accent-deep (6.50:1) so the state change is still visible. */
.btn--cart {
  background: var(--brand-green-deep);
  color: var(--color-text-inverse);
  border-color: var(--brand-green-deep);
  min-inline-size: 148px;
}

.btn--cart:hover {
  background: var(--color-accent-deep);
  border-color: var(--color-accent-deep);
  transform: translateY(-1px);
}

.btn--cart.is-added {
  background: var(--color-accent-deep);
  border-color: var(--color-accent-deep);
  transform: none;
}

/* Disabled during the flow only (blocks double-clicks) — stay green, not faded */
.btn--cart:disabled {
  opacity: 1;
  cursor: default;
  transform: none;
}

/* Spinner shown inside the cart button while "adding" */
.btn__spinner {
  inline-size: 18px;
  block-size: 18px;
  border: 2px solid rgba(255, 255, 255, .45);
  border-block-start-color: var(--color-text-inverse);
  border-radius: 50%;
  animation: btn-spin .7s linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn__spinner {
    animation-duration: 1.4s;
  }
}

/* Secondary = outlined on light */
.btn--secondary {
  background: transparent;
  color: var(--color-text);
  border-color: var(--color-border-strong);
}

.btn--secondary:hover {
  border-color: var(--color-text);
  background: var(--color-surface);
}

/* Ghost = quiet text button on light */
.btn--ghost {
  background: transparent;
  color: var(--color-accent-strong);
  border-color: transparent;
}

.btn--ghost:hover {
  color: var(--color-accent-deep);
  background: var(--color-accent-soft);
}

/* Inverse = for dark surfaces (hero glass / footer) */
.btn--inverse {
  background: var(--color-text-inverse);
  color: var(--color-text);
  border-color: var(--color-text-inverse);
}

.btn--inverse:hover {
  background: rgba(255, 255, 255, .88);
  transform: translateY(-1px);
}

/* Ghost-inverse = outlined light button on the hero video / dark bands */
.btn--ghost-inverse {
  background: rgba(255, 255, 255, .06);
  color: var(--color-text-inverse);
  border-color: rgba(255, 255, 255, .55);
  backdrop-filter: blur(2px);
}

.btn--ghost-inverse:hover {
  background: rgba(255, 255, 255, .14);
  border-color: var(--color-text-inverse);
  transform: translateY(-1px);
}

/* Icon-only button — ≥44px touch target */
.btn--icon {
  min-inline-size: 44px;
  min-block-size: 44px;
  padding: 0;
  border-radius: var(--radius-control);
  gap: 0;
}

/* Add-to-cart confirmed state (visual-only feedback) */
.btn.is-added {
  background: var(--color-accent-strong);
  color: var(--color-text-inverse);
  border-color: var(--color-accent-strong);
}

/* ============================================================
   Sticky social rail (§6) — fixed right edge, slide-out labels.
   Sits below overlays/sticky header in z-index. Hidden < lg so it
   never covers content (footer carries social on small screens).
   ============================================================ */
.social-rail {
  position: fixed;
  inset-inline-end: 0;
  inset-block-start: 50%;
  transform: translateY(-50%);
  z-index: var(--z-dropdown);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Each item is a FIXED 44px square — it never resizes or shifts on hover
   (that size change was the "jump"). Only the background colour animates. */
.social-rail__link {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 44px;
  block-size: 44px;
  background: var(--footer-band);
  color: var(--color-text-inverse);
  transition: background-color var(--transition-fast);
}

.social-rail__link:hover,
.social-rail__link:focus-visible {
  background: var(--color-accent-strong);
}

.social-rail__link .icon {
  inline-size: 20px;
  block-size: 20px;
  stroke-width: 2.5;
  color: var(--color-text-inverse);
}

/* Label = a tag pinned to the LEFT of the square and taken OUT OF FLOW, so
   revealing it can't change the square's size or position. Fades in only. */
.social-rail__label {
  position: absolute;
  inset-inline-end: 100%;
  /* sits just left of the icon square */
  inset-block: 0;
  display: flex;
  align-items: center;
  padding-inline: 12px;
  background: var(--color-accent-strong);
  color: var(--color-text-inverse);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--transition-fast), visibility 0s linear var(--transition-fast);
}

.social-rail__link:hover .social-rail__label,
.social-rail__link:focus-visible .social-rail__label {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-fast);
}

@media (prefers-reduced-motion: reduce) {

  .social-rail__link,
  .social-rail__label {
    transition: none;
  }
}

@media (max-width: 991px) {
  .social-rail {
    display: none;
  }
}

/* ============================================================
   Back-to-top — bottom-inline-end; stacks BELOW the social rail
   (rail is vertically centred, this sits in the corner → no collision)
   ============================================================ */
.back-to-top {
  position: fixed;
  inset-block-end: var(--space-lg);
  inset-inline-end: var(--space-lg);
  z-index: var(--z-dropdown);
  inline-size: 48px;
  block-size: 48px;
  border-radius: var(--radius-pill);
  background: var(--color-text);
  color: var(--color-text-inverse);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity var(--transition-base), transform var(--transition-base), background-color var(--transition-fast), visibility 0s linear var(--transition-base);
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity var(--transition-base), transform var(--transition-base), background-color var(--transition-fast);
}

.back-to-top:hover {
  background: var(--color-accent-strong);
}

.back-to-top:focus-visible {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: 3px;
}

/* ============================================================
   Spec chip / benefit tag (mono) — reused by hero + cards
   ============================================================ */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 5px var(--space-sm);
  border: var(--hairline);
  border-radius: var(--radius-chip, 2px);
  background: var(--color-surface);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-muted);
  text-transform: uppercase;
}

/* --color-accent-deep on the soft wash. --color-accent-strong measured
   4.49:1 against #E7F5F1 — one hundredth under AA, which is still under.
   5.80:1. */
.chip--accent {
  border-color: var(--color-accent);
  color: var(--color-accent-deep);
  background: var(--color-accent-soft);
}

/* Two more states the workspace needs and the catalogue never did.
   --color-danger-deep on --color-danger-soft is the pair already proved
   for .form-status--error; the warn pair is the product page's own
   pre-order chip, which is --brand-orange-soft under ink. Both carry
   their meaning in the word inside them, never in the fill alone. */
.chip--danger {
  border-color: var(--color-danger);
  color: var(--color-danger-deep);
  background: var(--color-danger-soft);
}

.chip--warn {
  border-color: var(--brand-orange);
  color: var(--color-text);
  background: var(--brand-orange-soft);
}

.chip--inverse {
  border-color: rgba(255, 255, 255, .28);
  background: rgba(255, 255, 255, .06);
  color: rgba(255, 255, 255, .9);
}

/* ============================================================
   Spec detail row — hairline "documentation" rows (hero stats,
   why-us strip, cards). Label (mono, muted) → value (bold).
   ============================================================ */
.spec-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-sm);
  border-block-start: var(--hairline);
}

.spec-row__label {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.spec-row__value {
  font-family: var(--font-heading);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}

/* ============================================================
   Category card — spec-styled, hairline, mono index.
   Monochrome by default → colour + zoom on hover/focus (the
   industrial "documentation comes alive" treatment). Whole card
   is one link (stretched-link pattern) = a single tab stop.
   Reused on the Categories page downstream (§13).
   ============================================================ */
.category-card {
  position: relative;
  display: flex;
  flex-direction: column;
  block-size: 100%;
  background: var(--color-bg);
  border: var(--hairline);
  transition: border-color var(--transition-base),
    box-shadow var(--transition-base),
    transform var(--transition-base);
}

.category-card:hover,
.category-card:focus-within {
  border-color: var(--color-accent);
  box-shadow: var(--shadow-hover);
  transform: translateY(-3px);
}

.category-card__media {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--color-surface-alt);
}

/* The <picture> wrapper must not become the percentage-height containing
   block: .category-card__img is sized 100%/100% against the media box, and
   an inline <picture> in between would resolve that against auto. Both card
   pictures are declared here together so the pair cannot drift.
   (.showcase-card__img is position:absolute and is unaffected either way —
   it is listed for the same reason.) */
.category-card__picture,
.showcase-card__picture {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}

.category-card__img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.04) brightness(.97);
  transition: filter var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

.category-card:hover .category-card__img,
.category-card:focus-within .category-card__img {
  filter: grayscale(0) contrast(1);
  transform: scale(1.05);
}

/* mono index chip, top-inline-start over the media */
.category-card__index {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  padding: 6px 10px;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  color: var(--color-text-inverse);
  background: rgba(10, 12, 14, .62);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}

.category-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: 1;
  padding: var(--space-lg);
}

.category-card__title {
  font-size: var(--fs-xl);
  color: var(--color-text);
}

.category-card__meta {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-muted);
  line-height: 1.6;
}

.category-card__link {
  /* 44px floor. The label does not move — only the hit area grows
     around it, centred. */
  min-block-size: var(--step-size);
  align-items: center;
  margin-block-start: auto;
  padding-block-start: var(--space-md);
  border-block-start: var(--hairline);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-accent-strong);
}

.category-card__link .icon {
  transition: transform var(--dur-base) var(--ease-out);
}

.category-card:hover .category-card__link .icon,
.category-card:focus-within .category-card__link .icon {
  transform: translateX(4px);
}

/* stretched link — the whole card is clickable, one tab stop */
.category-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.category-card__link:focus-visible {
  outline: none;
}

.category-card:focus-within {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: 3px;
}

/* ============================================================
   Product card — THE site content object (§11). Reused verbatim
   on the product listing + detail pages downstream. xTool-style
   treatment: rounded grey benefit tag, product image on white,
   name + one-line desc, AED price (mono), and TWO buttons —
   (View Details) filled + (Add to Cart) outline.
   ============================================================ */
.product-card {
  display: flex;
  flex-direction: column;
  block-size: 100%;
  inline-size: 100%;
  background: var(--color-bg);
  border: var(--hairline);
  transition: border-color var(--transition-base),
    box-shadow var(--transition-base),
    transform var(--transition-base);
}

.product-card:hover,
.product-card:focus-within {
  border-color: var(--color-accent);
  box-shadow: var(--shadow-hover);
  transform: translateY(-3px);
}

.product-card__media {
  position: relative;
  aspect-ratio: 1 / 1;
  /* fixed SQUARE — uniform across every card */
  background: var(--color-text-inverse);
  border-block-end: var(--hairline);
  overflow: hidden;
  /* clip the crop + hover zoom */
}

.product-card__img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* crop non-square photos to fill the square */
  object-position: center;
  transition: transform var(--dur-slow) var(--ease-out);
}

.product-card:hover .product-card__img {
  transform: scale(1.04);
}

/* rounded grey benefit eyebrow (top-inline-start) */
.product-card__tag {
  position: absolute;
  inset-block-start: var(--space-sm);
  inset-inline-start: var(--space-sm);
  max-inline-size: calc(100% - var(--space-md) * 2);
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--brand-blue);
  /* blue badge */
  color: var(--color-text-inverse);
  /* white text on blue */
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* offer badge (top-inline-end) */
.product-card__badge {
  position: absolute;
  inset-block-start: var(--space-sm);
  inset-inline-end: var(--space-sm);
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: var(--color-danger);
  color: var(--color-text-inverse);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
}

.product-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: 1;
  padding: var(--space-lg);
}

.product-card__title {
  font-size: var(--fs-lg);
  color: var(--color-text);
}

.product-card__desc {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
}

.product-card__footer {
  margin-block-start: auto;
  padding-block-start: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.product-card__price {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--color-text);
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
}

/* --color-text-muted, not --color-text-faint: the faint token is
   documented in tokens.css as large-text-only (~4.0:1) and this is
   --fs-sm. 3.90:1 -> 6.78:1; still visibly secondary to the live price. */
.product-card__price del {
  color: var(--color-text-muted);
  font-size: var(--fs-sm);
}

.product-card__price ins {
  text-decoration: none;
  color: var(--color-danger);
}

.product-card__price small {
  font-size: var(--fs-xs);
  color: var(--color-text-faint);
  font-weight: var(--fw-regular);
}

.product-card__actions {
  display: flex;
  gap: var(--space-sm);
}

.product-card__actions .btn {
  flex: 1 1 0;
  min-block-size: 44px;
  padding-inline: var(--space-sm);
  font-size: var(--fs-xs);
}

/* ============================================================
   SHOWCASE CARD — the full-width "Featured" row (xTool treatment).
   Large product image on white, grey eyebrow tag above it, name +
   one-line desc, AED price (mono) and the two standard buttons.
   Borderless: the 3-up grid + dividers live in .scroller--wide
   (home.css). Buttons sit left-aligned and size to content.
   ============================================================ */
.showcase-card {
  display: flex;
  flex-direction: column;
  inline-size: 100%;
  /* ============================================================
     4-UP RATIO LOCK (manifest items 6, 7, 8)
     Measured from the 3-up build: W3 = 480px, H3 = 733.78px,
     so R = W3 / H3 = 0.65415.
     The ratio is declared here as `aspect-ratio` rather than
     recomputed as pixel heights, so R holds at EVERY card width
     and every breakpoint (4 / 3 / 2 / 1 up) for free. The card is
     sized by WIDTH only — flex-basis on .scroller__item — and the
     height follows. __media is the single flexible child, so it
     absorbs whatever the fixed content leaves; every other child
     reserves a fixed line count so all cards in a row resolve to
     the same media height and the row bottom stays flush.
     ============================================================ */
  aspect-ratio: 480 / 734;
  block-size: auto;
  /* an explicit height would beat aspect-ratio */
  min-block-size: 0;
  /* the automatic minimum size would otherwise
                                     let content push the card past the ratio */
  padding: var(--space-lg);
  /* stepped down one token from --space-xl for the narrower card */
  background: transparent;
}

.showcase-card__eyebrow {
  align-self: flex-start;
  max-inline-size: 100%;
  margin-block-end: var(--space-md);
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  background: var(--brand-blue);
  /* blue badge */
  color: var(--color-text-inverse);
  /* white text on blue */
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.showcase-card__media {
  position: relative;
  /* No aspect-ratio of its own: the CARD now owns R (see above) and this is the
     one flexible child, so it takes the remainder. All cards in a row are the
     same width, and every other child reserves a fixed height, so every media
     box in a row resolves to the same size. */
  flex: 1 1 0;
  /* basis 0 — take the remainder, not the image's size */
  min-block-size: 0;
  /* let it actually shrink inside the flex column */
  overflow: hidden;
  /* clip the crop + hover zoom */
}

/* The image is taken OUT OF FLOW so it can never size the media box. With
   flex-basis:auto the box inherited each photo's natural aspect ratio, which
   made every card in a row a different height. */
.showcase-card__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* crop the photo to fill the box */
  object-position: center;
  transition: transform var(--dur-slow) var(--ease-out);
}

.showcase-card:hover .showcase-card__img,
.showcase-card:focus-within .showcase-card__img {
  transform: scale(1.04);
}

/* Discount badge overlay (Offers) — top-inline-end of the square image */
/* Discount badge overlay (Offers) — top-inline-end of the square image */
.showcase-card__badge {
  position: absolute;
  inset-block-start: var(--space-sm);
  inset-inline-end: var(--space-sm);
  z-index: 1;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: var(--color-danger);
  color: var(--color-text-inverse);
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
}

.showcase-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  flex: none;
  /* fixed block — __media is what flexes */
  padding-block-start: var(--space-md);
}

/* Title and desc each RESERVE two lines and ellipsis anything longer. Without the
   reservation a one-line name and a two-line name would leave different amounts of
   space for the media, and the row would go ragged. */
.showcase-card__title {
  font-size: var(--fs-lg);
  /* one token down from --fs-xl for the narrower card */
  line-height: 1.15;
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  min-block-size: 2.3em;
  /* 2 lines x 1.15 line-height */
}

.showcase-card__desc {
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--color-text-muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  min-block-size: 3.1em;
  /* 2 lines x 1.55 line-height */
}

.showcase-card__price {
  margin-block-start: var(--space-sm);
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--color-text);
}

.showcase-card__price small {
  font-size: var(--fs-xs);
  color: var(--color-text-faint);
  font-weight: var(--fw-regular);
}

.showcase-card__price del {
  color: var(--color-text-muted);
  font-size: var(--fs-sm);
  text-decoration: line-through;
}

.showcase-card__price ins {
  text-decoration: none;
  color: var(--color-danger);
}

.showcase-card__actions {
  margin-block-start: var(--space-md);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* Both buttons stay side by side in the narrower card by sharing the row and
   stepping padding/type down — the same treatment .product-card already uses.
   The 46px block size (>= the 44px touch target) is untouched. */
.showcase-card__actions .btn {
  flex: 1 1 0;
  min-block-size: 46px;
  padding-inline: var(--space-sm);
  font-size: var(--fs-xs);
}

/* ---- Side by side, measured -------------------------------------------
   `flex-wrap: wrap` above is a safety valve, and inside .product-grid it
   was firing on every desktop width: a 3-up card is ~270px, its content
   box ~222px, and VIEW DETAILS + ADD TO CART need 263px at the base
   tracking and padding — so the second button dropped to its own row and
   the card grew 54px. The row is what has to fit, so the row is what
   gives: no wrap, tighter padding, and the uppercase tracking relaxed to
   normal. Type size and the 46px target are unchanged. */
.product-grid .showcase-card__actions {
  flex-wrap: nowrap;
}

.product-grid .showcase-card__actions .btn {
  min-inline-size: 0;
  padding-inline: var(--space-xs);
  letter-spacing: var(--tracking-normal);
}

/* ============================================================
   CROSS-PAGE BLOCKS (moved here from css/pages/products.css)
   .page-head appears on every internal page, .breadcrumb on most,
   and .product-grid on both the catalogue list and the related-products
   strip. components.css is already in every page's cascade, so sharing
   them here costs no extra request and keeps ONE implementation (§8).
   None of these classes appear on the approved home page, so moving
   them cannot affect it.
   ============================================================ */

/* ---------------------------------------------------------------- Page head */
.page-head {
  padding-block: var(--space-xl) var(--space-lg);
  border-block-end: var(--hairline);
  background: var(--color-surface);
}

.page-head__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* products.html puts the trail UNDER the heading. It is the title's trail,
   so it sits closer to the title than to the lead — with the container's
   even 8px gap on both sides it read as belonging to neither. */
.page-head__crumbs {
  margin-block-end: var(--space-xs);
}

.page-head__title {
  font-family: var(--font-heading);
  font-size: clamp(var(--fs-2xl), 4vw, var(--fs-3xl));
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text);
}

.page-head__lead {
  font-size: var(--fs-md);
  color: var(--color-text-muted);
  max-inline-size: var(--measure);
}

/* ---------------------------------------------------------------- Breadcrumb
   Engineering Fix 6: margin and padding explicitly zeroed on the
   component itself, not just in the reset, so a collapsing margin can
   never reintroduce phantom spacing above the next block. */
.breadcrumb {
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  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;
}

.breadcrumb__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* A full 44px target on a short crumb like "Home" (it measured 33x18),
   grown from the tokens and pulled back out of the flow so the crumb row
   keeps its height. Same technique as .filters__clear. */
.breadcrumb__link,
.breadcrumb__current {
  display: inline-flex;
  align-items: center;
  line-height: 1.5;
  min-inline-size: var(--step-size);
  min-block-size: var(--step-size);
  justify-content: center;
  margin-block: calc((var(--step-size) - 1.5em) / -2);
}

.breadcrumb__link {
  color: var(--color-text-muted);
}

.breadcrumb__link:hover {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.breadcrumb__current {
  color: var(--color-text);
}

.breadcrumb__separator {
  color: var(--color-text-faint);
}

[dir="rtl"] .breadcrumb__separator {
  transform: scaleX(-1);
}

/* ---------------------------------------------------------------- Grid
   4 across ≥1200 · 3 at 992–1199 · 2 at 768–991 · 1 below (§3.1).
   .showcase-card keeps its own aspect-ratio, so the cards stay in
   proportion at every count without this file touching the card. */
.product-grid {
  display: grid;
  /* Three across, not four. At four the column is ~218px wide, and the
     card's aspect-ratio then resolves to ~333px of height — less than the
     eyebrow + two-line title + two-line description + price + two stacked
     buttons already need, so the flexible media box was squeezed to zero
     and the photograph disappeared completely. Three gives the card the
     width it was designed at. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Declared once, here, because the card's own padding is DERIVED from
     it — see .product-grid .showcase-card below. Change the gutter and
     the padding follows; they cannot drift apart. */
  --grid-gutter: var(--space-lg);
  gap: var(--grid-gutter);
  padding-block: var(--grid-gutter);
}

/* A closed hairline box on every side, not the scroller's open dividers.
   The divider treatment (border-inline-start + border-block-end, first
   column transparent) draws a rule only where two cards MEET, so the
   outer edges of the grid stayed open and the leftmost column had no
   left rule at all — the cards read as a ruled table rather than as
   tiles. A full border needs a real gap between cells, otherwise
   neighbours double up into a 2px line. Same hairline token, same
   --radius-card: 0. */
.product-grid>* {
  border: var(--hairline);
}

/* ---- The card inside a grid --------------------------------------------
   In the home page's scroller a card is ~480px wide, so its 480/734 ratio
   leaves the media box plenty of height. In a sidebar-constrained grid the
   card is far narrower and that same ratio starves the media. Inside
   .product-grid the MEDIA owns a ratio and the card's height follows its
   content, which guarantees the photograph is always visible at every
   column count. The scroller cards on the approved home page are
   untouched — this is scoped to .product-grid, which does not exist there. */
.product-grid .showcase-card {
  aspect-ratio: auto;
}

/* A grid card is drawn with a border on all four sides, so its padding is
   a visible white margin between that border and the content. It used to
   be a full 24px — what the BORDERLESS scroller card on the home page
   needs to keep its neighbours apart, and what a bordered tile does not.

   Half the gutter, so the inside of a card and the space between two of
   them are the same measure: content, 12px, border, 24px, border, 12px,
   content. Written as the gutter halved rather than as 12px, both because
   12 is not on the spacing scale and because the two numbers have to move
   together — a gutter change at some future breakpoint takes the padding
   with it. The scroller cards are untouched: .product-grid does not exist
   on the home page. */
.product-grid .showcase-card {
  padding: calc(var(--grid-gutter) / 2);
}

/* 5/4, not 4/3. Measured on the approved home page, the card's flexible
   media box resolves to 311x251.7 — a ratio of 1.236. A 4/3 box (1.333)
   is wider than that, and these are TALL portrait product photographs
   (e.g. 1065x1600), so `object-fit: cover` cropped a thin horizontal band
   out of the middle and the slice came back almost entirely background:
   the photo looked missing even though it had loaded. 5/4 is 1.25, within
   1.2% of the approved crop, so the grid frames the product the same way
   the home page does. */
.product-grid .showcase-card__media {
  flex: none;
  aspect-ratio: 5 / 4;
}

/* ============================================================
   FORM FIELDS
   New component (§6 justification: no form styling existed anywhere in
   finaldesign or its siblings — reset.css only makes controls inherit
   the page font). Shared here rather than written into contact.css,
   checkout-delivery and checkout-payment separately.

   Built from existing tokens only. Every control is at least
   --step-size tall, and every text input is --fs-md (16px) so iOS does
   not zoom the page when it takes focus.

   ACCESSIBILITY CONTRACT the markup must honour:
     · every control has a real <label for>, never a placeholder as label
     · a hint is wired with aria-describedby
     · an error is wired with aria-describedby too, and the control gets
       aria-invalid="true"; the message is text, never colour alone
   ============================================================ */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-inline-size: 0;
}

.field__label {
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* The asterisk is decorative: "(required)" is in the label text for AT. */
.field__req {
  color: var(--color-danger);
  margin-inline-start: 2px;
}

.field__input,
.field__select,
.field__textarea {
  min-block-size: var(--step-size);
  padding: var(--space-sm) var(--space-md);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  background: var(--color-bg);
  font-size: var(--fs-md);
  color: var(--color-text);
  inline-size: 100%;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.field__textarea {
  min-block-size: calc(var(--step-size) * 3);
}

.field__input::placeholder,
.field__textarea::placeholder {
  color: var(--color-text-faint);
}

.field__input:focus-visible,
.field__select:focus-visible,
.field__textarea:focus-visible {
  outline: none;
  border-color: var(--color-accent-strong);
  box-shadow: var(--focus-ring);
}

/* ---- Select ----
   A native <select> draws the platform's own chevron: on macOS a raised
   double-arrow in the system blue-grey, on Windows a filled triangle.
   Beside these flat hairline inputs it was the one control on the page
   that looked like it came from a different design. appearance:none
   removes it and the sprite's own chevron takes its place, so the arrow
   is drawn at the site's weight in the site's colour, and because it
   inherits currentColor it can never drift from the border and label
   around it.

   The chevron is a SIBLING of the select, not a background image. The
   sprite already holds this exact glyph (§8 — reuse before you build),
   and a background image would mean writing a second copy of it as a
   data URI with the colour hard-coded inside a stylesheet, which §1.4
   does not allow. */
.select-shell {
  position: relative;
  display: flex;
  min-inline-size: 0;
}

.select-shell__chevron {
  position: absolute;
  inset-inline-end: var(--space-sm);
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: 12px;
  block-size: 12px;
  color: var(--color-text-muted);
  pointer-events: none;
  /* the click belongs to the select underneath */
}

/* Every select on the site, wherever it lives: the checkout and contact
   forms use .field__select, the catalogue's sort control .sort__select.
   One arrow, one implementation (§8). */
.field__select,
.sort__select {
  appearance: none;
  -webkit-appearance: none;
  /* Safari < 15.4 still needs the prefix */
  cursor: pointer;
  /* Room for the chevron, so the longest option cannot run under it. */
  padding-inline-end: calc(var(--space-sm) * 2 + 12px);
}

.field__hint {
  font-size: var(--fs-tiny);
  color: var(--color-text-muted);
}

.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  font-size: var(--fs-tiny);
  font-weight: var(--fw-medium);
  color: var(--color-danger);
}

.field__error .icon {
  flex: none;
  inline-size: 14px;
  block-size: 14px;
  margin-block-start: 2px;
}

/* Invalid state is border + weight + an icon + a message — never colour
   on its own (WCAG 1.4.1). */
.field.is-invalid .field__input,
.field.is-invalid .field__select,
.field.is-invalid .field__textarea {
  border-color: var(--color-danger);
  border-width: 2px;
}

.field__input.input-error {
  border-color: var(--color-danger);
  border-width: 2px;
  box-shadow: none;
  outline: none;
}

/* ---- Fieldsets ---- */
.fieldset {
  border: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.fieldset__legend {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text);
  padding-block-end: var(--space-sm);
  border-block-end: var(--hairline);
  inline-size: 100%;
  margin-bottom: 10px;
}

/* ---- Two-up field row ---- */
.field-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-md);
}

@media (max-width: 575px) {
  .field-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---- Choice cards (radio / checkbox) ----
   The whole card is the label, so the target is the card, not the dot. */
.choice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  min-block-size: var(--step-size);
  padding: var(--space-md);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  cursor: pointer;
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.choice:hover {
  border-color: var(--color-text);
}

.choice__input {
  flex: none;
  inline-size: 18px;
  block-size: 18px;
  margin-block-start: 2px;
  accent-color: var(--color-accent-strong);
  cursor: pointer;
}

.choice__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-inline-size: 0;
}

.choice__title {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}

.choice__meta {
  font-size: var(--fs-tiny);
  color: var(--color-text-muted);
}

.choice__price {
  margin-inline-start: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--color-text);
  white-space: nowrap;
}

.choice:has(.choice__input:checked) {
  border-color: var(--color-text);
  background: var(--color-surface);
}

/* :has() fallback — checkout.js also sets .is-selected, so the state is
   visible in browsers without :has() support. */
.choice.is-selected {
  border-color: var(--color-text);
  background: var(--color-surface);
}

.choice:has(.choice__input:focus-visible) {
  box-shadow: var(--focus-ring);
}

.choice-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* ---- Inline consent checkbox ---- */
.consent {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  min-block-size: var(--step-size);
  padding-block: var(--space-sm);
  cursor: pointer;
}

.consent__input {
  flex: none;
  inline-size: 18px;
  block-size: 18px;
  margin-block-start: 2px;
  accent-color: var(--color-accent-strong);
  cursor: pointer;
}

.consent__text {
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.consent__text a {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- Secure note ----
   Written for the sign-in panel, and now also at the foot of the
   workspace's change-password form — the third user of one
   treatment, which is the point at which it stops being a page
   style. Deliberately identical to checkout.css's .pci-note:
   surface fill, accent rule on the leading edge, fine print. That
   one stays where it is — a PCI compliance statement is a
   different sentence, and checkout is the only page that makes it. */
.secure-note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--color-surface);
  border-inline-start: 3px solid var(--color-accent-strong);
  font-size: var(--fs-tiny);
  color: var(--color-text-muted);
}

.secure-note .icon {
  flex: none;
  inline-size: 16px;
  block-size: 16px;
  margin-block-start: 1px;
  color: var(--color-accent-strong);
}

/* ---- Form-level status banner ---- */
.form-status {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-radius: var(--radius-control);
  font-size: var(--fs-sm);
}

.form-status .icon {
  flex: none;
  inline-size: 18px;
  block-size: 18px;
  margin-block-start: 1px;
}

.form-status--error {
  background: var(--color-danger-soft);
  color: var(--color-danger-deep);
}

.form-status--success {
  background: var(--color-accent-tint);
  color: var(--color-accent-deep);
}

/* ---- .product-grid breakpoints -----------------------------------------
   These live WITH the component, not in products.css. When the grid was
   shared but its breakpoints were not, product-details.html — which loads
   a different page stylesheet — kept three columns at 360px, the cards
   collapsed to ~102px, and .btn--cart's 148px minimum overflowed the
   viewport by 46px. A component's responsive behaviour is part of the
   component. */
/* 1280, not 1200. .container caps at 1280px, so the catalogue's results
   area stops growing there — a 3-up card is 275px at every wider viewport,
   which leaves the two buttons 108px against the 94px their labels need.
   At 1200 the same 3 columns give a 248px card and only 95px per button:
   it fits by half a pixel, which is not a fit. Below 1280 the grid drops
   to 2. */
@media (max-width: 1279px) {
  .product-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 767px) {
  .product-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ============================================================
   TEXT ACTION
   A small uppercase underlined text button — "Clear all", "Empty the
   bag". It began life as .filters__clear inside products.css, which was
   wrong twice: an element class of the .filters block used outside that
   block breaks BEM, and cart.html does not load products.css, so on the
   bag page NONE of the styling applied and the control measured 110x24
   with a 16px font. Promoted here, where every page can reach it.

   The base is a full --step-size target. The --tight modifier pulls the
   surplus over the text's own line box back out of the flow, for rows
   like the filter sidebar's header that must not grow.
   ============================================================ */
.text-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1.5;
  min-inline-size: var(--step-size);
  min-block-size: var(--step-size);
  padding-inline: var(--space-xs);
  margin-inline: calc(var(--space-xs) * -1);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.text-action:hover {
  color: var(--color-accent-deep);
}

.text-action--tight {
  margin-block: calc((var(--step-size) - 1.5em) / -2);
}

.text-action:disabled {
  color: var(--color-text-faint);
  text-decoration: none;
  cursor: not-allowed;
}

/* The password label and its Show/Hide toggle share one row. The
   button is a SIBLING of the <label>, never inside it: a control
   nested in a label steals the label's click, so tapping "Show" would
   also focus the input it is meant to leave alone. */
.field__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

/* Position matters: this must sit AFTER .text-action, not up with the
   other .field elements. Both are single-class selectors, so declaring
   it earlier let .text-action's own --fs-xs win and the row grew by
   1.5px per password field — caught by a pixel diff, not by reading.

   --fs-tiny, not .text-action's --fs-xs, so the toggle sits on exactly
   the same 1.5em line box as .field__label beside it. --tight then
   resolves its calc against THIS size and pulls the surplus of the
   44px target back out of the flow, leaving the row the height of the
   label and the target the height the WCAG floor asks for. */
.field__reveal {
  font-size: var(--fs-tiny);
}

/* Without scripting there is nothing to toggle, so the control is not
   rendered at all rather than left on the page as a dead button. The
   flip to .js happens in the document head, before first paint. */
.no-js .field__reveal {
  display: none;
}

/* ============================================================
   INLINE LINKS IN SMALL NOTES
   axe's link-in-text-block: a link distinguished from its surrounding
   text by colour alone needs 3:1 against that text. .buy__max's phone
   link measured 1.29:1, so these get the underline instead — a cue that
   does not depend on colour at all (WCAG 1.4.1).
   ============================================================ */
.buy__max a,
.bag__note a,
.order-summary__note a,
.contact-detail__note a,
.field__hint a,
.errorpage__support a,
.choice__meta a {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ---- Order-summary skeleton lines --------------------------------------
   Reserve the summary's height at first paint so injecting the real lines
   cannot push the footer down. .skeleton-block / .skeleton-text share the
   shimmer defined here so checkout.css and product-details.css do not each
   declare their own. */
.skeleton-block,
.skeleton-text {
  background: linear-gradient(90deg,
      var(--drawer-skeleton-1) 0%, var(--drawer-skeleton-2) 50%, var(--drawer-skeleton-1) 100%);
  background-size: 200% 100%;
  animation: skeleton-sweep 1.2s var(--ease-inout) infinite;
}

.skeleton-text {
  block-size: 1.2em;
}

.order-line--skeleton .skeleton-text {
  align-self: center;
}

@media (prefers-reduced-motion: reduce) {

  .skeleton-block,
  .skeleton-text {
    animation: none;
  }
}

/* ============================================================
   FAVOURITE TOGGLE
   A pressed-state button, not a link: it changes data, it does not
   navigate. The outline heart is the sprite's only heart — the saved
   state fills it with currentColor rather than swapping in a second
   symbol that would have to be kept in step.

   Shaped like .btn--secondary so it sits beside Add to Cart as an equal
   rather than as decoration, and it clears 44px on its own.
   ============================================================ */
.fav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-block-size: var(--step-size);
  min-inline-size: var(--step-size);
  padding-inline: var(--space-md);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  transition: background-color var(--transition-fast), color var(--transition-fast),
    border-color var(--transition-fast);
}

.fav-btn:hover {
  border-color: var(--color-text);
  background: var(--color-surface);
}

.fav-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.fav-btn .icon {
  transition: fill var(--transition-fast), color var(--transition-fast);
}

.fav-btn.is-saved {
  border-color: var(--color-danger);
  color: var(--color-danger-deep);
}

.fav-btn.is-saved .icon {
  fill: currentColor;
}

/* Mid-request the control is disabled, so it must still read as itself
   rather than as a permanently dead button. */
.fav-btn:disabled {
  opacity: .6;
  cursor: progress;
}

/* ---- The card's top row -------------------------------------------------
   The category tag at the start, the favourite control at the end, above
   the photograph. The row is the EYEBROW'S height, not the control's: the
   button is centred on it OUT OF FLOW, so a 44px target cannot add 17px to
   a card whose media box is the one flexible child. The related strip has
   to stay pixel-identical to the home page's Latest cards, and it does.

   Centring on 50% rather than offsetting by a measured number means the
   rule holds for both card paddings — 24px in the home page's scroller,
   12px in the catalogue grid — with nothing to keep in sync. The 8.5px the
   button overhangs at each end falls in the card's own padding above and
   the row's 16px margin below. */
.showcase-card__top {
  position: relative;
  display: flex;
  align-items: center;
  margin-block-end: var(--space-md);
  /* The end of the row belongs to the control, so a long category name
     ellipsizes before it reaches the heart instead of running under it. */
  padding-inline-end: calc(var(--step-size) + var(--space-sm));
}

.showcase-card__top .showcase-card__eyebrow {
  margin-block-end: 0;
}

/* ---- The same toggle, on a card ----------------------------------------
   Icon-only and with nothing drawn behind it: no box, no disc, no border.
   Off the photograph it needs none of that — it sits on the card's own
   white, where a hairline glyph reads on its own.

   The button is still 44x44 and still the target. It is simply invisible:
   the 20px glyph is centred in it and the rest is live area. Hover reddens
   the heart rather than filling a box that is not there.

   Everything else is .fav-btn: the same is-saved fill, the same focus
   ring, the same disabled-while-pending treatment. */
.showcase-card__fav {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: 50%;
  transform: translateY(-50%);
  inline-size: var(--step-size);
  min-inline-size: var(--step-size);
  block-size: var(--step-size);
  padding-inline: 0;
  border-color: transparent;
  background: transparent;
}

.showcase-card__fav .icon {
  inline-size: 20px;
  block-size: 20px;
}

.showcase-card__fav:hover {
  background: transparent;
  border-color: transparent;
  color: var(--color-danger);
}

.showcase-card__fav.is-saved {
  border-color: transparent;
}


/* ============================================================
   TAB ROW + SCROLLER
   Promoted here from home.css unchanged: the product page reuses both,
   and a page stylesheet may not be a dependency of another page (§3
   cascade order). The home page still gets them at the same effective
   point in the cascade — nothing in components.css, header.css,
   footer.css or utilities.css matches .tab* or .scroller*.

   .tabs is a horizontal row of pill controls carrying no ARIA of its
   own: the home page uses it as a filter GROUP (aria-pressed) and the
   product page as a real TABLIST (aria-selected). One appearance, two
   correct semantics — which is why the selected style is written as
   :is(.tab.is-active, .tab[aria-selected="true"]).
   ============================================================ */
.scroller {
  position: relative;
  margin-block-start: var(--space-3xl);
}

.scroller:first-of-type {
  margin-block-start: 0;
}

.scroller__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  margin-block-end: var(--space-lg);
}

.scroller__title {
  font-size: var(--fs-2xl);
  color: var(--color-text);
}

.scroller__hint {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  /* On --color-surface the faint token is 3.56:1 — below AA for this size.
     3.56:1 -> 6.20:1. */
  color: var(--color-text-muted);
  text-transform: uppercase;
}

/* Tab-filter pill row (Featured scroller) */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  inline-size: 100%;
  margin-block-end: var(--space-lg);
}

.tab {
  min-block-size: 44px;
  padding-inline: var(--space-md);
  border: 1px solid var(--brand-orange);
  /* orange outline */
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast);
}

.tab:hover {
  color: var(--color-text);
  background: var(--brand-orange-soft);
}

/* :is() so a filter group (.is-active) and a real tablist (aria-selected)
   share ONE appearance without either selector gaining specificity over
   the other. Same technique as the header's docked/flat states. */
:is(.tab.is-active, .tab[aria-selected="true"]) {
  background: var(--brand-orange);
  /* solid orange fill when selected */
  color: var(--color-text);
  /* ink text — white fails on this orange */
  border-color: var(--brand-orange);
}

/* Viewport + track */
.scroller__viewport {
  position: relative;
}

.scroller__track {
  display: flex;
  gap: var(--space-lg);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-block-end: var(--space-sm);
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.scroller__track::-webkit-scrollbar {
  display: none;
}

.scroller__item {
  flex: 0 0 clamp(258px, 78vw, 300px);
  scroll-snap-align: start;
  display: flex;
}

/* Controls row under the cards — draggable progress (start) + prev/next arrows (end) */
.scroller__controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-block-start: var(--space-lg);
}

.scroller__nav {
  display: flex;
  gap: var(--space-xs);
  flex: none;
}

/* Sleek, sharp-cornered nav arrows — chevrons on a hairline, ink-fill on hover */
.scroller__arrow {
  inline-size: 46px;
  block-size: 46px;
  border-radius: var(--radius-control);
  /* sharp — matches the industrial buttons */
  border: var(--hairline-strong);
  background: transparent;
  color: var(--color-text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.scroller__arrow:hover {
  background: var(--color-text);
  color: var(--color-text-inverse);
  border-color: var(--color-text);
}

.scroller__arrow:focus-visible {
  outline: 2px solid var(--color-accent-strong);
  outline-offset: 3px;
}

.scroller__arrow:disabled {
  opacity: .28;
  cursor: not-allowed;
}

.scroller__arrow .icon {
  inline-size: 20px;
  block-size: 20px;
  stroke-width: 2.25;
}

/* Draggable progress bar */
.scroller__progress {
  position: relative;
  block-size: 6px;
  inline-size: min(240px, 62%);
  background: var(--scroller-track);
  border-radius: var(--radius-pill);
  cursor: pointer;
  touch-action: none;
}

.scroller__thumb {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  min-inline-size: 28px;
  background: var(--scroller-thumb);
  border-radius: var(--radius-pill);
  transition: background-color var(--transition-fast);
}

.scroller__progress:hover .scroller__thumb {
  background: var(--scroller-thumb-hover);
}

/* Full-width showcase variant (Featured) — xTool-style row of large,
   divider-separated columns that fill the width 3-up (2-up tablet,
   1-up mobile) instead of the compact fixed-width scroller cards. */
/* Break the card row out of the container to the full viewport width
   (edge-to-edge). Head, tabs and progress stay container-aligned. */
.scroller--wide .scroller__viewport {
  inline-size: 100vw;
  margin-inline: calc(50% - 50vw);
}

.scroller--wide .scroller__track {
  gap: 0;
  background: var(--color-bg);
  /* white product row spanning the screen */
  border-block: var(--hairline);
  /* framing rules top & bottom (xTool treatment) */
  padding-block-end: 0;
}

/* Card count per viewport (manifest items 6-8). The track's gap is 0 for this
   variant — the columns are separated by hairline dividers, not by a gap token —
   so the basis is an exact 100%/n with nothing to subtract. Height is never set
   here: .showcase-card's aspect-ratio derives it, so R holds at every step. */
.scroller--wide .scroller__item {
  flex: 0 0 25%;
  /* >=1200px — FOUR full-screen-width columns */
  border-inline-start: var(--hairline);
  /* vertical dividers between columns */
  align-items: flex-start;
  /* don't stretch the card — that would beat aspect-ratio */
}

/* Keep the border in the box model, just hide it: removing it outright made the
   first item 1px wider than the rest, and since card height now derives from
   width, that came back as a 1.5px taller first card. */
.scroller--wide .scroller__item:first-child {
  border-inline-start-color: transparent;
}

@media (max-width: 1199px) {
  .scroller--wide .scroller__item {
    flex-basis: calc(100% / 3);
  }

  /* 992-1199 — 3-up */
}

@media (max-width: 991px) {
  .scroller--wide .scroller__item {
    flex-basis: 50%;
  }

  /* 768-991 — 2-up */
}

@media (max-width: 767px) {
  .scroller--wide .scroller__item {
    flex-basis: 85%;
  }

  /* <768 — 1-up, ~15% peek */
}

/* ---------------------------------------------------------------- Assurances
   The home page's capability strip, carrying promises instead of numbers.
   Four distinct marks, because a column of four identical ticks says only
   "there are four of something".

   Promoted here from css/pages/product-details.css when the bag summary
   became the second page to use it. Every declaration is that file's,
   unchanged — including the responsive collapse, because a component that
   only reflows correctly on one page is not a component.

   TWO arrangements. The divider and padding rules belong to the FOUR-ACROSS
   one, so they are scoped with :not(--stack) rather than set on .assurance
   and then unset again in the modifier and in both breakpoints. The mark,
   the type and the colour are shared and stay unscoped. */
.assurances {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-block-start: var(--space-2xl);
}

.assurance {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.assurance .icon {
  flex: none;
  inline-size: 20px;
  block-size: 20px;
  color: var(--color-accent-strong);
  margin-block-start: 1px;
}

/* A promise may cite the clause it comes from. Same inline-link treatment
   as .consent__text a — underlined, so it does not rely on colour alone. */
.assurance a {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.assurance a:hover {
  color: var(--color-accent-deep);
}

/* ---- Four across, hairline-divided ---- */
.assurances:not(.assurances--stack) {
  border-block: var(--hairline);
}

.assurances:not(.assurances--stack)>.assurance {
  padding: var(--space-lg);
  border-inline-start: var(--hairline);
}

.assurances:not(.assurances--stack)>.assurance:first-child {
  border-inline-start: none;
}

/* ---- One column, for a narrow rail such as the bag summary, where four
   across is not on offer at any width. Row spacing instead of dividers:
   three hairlines inside a 340px box read as clutter, and the box already
   has the summary's own rules above it. ---- */
.assurances--stack {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-sm);
  margin-block-start: 0;
  padding-block-start: var(--space-md);
  border-block-start: var(--hairline);
}

.assurances--stack>.assurance {
  font-size: var(--fs-tiny);
}

@media (max-width: 991px) {

  /* Two-up, and the dividers move with the wrap. */
  .assurances:not(.assurances--stack) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .assurances:not(.assurances--stack)>.assurance {
    padding: var(--space-md);
  }

  .assurances:not(.assurances--stack)>.assurance:nth-child(1),
  .assurances:not(.assurances--stack)>.assurance:nth-child(3) {
    border-inline-start: none;
  }

  .assurances:not(.assurances--stack)>.assurance:nth-child(3),
  .assurances:not(.assurances--stack)>.assurance:nth-child(4) {
    border-block-start: var(--hairline);
  }
}

@media (max-width: 479px) {

  /* Below ~480px two cells put three words on a line. */
  .assurances:not(.assurances--stack) {
    grid-template-columns: minmax(0, 1fr);
  }

  .assurances:not(.assurances--stack)>.assurance {
    border-inline-start: none;
    border-block-start: var(--hairline);
  }

  .assurances:not(.assurances--stack)>.assurance:first-child {
    border-block-start: none;
  }
}

/* ============================================================
   PROMOTED COMPONENTS
   Each block below was written for one page and is now used by more
   than one, so it lives here where every page can reach it — the same
   move .tabs and .scroller made when the product page needed the home
   page's own components.

   Moved BYTE-IDENTICAL. Nothing was restyled on the way: the pages
   that already used them must render exactly as they did.

     .order-line   checkout.css  — the checkout summary and now the
                   account order and invoice detail pages. Its own
                   --skeleton modifier was already stranded up in this
                   file, referencing a base class defined in a
                   page stylesheet; that split is closed here.
     .review-block checkout.css  — the review step's address panels and
                   now every titled panel in the account workspace.
     .pagination   products.css  — the catalogue and now the order and
                   invoice listings.
   ============================================================ */

/* ---------------------------------------------------------------- Order lines */
.order-line {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr) auto;
  gap: var(--space-sm);
  align-items: center;
  font-size: var(--fs-sm);
}

.order-line__media {
  inline-size: 48px;
  block-size: 48px;
  overflow: hidden;
  background: var(--color-bg);
  border: var(--hairline);
}

.order-line__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* The name and the quantity are two stacked rows, not one run of inline
   text — without this they rendered as "UV Flatbed PrinterQty 1". */
.order-line__info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-inline-size: 0;
}

.order-line__name {
  color: var(--color-text);
}

.order-line__qty {
  font-family: var(--font-mono);
  font-size: var(--fs-tiny);
  color: var(--color-text-muted);
}

.order-line__total {
  font-family: var(--font-mono);
  color: var(--color-text);
  white-space: nowrap;
}

/* ---------------------------------------------------------------- Review blocks */
.review-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-lg);
  border: var(--hairline);
}

.review-block__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
}

.review-block__title {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-text);
}

.review-block__body {
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

.review-block__body p+p {
  margin-block-start: var(--space-xs);
}

/* ---------------------------------------------------------------- Pagination */
.pagination {
  display: flex;
  justify-content: center;
  padding-block: var(--space-xl);
}

.pagination__list {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.pagination__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--step-size);
  min-block-size: var(--step-size);
  padding-inline: var(--space-sm);
  border: var(--hairline);
  border-radius: var(--radius-control);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--color-text);
  transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.pagination__link:hover {
  border-color: var(--color-text);
  background: var(--color-surface);
}

.pagination__link[aria-current="page"] {
  background: var(--color-text);
  border-color: var(--color-text);
  color: var(--color-text-inverse);
}

.pagination__link[aria-disabled="true"] {
  color: var(--color-text-faint);
  border-color: var(--color-border);
  pointer-events: none;
}

.pagination__ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--space-lg);
  font-family: var(--font-mono);
  color: var(--color-text-faint);
}


/* ---- Toast / popup notifications ---- */
.toast-root {
  position: fixed;
  right: var(--space-lg);
  top: var(--space-lg);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-sm);
  pointer-events: none;
  color: var(--color-success);
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  max-inline-size: min(360px, calc(100vw - 32px));
  padding: var(--space-md) var(--space-lg);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-accent-strong);
  background: rgba(20, 24, 26, .96);
  color: var(--color-text-inverse);
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateY(8px);
  animation: toast-in var(--transition-base);
  animation-fill-mode: forwards;
  pointer-events: auto;
}

.toast--success {
  border-left-color: var(--color-success);
}

.toast--warning {
  border-left-color: var(--color-warning);
}

.toast--info {
  border-left-color: var(--color-accent-strong);
}

.toast--success svg {
  border-left-color: var(--color-success);
  stroke: var(--color-success);
}

.toast--warning svg {
  border-left-color: var(--color-warning);
  stroke: var(--color-warning);
}

.toast--info svg {
  border-left-color: var(--color-accent-strong);
  stroke: var(--color-accent-strong);
}

.toast__icon {
  flex: none;
  inline-size: 18px;
  block-size: 18px;
  margin-block-start: 1px;
}

.toast__text {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: #fff;
}

@keyframes toast-in {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.auth-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  background: rgba(10, 12, 14, .5);
}

.auth-modal {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  inline-size: min(100%, 420px);
  padding: var(--space-xl);
  background: var(--color-bg);
  border: var(--hairline);
  box-shadow: var(--shadow-lg);
}

.auth-modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
}

.auth-modal__title {
  margin: 0;
  font-family: var(--font-heading);
  font-size: clamp(var(--fs-lg), 2vw, var(--fs-xl));
  line-height: 1.1;
  color: var(--color-text);
}

.auth-modal__close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 36px;
  block-size: 36px;
  border: var(--hairline);
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: border-color var(--transition-fast), color var(--transition-fast), background-color var(--transition-fast);
}

.auth-modal__close:hover,
.auth-modal__close:focus-visible {
  border-color: var(--color-text);
  background: var(--color-surface);
  color: var(--color-text);
  outline: none;
}

.auth-modal__close .icon {
  inline-size: 16px;
  block-size: 16px;
}

.auth-modal__text {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

.auth-modal__actions {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-sm);
  margin-block-start: var(--space-xs);
}

.auth-modal__link {
  text-decoration: none;
}


/* ---- Reusable warning banner with dismiss control ---- */
.warning-msg {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  inline-size: 100%;
  max-inline-size: var(--measure);
  padding: var(--space-md) var(--space-lg);
  background: var(--color-warning-soft);
  border: 1px solid var(--color-warning-border);
  border-inline-start: 4px solid var(--color-warning);
  border-radius: var(--radius-control);
  color: var(--color-warning-deep);
}

.warning-msg__content {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  flex: 1 1 auto;
  min-inline-size: 0;
}

.warning-msg__content p {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  font-weight: var(--fw-medium);
}

.warning-msg .icon {
  flex: none;
  inline-size: 18px;
  block-size: 18px;
  margin-block-start: 2px;
}

.warning-msg__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--step-size);
  /* block-size: var(--step-size); */
  border: 0;
  background: transparent;
  border-radius: var(--radius-sm);
  color: currentColor;
  cursor: pointer;
  opacity: .8;
  transition: opacity var(--transition-fast), background-color var(--transition-fast);
}

.warning-msg__close:hover,
.warning-msg__close:focus-visible {
  opacity: 1;
  outline: none;
}

.warning-msg__close .icon {
  inline-size: 14px;
  block-size: 14px;
}

.warning-msg.is-hidden {
  display: none !important;
}