/* =========================================================
   PAVIA — Modern Elegant Fashion
   Mobile-first stylesheet · Editorial luxury aesthetic
   ========================================================= */

/* Fonts are loaded via <link rel="stylesheet"> in the page <head> for faster first paint. */

:root {
  /* Palette — elegant grayscale (white · black · gray) */
  --ink: #1a1a1a;
  --ink-soft: #444444;
  --muted: #6f6f6f;
  --muted-soft: #a8a8a8;
  --paper: #fafafa;
  --paper-warm: #f1f1f1;
  --cream: #ececec;
  --sand: #d4d4d4;
  --champagne: #a8a8a8;
  --cocoa: #3a3a3a;
  --cocoa-dark: #2a2a2a; /* used by the admin crop modal */
  --line: rgba(0, 0, 0, 0.10);
  --line-soft: rgba(0, 0, 0, 0.06);
  --line-strong: rgba(0, 0, 0, 0.16);

  /* Effects */
  --shadow-xs: 0 2px 8px rgba(0, 0, 0, 0.05);
  --shadow-sm: 0 8px 24px rgba(0, 0, 0, 0.07);
  --shadow-md: 0 18px 48px rgba(0, 0, 0, 0.10);
  --shadow-lg: 0 28px 80px rgba(0, 0, 0, 0.16);
  --shadow-drawer: -24px 0 64px rgba(0, 0, 0, 0.18);

  /* Geometry */
  --r-xs: 8px;
  --r-sm: 14px;
  --r-md: 20px;
  --r-lg: 28px;
  --r-xl: 36px;
  --r-pill: 999px;

  /* Layout */
  --container: 1240px;
  --header-h: 64px;
  --bottom-nav-h: 0px; /* mobile bottom nav removed; token kept at 0 so layout calc()s collapse cleanly */
  --gutter: 18px;

  /* Type */
  --display: 'Fraunces', 'Cormorant Garamond', Georgia, serif;
  --body: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  /* Alias used throughout admin.css `font:` shorthands. Was undefined, which made
     every `font: … var(--sans)` declaration invalid (dropped) — admin inputs/buttons
     fell back to tiny inherited text. */
  --sans: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* Easings */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* =========================================================
   Reset & base
   ========================================================= */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
  background: #e6e6e6; /* flat page background */
  min-height: 100vh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* Storefront uses the flat #e6e6e6 page background set on body (above). /admin
   shares this stylesheet but overrides the background in admin.css. */
/* Scroll lock for open drawers/modals. position:fixed (not just overflow:hidden)
   so iOS Safari can't scroll the background; JS sets top:-scrollY and restores it
   on close so the page doesn't jump. */
body.scroll-locked { position: fixed; left: 0; right: 0; width: 100%; overflow: hidden; }
a { color: inherit; text-decoration: none; }
img, svg { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: none; background: none; padding: 0; }
input, select, textarea { -webkit-appearance: none; appearance: none; }

::selection { background: var(--ink); color: var(--paper); }

/* Container & utility */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.eyebrow {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cocoa);
  margin: 0 0 12px;
}
.muted { color: var(--muted); }

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 400;
  font-variation-settings: "opsz" 144, "SOFT" 0;
  letter-spacing: -0.025em;
  line-height: 1.02;
  margin: 0;
  color: var(--ink);
}
h1 { font-size: clamp(38px, 7vw, 60px); font-weight: 300; }
h2 { font-size: clamp(30px, 5.5vw, 46px); font-weight: 400; }
h3 { font-size: clamp(20px, 4vw, 28px); font-weight: 500; }
h4 { font-size: clamp(17px, 3vw, 20px); font-weight: 500; }
p { margin: 0; line-height: 1.65; color: var(--ink-soft); }

.skip-link {
  position: absolute; left: -9999px; top: 8px;
  background: var(--ink); color: var(--paper);
  padding: 10px 16px; border-radius: var(--r-sm);
  font-size: 13px; font-weight: 600; z-index: 200;
}
.skip-link:focus { left: 8px; }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap;
}

/* "Load more" control under the product grid */
.load-more-wrap {
  display: flex;
  justify-content: center;
  margin-top: 32px;
}
.load-more-wrap[hidden] { display: none; }

/* =========================================================
   Header
   ========================================================= */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(250, 250, 250, 0.78);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
  border-bottom: 1px solid var(--line-soft);
  transition: background 0.3s var(--ease-out), border-color 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out);
}
.site-header.is-scrolled {
  background: rgba(250, 250, 250, 0.94);
  border-bottom-color: var(--line);
  box-shadow: 0 2px 24px rgba(0, 0, 0, 0.05);
}
.nav-wrap {
  position: relative;
  height: var(--header-h);
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
}
.brand {
  display: inline-flex; align-items: center;
  color: var(--ink);
  transition: opacity 0.3s var(--ease-out);
}
.brand:hover { opacity: 0.82; }
.brand-logo {
  display: block;
  /* Chromium ignores width:auto on an inline SVG that has intrinsic dims, so size
     both axes explicitly on the 982:333 viewBox ratio (height x 2.9489 = width). */
  height: 27px;
  width: 80px;
  color: var(--ink); /* drives the wordmark fill via currentColor */
}
.desktop-nav { display: none; }
.nav-actions { display: flex; align-items: center; gap: 6px; }
.icon-btn {
  width: 53px; height: 53px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  color: var(--ink);
  position: relative;
  transition: background 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.icon-btn:hover { background: rgba(0, 0, 0, 0.06); }
.icon-btn:active { transform: scale(0.94); }
.icon-btn svg { width: 24px; height: 24px; stroke: currentColor; stroke-width: 1.6; fill: none; }
.icon-btn .count {
  position: absolute; top: 4px; right: 4px;
  min-width: 22px; height: 22px; padding: 0 5px;
  background: var(--ink); color: var(--paper);
  font-size: 12px; font-weight: 700;
  border-radius: var(--r-pill);
  display: grid; place-items: center;
  border: 2px solid var(--paper);
  transform: scale(0); transition: transform 0.3s var(--ease-spring);
}
.icon-btn .count.is-visible { transform: scale(1); }
.icon-btn .count.is-bumping { animation: bump 0.45s var(--ease-spring); }
@keyframes bump {
  0%, 100% { transform: scale(1); }
  40%      { transform: scale(1.35); }
}

/* =========================================================
   Hero — full-bleed campaign photo under a sticky curtain
   =========================================================
   The hero pins to the viewport (position:sticky) while the opaque .page-sheet
   below slides up and OVER it on scroll — that reveal IS the transition into
   the collection. Works with no JS; app.js layers a camera-push zoom on the
   photo plus a copy/card parallax fade on top of it. Height accounts for the
   sticky header so the initial fold lands exactly on the sheet's rounded seam. */
.hero {
  position: sticky;
  top: 0;
  z-index: 1;
  min-height: calc(100vh - var(--header-h));
  min-height: calc(100svh - var(--header-h));
  display: flex;
  overflow: hidden; /* clips the backdrop's scroll zoom */
  padding: 14px 0 34px;
}
/* Campaign photo layer. The background color is the wall tone of the shot, so
   there is never a white flash while the image streams in. */
.hero-backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: #cdbfb0;
}
/* One transform group for the whole photographic composite — the base shot, the
   look panes and their borders. app.js writes the scroll camera-push scale HERE
   (not on the <img>) so every layer zooms as one and the panes stay in register
   with the photo they're cut into. */
.hero-backdrop__frame {
  position: absolute;
  inset: 0;
  transform-origin: 50% 30%;
  will-change: transform;
  /* Slow "settle" zoom on first paint — the frame eases from a hair over-scaled
     to rest, a quiet cinematic open. Transform-only (no opacity) so it never
     delays LCP, and app.js's scroll zoom writes an inline transform that simply
     takes over once the visitor starts scrolling. */
  animation: heroImgSettle 2s var(--ease-out) both;
}
.hero-backdrop picture,
.hero-backdrop img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.hero-backdrop img {
  object-fit: cover;
  /* Mobile-first crop position. The portrait <source> (hero-bg2-mobile-v3.webp)
     is already framed to the phone's ~0.56 aspect — model on the right, empty
     left wall for the copy — so this just centres it; object-position only
     nudges the small overflow on phones whose fold isn't exactly that aspect.
     The desktop landscape frame (full 1920/2560 source) overrides both at >=960px.
     This rule deliberately covers the look panes' images too — matching
     object-fit/object-position at every breakpoint is exactly what keeps a
     window's contents nailed to the base shot. */
  object-position: 50% 50%;
}
@keyframes heroImgSettle {
  from { transform: scale(1.06); }
  to   { transform: scale(1); }
}
/* One overlay now: the bottom fade that melts the photo into the sheet seam.
   The warm left scrim that used to sit under the copy is GONE (v0.7.7) — it read
   as a dark tint painted over the photograph, which is the opposite of what this
   shot is. The white copy is carried by its own shadow instead (see below). */
.hero-backdrop__wash {
  position: absolute;
  inset: 0;
  /* The wash sits over the look windows and their borders — clicks and drags
     have to reach them. */
  pointer-events: none;
  background:
    linear-gradient(0deg, rgba(230, 230, 230, 0.88) 0%, rgba(230, 230, 230, 0.32) 14%, rgba(230, 230, 230, 0) 34%);
}
.hero-grid {
  position: relative;
  z-index: 1;
  flex: 1;
  /* The grid spans the whole fold, including the half of it the model (and her
     look windows) occupy — so it has to let pointers through, and the copy
     column takes them back. */
  pointer-events: none;
  display: grid;
  /* Phone composition: copy top-left on the empty wall, the model owning the
     right side of the frame — the photo itself carries the rest of the fold. */
  grid-template-columns: 1fr;
  align-items: start;
}
/* Copy hugs the clear wall left of the model; her raised hands reach ~62% of a
   phone screen, so the column stays narrower than that — with no veil behind it,
   the width is the only thing keeping a long headline word off her sleeve. */
.hero-copy { position: relative; z-index: 2; max-width: min(560px, 58vw); display: flex; flex-direction: column; align-self: stretch; }
/* The column is stretched to the full fold and the CTAs are pushed to its foot,
   so most of it is empty space — and on a phone that empty space sits right on
   top of the look windows. Give the pointer back block by block: the lines and
   the buttons take their own boxes (selectable, clickable), the gap between them
   passes through to the windows behind. */
.hero-copy { pointer-events: none; }
.hero-copy > * { pointer-events: auto; }
/* The copy reads INK over the campaign photo (v0.9.2). It used to be white, and
   that was the wrong way round. The wall she stands on samples 185-220 luminance,
   a pale cream, so white type on it is about 1.8:1, and with both scrims gone
   (v0.7.7, v0.8.4) the drop-shadow was the ONLY thing carrying the words. Type
   held up by its own shadow reads soft and washed out, which is exactly what it
   looked like. Ink on that same wall is ~10:1 and needs nothing painted over the
   photograph, so the shot stays untouched, and it matches the CTAs sitting right
   under it (the primary is already ink, the ghost is ink on paper).
   The shadow flips with the colour: three centred, offsetless halos of warm
   white, so the wall LIGHTENS around the letterforms instead of a panel appearing
   behind them. That is what keeps the copy legible on the one stretch that is not
   pale, where a long line reaches her sleeve past the ~52% mark on phones. */
