/* ═══════════════════════════════════════════════════════════════
   Alta Web — shopping.css
   Styles for: Home, Category, Search Results, Product Detail, Wishlist
═══════════════════════════════════════════════════════════════ */

/* ── ══════════════════════════════════════════════════════
   HOME — THE THREE OPENING SECTIONS

   Editorial rather than dashboard: a photographic hero, a collage
   that says who the brand is, and a set of category doors. All
   three are photo-led, so each image slot is a `.ph` — a neutral
   placeholder that styles itself until a real picture is set on it
   with `--img: url(...)`. See the note above `.ph` below.

   The page ground is pure white, so these sections earn their
   separation from contrast and from the tinted band on the collage,
   not from a tinted background.
══════════════════════════════════════════════════════ ── */

/* ── Nav over the hero ────────────────────────────────────
   Only on pages that opt in with `.has-hero-overlay` on <body> —
   the home page. Everywhere else the navbar keeps its solid white
   bar, so this cannot leak into the account or checkout chrome.

   The nav is lifted out of the flow and laid over the hero, which
   is why the hero can start at the top of the viewport. It keeps
   its ordinary solid white bar and grey hairline from common.css,
   so the logo and links stand on white rather than on the photo;
   only the positioning is set here.                             */
/*
  The hairline drawn on the photograph itself — along the top of the
  marquee at the foot of the hero. Not --border or --faint: both are
  picked to sit on white, and against the photo's beige ground they
  are close enough to it to disappear.
*/
.has-hero-overlay { --hero-rule: rgba(50,48,44,0.18); }

.has-hero-overlay .navbar,
.has-hero-overlay .subnav {
  position: absolute;
  left: 0;
  right: 0;
}
.has-hero-overlay .navbar { top: 0; }
.has-hero-overlay .subnav { top: var(--nav-h); }

/* ── Photo slots ──────────────────────────────────────────
   Every image on these sections is one of these. With no --img it
   draws a soft neutral gradient with a faint grain, which reads as
   "photo goes here" rather than as a broken box; set --img and the
   picture covers it. Nothing here needs changing when the real
   photography arrives — only the --img value.            */
.ph {
  position: relative;
  overflow: hidden;
  background-color: var(--input-bg);
  background-image:
    var(--img, none),
    linear-gradient(135deg, var(--input-bg) 0%, #F2EDEE 55%, var(--border) 100%);
  background-size: cover, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}
/* A hairline inside the slot so a pale photo still reads as a card. */
.ph::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06);
  border-radius: inherit;
  pointer-events: none;
}

/* ── ─────────────────────────────────────────────────────
   1 — HERO
   Full-bleed dark photograph, inset from the page edges and
   rounded, with the copy stacked on the right half. The picture is
   the section; the panel behind it only shows through where the
   photograph has not loaded.
   ───────────────────────────────────────────────────── ── */