.hero-copy .eyebrow,
.hero-copy h1,
.hero-copy .hero-text {
  color: var(--ink);
  /* First layer is tight and nearly opaque, and that is the one that matters. It
     is invisible against the cream wall (near-white on near-white), and it is
     what keeps a word readable on the one dark thing in the column: her raised
     arm, which sweeps into the lower right of the copy on phones. The two wider
     layers are pure atmosphere, low enough not to fog the wall. */
  text-shadow: 0 0 2px rgba(255, 253, 248, 0.9), 0 0 7px rgba(255, 251, 243, 0.6), 0 0 26px rgba(255, 249, 238, 0.4);
}
/* The paragraph steps back a tone from the headline, hierarchy the white version
   could not express since every line was the same #fff. It stops at cocoa-dark
   rather than --ink-soft: the wall dips to ~160 luminance at the seam behind the
   copy, where #444 lands just under 4.5:1 and #2a2a2a clears it. */
.hero-copy .hero-text { color: var(--cocoa-dark); }
/* The headline is the biggest mass of ink, so it carries a wider halo. */
.hero-copy h1 {
  text-shadow: 0 0 3px rgba(255, 253, 248, 0.92), 0 0 11px rgba(255, 251, 243, 0.62), 0 0 36px rgba(255, 249, 238, 0.44);
}
/* Ghost CTA gets a faint paper fill here so its label never fights the photo. */
.hero .btn-ghost { background: rgba(250, 250, 250, 0.55); }
.hero .btn-ghost:hover { background: var(--ink); }
/* The primary CTA carries a soft shadow so it reads as a solid object lifted
   off the campaign photo, and deepens on hover. */
.hero .btn-primary { box-shadow: 0 12px 30px rgba(0, 0, 0, 0.16); }
.hero .btn-primary:hover { box-shadow: 0 16px 40px rgba(0, 0, 0, 0.22); }
.hero-copy h1 {
  margin: 8px 0 18px;
  font-style: normal;
  text-wrap: balance; /* even two-line breaks at every size (progressive) */
}
/* Entrance: the copy cascades up once on first paint — pure CSS, so it never
   waits on scripts. Each line rises and lifts out of a soft blur (a gentle
   editorial focus-pull). `backwards` fill (not `both`) matters: once each line
   has landed, the animation releases the transform so the scroll parallax that
   app.js writes inline can take over seamlessly. */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(26px); filter: blur(6px); }
}
/* Delays sit just past the splash's minimum visible window (500ms + fade), so
   the cascade's landing is what greets the visitor as the splash lifts. A hair
   more spacing between lines than before makes the stagger read as deliberate. */
.hero-copy .eyebrow       { animation: heroRise 0.75s var(--ease-out) 0.35s backwards; }
.hero-copy h1             { animation: heroRise 0.9s  var(--ease-out) 0.48s backwards; }
.hero-copy .hero-text     { animation: heroRise 0.9s  var(--ease-out) 0.62s backwards; }
.hero-copy .hero-actions  { animation: heroRise 0.9s  var(--ease-out) 0.86s backwards; }
.hero-copy h1 em {
  font-style: italic;
  font-weight: 300;
  color: var(--ink);
  /* The accent word "blooms" up to full ink just after the headline lands: a
     quiet emphasis, no transform (so it never fights the headline's rise or the
     scroll parallax). With ink copy the bloom darkens in rather than brightening
     up, starting translucent so the word is the last thing to settle. */
  animation: heroEmBloom 1.1s var(--ease-out) 1s both;
}
@keyframes heroEmBloom {
  from { color: rgba(26, 26, 26, 0.55); }
  to   { color: #1a1a1a; }
}
/* A short editorial rule leads the eyebrow — scoped to the hero so the shop and
   visit eyebrows keep their plain form. Kept as an inline pseudo (not flex + gap,
   which would scatter the eyebrow's own words) so the label text flows normally. */
.hero-text {
  font-size: 16px;
  max-width: 540px;
  color: var(--ink-soft);
  margin-bottom: 18px;
}
.hero-actions {
  display: flex; gap: 10px; flex-wrap: wrap;
  /* Pinned to the bottom of the copy column: with the column stretched to the
     full hero height, `margin-top:auto` drops the CTAs to the lower-left of the
     fold so they always sit inside the pinned first screen (never clipped under
     the rising sheet). On desktop the column is `align-self:center`, so it hugs
     its content height and this auto-margin collapses — CTAs sit under the copy
     exactly as before. */
  margin-top: auto;
}

/* ---------------------------------------------------------
   Hero look windows — five other outfits, on the same model
   ---------------------------------------------------------
   assets/hero-look1..5 are the SAME frame as the hero shot with a different
   garment on her. Each window is a small overflow:hidden box holding a copy of
   the photo restated at the frame's full size (--fw/--fh, written by
   js/hero-looks.js) with the base photo's exact object-fit/position (the shared
   `.hero-backdrop img` rules above), so all that shows through the box is her,
   in another look, in register with the shot behind it. js/hero-looks.js sizes
   the boxes from the live cover-crop and drives the drag; without it, or
   without the images, nothing here renders and the hero is exactly as it was.

   The box translates and its <picture> counter-translates by the same amount,
   so dragging slides the WINDOW across a photograph that never moves. Both are
   permanently promoted, window-sized transform layers: every kind of motion
   this collage does (drift, drag, spring, swap) then runs on the compositor.
   The old shape was full-bleed panes cut down with a per-frame clip-path, and
   on phones that repainted five viewport-sized layers on every frame a box
   moved, which is what the mobile jitter was. */
.hero-looks,
.hero-look-edges {
  position: absolute;
  inset: 0;
  pointer-events: none;   /* only the borders take input, see below */
}
.hero:not(.has-looks) .hero-looks,
.hero:not(.has-looks) .hero-look-edges { display: none; }
/* A pane the packer could not seat without touching a neighbour sits this one
   out rather than overlapping. */
.hero-look.is-out,
.hero-look-edge.is-out { display: none; }

.hero-look {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--ww, 0px);
  height: var(--wh, 0px);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.9s var(--ease-out);
  transform: translate3d(calc(var(--wx, 0px) + var(--dx, 0px)), calc(var(--wy, 0px) + var(--dy, 0px)), 0);
  will-change: transform;
}
.hero-look.is-in { opacity: 1; }
/* The photograph inside the window: restated at the frame's full size and
   pinned to the frame's origin by the counter-translate, so the box is the only
   thing that moves. Its raster is clipped to the box by the overflow above, so
   the promoted layer stays window-sized, never viewport-sized. */
.hero-look picture {
  width: var(--fw, 100%);
  height: var(--fh, 100%);
  transform: translate3d(calc(-1 * (var(--wx, 0px) + var(--dx, 0px))), calc(-1 * (var(--wy, 0px) + var(--dy, 0px))), 0);
  will-change: transform;
}

/* Every few seconds the windows trade seats: --wx/--wy/--ww/--wh are driven per
   frame for the two seconds a swap takes, so the box slides AND morphs into the
   rectangle it's taking over — the frames change places while the photograph
   under them never moves. That morph animates width/height (cheap: the box and
   its fixed-size picture are the whole layout scope); the travel itself is the
   same composited transform as everything else. */
/* Slower crossfade for the other kind of swap: on a fold too cramped to seat all
   five, a benched look takes a seat in place rather than flying to it. Same 2s
   beat as a flight, not the 0.9s entrance. */
.hero-look.is-dissolving,
.hero-look-edge.is-dissolving { transition-duration: 2s; }

/* The border is a separate, unclipped element so it can carry a soft outer
   shadow (inside the pane it would be cut off at the window edge). It rides the
   same --wx/--wy/--dx/--dy, so it traces its window exactly. */
.hero-look-edge {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--ww, 0px);
  height: var(--wh, 0px);
  transform: translate3d(calc(var(--wx, 0px) + var(--dx, 0px)), calc(var(--wy, 0px) + var(--dy, 0px)), 0);
  border: 1px solid rgba(255, 255, 255, 0.82);
  /* A hairline of the photo's own warm shadow under the white keeps the frame
     legible where a window lands on the pale wall rather than on fabric. */
  box-shadow: 0 0 0 1px rgba(58, 43, 31, 0.08), 0 18px 40px -26px rgba(46, 34, 24, 0.55);
  pointer-events: auto;
  cursor: grab;
  /* Vertical swipes stay the page's — a window only becomes a drag once a
     finger commits sideways (js/hero-looks.js). This hero has history here:
     v0.7.3 and v0.7.5 were both scroll-dead-zone fixes. */
  touch-action: pan-y;
  opacity: 0;
  transition: opacity 0.9s var(--ease-out), border-color 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out);
  /* Promoted like the boxes: the border and its shadow move every frame the
     window does, and repainting a blurred shadow per frame is exactly the kind
     of work a phone GPU drops frames on. */
  will-change: transform;
}
.hero-look-edge.is-in { opacity: 1; }
/* In flight the frame lifts off the photograph — a deeper, longer shadow reads
   as "carried across" rather than "sliding on the surface". Listed before the
   hover/held rule (same specificity) so a window you grab mid-swap still shows
   the held treatment. */
.hero-look-edge.is-flying {
  border-color: #ffffff;
  box-shadow: 0 0 0 1px rgba(58, 43, 31, 0.14), 0 30px 64px -22px rgba(46, 34, 24, 0.66);
}
.hero-look-edge:hover,
.hero-look-edge.is-held {
  border-color: #ffffff;
  box-shadow: 0 0 0 1px rgba(58, 43, 31, 0.12), 0 22px 50px -24px rgba(46, 34, 24, 0.6);
}
.hero-look-edge.is-held { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
  /* Static collage: the looks still read, they just don't drift or drag. */
  .hero-look { transition: none; }
  .hero-look-edge { cursor: default; transition: none; }
}
/* =========================================================
   Buttons
   ========================================================= */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 8px;
  min-height: 48px; padding: 0 22px;
  border-radius: var(--r-pill);
  font-weight: 600;
  font-size: 14px;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: transform 0.2s var(--ease-out), box-shadow 0.25s var(--ease-out), background 0.25s var(--ease-out), color 0.25s var(--ease-out), border-color 0.25s var(--ease-out);
  white-space: nowrap;
  border: 1px solid transparent;
  position: relative;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
.btn:active { transform: translateY(0) scale(0.97); transition-duration: 0.08s; }
.icon-btn:active { transform: scale(0.9); transition-duration: 0.08s; }
.btn:focus-visible { outline: 2px solid var(--cocoa); outline-offset: 3px; }
/* Shared keyboard-focus ring for the icon/overlay controls that lacked one. */
.icon-btn:focus-visible,
.close-btn:focus-visible,
.modal-close:focus-visible,
.wish-btn:focus-visible,
.filter-toggle:focus-visible,
.search-clear:focus-visible,
.category-pills button:focus-visible,
.size-options button:focus-visible,
.color-swatches button:focus-visible,
.filter-chip:focus-visible,
.qty-control button:focus-visible,
.remove-btn:focus-visible,
.modal-gallery button:focus-visible { outline: 2px solid var(--cocoa); outline-offset: 2px; }
.btn-primary { background: var(--ink); color: var(--paper); }
.btn-primary:hover { background: var(--cocoa); }
.btn-secondary {
  background: var(--paper);
  border-color: var(--line);
  color: var(--ink);
}
.btn-secondary:hover {
  background: var(--cream);
  border-color: var(--line-strong);
}
.btn-ghost { background: transparent; border-color: var(--ink); color: var(--ink); }
.btn-ghost:hover { background: var(--ink); color: var(--paper); }
.btn-soft { background: var(--paper); border-color: var(--line); color: var(--ink); }
.btn-soft:hover { border-color: var(--ink-soft); }
.btn.full { width: 100%; }
.btn-sm { min-height: 40px; padding: 0 16px; font-size: 13px; }
.btn-lg { min-height: 54px; padding: 0 28px; font-size: 15px; }
.stack-gap-sm { margin-top: 6px; }