.hero {
  /* Full-bleed and full-height. The navbar sits on top of it rather
     than above it, so the hero starts at y=0 and the nav is pulled
     back over it by .has-hero-overlay below. */
  position: relative;
  background: var(--ink);
}
.hero-frame {
  position: relative;
  /* svh, not vh: on mobile the browser chrome collapses as you scroll,
     and vh would size to the tall state and crop the CTA on arrival. */
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  background-color: var(--ink);
}
/* Keeps the copy clear of the overlaid nav and off the very bottom. */
.hero-frame::after {
  content: '';
  display: block;
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* The photograph. Kept as its own layer rather than a background on
   .hero-frame so the scrim below can sit between it and the copy. */
/*
  The wall in this photograph was extended to the right in the file
  itself (see read_me), so the picture already carries the full width
  of the hero — no panel, no fade, nothing to blend. It is cropped
  from the left so the subject keeps her position as the window
  widens, and the spare wall on the right is what gets cut.
*/
.hero-photo {
  position: absolute;
  inset: 0;
  background-color: #FFFFFF;
  background-image: var(--img, none);
  background-size: cover;
  /* Centred: the products ring the edges of this picture and the middle
     is open, which is where the copy now sits. */
  background-position: center;
  background-repeat: no-repeat;
}
/*
  No wash on the wide layout: the nav above is solid white, so nothing
  has to be lifted off the photograph, and the copy sits in the
  picture's open middle. The layer stays for the stacked layout below,
  where the copy is laid over the busier parts of the picture and needs
  the photo's own colour behind it.
*/
.hero-scrim {
  position: absolute;
  inset: 0;
}

.hero-body {
  position: relative;
  z-index: 1;
  /* No width: as a flex item the block sizes to its own content, and
     auto margins on both sides centre it in the frame. Line breaks are
     therefore set by the copy — the <br> in the headline and
     .hero-desc's own max-width — rather than by a column measure. */
  margin-inline: auto;
  /* Top padding clears the nav + sub-nav sitting over the hero; the
     equal sides are what keep the block centred rather than merely
     centre-aligned. */
  padding: calc(var(--nav-h) + var(--subnav-h)) 32px 96px;
  text-align: center;
}
.hero-title {
  font-family: var(--font-head);
  font-size: clamp(40px, 5.4vw, 76px);
  font-weight: 700;
  line-height: 1.06;
  letter-spacing: -1.2px;
  color: var(--black);
  margin: 0;
}
.hero-desc {
  font-size: 15px;
  line-height: 1.7;
  color: var(--dark);
  margin: 20px auto 0;
  max-width: 440px;
}
.hero-actions {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 30px;
}

/* ── Hero motion ──────────────────────────────────────────
   Only the copy moves now. Each line rises a little and fades up,
   one after the next, so the eye is walked down the stack in the
   order it should be read rather than meeting all of it at once.

   The photograph used to drift as well, scaling slowly about the
   point the old subject stood on. The picture is a flat-lay with its
   products ringing the edges, and a scale of any size pushed them out
   of frame — so the drift is gone rather than re-aimed.

   The copy animation is off under prefers-reduced-motion at the end
   of this file: nothing here carries meaning that is lost when it
   stops.                                                         */

@keyframes hero-rise {
  from { opacity: 0; transform: translate3d(0, 20px, 0); }
  to   { opacity: 1; transform: none; }
}

/* --px / --py are written by initHeroParallax() in home.js and read by
   the orb keyframes below. They default to 0 when the script never
   runs, so the hero is inert without it. */
.hero-frame { --px: 0px; --py: 0px; }

.hero-title,
.hero-desc,
.hero-actions {
  /* `both` so the opening frame applies before the delay elapses —
     without it each line flashes at full opacity, then starts. */
  animation: hero-rise 0.75s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.hero-title   { animation-delay: 0.06s; }
.hero-desc    { animation-delay: 0.22s; }
.hero-actions { animation-delay: 0.36s; }

/* The arrow leans out of the button on hover — the same gesture the
   pill already makes when it lifts, carried through to the glyph. */
.hero-cta-primary svg {
  transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}
.hero-cta-primary:hover svg { transform: translate3d(2px, -2px, 0); }

/* ── Drifting light ───────────────────────────────────────
   Three blurred discs behind the copy, each on its own duration so
   they never fall into step and never repeat the same arrangement.
   Soft-light blending keeps them as changes in the light on the wall
   rather than shapes sitting on top of it.                      */
.hero-orbs {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.hero-orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(52px);
  mix-blend-mode: soft-light;
  will-change: transform;
}
.hero-orb--1 {
  width: 340px; height: 340px;
  right: 8%; top: 12%;
  background: radial-gradient(circle, rgba(47,107,85,0.85) 0%, rgba(47,107,85,0) 70%);
  animation: orb-a 19s ease-in-out infinite alternate;
}
.hero-orb--2 {
  width: 260px; height: 260px;
  right: 26%; bottom: 14%;
  background: radial-gradient(circle, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 70%);
  animation: orb-b 25s ease-in-out infinite alternate;
}
.hero-orb--3 {
  width: 400px; height: 400px;
  left: 30%; top: 30%;
  background: radial-gradient(circle, rgba(255,140,170,0.7) 0%, rgba(255,140,170,0) 70%);
  animation: orb-c 31s ease-in-out infinite alternate;
}
@keyframes orb-a {
  from { transform: translate3d(calc(var(--px) * -1.8), calc(var(--py) * -1.8), 0) scale(1); }
  to   { transform: translate3d(calc(var(--px) * -1.8 - 90px), calc(var(--py) * -1.8 + 60px), 0) scale(1.18); }
}
@keyframes orb-b {
  from { transform: translate3d(calc(var(--px) * 2.2), calc(var(--py) * 2.2), 0) scale(1.1); }
  to   { transform: translate3d(calc(var(--px) * 2.2 + 70px), calc(var(--py) * 2.2 - 55px), 0) scale(0.9); }
}
@keyframes orb-c {
  from { transform: translate3d(calc(var(--px) * 1.4), calc(var(--py) * 1.4), 0) scale(0.95); }
  to   { transform: translate3d(calc(var(--px) * 1.4 + 60px), calc(var(--py) * 1.4 + 45px), 0) scale(1.15); }
}

/* ── Claims strip ─────────────────────────────────────────
   The one part of the hero that moves continuously and obviously.
   The track holds the list twice and travels exactly -50%, so the
   second run is in the first run's place at the moment it resets —
   which is what makes the loop invisible. Any other distance and it
   would visibly jump.                                           */
.hero-marquee {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  border-top: 1px solid var(--hero-rule, rgba(50,48,44,0.18));
  background: rgba(255,255,255,0.35);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.hero-marquee-track {
  display: flex;
  align-items: center;
  width: max-content;
  padding: 15px 0;
  animation: marquee 34s linear infinite;
  will-change: transform;
}
/*
  Two of these, identical. -50% of the track is exactly one group, so
  the reset lands the second group where the first started and the
  seam is invisible.

  min-width is the part that was missing: with ten items the group
  measured 994px against a 1910px strip, so the second half ran out
  of content most of the way across and the tail of the loop went
  blank before snapping back. Forcing each group to a viewport wide
  means there is always something in frame, and space-around shares
  out any slack — identically in both groups, so they still line up.
*/
.hero-marquee-group {
  display: flex;
  align-items: center;
  justify-content: space-around;
  gap: 22px;
  min-width: 100vw;
  /* Keeps the spacing across the join equal to the spacing within. */
  padding-right: 22px;
  flex-shrink: 0;
}
.hero-marquee-group span {
  font-family: var(--font-head);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 2.4px;
  text-transform: uppercase;
  color: var(--black);
  white-space: nowrap;
}
.hero-marquee-group i { color: var(--primary); font-style: normal; }
@keyframes marquee {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* Pausing on hover makes the claims readable instead of a blur. */
.hero-marquee:hover .hero-marquee-track { animation-play-state: paused; }

/* ── Scroll cue ───────────────────────────────────────────
   Sits above the strip, on the copy's side so it never lands on the
   model. The dot falls inside the mouse; the whole cue breathes.  */
.hero-cue {
  position: absolute;
  bottom: 84px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(50,48,44,0.62);
  animation: cue-breathe 2.6s ease-in-out infinite;
}
.hero-cue-dot { animation: cue-dot 2.6s ease-in-out infinite; }
@keyframes cue-breathe {
  0%, 100% { transform: translate3d(-50%, 0, 0); }
  50%      { transform: translate3d(-50%, 7px, 0); }
}
@keyframes cue-dot {
  0%, 100% { transform: translate3d(0, 0, 0); opacity: 1; }
  50%      { transform: translate3d(0, 7px, 0); opacity: 0.25; }
}
/* Pill shape and the arrow come from the reference; the colour is
   this site's own primary, not the reference's gold. */
.hero-cta-primary {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  height: 46px;
  padding: 0 26px;
  border: none;
  border-radius: 999px;
  background: var(--primary-grad);
  color: var(--on-primary);
  font-family: var(--font-head);
  font-size: 13.5px;
  font-weight: 700;
  cursor: pointer;
  transition: filter 0.15s, transform 0.15s;
}
.hero-cta-primary:hover { filter: brightness(0.90); transform: translateY(-1px); }
/* The hero's button stands alone, so it takes a wider stance than the
   same button in the collage below. */
.hero-actions .hero-cta-primary {
  justify-content: center;
  padding: 0 44px;
}

/* ── ─────────────────────────────────────────────────────
   2 — COLLAGE
   The brand's own section: a heading and a short claim on the
   left, a tall portrait in the middle, a rotating seal and a
   second picture on the right. Deliberately asymmetric and
   overlapping — it should not read as a grid of cards.

   Laid out on an explicit 12-column grid so the overlaps are
   stated rather than produced by negative margins.
   ───────────────────────────────────────────────────── ── */
.collage {
  /*
    Pure white, not the tinted band it used to be: that was a pale pink
    left from the old palette, and it read as a smudge between the
    hero's warm off-white above and the category doors below.

    Held in a variable because the step cut out of the picture below is
    drawn by painting band-coloured shapes over it — four separate
    declarations that are only invisible while they match this one
    exactly. Changing the band without changing them leaves the notch
    showing as a patch of the old colour.
  */
  --band: #FFFFFF;
  /* The same colour at zero alpha. Not the `transparent` keyword: that
     is transparent BLACK, and the antialiased rim of the quarter-discs
     below would blend towards grey against it. */
  --band-fade: rgba(255,255,255,0);
  background: var(--band);
  /* Generous, and deliberately so: the band is the only thing
     separating this section from the hero above and the category
     doors below, both of which are full-bleed and busy. The air is
     what lets it read as its own page rather than a strip wedged
     between two photographs. */
  padding: 190px 0;
}
.collage-inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0 24px;
  display: grid;
  /* Three columns, one row. The left column is a single cell holding
     its own stack — an earlier version spanned the portrait across two
     grid rows, which stretched the first row to half the portrait's
     height and tore a gap between the heading and the clip. */
  grid-template-columns: 4.2fr 4.4fr 3.4fr;
  column-gap: 32px;
  align-items: start;
}
.collage-left {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 22px;
}
.collage-title {
  font-family: var(--font-head);
  font-size: clamp(26px, 2.6vw, 36px);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.4px;
  color: var(--black);
}
.collage-sub {
  font-size: 13px;
  color: var(--mid);
  margin-top: 10px;
}
/* The small video still under the heading. */
.collage-clip {
  width: 100%;
  aspect-ratio: 16 / 10;
  border-radius: 18px;
}
.collage-play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 48px;
  height: 48px;
  border: none;
  border-radius: 50%;
  background: rgba(255,255,255,0.92);
  color: var(--black);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 1;
  transition: transform 0.15s, background 0.15s;
}
.collage-play:hover { transform: scale(1.08); background: #FFFFFF; }
/* The tall centre portrait, lifted so it breaks the top line of the
   row — the overlap is what stops this reading as three cards. */
.collage-portrait {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: 22px;
  margin-top: -28px;
  /* The notch and the button reach outside these bounds; border-radius
     still clips the background image, so nothing is lost by this. */
  overflow: visible;
}
.collage-side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 20px;
  padding-top: 28px;
}
/* The rotating seal. Text on a circle needs SVG; the dot is drawn
   here so the SVG only carries the letters. */
.collage-seal {
  position: relative;
  width: 132px;
  height: 132px;
}
.collage-seal svg {
  width: 100%;
  height: 100%;
  animation: seal-spin 22s linear infinite;
}
.collage-seal-dot {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--black);
}
@keyframes seal-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .collage-seal svg { animation: none; }
  .hero-title,
  .hero-desc,
  .hero-actions { animation: none; }
  .hero-cta-primary svg { transition: none; }
  .hero-orb,
  .hero-marquee-track,
  .hero-cue,
  .hero-cue-dot { animation: none; }
  /* Still centred once the animation that was doing it stops. */
  .hero-cue { transform: translateX(-50%); }
}
.collage-still {
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 18px;
}
/*
  The step cut out of the picture's bottom-left corner, and the button
  that sits in it.

  The shape, read off the reference: the picture's left edge comes
  straight down, turns right along the top of the step, turns down
  again at the step's right edge, runs to the picture's bottom, and
  only there curves right into the bottom edge. The band colour simply
  continues through the gap — it does NOT wrap around the button. An
  earlier attempt drew a pill behind the button instead, which enclosed
  it on all four sides and was the wrong shape entirely.

  Proportions are taken from the reference rather than guessed: the
  step is about 27% of the picture's width and 15% of its height, so
  they are written as percentages and hold at any column width.
*/
.collage-notch {
  position: absolute;
  left: -60px;                    /* runs out past the picture into the band */
  bottom: 0;
  width: calc(27% + 60px);
  height: 15%;
  background: var(--band);
  /* The inward turn where the step's top meets its right edge. */
  border-top-right-radius: 20px;
  z-index: 1;
}
/*
  Rounds the picture's new bottom-left corner — the one the step
  creates, which is inside the element's box and so out of reach of
  border-radius. The gradient is band colour everywhere except a
  quarter-disc, and that disc is the corner left showing.
*/
.collage-notch::after {
  content: '';
  position: absolute;
  left: 100%;
  bottom: 0;
  width: 20px;
  height: 20px;
  background: radial-gradient(circle at 100% 0, var(--band-fade) 0 20px, var(--band) 20px);
}