.text-btn {
  font-size: 13px;
  font-weight: 600;
  color: var(--cocoa);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
  letter-spacing: 0.04em;
  background: none; border: none; cursor: pointer;
  transition: color 0.2s var(--ease-out);
}
.text-btn:hover { color: var(--ink); }

/* =========================================================
   Reveal animations (intersection observer)
   ========================================================= */
.reveal {
  opacity: 0;
  transform: translateY(26px) scale(0.985);
  transition: opacity 0.7s var(--ease-out), transform 0.8s var(--ease-out);
}
.reveal.is-visible {
  opacity: 1;
  transform: none;
}
.reveal-delay-100 { transition-delay: 0.1s; }
.reveal-delay-120 { transition-delay: 0.12s; }
.reveal-delay-150 { transition-delay: 0.15s; }

/* Scroll-SCRUBBED section intros (CSS view timeline): unlike .reveal, these
   track the scroll position itself — the title rises/settles as it crosses the
   lower viewport, and reverses when you scroll back. Native and compositor-
   friendly (transform/opacity only, no JS). Browsers without view() support
   simply skip it (elements stay visible), and reduced-motion never enters. */
@keyframes scrollRise {
  from { opacity: 0; transform: translateY(36px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .shop-title,
    .section-header .eyebrow,
    .section-title-compact {
      animation: scrollRise linear both;
      animation-timeline: view();
      animation-range: entry 20% entry 75%;
    }
  }
}
/* Hero is above the fold — paint it immediately so first paint is fast and never
   waits for scripts. Below-the-fold .reveal items still animate in on scroll. */
.hero .reveal { opacity: 1; transform: none; transition: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  /* Suppress decorative motion across the storefront for reduced-motion users.
     animation-delay too — a delayed entrance keyframe with `backwards` fill
     would otherwise hold elements invisible for the delay's length. */
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .product-card:hover .product-media img { transform: none; }
  .skeleton-media, .skeleton-line { animation: none !important; }
  .whatsapp-fab { animation: none !important; }
}

/* ===== P16: offline banner + load-error / retry ===== */
.offline-banner {
  position: sticky;
  top: 0;
  z-index: 80;
  padding: 9px 16px;
  background: var(--ink);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
}
.offline-banner[hidden] { display: none; }

/* Owner-controlled announcement strip (top of store; hidden until enabled). */
.announcement-bar {
  position: relative;
  z-index: 60;
  padding: 8px 16px;
  background: var(--ink);
  color: var(--paper);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-align: center;
}
.announcement-bar[hidden] { display: none; }

.noscript-links { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 14px; }

/* Scroll containment — a big part of the "smoother, not laggy" mobile feel.
   overscroll-behavior:none on the page stops the browser's pull-to-refresh /
   rubber-band bounce at the top and bottom of the document. `contain` on the
   VERTICAL scrollers (drawers, modal bodies) keeps a swipe that reaches their
   edge from chaining into the page behind them.
   The HORIZONTAL strips (.category-pills, .recent-scroll, .modal-gallery) must
   NOT appear here: the shorthand sets overscroll-behavior-y too, and since the
   strips are overflow-y:hidden they can never consume a vertical wheel/swipe —
   y-containment then blocks that gesture from chaining to the page, so hovering
   the strip dead-locks page scrolling. Each strip sets overscroll-behavior-x:
   contain in its own rule, which is all the containment they need. */
html { overscroll-behavior-y: none; }
.drawer-body,
.modal-body,
.modal-details { overscroll-behavior: contain; }


/* =========================================================
   Sections
   ========================================================= */
.section {
  padding: 56px 0;
}
.section-header {
  margin-bottom: 28px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.section-header .eyebrow { margin-bottom: 4px; }
.section-header p { color: var(--muted); max-width: 480px; }

/* =========================================================
   The sheet — everything after the hero rides on this layer
   ========================================================= */
/* Slides up and over the pinned hero photo (the scroll transition). Must stay
   opaque, and must NOT get overflow:hidden — that would break the sticky
   toolbar inside; the first section repeats the top radius instead. */
.page-sheet {
  position: relative;
  z-index: 2;
  background: #e6e6e6; /* = body background, so the seam disappears once scrolled past */
  border-radius: clamp(22px, 4.5vw, 34px) clamp(22px, 4.5vw, 34px) 0 0;
  box-shadow: 0 -22px 56px rgba(0, 0, 0, 0.20);
}

/* =========================================================
   SHOP — the centerpiece
   ========================================================= */
.shop-section {
  background: rgba(255, 255, 255, 0.55);
  /* Top radius mirrors .page-sheet so this section's veil never squares off the
     sheet's rounded corners (no overflow clip allowed on the sheet, see above).
     border-bottom only — a top border would draw a line across the curved seam. */
  border-radius: clamp(22px, 4.5vw, 34px) clamp(22px, 4.5vw, 34px) 0 0;
  border-bottom: 1px solid var(--line-soft);
  padding: 42px 0 48px;
  position: relative;
  /* Anchor target: land #shop below the sticky header. */
  scroll-margin-top: calc(var(--header-h) + 8px);
}
/* Architectural grid backdrop — a fine hairline grid drifting slowly on the
   diagonal behind the product grid. It's drawn with two repeating
   linear-gradients (no per-line DOM, no filter: blur()), the drift is
   transform-only on ::before so nothing ever re-rasterizes, and a radial mask
   fades the whole layer out through the middle where the cards sit — so the
   lines only read in the breathing room at the edges, never behind a product
   photo. The shape sits under .container (z 0 vs 1). */
.shop-grid-bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: inherit; /* respect the section's rounded top corners */
  pointer-events: none;
  z-index: 0;
  /* Vignette the grid away from the centre so it never competes with the
     products — strongest along the outer edges, gone through the middle. */
  -webkit-mask: radial-gradient(78% 68% at 50% 40%, transparent 22%, #000 100%);
          mask: radial-gradient(78% 68% at 50% 40%, transparent 22%, #000 100%);
}
.shop-grid-bg::before {
  content: "";
  position: absolute;
  /* Oversized so the diagonal drift never exposes an untiled edge. */
  inset: -60px;
  background-image:
    linear-gradient(rgba(0, 0, 0, 0.08) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 0, 0, 0.08) 1px, transparent 1px);
  background-size: 46px 46px;
  will-change: transform;
  animation: shop-grid-drift 34s linear infinite;
}
.shop-section > .container { position: relative; z-index: 1; }
/* Exactly one cell of travel, so the pattern (which repeats every 46px) wraps
   seamlessly at the loop point — no visible jump. */
@keyframes shop-grid-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(46px, 46px, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .shop-grid-bg::before { animation: none; }
}

.shop-header {
  margin-bottom: 18px;
}
.shop-header .eyebrow { margin-bottom: 4px; }
.shop-title {
  font-style: italic;
  font-weight: 400;
  font-size: clamp(26px, 5.5vw, 40px);
  line-height: 1.02;
}

/* Sticky toolbar */
.toolbar {
  position: sticky;
  top: var(--header-h);
  z-index: 30;
  background: rgba(250, 250, 250, 0.92);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  border-radius: var(--r-md);
  padding: 12px;
  margin-bottom: 18px;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-xs);
  transition: box-shadow 0.3s var(--ease-out);
}
.toolbar.is-stuck { box-shadow: var(--shadow-sm); }
.toolbar-row {
  display: grid;
  /* minmax(0,1fr) lets the search column shrink to fit; a plain 1fr keeps its
     min-content floor and pushes the Filters button past the toolbar padding. */
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
}
.search-field {
  position: relative;
  display: flex; align-items: center;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 0 14px;
  height: 44px;
  transition: border-color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out);
}
.search-field:focus-within {
  border-color: var(--cocoa);
  /* An outline, not only a shadow: a shadow ring is specificity 0-1-0 and the next component
     shadow declared after it wins in silence. The input itself has outline: none, so the
     wrapper is where a keyboard user's ring has to be drawn. */
  outline: 2px solid var(--cocoa);
  outline-offset: 2px;
}
.search-field svg { width: 16px; height: 16px; stroke: var(--muted); stroke-width: 1.8; fill: none; flex-shrink: 0; }
.search-field input {
  flex: 1;
  border: none; background: transparent; outline: none;
  font-size: 14px;
  padding: 0 8px;
  min-width: 0;
}
/* --muted (not --muted-soft): placeholder text needs ~4.5:1 contrast on paper. */
.search-field input::placeholder { color: var(--muted); }
/* Suppress the browser's native search "x" so only the custom .search-clear shows. */
.search-field input[type="search"]::-webkit-search-cancel-button,
.search-field input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.search-clear {
  width: 30px; height: 30px;
  border-radius: 50%;
  display: none; place-items: center;
  background: var(--cream); color: var(--ink);
  font-size: 14px;
  flex-shrink: 0;
}
.search-field.has-value .search-clear { display: grid; }

.filter-toggle {
  height: 44px; padding: 0 16px;
  border-radius: var(--r-pill);
  background: var(--ink); color: var(--paper);
  font-size: 13px; font-weight: 600;
  display: inline-flex; align-items: center; gap: 6px;
  transition: background 0.2s var(--ease-out);
}
.filter-toggle svg { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.8; fill: none; }
.filter-toggle:hover { background: var(--cocoa); }
.filter-toggle .filter-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--champagne);
  display: none;
}
.filter-toggle.has-active .filter-dot { display: inline-block; }

/* Filter expand panel */
.filter-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s var(--ease-out);
}
.filter-panel.is-open { grid-template-rows: 1fr; }
.filter-panel-inner {
  overflow: hidden;
  display: grid;
  gap: 12px;
  padding-top: 0;
}
.filter-panel.is-open .filter-panel-inner { padding-top: 12px; }
.filter-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.filter-field { display: grid; gap: 4px; }
.filter-field label {
  font-size: 10px; letter-spacing: 0.16em;
  text-transform: uppercase; font-weight: 700;
  color: var(--muted);
}
.filter-field select, .filter-field input {
  height: 40px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--paper);
  font-size: 13px;
  outline: none;
  transition: border-color 0.2s var(--ease-out);
}
.filter-field select:focus, .filter-field input:focus { border-color: var(--cocoa); }

/* Active-filter chips (removable summary of what's narrowing the grid) */
.active-filters {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin: 14px 4px 0;
}
.active-filters[hidden] { display: none; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 8px 0 14px;
  border-radius: var(--r-pill);
  background: var(--ink); color: var(--paper);
  font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
  border: 1px solid var(--ink);
  animation: chipIn 0.3s var(--ease-spring) both;
  transition: background 0.2s var(--ease-out), transform 0.15s var(--ease-out);
}
.filter-chip:hover { background: var(--cocoa); border-color: var(--cocoa); }
.filter-chip:active { transform: scale(0.95); }
.filter-chip .filter-chip-x {
  display: grid; place-items: center;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  font-size: 13px; line-height: 1;
}
.filter-chip.clear-chip {
  background: transparent; color: var(--cocoa);
  border: 1px dashed var(--line-strong);
  padding: 0 14px;
}
.filter-chip.clear-chip:hover { background: var(--cream); color: var(--ink); border-color: var(--ink-soft); }
@keyframes chipIn { from { opacity: 0; transform: translateY(6px) scale(0.96); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .filter-chip { animation: none; } }

/* Category pills */
.category-pills {
  display: flex; gap: 6px;
  margin-top: 10px;
  overflow-x: auto;
  /* Pin the vertical axis: with overflow-x set and overflow-y left at its
     default, the spec computes overflow-y to auto, turning this pill strip into
     a vertical scroll container too — the padding-block sliver then traps the
     mouse wheel / vertical swipe here instead of scrolling the page. Same fix as
     .recent-scroll below. */
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Do NOT pin touch-action to pan-x here. On Windows precision touchpads the
     browser honours touch-action for two-finger scroll, so pan-x would swallow
     every vertical scroll while the pointer is over the pills. overflow-y:hidden
     already stops the strip from ever scrolling vertically, so a vertical
     gesture falls through to the page on its own. */
  overscroll-behavior-x: contain;
  /* Bleed to the toolbar's inner edge (constant 12px padding), not past the
     rounded corner via the larger --gutter. */
  margin-inline: -12px;
  padding-inline: 12px;
  padding-block: 4px;
}
.category-pills::-webkit-scrollbar { display: none; }
.category-pills button {
  flex-shrink: 0;
  height: 36px; padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--paper);
  font-size: 12px; font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  transition: all 0.2s var(--ease-out);
}
.category-pills button:hover { border-color: var(--ink-soft); }
.category-pills button.is-active {
  background: var(--ink); color: var(--paper);
  border-color: var(--ink);
}