.collage-notch::before {
  content: '';
  position: absolute;
  left: 60px;
  top: -20px;
  width: 20px;
  height: 20px;
  background: radial-gradient(circle at 100% 0, var(--band-fade) 0 20px, var(--band) 20px);
}

/*
  Wrapper for the button and the line beneath it. Anchored by its TOP,
  not its bottom: anchoring the bottom would pin the note's last line to
  the picture and shove the button up out of the step whenever the text
  reflowed. 100% is the picture's height, so `100% - 66px` puts the
  button's top 66px above the picture's bottom — where it sat alone.
*/
.collage-cta-slot {
  position: absolute;
  left: -380px;
  top: calc(100% - 66px);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  /* Wide enough that the note clears the picture's bottom edge and lands
     on the band, rather than putting dark text over the photograph. */
  gap: 26px;
}
.collage-cta {
  position: relative;
  margin: 0;
}
.collage-note {
  font-size: 15px;
  line-height: 1.7;
  color: var(--mid);
  width: 470px;
  margin: 0;
}

/* ── ─────────────────────────────────────────────────────
   3 — CATEGORY DOORS
   Full-bleed panels that touch, each a photograph with the
   category name over it. The row is built by home.js from the
   real category tree, so the count is whatever the shop has —
   the grid below adapts rather than assuming three.
   ───────────────────────────────────────────────────── ── */
.doors {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  background: var(--ink);
}
.door {
  position: relative;
  min-height: 420px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  text-decoration: none;
  overflow: hidden;
}
.door-photo {
  position: absolute;
  inset: 0;
  background-color: #2C2427;
  background-image: var(--img, none);
  background-size: cover;
  background-position: center;
  transition: transform 0.5s ease;
}
.door:hover .door-photo { transform: scale(1.05); }
/* Enough scrim for white type at any photograph. */
.door::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(12,8,10,0.30) 0%, rgba(12,8,10,0.55) 100%);
}
.door-body {
  position: relative;
  z-index: 1;
  padding: 24px;
}
.door-name {
  font-family: var(--font-head);
  font-size: 26px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #FFFFFF;
}
.door-sub {
  font-size: 12.5px;
  color: rgba(255,255,255,0.78);
  margin-top: 8px;
}
.door-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 18px;
  height: 38px;
  padding: 0 20px;
  border: 1px solid rgba(255,255,255,0.55);
  border-radius: 999px;
  color: #FFFFFF;
  font-size: 12.5px;
  font-weight: 600;
  transition: background 0.15s, border-color 0.15s;
}
.door:hover .door-cta { background: rgba(255,255,255,0.16); border-color: #FFFFFF; }
/* The empty state, when the catalogue has no categories yet. */
.doors-empty {
  grid-column: 1 / -1;
  min-height: 260px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: rgba(255,255,255,0.75);
  text-align: center;
  padding: 24px;
}
.doors-empty strong {
  font-family: var(--font-head);
  font-size: 17px;
  font-weight: 700;
  color: #FFFFFF;
}

/* ── Narrow screens ───────────────────────────────────────
   The collage's overlaps only work with room for them, so below
   the desktop breakpoint it unwinds into one column and the doors
   stack. The hero loses its side-by-side split and centres.  */
@media (max-width: 1024px) {
  .hero-body {
    width: 100%;
    padding: calc(var(--nav-h) + var(--subnav-h)) 32px 0;
    margin: 0;
  }
  /* Stacked layout: the copy sits over the picture rather than beside
     it, so the wash is carried down the whole frame to keep the dark
     type off the busiest part of the photograph. #EAD6BB is the median
     of the photo's bare marble (hero_bg.png) — white here would sit as
     a pale panel on top of a beige picture. */
  .hero-scrim {
    background: linear-gradient(
      180deg,
      rgba(234,214,187,0.92) 0%,
      rgba(234,214,187,0.55) 30%,
      rgba(234,214,187,0.62) 60%,
      rgba(234,214,187,0.88) 100%
    );
  }
  .collage-inner { grid-template-columns: 1fr; row-gap: 28px; }
  .collage-portrait { margin-top: 0; aspect-ratio: 4 / 5; }
  .collage-side { align-items: flex-start; padding-top: 0; }
  /* Stacked: there is no gutter to straddle, so the scoop is dropped
     and the button sits inside the picture's corner instead. */
  /* Stacked: no gutter to straddle, so the button sits fully inside
     the picture's corner and the scoop is dropped. */
  /* Stacked: no gutter to straddle, so the step is dropped and the
     button sits inside the picture's corner. */
  .collage-notch { display: none; }
  .collage-cta-slot {
    left: 20px;
    right: 20px;
    top: auto;
    bottom: 20px;
    align-items: flex-start;
  }
  /* The note is a fixed 470px so its first line can run under the
     button at desktop width. There is no room for that here. */
  .collage-note { width: auto; max-width: 100%; }
  .doors { grid-auto-flow: row; }
  .door { min-height: 300px; }
}
@media (max-width: 640px) {
  /*
    A phone shows a slice about a quarter of the picture wide. Centred,
    that slice is the open middle of the flat-lay and no product is in
    it at all. 20% pulls it left onto the column of brushes, compact and
    foundation, so the picture still says what it is.
  */
  .hero-photo { background-position: 20% center; }

  .hero-body { padding: calc(var(--nav-h) + var(--subnav-h)) 20px 0; }
  /* The headline's <br> is placed for two wide lines. A phone can't
     hold either of them, so a forced break there strands a word on a
     line of its own ("a" at 360px, "you." at 390px). Drop it and let
     the browser even the lines out instead. */
  .hero-title br { display: none; }
  .hero-title { text-wrap: balance; }
  .hero-actions { flex-direction: column; align-items: stretch; }
  .collage { padding: 104px 0 112px; }
  .collage-inner { padding: 0 16px; }
}

/* ── ══════════════════════════════════════════════════════
   HOME — EMAIL CAPTURE

   A row at the top of the footer rather than a section above it. It
   was a full-bleed dark band first, which put the same ink either
   side of a strip of white page and read as two slabs fighting each
   other. Sharing the footer's ground and separating with a hairline
   makes it the first thing in the footer instead of a rival to it.

   The field styling below is shared, so it survives if the form is
   ever rolled out to the footer on every page.
══════════════════════════════════════════════════════ ── */
.footer-signup {
  border-bottom: 1px solid var(--footer-rule);
}
.footer-signup-inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 40px 32px;
  display: grid;
  grid-template-columns: 1fr minmax(300px, 440px);
  gap: 40px;
  align-items: center;
}
.footer-signup-title {
  font-family: var(--font-head);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
  color: var(--footer-text);
}
.footer-signup-sub {
  font-size: 13px;
  line-height: 1.6;
  color: var(--footer-text);
  margin-top: 6px;
  max-width: 420px;
}