/* Shop meta */
.shop-meta {
  display: flex; justify-content: space-between; align-items: center;
  margin: 14px 4px 16px;
  font-size: 13px;
  color: var(--muted);
}
.shop-meta strong { color: var(--ink); font-weight: 700; }

/* =========================================================
   Product grid
   ========================================================= */
.product-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
.product-card {
  background: var(--paper);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  overflow: hidden;
  transition: transform 0.35s var(--ease-out), box-shadow 0.35s var(--ease-out), border-color 0.35s var(--ease-out);
  display: flex; flex-direction: column;
  position: relative;
  cursor: pointer; /* whole card opens quick view */
}
.product-card:hover {
  transform: translateY(-4px);
  border-color: var(--line);
  box-shadow: var(--shadow-md);
}
.product-media {
  position: relative;
  aspect-ratio: 4/5;
  overflow: hidden;
  background: var(--cream);
}
.product-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease-out);
}
.product-card:hover .product-media img { transform: scale(1.06); }

/* Lazy/throttled images fade in once decoded (G-DRIVE-COST). The container keeps
   a soft tone underneath so a not-yet-loaded image never flashes as a blank box. */
.lazy-img { opacity: 0; transition: opacity 0.5s var(--ease-out, ease); }
.lazy-img.is-loaded { opacity: 1; }
.product-media:has(.lazy-img:not(.is-loaded)),
.modal-gallery button:has(.lazy-img:not(.is-loaded)) {
  background:
    linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.45) 50%, transparent 70%) ,
    var(--cream, #efe6d4);
  background-size: 220% 100%, auto;
  animation: lazy-shimmer 1.4s ease-in-out infinite;
}
@keyframes lazy-shimmer { from { background-position: 180% 0, 0 0; } to { background-position: -60% 0, 0 0; } }
/* Quick-view hero while its image is still arriving (class set by app.js). */
.image-wrap.is-waiting {
  background:
    linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.45) 50%, transparent 70%),
    var(--cream, #efe6d4);
  background-size: 220% 100%, auto;
  animation: lazy-shimmer 1.4s ease-in-out infinite;
}
.image-wrap.is-waiting img { opacity: 0; }
/* Small thumbs (bag / wishlist / recently viewed) keep a soft tone underneath
   so a still-loading image never flashes as a white hole in the row. */
.cart-row img, .recent-card img { background-color: var(--cream); }
@media (prefers-reduced-motion: reduce) {
  .lazy-img { transition: none; }
  .product-media:has(.lazy-img:not(.is-loaded)),
  .modal-gallery button:has(.lazy-img:not(.is-loaded)),
  .image-wrap.is-waiting { animation: none; }
}

/* Badges */
.badge-stack {
  position: absolute; top: 10px; left: 10px;
  display: flex; flex-direction: column; gap: 4px;
  z-index: 2;
}
.badge {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 5px 10px;
  font-size: 9px; font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  box-shadow: var(--shadow-xs);
}
.badge.new { background: var(--ink); color: var(--paper); border-color: var(--ink); }

/* Wishlist heart */
.wish-btn {
  position: absolute; top: 10px; right: 10px;
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(250, 250, 250, 0.92);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: grid; place-items: center;
  z-index: 2;
  transition: transform 0.2s var(--ease-spring), background 0.2s var(--ease-out);
}
.wish-btn svg { width: 16px; height: 16px; stroke: var(--ink); stroke-width: 1.8; fill: none; transition: fill 0.2s var(--ease-out), stroke 0.2s var(--ease-out); }
.wish-btn:hover { transform: scale(1.1); }
.wish-btn.is-active { background: var(--ink); }
.wish-btn.is-active svg { stroke: var(--paper); fill: var(--paper); }
.wish-btn.is-pulsing svg { animation: heartPulse 0.5s var(--ease-spring); }
@keyframes heartPulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.4); }
}

/* Add-to-bag success micro-confirmation (toggled via .is-added in app.js).
   Transform-only; auto-neutralized by the reduced-motion override above. */
.qty-add-row .btn.is-added { animation: addPop 0.45s var(--ease-spring); }
@keyframes addPop {
  0%, 100% { transform: none; }
  45%      { transform: scale(0.94); }
}

/* Product info */
.product-info {
  padding: 11px 12px 13px;
  display: flex; flex-direction: column;
  gap: 5px;
  flex: 1;
}
.product-category {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--muted);
}
.product-name {
  font-family: var(--display);
  font-weight: 500;
  font-size: 13.5px;
  line-height: 1.2;
  letter-spacing: -0.005em;
  color: var(--ink);
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.product-price {
  margin-top: 4px;
  display: flex; align-items: baseline; gap: 6px;
  flex-wrap: wrap;
}
.product-price .now {
  font-weight: 700;
  font-size: 13.5px;
  color: var(--ink);
}
/* Color swatches mini */
.color-mini {
  display: flex; gap: 4px;
  margin-top: 6px;
}
.color-mini span {
  width: 12px; height: 12px;
  border-radius: 50%;
  border: 1px solid var(--line);
  display: inline-block;
}
.color-mini-count { font-size: 10px; color: var(--muted); margin-left: 2px; }

/* Empty state */
.empty-state {
  grid-column: 1 / -1;
  text-align: center;
  padding: 60px 20px;
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
  background: rgba(255, 255, 255, 0.5);
}
.empty-state h3 { font-style: italic; }
.empty-state p { color: var(--muted); margin-top: 8px; }

/* Branded catalog states ("collection coming soon" / "taking a short pause"):
   the wordmark + soft copy + gentle actions read as boutique, not as an error. */
.empty-state.boutique-pause { padding: 72px 20px; }
.empty-state.boutique-pause .pavia-wordmark {
  width: min(150px, 44vw);
  height: auto;
  margin: 0 auto 18px;
  color: var(--muted);
  display: block;
}
.empty-state.boutique-pause p {
  max-width: 42ch;
  margin-inline: auto;
}
.empty-state-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 20px;
}

/* Product card skeleton */
.skeleton {
  background: var(--paper);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  overflow: hidden;
}
.skeleton-media {
  aspect-ratio: 4/5;
  background: linear-gradient(110deg, var(--cream) 30%, rgba(255, 255, 255, 0.6) 50%, var(--cream) 70%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}
.skeleton-info { padding: 12px 14px 16px; display: grid; gap: 8px; }
.skeleton-line {
  height: 10px; border-radius: 4px;
  background: linear-gradient(110deg, var(--cream) 30%, rgba(255, 255, 255, 0.6) 50%, var(--cream) 70%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}
.skeleton-line.short { width: 50%; }
.skeleton-line.med { width: 80%; }
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* =========================================================
   Grid list transitions (Phase 1)
   Filter/sort changes crossfade ONLY the product grid via the View
   Transitions API (scoped with the .vt-grid flag JS toggles on <html>);
   the rest of the page swaps instantly. Unsupported browsers and
   reduced-motion users get an instant swap — identical behavior, no polish.
   ========================================================= */
/* The grid is named ONLY while the .vt-grid flag is active, so it animates as its
   own group during filter/sort changes yet stays part of the page root during
   other transitions (e.g. the quick-view morph) — avoiding cross-transition glitches. */
html.vt-grid .product-grid { view-transition-name: pavia-grid; }
@keyframes pavia-vt-fade-in  { from { opacity: 0; } }
@keyframes pavia-vt-fade-out { to   { opacity: 0; } }
html.vt-grid::view-transition-group(root),
html.vt-grid::view-transition-old(root),
html.vt-grid::view-transition-new(root) { animation: none; }
html.vt-grid::view-transition-old(pavia-grid) { animation: pavia-vt-fade-out 0.20s var(--ease-out) both; }
html.vt-grid::view-transition-new(pavia-grid) { animation: pavia-vt-fade-in 0.30s var(--ease-out) both; }

/* Quick-view open morph (Phase 2): the tapped card image flies into the modal
   hero image. Only the named hero is its own group; the backdrop + detail panel
   crossfade with the page root. */
html.vt-modal::view-transition-group(product-hero) {
  animation-duration: 0.42s;
  animation-timing-function: var(--ease-out);
}
html.vt-modal::view-transition-old(root),
html.vt-modal::view-transition-new(root) { animation-duration: 0.34s; }

/* =========================================================
   Recently viewed
   ========================================================= */
.recently-viewed-section {
  padding: 32px 0 56px;
  display: none;
}
.recently-viewed-section.is-visible { display: block; }
.section-title-compact {
  font-size: clamp(24px, 5vw, 38px);
  font-style: italic;
  font-weight: 400;
}
/* Rail wraps the scroll strip so the prev/next arrows can float over its edges.
   Arrows appear ONLY when the strip overflows (can-left / can-right toggled from
   app.js) — a handful of items that already fit shows no arrows at all. */
.recent-rail { position: relative; }
.recent-nav {
  position: absolute;
  top: 44%;                      /* sit over the card image, not the label band */
  transform: translateY(-50%);
  z-index: 3;
  width: 38px; height: 38px;
  border-radius: 50%;
  display: none;
  align-items: center; justify-content: center;
  padding: 0;
  background: rgba(250, 250, 250, 0.94);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  color: var(--ink);
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out), transform 0.15s var(--ease-out);
}
.recent-nav svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.recent-nav-prev { left: 2px; }
.recent-nav-next { right: 2px; }
.recent-nav:hover { background: var(--paper); border-color: var(--ink-soft); }
.recent-nav:active { transform: translateY(-50%) scale(0.9); }
/* A text button in a filter row is still a target: 21px tall failed WCAG 2.2. */
.text-btn { min-height: 24px; padding-block: 2px; }

.recent-nav:focus-visible { outline: 2px solid var(--cocoa); outline-offset: 2px; }
.recent-rail.can-left .recent-nav-prev { display: inline-flex; }
.recent-rail.can-right .recent-nav-next { display: inline-flex; }

.recent-scroll {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  /* Pin the vertical axis: with overflow-x set and overflow-y left at its
     default, the spec computes overflow-y to auto, turning this strip into a
     vertical scroll container too. The padding-bottom sliver then traps the
     mouse wheel here instead of scrolling the page. */
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin-inline: calc(-1 * var(--gutter));
  padding-inline: var(--gutter);
  padding-bottom: 6px;
  scroll-snap-type: x mandatory;
  /* No touch-action: pan-x — on Windows precision touchpads that would swallow
     vertical two-finger scroll whenever the pointer is over the strip (even when
     only 5 cards fit and nothing scrolls sideways). overflow-y:hidden already
     stops any vertical scroll of the strip, so vertical gestures fall through to
     the page on their own. */
  overscroll-behavior-x: contain;
}
.recent-scroll::-webkit-scrollbar { display: none; }
.recent-card {
  flex: 0 0 140px;
  scroll-snap-align: start;
  cursor: pointer;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--paper);
  border: 1px solid var(--line-soft);
  transition: transform 0.25s var(--ease-out);
}
.recent-card:hover { transform: translateY(-3px); }
.recent-card img {
  width: 100%; aspect-ratio: 1/1.1;
  object-fit: cover; background: var(--cream);
}
.recent-card div {
  padding: 8px 10px 10px;
  font-size: 12px;
}
.recent-card strong {
  display: block;
  font-family: var(--display);
  font-weight: 500;
  font-size: 13px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.recent-card span {
  font-size: 11px;
  color: var(--muted);
  font-weight: 600;
}

/* =========================================================
   Visit / contact section
   ========================================================= */
.visit {
  background: var(--ink); color: var(--paper);
  padding: 56px 0;
}
.visit h2, .visit h3, .visit p { color: var(--paper); }
.visit .eyebrow { color: var(--champagne); }
.visit-title {
  font-style: italic;
  font-weight: 300;
  font-size: clamp(28px, 6vw, 46px);
}
.mt-8 { margin-top: 8px; }
.visit-card {
  max-width: 620px;
  margin-inline: auto;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--r-lg);
  padding: clamp(26px, 4vw, 40px);
  background: rgba(255, 255, 255, 0.04);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  text-align: center;
}
.contact-list { display: grid; gap: 8px; margin-top: 22px; text-align: left; }
.contact-list a {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--r-md);
  font-weight: 600; font-size: 14px;
  transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out);
}
.contact-list a:hover {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.28);
}
.contact-list a span { color: var(--champagne); font-size: 12px; }
.visit-address, .visit-hours {
  margin-top: 6px;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.72);
}
.visit-address[hidden], .visit-hours[hidden], .visit-directions[hidden] { display: none; }