.signup-form {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 10px;
}
.signup-input {
  height: 46px;
  padding: 0 18px;
  border-radius: 999px;
  border: 1.5px solid rgba(24,24,24,0.22);
  background: #FFFFFF;
  color: var(--footer-text);
  font-family: var(--font-body);
  font-size: 13.5px;
  outline: none;
  transition: border-color 0.15s, background 0.15s;
  min-width: 0;   /* or a long placeholder widens the grid track */
}
.signup-input::placeholder { color: rgba(24,24,24,0.5); }
.signup-input:focus {
  border-color: var(--primary);
}
.signup-input.error { border-color: var(--error); }

.signup-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 46px;
  padding: 0 24px;
  border: none;
  border-radius: 999px;
  /* The primary green, like every other main button. It was gold
     while the footer was dark green, where green disappeared. */
  background: var(--primary-grad);
  color: var(--on-primary);
  font-family: var(--font-head);
  font-size: 13.5px;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
  transition: filter 0.15s, transform 0.15s;
}
.signup-btn:hover { filter: brightness(0.9); transform: translateY(-1px); }
.signup-btn:disabled { opacity: 0.6; pointer-events: none; transform: none; }
.signup-btn svg { transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1); }
.signup-btn:hover svg { transform: translate3d(2px, -2px, 0); }

.signup-note {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--footer-text);
  margin-top: 2px;
}
.signup-note.is-error { color: var(--error); }
.signup-note.is-done { color: var(--success); }

@media (max-width: 860px) {
  .footer-signup-inner {
    grid-template-columns: 1fr;
    gap: 22px;
    padding: 32px 20px;
  }
  /* Stacked, the button goes full width under the field rather than
     squeezing the input to nothing. */
  .signup-form { grid-template-columns: 1fr; }
  .signup-btn { justify-content: center; }
}

/* ── Trending / New Arrivals section ───────────────────────
   The first of these follows the category doors, which are a hard
   full-bleed dark band — with no top padding the heading sat flush
   against that edge. The space is on the section rather than on the
   doors so it travels with the rows wherever they are placed. */
.products-section { padding: 80px 0 48px; }

/* ── ══════════════════════════════════════════════════════
   FILTER SIDEBAR (Category & Search pages)
══════════════════════════════════════════════════════ ── */
/* Sticky, and taller than a laptop screen once every card is open —
   so it scrolls on its own. Without a height it could only be moved
   by scrolling the whole page, and its top and bottom stayed out of
   reach until the product grid had been scrolled to its ends. The
   height matches its sticky offset (nav + 20px) with 20px spare
   below. The drawer on small screens overrides both. */
.filter-sidebar {
  max-height: calc(100vh - var(--nav-h) - 40px);
  overflow-y: auto;
  /* Scrolls, but without a visible bar beside the cards. */
  scrollbar-width: none;
}
.filter-sidebar::-webkit-scrollbar { display: none; }

.filter-card {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: 14px;
}
.filter-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.filter-card-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--dark);
}
.filter-card-head svg { transition: transform 0.16s ease; }
/* Collapsed cards keep their head as the handle back open, and the
   chevron points at the state it will move to. */
.filter-card.collapsed .filter-card-head { border-bottom-color: transparent; }
.filter-card.collapsed .filter-card-head svg { transform: rotate(-90deg); }
.filter-card.collapsed .filter-card-body { display: none; }

.filter-card-body { padding: 12px 16px; }

.filter-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 0;
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color 0.12s;
}
.filter-option:hover .filter-option-label { color: var(--black); }
.filter-option:hover .filter-option-count { color: var(--mid); }

/* The facets are divs rather than inputs, so the focus ring has to
   be drawn on the row itself for keyboard users. */
.filter-option:focus-visible,
.rating-filter-item:focus-visible,
.size-chip:focus-visible,
.color-swatch:focus-visible,
.filter-card-head:focus-visible,
.clear-filters-btn:focus-visible,
.active-filter-chip:focus-visible,
.view-btn:focus-visible {
  outline: 2px solid var(--warn);
  outline-offset: 2px;
}
.filter-option-label {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  color: var(--dark);
}
.filter-option-count { font-size: 11px; color: var(--subtle); }

/* Price range visual */
.price-range-track {
  position: relative;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  margin: 18px 6px 24px;
  cursor: pointer;
  /* The thumbs drag on pointer events, so the browser must not
     claim the gesture for scrolling first. */
  touch-action: none;
}
/* The bar is 4px tall, which is far too thin to aim at. This gives
   it a ~26px invisible grab strip. It is the track's first child, so
   the fill and thumbs still paint over it and keep their own hits. */
.price-range-track::before {
  content: '';
  position: absolute;
  inset: -11px 0;
}
.price-range-fill {
  position: absolute;
  left: 18%;
  right: 22%;
  height: 100%;
  background: var(--primary-grad);
  border-radius: 2px;
}
.price-range-thumb {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: white;
  border: 2px solid var(--blue);
  box-shadow: var(--shadow-sm);
  cursor: grab;
  touch-action: none;
  transition: box-shadow 0.15s, border-width 0.1s;
}
/* Same idea for the 16px handle — a 32px target to grab. */
.price-range-thumb::before {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
}
.price-range-thumb:hover { box-shadow: 0 0 0 6px var(--primary-lt); }
.price-range-thumb.dragging {
  cursor: grabbing;
  border-width: 3px;
  box-shadow: 0 0 0 8px var(--primary-lt);
}
.price-range-thumb:focus-visible {
  outline: 2px solid var(--warn);
  outline-offset: 3px;
}
.price-inputs { display: flex; gap: 10px; align-items: center; }
.price-input {
  flex: 1;
  /* A flex item won't shrink below its intrinsic width while
     min-width is auto, and an input's intrinsic width comes from
     its size attribute (~172px by default) — enough to push the
     pair out of the sidebar. */
  min-width: 0;
  height: 36px;
  padding: 0 10px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--dark);
  outline: none;
  text-align: center;
}
.price-input:focus { border-color: var(--primary-text); }
.price-sep { color: var(--subtle); font-size: 12px; }

/* Rating filter */
.rating-filter-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 0;
  cursor: pointer;
  font-size: 12.5px;
  color: var(--mid);
}
.rating-filter-item:hover { color: var(--dark); }

/* Color swatches filter */
.color-swatches { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 0; }
.color-swatch {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  cursor: pointer;
  border: 2px solid transparent;
  transition: border-color 0.15s;
}
.color-swatch.selected { border-color: var(--primary-text); }
.color-swatch-inner {
  width: 100%;
  height: 100%;
  border-radius: 50%;
}

/* Size filter */
.size-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0; }
.size-chip {
  height: 32px;
  min-width: 40px;
  padding: 0 10px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
  color: var(--mid);
  cursor: pointer;
  transition: all 0.15s;
}
.size-chip.selected { border-color: var(--primary-text); background: var(--blue-lt); color: var(--primary-text); }
.size-chip:hover { border-color: var(--faint); color: var(--dark); }

/* Clear filters */
.clear-filters-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--error);
  cursor: pointer;
  padding: 8px 0;
}

/* ── Category/Search page top bar ─────────────────────────── */
.listing-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 18px;
}
.listing-result-count { font-size: 13px; color: var(--subtle); }
.listing-result-count strong { color: var(--dark); font-weight: 700; }
.listing-sort { display: flex; align-items: center; gap: 8px; }
.listing-sort-label { font-size: 12.5px; color: var(--subtle); }
.listing-view-toggle { display: flex; gap: 4px; margin-left: 12px; }
.view-btn {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.15s;
  /* Both icons draw with currentColor so the active state only has to
     flip one property — the inactive icon used to be a hard-coded
     grey, which left it unreadable once the button filled orange. */
  color: var(--mid);
}
.view-btn:hover { border-color: var(--faint); color: var(--dark); }
.view-btn.active { background: var(--blue); border-color: var(--blue); color: var(--on-primary); }

/* Active filters chips bar */
.active-filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.active-filter-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 12px;
  background: var(--blue-lt);
  border: 1px solid rgba(47,107,85,0.25);
  border-radius: 20px;
  font-size: 12px;
  color: var(--primary-text);
  font-weight: 600;
  cursor: pointer;
}
.active-filter-chip:hover { background: rgba(47,107,85,0.18); }

/* ── ══════════════════════════════════════════════════════
   PRODUCT DETAIL PAGE
══════════════════════════════════════════════════════ ── */
.product-detail-layout {
  display: grid;
  grid-template-columns: 1fr 480px;
  gap: 48px;
  align-items: flex-start;
  padding: 24px 0 48px;
}