/* =========================================================
   Footer
   ========================================================= */
.site-footer {
  background: var(--ink); color: var(--paper);
  padding: 28px 0 calc(var(--bottom-nav-h) + 24px);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 12px;
  text-align: center;
  position: relative;
}
.site-footer p { color: rgba(255, 255, 255, 0.6); }
.footer-tagline {
  margin-top: 6px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  /* 0.4 measured 3.86:1 on the --ink footer and failed at 11px. 0.55 is 6.2:1. */
  color: rgba(255, 255, 255, 0.55);
}
.footer-brand {
  display: inline-block;
  color: rgba(255, 255, 255, 0.92);
  margin-bottom: 20px;
  transition: opacity 0.3s var(--ease-out);
}
.footer-brand:hover { opacity: 0.7; }
/* Explicit width + height (and max-width:none) so the inline-block parent's
   shrink-to-fit doesn't collapse the SVG to 0 via the global svg{max-width:100%}.
   124 x (333/982) = 42. */
.footer-brand .pavia-wordmark { width: 124px; height: 42px; max-width: none; }
.site-footer .social {
  display: flex; justify-content: center; gap: 10px;
  margin-bottom: 18px;
}
.site-footer .social a {
  width: 38px; height: 38px;
  border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.18);
  transition: background 0.2s var(--ease-out);
}
.site-footer .social a:hover { background: rgba(255, 255, 255, 0.08); }
.site-footer .social svg { width: 16px; height: 16px; stroke: var(--paper); stroke-width: 1.6; fill: none; }
.footer-version {
  position: absolute;
  left: 16px;
  bottom: calc(var(--bottom-nav-h) + 16px + env(safe-area-inset-bottom, 0px));
  color: var(--paper);
  font-size: 10px;
  opacity: 0.5;
}

/* =========================================================
   Floating WhatsApp button
   ========================================================= */
.whatsapp-fab {
  position: fixed;
  right: 16px;
  bottom: calc(var(--bottom-nav-h) + 16px + env(safe-area-inset-bottom, 0px));
  z-index: 35;
  width: 52px; height: 52px;
  border-radius: 50%;
  background: #25d366;
  display: grid; place-items: center;
  box-shadow: 0 10px 30px rgba(37, 211, 102, 0.42);
  transition: transform 0.25s var(--ease-spring), box-shadow 0.25s var(--ease-out);
}
.whatsapp-fab:hover { transform: scale(1.06); box-shadow: 0 14px 36px rgba(37, 211, 102, 0.52); }
.whatsapp-fab svg { width: 26px; height: 26px; fill: white; }
.whatsapp-fab::after {
  content: '';
  position: absolute; inset: -4px;
  border-radius: 50%;
  border: 2px solid #25d366;
  opacity: 0.4;
  animation: pulse 2.2s infinite var(--ease-out);
}
@keyframes pulse {
  0%   { transform: scale(0.9); opacity: 0.5; }
  100% { transform: scale(1.4); opacity: 0; }
}

/* =========================================================
   Drawers (cart + wishlist + orders)
   ========================================================= */
.drawer {
  position: fixed; inset: 0;
  z-index: 80;
  background: rgba(0, 0, 0, 0.42);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  /* visibility, not opacity alone. A closed drawer at opacity 0 is still in the tab order, so
     a keyboard walked out of the page into an invisible cart and an invisible close button,
     and aria-hidden on a focusable element is the one ARIA rule that has no exceptions. The
     zero length visibility transition is delayed by the fade so the panel still fades out. */
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.32s var(--ease-out), visibility 0s linear 0.32s;
}
.drawer.is-open { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity 0.32s var(--ease-out), visibility 0s; }
.drawer-panel {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 100%;
  max-width: 460px;
  background: var(--paper);
  box-shadow: var(--shadow-drawer);
  transform: translateX(100%);
  transition: transform 0.4s var(--ease-out);
  display: flex; flex-direction: column;
}
.drawer.is-open .drawer-panel { transform: translateX(0); }
.drawer-header {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 16px;
  padding: 24px 22px 18px;
  border-bottom: 1px solid var(--line);
}
.drawer-header h2 {
  font-style: italic; font-size: 28px; font-weight: 400;
}
.close-btn {
  width: 38px; height: 38px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--paper);
  display: grid; place-items: center;
  transition: background 0.2s var(--ease-out), transform 0.2s var(--ease-out);
}
.close-btn svg,
.modal-close svg {
  width: 18px;
  height: 18px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
}
.close-btn:hover { background: var(--cream); }
.close-btn:active { transform: scale(0.92); }

.drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px;
  -webkit-overflow-scrolling: touch;
}
.drawer-body::-webkit-scrollbar { width: 6px; }
.drawer-body::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }

/* Cart row */
.cart-row {
  display: grid;
  grid-template-columns: 76px 1fr;
  gap: 14px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line-soft);
  position: relative;
  animation: slideIn 0.4s var(--ease-out);
}
@keyframes slideIn {
  from { opacity: 0; transform: translateX(20px); }
  to   { opacity: 1; transform: none; }
}
.cart-row.is-removing { animation: slideOut 0.35s var(--ease-out) forwards; }
@keyframes slideOut {
  to { opacity: 0; transform: translateX(60px); }
}
.cart-row img {
  width: 76px; height: 96px;
  border-radius: var(--r-sm);
  object-fit: cover;
  background: var(--cream);
}
.cart-row-content { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.cart-row h3 {
  font-family: var(--display);
  font-size: 15px; font-weight: 500;
  line-height: 1.2;
  margin: 0;
}
.cart-row .meta { font-size: 12px; color: var(--muted); }
.cart-row .price-row {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: 6px;
}
.cart-row strong { font-weight: 700; font-size: 14px; }

.qty-control {
  display: inline-flex; align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  overflow: hidden;
}
.qty-control button {
  width: 28px; height: 28px;
  display: grid; place-items: center;
  font-size: 16px; font-weight: 600;
  color: var(--ink);
  transition: background 0.15s var(--ease-out);
}
.qty-control button:hover { background: var(--cream); }
.qty-control span {
  min-width: 26px; text-align: center;
  font-weight: 700; font-size: 13px;
}
.remove-btn {
  position: absolute; top: 16px; right: 0;
  width: 26px; height: 26px;
  border-radius: 50%;
  display: grid; place-items: center;
  color: var(--muted);
  font-size: 14px;
  transition: color 0.2s var(--ease-out), background 0.2s var(--ease-out);
}
.remove-btn:hover { color: var(--cocoa); background: var(--cream); }

/* Cart summary */
.cart-summary {
  border-top: 1px solid var(--line);
  padding: 18px 20px 18px;
  background: var(--paper);
}
.summary-line {
  display: flex; justify-content: space-between;
  margin-bottom: 8px;
  font-size: 13px;
  color: var(--ink-soft);
}
.summary-line.total {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line-soft);
  font-size: 16px;
  color: var(--ink);
  font-weight: 700;
}
.summary-line.total span:first-child {
  font-family: var(--display); font-style: italic;
  font-weight: 400; font-size: 18px;
}
.cart-summary .btn { margin-top: 8px; }

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

.empty-drawer {
  text-align: center;
  padding: 48px 16px;
  color: var(--muted);
}
.empty-drawer svg {
  width: 48px; height: 48px;
  stroke: var(--muted-soft); stroke-width: 1.4; fill: none;
  margin: 0 auto 16px;
}
.empty-drawer h3 { font-style: italic; color: var(--ink); }
.empty-drawer p { margin: 8px 0 18px; }

/* =========================================================
   Modal (product quick view + checkout)
   ========================================================= */
.modal {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: grid; place-items: end center;
  padding: 0;
  /* Same reason as .drawer above: a closed modal must leave the tab order, not merely fade. */
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.3s var(--ease-out), visibility 0s linear 0.3s;
}
.modal.is-open { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity 0.3s var(--ease-out), visibility 0s; }
.modal-card {
  width: 100%;
  max-width: 100%;
  max-height: 92vh;
  background: var(--paper);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  overflow: hidden;
  position: relative;
  transform: translateY(100%);
  transition: transform 0.42s var(--ease-out);
  display: flex; flex-direction: column;
}
.modal.is-open .modal-card { transform: translateY(0); }
.modal-close {
  position: absolute; top: 14px; right: 14px;
  z-index: 3;
  width: 38px; height: 38px;
  border-radius: 50%;
  background: rgba(250, 250, 250, 0.94);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: grid; place-items: center;
  font-size: 22px;
  border: 1px solid var(--line);
}