/* Image gallery */
.pd-gallery { display: flex; gap: 14px; }
.pd-thumbnails {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex-shrink: 0;
}
.pd-thumb {
  width: 72px;
  height: 80px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--input-bg);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.15s;
}
.pd-thumb.active { border-color: var(--primary-text); }
.pd-main-img {
  flex: 1;
  aspect-ratio: 3/3.5;
  border-radius: var(--radius-lg);
  background: var(--input-bg);
  overflow: hidden;
  position: relative;
}
.pd-main-img-bg { width: 100%; height: 100%; }

/* ── Detail page while loading ─────────────────────────────────
   The page ships with no product in it; product.js fills every
   field from the API. Until it does, an empty field reads as a
   shimmer bar, and anything that only exists for some products
   (a sale price, colour and size pickers) stays out of the way.
   Each rule stops matching the moment its field is written.     */
.pd-main-img-bg:not([style]),
.pd-brand-name:empty,
.pd-title:empty,
.pd-rating-score:empty,
.pd-price:empty,
.pd-description .body-lg:empty,
.rating-big:empty {
  background-image: var(--skeleton-fill);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: shimmer 1.8s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
.pd-brand-name:empty    { display: inline-block; width: 90px; height: 12px; }
.pd-title:empty         { width: 70%; height: 32px; }
.pd-rating-score:empty  { display: inline-block; width: 140px; height: 16px; }
.pd-price:empty         { display: inline-block; width: 110px; height: 34px; }
.pd-description .body-lg:empty { height: 72px; }
.rating-big:empty       { width: 72px; height: 56px; margin: 0 auto; }
.pd-thumbnails:empty    { width: 72px; }

.pd-price-old:empty,
.pd-discount-badge:empty,
.pd-section-label:empty,
.pd-swatches:empty,
.pd-sizes:empty,
.pd-description ul:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
  .pd-main-img-bg:not([style]),
  .pd-brand-name:empty, .pd-title:empty, .pd-rating-score:empty,
  .pd-price:empty, .pd-description .body-lg:empty, .rating-big:empty { animation: none; }
}

.pd-img-dots {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 6px;
}
.pd-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.5); }
.pd-dot.active { background: white; width: 20px; border-radius: 4px; }

/* Product info */
.pd-info {}
.pd-brand-tag { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.pd-brand-name {
  font-size: 12px;
  font-weight: 700;
  color: var(--primary-text);
  letter-spacing: 0.5px;
  text-transform: uppercase;
}
.pd-title {
  font-family: var(--font-head);
  font-size: 26px;
  font-weight: 700;
  color: var(--black);
  line-height: 1.25;
  margin-bottom: 12px;
}
.pd-rating-row { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.pd-rating-score {
  font-family: var(--font-head);
  font-size: 15px;
  font-weight: 700;
  color: var(--black);
}
.pd-review-count { font-size: 13px; color: var(--primary-text); cursor: pointer; }
.pd-sold { font-size: 12.5px; color: var(--subtle); padding-left: 10px; border-left: 1px solid var(--border); }

.pd-price-row { display: flex; align-items: baseline; gap: 10px; margin-bottom: 20px; }
.pd-price {
  font-family: var(--font-head);
  font-size: 30px;
  font-weight: 700;
  color: var(--black);
}
.pd-price-old { font-size: 16px; color: var(--faint); text-decoration: line-through; }
.pd-discount-badge {
  background: var(--error-lt);
  color: var(--error);
  padding: 3px 9px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 700;
}

.pd-section-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--mid);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 10px;
}

/* Color swatches */
.pd-swatches { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
.pd-swatch {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  border: 2.5px solid transparent;
  transition: border-color 0.15s;
}
.pd-swatch.active { border-color: var(--primary-text); }

/* Size selector */
.pd-sizes { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
.pd-size {
  height: 38px;
  min-width: 48px;
  padding: 0 12px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--mid);
  cursor: pointer;
  transition: all 0.15s;
}
.pd-size.active { border-color: var(--primary-text); background: var(--blue-lt); color: var(--primary-text); }
.pd-size.out { color: var(--faint); border-color: var(--border); text-decoration: line-through; cursor: default; }
.pd-size:not(.out):hover { border-color: var(--faint); }

/* Qty row */
.pd-qty-row { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
/* .qty-ctrl / .qty-btn / .qty-val live in common.css — shared with the cart page */
.qty-stock { font-size: 12.5px; color: var(--success); font-weight: 600; }

/* Add to cart row */
.pd-cta-row { display: flex; gap: 10px; margin-bottom: 20px; }
.pd-add-cart {
  flex: 1;
  height: 52px;
  background: var(--primary-grad);
  color: var(--on-primary);
  border: none;
  border-radius: var(--radius-md);
  font-family: var(--font-head);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transition: filter 0.15s;
}
.pd-add-cart:hover { filter: brightness(0.90); }
.pd-wish-btn {
  width: 52px;
  height: 52px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.15s;
  flex-shrink: 0;
}
.pd-wish-btn:hover { border-color: var(--error); background: var(--error-lt); }
.pd-wish-btn.active { background: var(--error-lt); border-color: var(--error); }

/* Delivery info */
.pd-delivery-info {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}
.pd-delivery-row { display: flex; align-items: center; gap: 10px; font-size: 12.5px; color: var(--dark); }
.pd-delivery-icon { color: var(--primary-text); flex-shrink: 0; }

/* Product tabs */
.pd-tabs {
  border-bottom: 1px solid var(--border);
  display: flex;
  gap: 0;
  margin: 40px 0 24px;
}
.pd-tab {
  padding: 12px 24px;
  font-size: 14px;
  font-weight: 600;
  color: var(--subtle);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color 0.15s, border-color 0.15s;
}
.pd-tab:hover { color: var(--dark); }
.pd-tab.active { color: var(--primary-text); border-bottom-color: var(--primary-text); }

/* Review cards on PD page */
.review-card-web {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 18px 20px;
  margin-bottom: 14px;
}
.review-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px; }
.reviewer-info {}
.reviewer-name { font-size: 14px; font-weight: 700; color: var(--dark); }
.review-meta { font-size: 11.5px; color: var(--subtle); margin-top: 2px; }
.review-date { font-size: 12px; color: var(--subtle); }
.review-text { font-size: 13.5px; color: var(--dark); line-height: 1.65; }
.review-verified-web {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  color: var(--success);
  font-weight: 600;
  margin-top: 8px;
}

/* Rating summary for reviews section */
.rating-summary {
  display: flex;
  gap: 40px;
  align-items: flex-start;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 24px;
  margin-bottom: 24px;
}
.rating-big { font-family: var(--font-head); font-size: 56px; font-weight: 700; color: var(--black); line-height: 1; }
.rating-sub { font-size: 12px; color: var(--subtle); margin-top: 4px; }
/* Held to half the card. Stretched across the full width the tracks
   become long thin ribbons and the differences between ratings get
   harder to read, not easier. The last column is pinned right so
   capping the bars doesn't leave the card ending in dead space. */
.rating-bars {
  flex: 0 1 50%;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.rating-summary > :last-child { margin-left: auto; }
.rbar-row { display: flex; align-items: center; gap: 10px; }
.rbar-label { font-size: 12px; color: var(--mid); width: 36px; text-align: right; flex-shrink: 0; }
.rbar-track { flex: 1; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
.rbar-fill { height: 100%; background: var(--primary-grad); border-radius: 4px; }
.rbar-count { font-size: 11.5px; color: var(--subtle); width: 28px; flex-shrink: 0; }

/* ── Wishlist page ────────────────────────────────────────── */
.wishlist-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}
.wishlist-title {
  font-family: var(--font-head);
  font-size: 22px;
  font-weight: 700;
  color: var(--black);
}
.wishlist-subtitle { font-size: 13.5px; color: var(--subtle); margin-top: 3px; }


/* ── ══════════════════════════════════════════════════════
   RESPONSIVE — LISTING, SEARCH & PRODUCT

   Same breakpoints as common.css. The home page's own rules live
   with their sections further up this file.
══════════════════════════════════════════════════════ ── */

@media (max-width: 1024px) {
  /* Gallery above the buy box, which is the order they are read in. */
  .product-detail-layout { grid-template-columns: 1fr; gap: 24px; }
  .product-detail-layout > * { min-width: 0; }
}

/* ── Filters as a drawer ──────────────────────────────────
   Below the breakpoint the sidebar leaves the flow and slides in
   over the page, opened from a button in the bar above the grid.
   The button, backdrop and close control are built by listing.js;
   if that script never runs, none of the rules below apply and the
   sidebar stays where it was — unstyled but reachable.        */
.filter-drawer-open,
.filter-drawer-close,
.filter-drawer-backdrop { display: none; }

@media (max-width: 1024px) {
  .filter-drawer-open {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 36px;
    padding: 0 16px;
    border: 1.5px solid var(--border);
    border-radius: 999px;
    background: var(--white);
    color: var(--black);
    font-family: var(--font-head);
    font-size: 13px;
    font-weight: 600;
  }
  .filter-drawer-open:hover { border-color: var(--faint); }

  .filter-sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: calc(var(--z-modal) + 1);
    width: min(320px, 86vw);
    max-height: none;
    overflow-y: auto;
    padding: 56px 16px 24px;
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    transform: translateX(-100%);
    transition: transform 0.24s cubic-bezier(0.16, 1, 0.3, 1);
  }
  body.filters-open .filter-sidebar { transform: none; }

  .filter-drawer-close {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 12px;
    right: 12px;
    width: 34px;
    height: 34px;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--white);
    color: var(--black);
  }

  .filter-drawer-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    background: rgba(10,7,8,0.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.24s ease;
  }
  body.filters-open .filter-drawer-backdrop { opacity: 1; pointer-events: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .filter-sidebar,
  .filter-drawer-backdrop { transition: none; }
}

@media (max-width: 640px) {
  .filter-card { margin-bottom: 10px; }

  /*
    The review header is the big average, the bar chart at half width
    and a pinned-right column, with a 40px gap. There is not 40px to
    spare at 360px, so it stacks: average on top, bars full width.
  */
  .rating-summary {
    flex-direction: column;
    gap: 16px;
    padding: 18px 16px;
  }
  .rating-summary > * { min-width: 0; }
  .rating-bars { flex: 1 1 auto; width: 100%; }
  /* The right-pinned column has nothing to pin against once stacked. */
  .rating-summary > :last-child { margin-left: 0; }

  /* The tab row scrolls rather than wrapping — Description / Reviews /
     Size Guide / Shipping will not fit on one phone line. */
  .pd-tabs {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .pd-tabs::-webkit-scrollbar { display: none; }
  .pd-tab { flex: 0 0 auto; white-space: nowrap; }

  /* The bar above the grid: count on one line, sort control below. */
  .listing-topbar {
    flex-wrap: wrap;
    gap: 10px;
  }
}

@media (max-width: 360px) {
  /* Sort control and the grid/list toggle share one line and together
     need 300px. Let the toggle drop below rather than push the page. */
  .listing-sort { flex-wrap: wrap; justify-content: flex-end; row-gap: 8px; }
  .listing-sort > * { min-width: 0; }
}