.modal-body { overflow-y: auto; -webkit-overflow-scrolling: touch; }
.modal-product { display: flex; flex-direction: column; }
.modal-product .image-wrap {
  width: 100%; aspect-ratio: 1/1.1;
  background: var(--cream);
  position: relative;
  overflow: hidden; /* keep the tap-to-zoom scaled image inside its frame */
}
.modal-product .image-wrap img {
  width: 100%; height: 100%;
  object-fit: cover;
}
/* Tap-to-zoom the main image (transform-origin is set in JS to the click point). */
.modal-product .image-wrap img[data-modal-zoom] { cursor: zoom-in; transition: transform 0.35s ease; }
.modal-product .image-wrap img[data-modal-zoom].is-zoomed { transform: scale(2); cursor: zoom-out; }
@media (prefers-reduced-motion: reduce) {
  .modal-product .image-wrap img[data-modal-zoom] { transition: none; }
}
/* Title + share button row in the modal details. */
.modal-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.modal-share {
  display: inline-flex; align-items: center; gap: 6px;
  flex: 0 0 auto;
  padding: 7px 12px; border-radius: 999px;
  border: 1px solid var(--sand, #d9c2a3); background: transparent;
  color: var(--cocoa, #5a3e30); font-size: 13px; font-weight: 600;
  cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease;
}
.modal-share:hover { background: rgba(0, 0, 0, 0.07); }
.modal-share:focus-visible { outline: 2px solid var(--cocoa, #5a3e30); outline-offset: 2px; }
.modal-share svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* Gallery position counter (e.g. "2 / 4") overlaid on the modal hero image. */
.modal-counter {
  position: absolute;
  top: 12px; left: 12px;
  z-index: 2;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
/* Media column: hero image on top, thumbnail strip underneath. */
.modal-media { display: flex; flex-direction: column; min-width: 0; position: relative; }
/* Thumbnail strip under the quick-view hero. Tap a thumb to swap the main
   image; the strip also auto-rotates every 3s (paused 15s after any manual
   pick — see gallery auto-rotation in app.js). The wrap positions the scroll
   arrows over the strip's edges; the strip itself sits on a transparent
   background so the thumbnails read as cards floating over the hero photo.
   The wrap is pinned to the bottom of the media column (over the hero image)
   rather than sitting in flow, so its background stays fully transparent — the
   photo shows through the gaps between the floating thumb cards instead of a
   white paper band. */
.modal-gallery-wrap { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; }
.modal-gallery {
  display: flex;
  gap: 12px;
  padding: 16px 16px 18px;
  overflow-x: auto;
  overflow-y: hidden;            /* keep thumbnail shadows from spawning a v-scrollbar */
  /* No touch-action: pan-x — it blocks vertical touchpad scroll over the strip
     on Windows. overflow-y:hidden already keeps a vertical swipe from being
     trapped here, so it falls through to the modal body. */
  overscroll-behavior-x: contain;
  background: transparent;
  scroll-behavior: smooth;
  scroll-padding-inline: 36px;   /* keep the active thumb clear of the edge fade */
  scrollbar-width: none;         /* arrows + edge fade replace the raw scrollbar */
}
.modal-gallery::-webkit-scrollbar { display: none; }
/* Soft-fade whichever edge still has thumbnails hidden past it, so the strip
   "wraps" gracefully instead of hard-cutting a thumbnail in half. Classes are
   toggled by setupGalleryNav() in app.js from the live scroll position. */
.modal-gallery-wrap.can-left.can-right .modal-gallery {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
}
.modal-gallery-wrap.can-right:not(.can-left) .modal-gallery {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(90deg, #000 calc(100% - 40px), transparent 100%);
}
.modal-gallery-wrap.can-left:not(.can-right) .modal-gallery {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 40px);
          mask-image: linear-gradient(90deg, transparent 0, #000 40px);
}
.modal-gallery button {
  width: 60px;
  height: 76px;
  border-radius: var(--r-sm);
  overflow: hidden;
  border: 1px solid var(--line);
  padding: 0;
  background: var(--cream);
  flex: 0 0 auto;
  cursor: pointer;
  opacity: 0.8;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  transition: opacity 0.25s var(--ease-out), border-color 0.25s var(--ease-out), transform 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out);
}
.modal-gallery button:hover {
  opacity: 1;
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
}
.modal-gallery button.is-selected {
  border-color: var(--ink);
  box-shadow: 0 0 0 2px var(--ink), 0 4px 12px rgba(0, 0, 0, 0.16);
  opacity: 1;
}
.modal-gallery img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Scroll arrows — hidden by default, revealed on the side that can still scroll. */
.gallery-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 32px; height: 32px;
  border-radius: 50%;
  display: none;
  align-items: center; justify-content: center;
  padding: 0;
  background: rgba(250, 250, 250, 0.94);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  color: var(--ink);
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: background 0.2s var(--ease-out), border-color 0.2s var(--ease-out), transform 0.15s var(--ease-out);
}
.gallery-nav svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.gallery-nav-prev { left: 6px; }
.gallery-nav-next { right: 6px; }
.gallery-nav:hover { background: var(--paper); border-color: var(--ink-soft); }
.gallery-nav:active { transform: translateY(-50%) scale(0.9); }
.gallery-nav:focus-visible { outline: 2px solid var(--cocoa); outline-offset: 2px; }
.modal-gallery-wrap.can-left .gallery-nav-prev { display: inline-flex; }
.modal-gallery-wrap.can-right .gallery-nav-next { display: inline-flex; }
@media (prefers-reduced-motion: reduce) {
  .modal-gallery { scroll-behavior: auto; }
}
/* Soft crossfade when the hero swaps (thumbnail tap, swipe, or auto-rotate). */
.modal-product .image-wrap img[data-modal-zoom].is-swapping { animation: hero-swap 0.45s var(--ease-out); }
@keyframes hero-swap { from { opacity: 0.3; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .modal-product .image-wrap img[data-modal-zoom].is-swapping { animation: none; }
}
.modal-details {
  padding: 22px 20px 28px;
  display: flex; flex-direction: column; gap: 12px;
}
.modal-details h2 {
  font-family: var(--display);
  font-style: italic; font-weight: 400;
  font-size: clamp(26px, 5.4vw, 34px);
  line-height: 1.06;
}
/* Clamp the description so a long one can't force the quick-view to scroll on
   normal screens. The essentials (price, size, color, add to bag) stay in view. */
.modal-desc {
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.5;
}
.modal-price-row {
  display: flex; align-items: baseline; gap: 8px;
  flex-wrap: wrap;
}
.modal-price-row .now { font-size: 22px; font-weight: 700; }

.option-group {
  display: grid;
  gap: 8px;
}
.option-group .label {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--muted);
  display: flex; justify-content: space-between;
}
.option-group .label span:last-child { color: var(--ink); }
.size-options, .color-swatches {
  display: flex; flex-wrap: wrap; gap: 6px;
}
.size-options button {
  min-width: 44px;
  height: 40px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  font-size: 13px; font-weight: 600;
  transition: all 0.2s var(--ease-out);
}
.size-options button:hover { border-color: var(--ink-soft); }
.size-options button.is-selected {
  background: var(--ink); color: var(--paper); border-color: var(--ink);
}
.color-swatches button {
  height: 36px; padding: 0 12px 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--paper);
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600;
  transition: all 0.2s var(--ease-out);
}
.color-swatches button:hover { border-color: var(--ink-soft); }
.color-swatches button.is-selected { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.color-dot {
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 1px solid rgba(0,0,0,0.12);
  flex-shrink: 0;
}

.qty-add-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 10px;
  align-items: center;
  margin-top: 6px;
}
.qty-add-row .qty-control {
  height: 48px; border-radius: var(--r-pill);
}
.qty-add-row .qty-control button { width: 38px; height: 100%; font-size: 18px; }
.qty-add-row .qty-control span { min-width: 30px; font-size: 15px; }
.qty-add-row .btn { height: 48px; }
.qty-control button:disabled,
.btn:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}
.stock-note {
  color: var(--muted);
  font-size: 12px;
  margin-top: -4px;
}

.modal-meta {
  display: flex; flex-direction: column; gap: 5px;
  font-size: 11.5px;
  color: var(--muted);
  border-top: 1px solid var(--line-soft);
  padding-top: 10px;
  margin-top: auto;
}
.modal-meta div { display: flex; gap: 8px; align-items: center; }
.modal-meta svg { width: 13px; height: 13px; stroke: var(--cocoa); stroke-width: 1.6; fill: none; flex-shrink: 0; }

/* =========================================================
   Checkout modal (full-screen)
   ========================================================= */
.checkout-card { padding: 0; }
.checkout-body {
  padding: 24px 20px;
  display: flex; flex-direction: column;
  gap: 20px;
}
.checkout-title {
  font-style: italic;
  font-weight: 400;
  font-size: clamp(28px, 6vw, 38px);
}
.checkout-note {
  margin-top: 8px;
  font-size: 14px;
}
.checkout-submit { margin-top: 14px; }
.checkout-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
}
.checkout-fields {
  display: grid; gap: 12px;
}
.field {
  display: grid; gap: 6px;
}
.field label {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--muted);
}
.field input, .field select, .field textarea {
  height: 46px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--paper);
  font-size: 14px;
  outline: none;
  transition: border-color 0.2s var(--ease-out), box-shadow 0.2s var(--ease-out);
  font-family: var(--body);
}
.field textarea {
  min-height: 80px;
  padding: 12px 14px;
  resize: vertical;
}
/* A real outline, not a box-shadow. A shadow ring is specificity 0-1-0 and any component
   shadow declared later deletes it in silence; an outline cannot be overwritten that way and
   it survives forced-colors mode. The 8% shadow it replaces was also close to invisible. */
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible,
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--cocoa);
  outline: 2px solid var(--cocoa);
  outline-offset: 2px;
}

/* The radio group is a fieldset now, so its legend carries the group's name. */
.field legend {
  padding: 0;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--muted);
}
fieldset.field { margin: 0; padding: 0; border: 0; }

/* What placing an order accepts, above the button rather than after it. */
.checkout-terms { font-size: 12px; line-height: 1.6; color: var(--muted); }
.checkout-terms a { color: var(--cocoa); text-decoration: underline; text-underline-offset: 2px; }
.payment-options {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 4px;
}
.payment-option {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 14px;
  cursor: pointer;
  transition: all 0.2s var(--ease-out);
  background: var(--paper);
}
.payment-option:hover { border-color: var(--ink-soft); }
.payment-option input { display: none; }
.payment-option .opt-icon {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--cream);
  display: grid; place-items: center;
  margin-bottom: 8px;
}
.payment-option .opt-icon svg { width: 18px; height: 18px; stroke: var(--cocoa); stroke-width: 1.6; fill: none; }
.payment-option strong { display: block; font-size: 13px; }
.payment-option p { margin-top: 2px; font-size: 11px; color: var(--muted); }
/* .is-checked is mirrored from the radio state by app.js (syncPaymentSelected)
   instead of :has(input:checked), which older Safari/Firefox lack — there the
   selected card would have had NO visible indicator at all. */
.payment-option.is-checked {
  border-color: var(--ink); background: var(--ink); color: var(--paper);
}
.payment-option.is-checked p { color: var(--muted-soft); }
.payment-option.is-checked .opt-icon { background: rgba(255, 255, 255, 0.12); }
.payment-option.is-checked .opt-icon svg { stroke: var(--paper); }
.payment-option[hidden] { display: none; }

/* Order preview */
.order-preview {
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 18px;
}
.order-preview h3 {
  font-family: var(--display);
  font-style: italic; font-weight: 400;
  font-size: 22px; margin-bottom: 12px;
}
.order-preview .summary-line {
  font-size: 13px;
  padding: 6px 0;
  border-bottom: 1px solid var(--line-soft);
}
.order-preview .summary-line.total { padding-top: 10px; }
.checkout-success {
  margin-top: 12px;
  padding: 12px 14px;
  border: 1px solid rgba(0, 0, 0, 0.24);
  border-radius: var(--r-md);
  background: rgba(0, 0, 0, 0.08);
  color: #1f1f1f;
  font-size: 13px;
  font-weight: 600;
}
.checkout-submit.is-loading {
  pointer-events: none;
  opacity: 0.88;
}
/* Real progress cue while the order is being sent: a small spinner beside the
   "Placing order..." label (the .btn is a flex row, so ::after slots inline). */
.checkout-submit.is-loading::after {
  content: '';
  width: 15px; height: 15px;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 2px solid rgba(250, 250, 250, 0.35);
  border-top-color: var(--paper);
  animation: btn-spin 0.7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .checkout-submit.is-loading::after { display: none; }
}

/* Animated "Order sent" confirmation (P1 — replaces the WhatsApp redirect). */
.checkout-success.is-visible { animation: order-confirm-rise 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) both; }
/* When confirmed, collapse the form + summary + submit/copy: show only the card. */
.checkout-card.is-confirmed .checkout-grid { grid-template-columns: 1fr; }
.checkout-card.is-confirmed .checkout-body > div:first-child,
.checkout-card.is-confirmed .checkout-fields,
.checkout-card.is-confirmed .order-preview > h3,
.checkout-card.is-confirmed .order-preview > [data-checkout-summary],
.checkout-card.is-confirmed .checkout-submit { display: none; }
.checkout-card.is-confirmed .checkout-success {
  margin-top: 0;
  border: 0;
  background: transparent;
  padding: 8px 4px;
}
.order-confirm { text-align: center; display: grid; gap: 8px; justify-items: center; }
.order-confirm-check {
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.14);
  color: #1f1f1f;
  margin-bottom: 2px;
  animation: order-confirm-pop 0.45s 0.08s cubic-bezier(0.2, 0.8, 0.3, 1.4) both;
}
.order-confirm-check svg { width: 26px; height: 26px; }
.order-confirm h3 { font-size: 18px; color: var(--ink, #1a1612); margin: 0; }
.order-confirm-ref { font-size: 13px; color: var(--muted, #6b6258); font-weight: 600; }
.order-confirm-ref strong { color: #1f1f1f; letter-spacing: 0.04em; }
.order-confirm-text { font-size: 13px; line-height: 1.5; color: var(--muted, #6b6258); font-weight: 500; }
.order-confirm-wa {
  font-size: 13px; font-weight: 600; color: #128c7e; /* WhatsApp brand green (kept) */
  text-decoration: underline; text-underline-offset: 3px;
}
.order-confirm .btn { margin-top: 6px; }
@keyframes order-confirm-rise {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes order-confirm-pop {
  from { opacity: 0; transform: scale(0.6); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .checkout-success.is-visible,
  .order-confirm-check { animation: none; }
}

/* =========================================================
   Toasts
   ========================================================= */
.toast-region {
  position: fixed;
  left: 16px; right: 16px;
  bottom: calc(var(--bottom-nav-h) + 80px + env(safe-area-inset-bottom, 0px));
  z-index: 110;
  display: flex; flex-direction: column;
  gap: 8px;
  pointer-events: none;
}
.toast {
  background: var(--ink);
  color: var(--paper);
  padding: 12px 16px;
  border-radius: var(--r-md);
  font-size: 13px; font-weight: 500;
  box-shadow: var(--shadow-md);
  animation: toastIn 0.4s var(--ease-out);
  pointer-events: auto;
  display: flex; align-items: center; gap: 10px;
}
.toast::before {
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--champagne); color: var(--ink);
  display: grid; place-items: center;
  font-size: 12px; font-weight: 700;
  flex-shrink: 0;
}
.toast.is-success::before { content: '✓'; }
.toast.is-error::before { content: '!'; background: var(--paper); color: var(--ink); }
.toast.is-info::before { content: 'i'; background: var(--ink); color: var(--paper); }
.toast.is-leaving { animation: toastOut 0.3s var(--ease-out) forwards; }
@keyframes toastIn {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}
@keyframes toastOut {
  to { opacity: 0; transform: translateY(20px); }
}

@media (max-width: 639px) {
  body {
    padding-bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom, 0px));
  }

  .hero {
    /* Trim ~130px off the full-viewport hero so it stops leaving a tall empty
       band under the CTAs. That reclaimed headroom becomes a peek of the shop
       sheet below — which reads as intentional and invites the scroll instead
       of sitting there as dead wall. */
    min-height: calc(100vh - var(--header-h) - 84px);
    min-height: calc(100svh - var(--header-h) - 84px);
    padding: 12px 0 30px;
  }

  .hero-grid {
    gap: 16px;
    /* Top-align so all the leftover wall sits BELOW the copy — that space is the
       featured strip's fade runway: it lifts + fades away into it before the shop
       sheet climbs up, so the three looks tuck away cleanly instead of being cut. */
    align-items: start;
  }
  .hero-copy { padding-top: clamp(6px, 3vh, 22px); }

  /* Phones: the model bleeds off the right edge with her face around the 70%
     line, so the copy hugs a narrow left column on the empty wall and never
     touches her face. Buttons hug their labels (full-width bars would cross
     her waistline). */
  .hero-copy {
    max-width: 58vw;
  }
  .hero-copy h1 {
    font-size: clamp(27px, 8.6vw, 36px);
  }
  .hero-text {
    font-size: 14px;
    /* Narrower than the copy column (58vw). The column is sized off her raised
       hands, which reach ~62% of the screen; her ARM cuts in further left than
       that, crossing the copy's lower right from about 56% down to 45%. The
       headline is short enough to never reach it, so this paragraph's full
       measure was the only thing that did. 46vw is measured, not picked: at 50vw
       the last words of two lines still cross her (worst 2.9:1), and at 43vw the
       paragraph gains a LINE and drops into the rows where her arm has swept
       further left, which is worse again. 46vw is the widest measure that clears
       her without costing a line. */
    max-width: 46vw;
  }
  .hero-actions {
    flex-direction: column;
    align-items: flex-start;
  }

  /* Buttons stay narrower than ~50vw so their rows never reach her face
     (she stands right of the 50% line on phones). Where a button edge can
     still meet her wrist/lens on some aspect ratios, the ghost turns solid
     paper — a clean chip over the photo instead of a translucent smudge. */
  .hero-actions .btn-lg {
    min-height: 50px;
    padding: 0 24px;
    font-size: 14px;
  }
  .hero .btn-ghost { background: rgba(250, 250, 250, 0.92); }

  .product-grid {
    gap: 14px 10px;
  }

  .product-info {
    padding: 12px;
  }

  .toolbar {
    top: 64px;
    border-radius: var(--r-sm);
  }

  /* The floating WhatsApp button is the primary contact path on mobile (the
     bottom nav that used to carry it is gone) — keep it visible here. */

  /* Mobile touch targets — nudge small controls toward the ~44px guideline.
     Scoped to phones (base rules untouched) so desktop density is unchanged. */
  .qty-control button { width: 40px; height: 40px; }
  .remove-btn { width: 40px; height: 40px; top: 16px; }
  .filter-field select,
  .filter-field input { height: 44px; }
  .category-pills button,
  .color-swatches button,
  .filter-chip { height: 40px; }
  .filter-chip .filter-chip-x { width: 24px; height: 24px; font-size: 15px; }

  /* iOS Safari auto-zooms the page when a focused form control is < 16px. Bump
     every storefront + studio control to 16px on phones to stop the jarring zoom
     (!important to beat the classed per-control sizes). */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select,
  textarea {
    font-size: 16px !important;
  }

  .drawer-panel,
  .modal-card {
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
}

/* Small/short portrait phones (SE-class, 320-414 x <=700): the viewport-sized
   hero can't fit the full-scale copy, and anything past the fold hides under
   the sheet forever (the hero is pinned). Compact the type and buttons so the
   whole composition lands inside the first screen. */
@media (max-width: 639px) and (orientation: portrait) and (max-height: 700px) {
  .hero-copy h1 { font-size: clamp(25px, 7.6vw, 31px); }
  .hero-text { font-size: 13px; margin-bottom: 18px; }
  .hero-actions { gap: 8px; }
  .hero-actions .btn-lg { min-height: 48px; padding: 0 22px; font-size: 14px; }
}

/* Landscape phones (<960px, rotated): the <picture> swaps to the landscape
   frame (model at ~83%, wall left) — a mini desktop composition. Copy sits
   left, CTAs inline. */
@media (max-width: 959px) and (orientation: landscape) {
  .hero { padding: 12px 0 26px; }
  .hero-backdrop img { object-position: 85% 24%; }
  .hero-copy { max-width: min(560px, 56vw); }
  .hero-copy h1 { font-size: clamp(26px, 4.6vw, 40px); }
  .hero-text { font-size: 13.5px; margin-bottom: 16px; }
  .hero-actions { flex-direction: row; }
  .hero-actions .btn { width: auto; }
}
@media (max-width: 959px) and (orientation: landscape) and (max-height: 420px) {
  .modal-card { max-height: 96vh; }
  .modal-desc { -webkit-line-clamp: 2; }
}

/* =========================================================
   Responsive — Tablet
   ========================================================= */
@media (min-width: 640px) {
  :root {
    --gutter: 28px;
    --header-h: 72px;
  }
  .product-grid { grid-template-columns: repeat(4, 1fr); gap: 14px; }
  /* Tablets keep the phone composition (copy top-left on the wall, model at
     ~62%, fan bottom) — a two-column split here would push the copy onto her
     face. Two columns start at 960px. */
  .hero { padding: 24px 0 44px; }
  .filter-row { grid-template-columns: repeat(3, 1fr); }
  .modal-product { flex-direction: row; min-height: 520px; align-items: stretch; }
  .modal-product .modal-media { width: 50%; }
  .modal-product .image-wrap { width: 100%; flex: 1; min-height: 0; aspect-ratio: auto; }
  .modal-details { width: 50%; padding: 28px 28px; overflow-y: auto; }
  /* Side-by-side: the image fills its column at any height. */
  .modal-product .image-wrap img { position: absolute; inset: 0; }
  .checkout-grid { grid-template-columns: 1.3fr 1fr; gap: 28px; }
  .recent-card { flex: 0 0 160px; }
  /* Quick-view becomes a centered, fully-rounded dialog on tablets instead of a
     full-bleed bottom sheet; desktop's max-width:1000px still wins below. */
  .modal { place-items: center; padding: 20px; }
  .modal-card {
    max-width: min(820px, 100%);
    border-radius: var(--r-xl);
    transform: translateY(20px) scale(0.96); opacity: 0;
    transition: transform 0.4s var(--ease-out), opacity 0.4s var(--ease-out);
  }
  .modal.is-open .modal-card { transform: none; opacity: 1; }
}

/* Portrait tablets: the model is larger in the frame than on phones, so pull
   the paragraph in a touch — its longest line was grazing her raised hand. */
@media (min-width: 640px) and (max-width: 959px) and (orientation: portrait) {
  .hero-copy { max-width: min(500px, 53vw); }
}

/* =========================================================
   Responsive — Desktop
   ========================================================= */
@media (min-width: 960px) {
  :root {
    --gutter: 36px;
    --header-h: 76px;
  }
  .brand-logo {
    height: 30px;
    width: 88px;
  }
  .desktop-nav {
    /* Truly center Shop/Visit in the header, independent of the differing
       widths of the brand (left) and the action icons (right). */
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 30px;
    font-size: 14px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--ink-soft);
  }
  .desktop-nav a {
    position: relative;
    padding: 12px 4px;
    transition: color 0.2s var(--ease-out);
  }
  .desktop-nav a::after {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 1px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.3s var(--ease-out);
  }
  .desktop-nav a:hover { color: var(--ink); }
  .desktop-nav a:hover::after { transform: scaleX(1); transform-origin: left; }
  .nav-actions { gap: 10px; }
  .icon-btn {
    width: 46px;
    height: 46px;
  }
  .icon-btn svg {
    width: 22px;
    height: 22px;
  }
  .icon-btn .count {
    top: 2px;
    right: 2px;
    min-width: 24px;
    height: 24px;
    font-size: 13px;
  }

  .site-footer { padding-bottom: 36px; }
  .toast-region { bottom: 24px; left: auto; right: 24px; max-width: 360px; }
  .whatsapp-fab { right: 24px; bottom: 24px; }
  .footer-version { left: 24px; bottom: 24px; }

  .product-grid { grid-template-columns: repeat(5, 1fr); gap: 16px; }
  .section { padding: 56px 0; }
  /* Desktop: the model owns the RIGHT of the landscape frame (~83%), standing
     on the empty wall's curtain light. The copy lives in the LEFT column so
     the whole right half of the fold is hers; the grid's right column is
     intentionally empty — the photo carries it. */
  .hero { padding: 28px 0 52px; }
  .hero-grid {
    grid-template-columns: minmax(0, 760px) 1fr;
    gap: 44px;
    align-items: start;
  }
  /* Center the copy on the empty wall so the fuller two-line statement owns the
     vertical space the featured-looks strip used to fill — the column reads as a
     balanced block, not a top-anchored line with a void beneath it. */
  .hero-copy { grid-column: 1; max-width: none; align-self: center; padding-top: clamp(8px, 3vh, 40px); }
  /* With the fold to itself, the copy scales up into a full statement — the empty
     wall left of the model is the headline's canvas. vw-driven with caps so it
     grows with the screen but never crowds her (column caps at 760px). */
  .hero .eyebrow { font-size: 15px; letter-spacing: 0.24em; margin-bottom: 20px; }
  .hero-copy h1 { font-size: clamp(62px, 6.2vw, 104px); margin: 12px 0 28px; }
  .hero-text { font-size: clamp(19px, 1.55vw, 24px); max-width: 600px; margin-bottom: 18px; }
  .hero-actions { gap: 14px; }
  .hero-actions .btn { min-height: 62px; padding: 0 34px; font-size: 16.5px; }
  .hero-actions .btn-lg { min-height: 66px; padding: 0 40px; font-size: 17.5px; }
  /* Landscape frame is cropped top/bottom here — a high Y keeps her face in the
     upper third; the 88% X only matters on narrow-but-tall desktop windows
     (960-1300px), where it slides the crop window right so she stays in frame. */
  .hero-backdrop img { object-position: 88% 28%; }
  /* Desktop keeps ONLY the bottom seam fade (tuned a hair shorter than the base
     rule — the frame is taller here). The dark left scrim that used to sit under
     the copy is gone: it read as a black panel painted over the photograph. It
     stays gone. v0.9.2 solved the legibility it was there for by making the copy
     ink instead of white, so nothing is painted over the shot at any size. */
  .hero-backdrop__wash {
    background:
      linear-gradient(0deg, rgba(230, 230, 230, 0.85) 0%, rgba(230, 230, 230, 0.30) 12%, rgba(230, 230, 230, 0) 30%);
  }

  .modal { place-items: center; padding: 24px; }
  .modal-card { max-width: 1000px; max-height: 90vh; border-radius: var(--r-xl); transform: translateY(20px) scale(0.96); opacity: 0; transition: transform 0.4s var(--ease-out), opacity 0.4s var(--ease-out); }
  .modal.is-open .modal-card { transform: none; opacity: 1; }
  .modal-product .modal-media { aspect-ratio: 1/1.05; }
  .modal-details { padding: 30px 30px; gap: 13px; }

  .filter-row { grid-template-columns: repeat(4, 1fr); }
}

/* Short widescreens (laptops, side-by-side modal): clamp the description and
   tighten spacing so the quick-view still fits without an internal scrollbar. */
@media (min-width: 640px) and (max-height: 820px) {
  .modal-card { max-height: 94vh; }
  .modal-details { gap: 10px; padding: 22px 24px; }
  .modal-desc { -webkit-line-clamp: 3; }
  .modal-details h2 { font-size: clamp(23px, 3.4vw, 29px); }
  .qty-add-row .btn, .qty-add-row .qty-control { height: 44px; }
  .modal-meta { gap: 4px; padding-top: 8px; font-size: 11px; }
}

/* Short desktop windows (1366x768-class, small laptops, zoomed-in browsers):
   compress the copy's air so headline + CTAs land inside the fold — anything
   past it hides under the sheet forever (the hero is pinned). Height-driven
   headline so it stays as large as the fold allows. */
@media (min-width: 960px) and (max-height: 780px) {
  .hero { padding-bottom: 40px; }
  .hero-copy { padding-top: clamp(4px, 3vh, 24px); }
  .hero-copy h1 { font-size: clamp(48px, 10.6vh, 80px); }
  .hero-text { font-size: clamp(17px, 1.4vw, 21px); margin-bottom: 14px; }
}

/* =========================================================
   Print (hide interactive)
   ========================================================= */
@media print {
  .site-header, .whatsapp-fab, .drawer, .modal, .hero-backdrop { display: none !important; }
  /* The photo is gone on paper. The copy is already ink, but the warm-white
     halo would print as a smudge around every letter, so drop it. */
  .hero-copy .eyebrow,
  .hero-copy h1,
  .hero-copy h1 em,
  .hero-copy .hero-text {
    color: var(--ink) !important;
    text-shadow: none !important;
    animation: none !important;
  }
}

/* =========================================================
   Animation polish (storefront micro-interactions)
   GPU transform/opacity only; auto-neutralized by the global
   prefers-reduced-motion override above.
   ========================================================= */
/* Tactile press on cards and key controls.
   NB: no persistent `will-change: transform` here. Leaving it on permanently
   promotes every card to its own compositing layer, and on the fractional grid
   tracks (repeat(N,1fr) rarely lands on whole pixels) the text then rasterizes
   at a sub-pixel offset and reads blurry — which is exactly why the fixed-width
   .recent-card labels looked crisper. The browser promotes on demand for the
   brief :active transform, so the press still feels tactile. */
.product-card:active { transform: translateY(-2px) scale(0.995); }
/* Touch devices never get the hover lift, so give the press a clearer feel. */
@media (hover: none) {
  .product-card:active { transform: scale(0.97); box-shadow: var(--shadow-sm); }
}
.product-card:hover .product-media img { filter: brightness(1.02); }
.btn:active { transform: scale(0.97); }
.wish-btn:active { transform: scale(0.92); }
.icon-btn:active { transform: scale(0.9); }

/* Add-to-bag confirmation: a soft pop on the cart/wishlist counters. */
.count.is-visible, .nav-count { transition: transform 0.3s var(--ease-spring); }

/* Category pills + filter chips: smoother state change. */
.category-pills button { transition: color 0.25s var(--ease-out), background 0.25s var(--ease-out), border-color 0.25s var(--ease-out), transform 0.2s var(--ease-out); }
.category-pills button:active { transform: scale(0.96); }

/* Drawer contents ease in just after the panel arrives, for a layered feel. */
@media (prefers-reduced-motion: no-preference) {
  .drawer.is-open .drawer-body > *,
  .drawer.is-open .cart-summary {
    animation: drawerItemRise 0.5s var(--ease-out) both;
    animation-delay: 0.12s;
  }
}
@keyframes drawerItemRise {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* ============================================================================
   Legal pages (/privacy/, /terms/, /cookies/)
   The boutique's own shapes rather than a document template: each clause is a
   soft card on the warm ground, the way a product card sits on the shop, with
   the number as a small pill and the heading in Fraunces. The jump links reuse
   the category pill row. These pages load none of the store's scripts, so they
   carry no cart, no wishlist and no Firebase.
   ========================================================================= */
.legal-main {
  padding: calc(var(--header-h) + 44px) 0 88px;
  background: var(--paper);
}

.legal-head {
  max-width: 660px;
  margin: 0 auto 34px;
  text-align: center;
}

.legal-head h1 {
  margin: 10px 0 0;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(32px, 6vw, 52px);
  line-height: 1.08;
  letter-spacing: 0.01em;
}

.legal-lead {
  margin: 14px auto 0;
  max-width: 52ch;
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1.7;
}

.legal-updated {
  margin: 16px 0 0;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.legal-wrap { max-width: 860px; margin: 0 auto; }

.legal-summary {
  padding: 26px 28px;
  border-radius: var(--r-lg);
  background: #fff;
  box-shadow: var(--shadow-sm);
}

.legal-summary h2 {
  margin: 0 0 12px;
  font-family: var(--display);
  font-weight: 500;
  font-size: 22px;
}

.legal-summary p { margin: 12px 0 0; font-size: 15px; line-height: 1.72; color: var(--ink-soft); }
.legal-summary p:first-of-type { margin-top: 0; }

.legal-jump {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 26px 0 8px;
}

.legal-jump a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 7px 16px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: #fff;
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1.3;
  text-decoration: none;
  transition: border-color 0.2s var(--ease-out), color 0.2s var(--ease-out);
}

.legal-jump a:hover { border-color: var(--cocoa); color: var(--ink); }
.legal-jump a:focus-visible { outline: 2px solid var(--cocoa); outline-offset: 2px; }

.legal-clause {
  margin-top: 18px;
  padding: 26px 28px;
  border-radius: var(--r-lg);
  background: var(--paper-warm);
  scroll-margin-top: calc(var(--header-h) + 20px);
}

.legal-clause-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 6px;
}

.legal-num {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 26px;
  padding: 0 9px;
  border-radius: var(--r-pill);
  background: var(--cocoa);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
}

.legal-clause-head h2 {
  margin: 0;
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(20px, 3vw, 26px);
  line-height: 1.2;
}

.legal-clause h3 {
  margin: 20px 0 4px;
  font-family: var(--body);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.legal-clause p { margin: 12px 0 0; font-size: 15px; line-height: 1.75; color: var(--ink-soft); }
.legal-clause ul { margin: 12px 0 0; padding: 0; list-style: none; display: grid; gap: 10px; }

.legal-clause li {
  position: relative;
  padding-left: 22px;
  font-size: 15px;
  line-height: 1.7;
  color: var(--ink-soft);
}

.legal-clause li::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 0.66em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--champagne);
}

.legal-clause strong { color: var(--ink); font-weight: 600; }

.legal-clause a {
  color: var(--cocoa);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.legal-note {
  margin-top: 16px;
  padding: 16px 18px;
  border-radius: var(--r-md);
  background: #fff;
  font-size: 14px;
  line-height: 1.7;
  color: var(--ink-soft);
}

.legal-note p { margin: 10px 0 0; font-size: 14px; }
.legal-note p:first-child { margin-top: 0; }

.legal-others {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 34px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}

.legal-footer {
  padding: 34px 0 calc(34px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--paper);
  text-align: center;
}

.legal-footer p { margin: 0; font-size: 13px; color: var(--muted); }
.legal-footer nav { display: flex; flex-wrap: wrap; justify-content: center; gap: 2px 10px; margin-top: 10px; }

.legal-footer nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 4px 6px;
  color: var(--ink-soft);
  font-size: 13px;
  text-decoration: none;
}

.legal-footer nav a:hover { color: var(--ink); text-decoration: underline; }
.legal-footer nav a:focus-visible { outline: 2px solid var(--cocoa); outline-offset: 2px; }

@media (max-width: 560px) {
  .legal-main { padding-top: calc(var(--header-h) + 24px); }
  .legal-summary, .legal-clause { padding: 20px 18px; border-radius: var(--r-md); }
  /* At 320px the pill and the heading on one line leave four words a row. */
  .legal-clause-head { flex-direction: column; align-items: flex-start; gap: 8px; }
}

/* The four policies, under the tagline in the storefront footer. Same size as the rest of the
   footer type: a legal link nobody can read is not a disclosure. */
.footer-legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 14px;
  margin-top: 4px;
}

.footer-legal a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 4px 2px;
  /* The storefront footer is --ink, so this is a light-on-dark link. --ink-soft here measured
     1.79:1. White at 0.72 over #1a1a1a is 9.7:1. */
  color: rgba(255, 255, 255, 0.72);
  font-size: 13px;
  text-decoration: none;
}

.footer-legal a:hover { color: #fff; text-decoration: underline; }
.footer-legal a:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
