/* ============================================================
   文禾电商 — Ghana COD Storefront
   Design system v1.0
   ------------------------------------------------------------
   Palette  : warm cream + pepper-red + kente gold + leaf green
   Type     : Bricolage Grotesque (display) / Plus Jakarta Sans (UI)
   Radius   : 10 / 16 / 24 / pill
   Motion   : 180ms base, ease-out, reduced-motion safe
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  /* surfaces — contrast vs --cream (#fdf7ee) noted for every text colour */
  --cream: #fdf7ee;
  --sand: #f2e3cd;
  --paper: #ffffff;
  --ink: #1a120b;        /* 15.6:1 */
  /* --surface is the deep brand dark, kept constant in both colour schemes.
     It is the background of the announcement bar, footer, flash sale and
     other "dark surface" components. Crucially it MUST NOT be the same token
     as --ink (the body text colour), because in dark mode --ink flips to a
     light colour — if those surfaces also used --ink they would become light
     boxes with white-on-white text. */
  --surface: #1a120b;
  --on-surface: #fdf7ee;
  /* The flash-sale band's own colour scheme: its surface, the text on it, the
     note above the title, and the countdown tiles. Light mode runs the band
     LIGHT — a soft brand tint carrying the page's usual dark text — because a
     near-black band that size was the heaviest thing on an otherwise cream page.
     Dark mode keeps the deep surface it always had, which is where the gold note
     and the glass tiles come from.
     Deliberately NOT --surface: that token is shared with the announcement bar
     and the footer, which must stay near-black in both schemes.
     Also deliberately tokens rather than a .flash override inside the dark
     block: a component rule there sits ABOVE the base rule in the file and loses
     the specificity tie to it — which is why the .why panel has silently ignored
     its dark-mode background since the day it was written. */
  --flash-bg: var(--brand-sf);
  --flash-fg: var(--ink);
  --flash-note: var(--warn);        /* amber text that reads on a light surface */
  --flash-chip: var(--paper);
  --flash-chip-line: var(--line);
  --ink-2: #43362a;      /* 10.9:1  body copy */
  --ink-3: #5c4c3b;      /*  7.7:1  captions, hints, meta  (was #7c6a58 = 4.9:1) */
  --line: #dbcbab;       /* dividers, decorative rules */
  --line-2: #ece0cf;     /* inner hairlines */
  /* There is deliberately no --line-card any more, so please do not add one
     back. Cards and panels (product card, .cat, .rev, .faq__i, .gallery__main,
     .summary, .form-card, .ocard) are bounded by --sh-rest: at 2.1:1 a 1px tan
     outline is heavy enough to pull the eye and thin enough to read as a
     mistake, and on cream it looks like tape around a white box. Anything that
     genuinely needs a hard edge is an interactive control — input, select,
     button, radio card — and those use --line-strong, which is 3:1 for WCAG
     1.4.11. --sh-1 is what is left for the small floating chips (.card__quick),
     where a four-layer shadow would be four times the fill for no visible gain. */
  --line-strong: #a08a66;/*  3.1:1  borders of interactive controls (WCAG 1.4.11) */

  /* brand */
  --brand: #d6341c;      /* 4.5:1 on cream · white text on top = 4.8:1 */
  --brand-dk: #b62a13;   /* 5.9:1 on cream — small brand-coloured text */
  --brand-sf: #fdeae5;
  --gold: #efa51c;       /* fills, and text on DARK surfaces only */
  --gold-sf: #fdf2d6;
  --star: #b87900;       /* 3.4:1 on cream — gold used as icon/text on light */
  --leaf: #0b6e4f;
  --leaf-sf: #e2f2ea;
  --sky: #1668a8;
  --sky-sf: #e5f0f9;

  /* semantic */
  --ok: var(--leaf);
  --warn: #8f5100;
  --danger: #b52a10;

  /* radius */
  --r-xs: 8px;
  --r-sm: 10px;
  --r: 16px;
  --r-lg: 24px;
  --r-pill: 999px;

  /* elevation — warm shadows, never grey */
  --sh-1: 0 1px 2px rgba(58, 36, 12, .06), 0 2px 8px rgba(58, 36, 12, .05);
  --sh-2: 0 2px 6px rgba(58, 36, 12, .08), 0 10px 24px rgba(58, 36, 12, .08);
  --sh-3: 0 8px 18px rgba(58, 36, 12, .10), 0 24px 48px rgba(58, 36, 12, .12);
  /* The brand glow under a button on hover. Brighter and wider than it was so
     the primary actions (加入购物车, 去结算…) visibly bloom instead of just
     lifting — see .btn:hover. */
  --sh-brand: 0 8px 24px rgba(214, 52, 28, .42);
  /* Layered shadows, after Tobias Ahlin's "Smoother & sharper shadows with
     layered box-shadows" (tobiasahlin.com, 2019) — the technique the client
     pointed at for the cards.
     A single box-shadow is one blurred silhouette of the box: one blur radius,
     therefore one gaussian falloff, which has to be either soft or tight and can
     never be both. Stacking several shadows whose offset and blur each double
     (1 → 2 → 4 → 8 → 16 → 32) sums those falloffs, and a sum can hold a crisp
     short transition next to a long soft one — the way a real penumbra behaves,
     tight where the object meets the surface and fading outwards from there.
     His rule, and the one that is easy to get wrong: more layers means LOWER
     alpha on every one of them, or the stack just gets darker instead of
     smoother.      His published values are 4 layers → .15, 5 → .12, 6 → .11 (for
     black). Ours run lighter than that because the colour is warm ink rather
     than black, and four layers is where this stops being worth it: the shadow
     paints on every card of a grid of cards, on phones, and the 16px rung of his
     ladder costs a wide fill for a difference nobody sees at rest.
     The alpha was cut from .13 to .09 after looking at a full grid of it: at .13
     ten cards at once read as a heavy page rather than as ten objects sitting on
     one. Alpha is the dial for that — the ladder above decides the shape of the
     falloff, and shape was never the complaint. */
  --sh-rest:
    0 1px 1px rgba(58, 36, 12, .09),
    0 2px 2px rgba(58, 36, 12, .09),
    0 4px 4px rgba(58, 36, 12, .09),
    0 8px 8px rgba(58, 36, 12, .09);
  /* Hover carries the same ladder two rungs further. That is what "higher off
     the page" looks like: the shadow does not merely darken, it reaches further
     out, so the card reads as further from the surface rather than as dirtier.
     Per-layer alpha drops, because there are now six layers.
     The last layer is the geometry the client asked for from gamer520.com: a
     large Y offset with a large negative spread — the negative spread pulls the
     shade in from the sides and the offset pushes it down, leaving a pool
     directly under the card instead of a halo around it. It is kept because that
     pool is the shape they chose; the five layers above it are what make the
     pool's edges fade out instead of stopping.
     Its alpha is .26 rather than the .30 it was on its own: the layers above
     already darken that area, and .30 on top of them came out muddy.
     The COLOUR is ours, not the reference's. They use rgba(136,161,206,.3), a
     blue-grey; this stylesheet's elevation is warm by rule (see --sh-1..3) and on
     a cream page that blue reads as a cold dirty haze. */
  --sh-lift:
    0 1px 1px rgba(58, 36, 12, .10),
    0 2px 2px rgba(58, 36, 12, .10),
    0 4px 4px rgba(58, 36, 12, .10),
    0 8px 8px rgba(58, 36, 12, .10),
    0 16px 16px rgba(58, 36, 12, .10),
    0 34px 20px -24px rgba(58, 36, 12, .26);

  /* spacing — 4pt grid */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px;
  --s5: 20px; --s6: 24px; --s7: 32px; --s8: 40px;
  --s9: 56px; --s10: 72px; --s11: 96px;

  /* layout */
  --wrap: 1200px;
  --hdr: 60px;

  /* motion */
  --ease: cubic-bezier(.22, .61, .36, 1);
  --ease-out-back: cubic-bezier(.34, 1.4, .64, 1);
  --t: 180ms;

  --font-display: "Bricolage Grotesque", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", "Trebuchet MS", sans-serif;
  --font-body: "Plus Jakarta Sans", "Segoe UI", system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;

  /* text colour to sit ON top of --surface (the deep brand dark) and on top of
     the brand red. White reads well on both; defined once so dark-mode flips
     stay consistent. */
  --on-brand: #ffffff;
  --on-surface: #fdf7ee;
}

/* Dark mode — battery-friendly for low-cost Androids */
@media (prefers-color-scheme: dark) {
  :root {
    --cream: #120e0a;
    --sand: #1c1611;
    --paper: #1a1410;
    --ink: #f7efe3;
    --ink-2: #d8c7b3;
    --ink-3: #a08e7c;
    /* --surface stays the deep brand dark in dark mode too, so the
       announcement bar, footer and flash sale keep their high-contrast
       dark surfaces with light text. */
    --surface: #1a120b;
    --on-brand: #1a0f0a;   /* bright coral needs dark text on top */
    --on-surface: #fdf7ee;
    /* The band goes back to the deep surface in the dark, and the note to gold:
       gold reads on a dark surface, and the amber above does not. */
    --flash-bg: #1a120b;
    --flash-fg: var(--on-surface);
    --flash-note: var(--gold);
    --flash-chip: rgba(255, 255, 255, .1);
    --flash-chip-line: rgba(255, 255, 255, .16);
    --star: #ffd166;   /* brighter gold on dark cards — #ffc14d was too close to the paper bg */
    --line: #2f2620;
    --line-2: #241d17;
    --line-strong: #7a6857;
    --brand: #ff5138;
    --brand-dk: #ff6f5a;
    --brand-sf: #3a1710;
    --gold: #ffc14d;
    --gold-sf: #33260c;
    --leaf: #35c28d;
    --leaf-sf: #10291f;
    --sky: #62b0e8;
    --sky-sf: #10222f;
    --warn: #ffc14d;
    --danger: #ff8a75;
    --sh-1: 0 1px 2px rgba(0, 0, 0, .5);
    --sh-2: 0 10px 24px rgba(0, 0, 0, .45);
    --sh-3: 0 24px 48px rgba(0, 0, 0, .55);
    --sh-brand: 0 8px 24px rgba(255, 81, 56, .45);
    /* Black rather than a warm tint: on a dark page a tinted shadow reads as a
      glow, and the reference does the same thing (.ripro-dark .post-grid).
      The alphas are the light-mode ladder raised, because a shadow has less to
      work with against a dark page — the layers are the same shape, they just
      have to be darker to register at all. */
    --sh-rest:
      0 1px 1px rgba(0, 0, 0, .20),
      0 2px 2px rgba(0, 0, 0, .20),
      0 4px 4px rgba(0, 0, 0, .20),
      0 8px 8px rgba(0, 0, 0, .20);
    --sh-lift:
      0 1px 1px rgba(0, 0, 0, .26),
      0 2px 2px rgba(0, 0, 0, .26),
      0 4px 4px rgba(0, 0, 0, .26),
      0 8px 8px rgba(0, 0, 0, .26),
      0 16px 16px rgba(0, 0, 0, .26),
      0 34px 20px -24px rgba(0, 0, 0, .48);
  }

  /* In dark mode the .why "How it works" panel sits on the dark page bg.
     Without this override its var(--sand) background is essentially the same
     colour as --cream and the panel disappears. Give it a warmer, lifted tone. */
  .why { background: #2a1f17; }

  /* Ratings: pin star colour outright so no token/cache mishap ever renders
     them dark — always a bright gold against the dark paper. */
  .stars, .stars svg { color: #ffd166; }
  .stars svg { width: 16px; height: 16px; }
  .rating span, .card__meta .rating span { color: #cdb9a3; }
}

/* ---------- 2. Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* `clip` is the modern, reliable way to suppress a stray horizontal
     scrollbar on narrow viewports without creating a scroll container
     (the way `hidden` does). The previous `body { overflow-x: hidden }`
     alone let the html element still report — and on some mobile browsers
     even render — a horizontal scrollbar whenever a descendant (here the
     decorative .hero__blob) extended past the viewport. */
  overflow-x: clip;
}
body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}
img, svg, video { max-width: 100%; display: block; }
img { height: auto; }
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: none; }
ul, ol { margin: 0; padding: 0; list-style: none; }
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -.02em;
  margin: 0;
  text-wrap: balance;
}
p { margin: 0; text-wrap: pretty; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 4px; }
::selection { background: var(--gold); color: var(--ink); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ---------- 3. Utilities ---------- */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--s4); }
@media (min-width: 768px) { .wrap { padding-inline: var(--s6); } }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.center { text-align: center; }
.stack { display: grid; gap: var(--s4); }
.row { display: flex; align-items: center; gap: var(--s3); }
.spread { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.hide-sm { display: none; }
@media (min-width: 768px) { .hide-sm { display: revert; } }
/* menu button: only on phones, where the drawer is the nav */
.burger { display: none; }
@media (max-width: 767px) { .burger { display: grid; } }
.mt-0 { margin-top: 0; }

.eyebrow {
  font-family: var(--font-display);
  font-size: 12px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: var(--brand);
}

/* kente thread — a 3-colour accent rule, used sparingly */
.kente {
  height: 4px; border-radius: 99px;
  background: linear-gradient(90deg, var(--brand) 0 33%, var(--gold) 33% 66%, var(--leaf) 66% 100%);
}

/* ---------- 4. Buttons ---------- */
.btn {
  --bg: var(--brand); --fg: var(--on-brand);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: 48px; padding: 0 var(--s5);
  background: var(--bg); color: var(--fg);
  font-family: var(--font-display); font-weight: 700; font-size: 16px;
  letter-spacing: -.01em;
  border-radius: var(--r-pill);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease),
              background-color var(--t) var(--ease), opacity var(--t) var(--ease);
  will-change: transform;
}
.btn:hover { box-shadow: var(--sh-brand); transform: translateY(-1px); }
.btn:active { transform: scale(.975); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .5; pointer-events: none; }
.btn--lg { min-height: 56px; font-size: 18px; padding: 0 var(--s7); }
.btn--block { width: 100%; }
.btn--dark { --bg: var(--surface); --fg: var(--on-surface); }
.btn--dark:hover { box-shadow: var(--sh-2); }
.btn--leaf { --bg: var(--leaf); }
.btn--leaf:hover { box-shadow: 0 6px 18px rgba(11, 110, 79, .3); }
.btn--ghost {
  --bg: transparent; --fg: var(--ink);
  border: 1.5px solid var(--line-strong);
}
.btn--ghost:hover { --bg: var(--sand); box-shadow: none; }
.btn--sm { min-height: 42px; font-size: 14px; padding: 0 var(--s4); }
.btn--icon { padding: 0; width: 44px; min-height: 44px; }
/* 图标与文字混排的按钮：图标必须显式定尺，否则 viewBox-only 的 svg 会被拉伸 */
.btn > svg { flex: none; width: 20px; height: 20px; }

.link-underline {
  font-weight: 700; color: var(--brand);
  border-bottom: 2px solid var(--brand-sf);
  transition: border-color var(--t) var(--ease);
}
.link-underline:hover { border-color: var(--brand); }

/* ---------- 5. Chips / badges ---------- */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; border-radius: var(--r-pill);
  background: var(--sand); color: var(--ink-2);
  font-size: 13px; font-weight: 700; white-space: nowrap;
}
.chip--brand { background: var(--brand-sf); color: var(--brand-dk); }
.chip--gold { background: var(--gold-sf); color: #8a5e00; }
.chip--leaf { background: var(--leaf-sf); color: var(--leaf); }
.chip--outline { background: transparent; border: 1.5px solid var(--line-strong); }

.badge {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  padding: 5px 10px; border-radius: var(--r-xs);
  background: var(--brand); color: var(--on-brand);
  font-family: var(--font-display); font-size: 12px; font-weight: 800;
  letter-spacing: .04em; text-transform: uppercase;
}
.badge--gold { background: var(--gold); color: #3d2800; }
.badge--leaf { background: var(--leaf); color: #fff; }
.badge--out { background: var(--ink-3); color: #fff; }

/* ---------- 6. Announcement bar ---------- */
.announce {
  background: var(--surface);
  color: var(--on-surface);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .02em;
  padding: 10px 0;
  text-align: center;
}
.announce b { color: var(--gold); }
@media (max-width: 480px) { .announce { font-size: 13px; padding: 9px 0; } }

/* ---------- 7. Header ---------- */
.header {
  position: sticky; top: 0; z-index: 60;
  background: color-mix(in srgb, var(--cream) 88%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--line);
}
.header__in {
  display: flex; align-items: center; gap: var(--s4);
  height: var(--hdr);
}
.logo {
  display: inline-flex; align-items: baseline; gap: 1px;
  font-family: var(--font-display); font-size: 24px; font-weight: 800;
  letter-spacing: -.03em; flex: none;
}
.logo__mark { width: 26px; height: 26px; align-self: center; margin-right: 6px; flex: none; color: var(--brand); }
/* logo wordmark — single text mark (文禾电商) instead of the old
   hand-lettered two-tone mark */
.logo { align-items: center; gap: 10px; }
.logo__text { font-family: var(--font-display); font-weight: 800; font-size: 22px; letter-spacing: -.02em; color: var(--ink); }
.footer .logo__mark { width: 34px; height: 34px; }
.footer .logo__text { color: var(--on-surface); font-size: 26px; }

.search {
  flex: 1 1 auto; max-width: 460px; margin-left: auto;
  position: relative;
}
.search input {
  width: 100%; height: 44px;
  padding: 0 16px 0 42px;
  background: var(--paper);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-pill);
  font-size: 16px; /* never below 16px — iOS zooms the viewport on focus */
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.search input::placeholder { color: var(--ink-3); }
.search input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-sf); }
.search__icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ink-3); }

.icon-btn {
  position: relative; flex: none;
  width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  transition: background-color var(--t) var(--ease), transform var(--t) var(--ease);
}
.icon-btn:hover { background: var(--sand); }
.icon-btn:active { transform: scale(.92); }
.cart-count {
  position: absolute; top: 2px; right: 0;
  min-width: 20px; height: 20px; padding: 0 6px;
  background: var(--brand); color: #fff;
  border: 2px solid var(--cream);
  border-radius: var(--r-pill);
  font-family: var(--font-display); font-size: 11px; font-weight: 800;
  display: grid; place-items: center;
}
.cart-count[hidden] { display: none; }
.cart-count.is-bump { animation: bump .45s var(--ease-out-back); }
@keyframes bump { 0% { transform: scale(1); } 40% { transform: scale(1.5); } 100% { transform: scale(1); } }

/* category rail */
.catrail {
  border-bottom: 1px solid var(--line);
  background: var(--cream);
}
.catrail__in {
  display: flex; gap: var(--s2);
  overflow-x: auto; scrollbar-width: none;
  padding: 10px var(--s4);
  max-width: var(--wrap); margin-inline: auto;
  scroll-snap-type: x proximity;
}
.catrail__in::-webkit-scrollbar { display: none; }
.catrail a {
  flex: none; scroll-snap-align: start;
  padding: 10px 16px; border-radius: var(--r-pill);
  background: var(--paper); border: 1.5px solid var(--line-strong);
  font-size: 14px; font-weight: 700; white-space: nowrap;
  transition: background-color var(--t) var(--ease), border-color var(--t) var(--ease), color var(--t) var(--ease);
}
.catrail a:hover { border-color: var(--brand); color: var(--brand); }
.catrail a[aria-current="page"] { background: var(--ink); border-color: var(--ink); color: var(--cream); }

/* mobile nav drawer */
.drawer {
  position: fixed; inset: 0 auto 0 0; z-index: 90;
  width: min(84vw, 340px); background: var(--paper);
  border-right: 1px solid var(--line);
  padding: var(--s6) var(--s5);
  transform: translateX(-102%);
  transition: transform 240ms var(--ease);
  box-shadow: var(--sh-3);
}
.drawer.is-open { transform: none; }
.drawer__backdrop {
  position: fixed; inset: 0; z-index: 89;
  background: rgba(26, 18, 11, .45);
  opacity: 0; pointer-events: none;
  transition: opacity 240ms var(--ease);
}
.drawer__backdrop.is-open { opacity: 1; pointer-events: auto; }
.drawer a { display: block; padding: 12px 0; font-weight: 700; border-bottom: 1px solid var(--line-2); }
.drawer__close { position: absolute; top: 14px; right: 14px; }

/* ---------- 8. Hero ---------- */
.hero { position: relative; padding: var(--s7) 0 var(--s8); }
.hero__grid {
  display: grid; gap: var(--s6);
  align-items: center;
  /* minmax(0,1fr), not the implicit auto track: an auto track only shrinks to its
     items' min-content, and the hero photo's intrinsic size pushes the column
     wider than the .wrap content box — which `.hero { overflow:clip }` then
     slices off at the right on phones. Pinning the track (and the items'
     min-width) to the container makes the column fit instead of spilling out. */
  grid-template-columns: minmax(0, 1fr);
}
.hero__grid > * { min-width: 0; }
@media (min-width: 900px) {
  .hero__grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: var(--s9); }
  .hero { padding: var(--s8) 0 var(--s9); }
}
.hero h1 {
  font-size: clamp(36px, calc(16px + 4.5vw), 60px);
  margin: var(--s3) 0 var(--s4);
}
.hero h1 em {
  font-style: normal; color: var(--brand);
  position: relative; white-space: nowrap;
}
.hero h1 em::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: .08em;
  height: .16em; background: var(--gold); opacity: .5; border-radius: 2px; z-index: -1;
}
.hero p.lede { font-size: clamp(16px, 4.4vw, 19px); color: var(--ink-2); max-width: 46ch; }
/* Mobile, multi-slide hero. Here the headline and lede sit above the photo, so a
   longer or shorter slide used to resize the text block and shove the photo — and
   the whole page under it — on every advance, which reads as the page scrolling
   by itself. Reserving a fixed number of lines and clamping the overflow keeps
   the block's height constant, so a slide change no longer moves anything. Scoped
   to .hero--multi (set by setHero only when the carousel really rotates), so a
   single or default hero keeps its full, unclamped copy. */
@media (max-width: 899px) {
  .hero--multi h1 {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
    overflow: hidden; min-height: calc(3 * 1.08em);
  }
  .hero--multi p.lede {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
    overflow: hidden; min-height: calc(3 * 1.6em);
  }
}
.hero__price {
  display: flex; align-items: baseline; gap: var(--s3);
  margin: var(--s5) 0 var(--s4);
  /* Price + struck-through was-price + saving chip are wide together; wrapping
     keeps the chip on screen on a phone instead of pushing it past the edge,
     where `overflow:clip` would cut it. */
  flex-wrap: wrap;
}
.hero__price b { font-family: var(--font-display); font-size: clamp(34px, 9vw, 48px); letter-spacing: -.03em; }
.hero__price s { color: var(--ink-3); font-size: 18px; }
@media (max-width: 420px) {
  .hero__price b { font-size: 30px; }
  .hero__price s { font-size: 16px; }
}
.hero__frame {
  position: relative; border-radius: var(--r-lg); overflow: hidden;
  box-shadow: var(--sh-3); background: var(--sand);
  aspect-ratio: 4 / 5;
  /* Swipe-to-step owns the horizontal axis; vertical drags stay the page's, so a
     finger that means to scroll still scrolls. The photo is un-selectable and not
     natively draggable, so a drag pans the slide instead of ghosting the image. */
  touch-action: pan-y; cursor: grab;
  -webkit-user-select: none; user-select: none;
}
@media (min-width: 900px) { .hero__frame { aspect-ratio: 5 / 6; } }
.hero__frame img {
  width: 100%; height: 100%; object-fit: cover; -webkit-user-drag: none;
  /* A hard backface stops a translated photo leaving a faint trail on the GPUs
     that skip promoting a plain 2D layer. */
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
}
/* The incoming photo during a swipe / animated step: a second <img> stacked
   exactly over the base one. Parked it is hidden outright rather than merely
   transparent — a layer that still paints at opacity 0 is what ghosts through a
   fast drag — and will-change is left to .is-swiping, so no stale composited
   layer survives between gestures. */
.hero__layer {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; visibility: hidden; pointer-events: none;
  -webkit-user-drag: none;
}
.hero__frame.is-swiping { cursor: grabbing; }
.hero__frame.is-swiping img { will-change: transform; }
.hero__stamp {
  position: absolute; right: 14px; bottom: 14px;
  background: var(--paper); border-radius: var(--r);
  padding: 12px 16px; box-shadow: var(--sh-2);
  display: grid; gap: 2px;
}
.hero__stamp b { font-family: var(--font-display); font-size: 22px; }
.hero__stamp span { font-size: 12px; color: var(--ink-3); font-weight: 600; }
/* Hero slide dots. The element ships `hidden` and only the rotator unhides it, so
   a single-slide hero — including every shop that never opens 装修 — shows none.
   `[hidden]` has to be restated because the display:flex below outranks the
   browser's own [hidden] rule. */
.hero__dots {
  position: absolute; left: 14px; bottom: 14px; display: flex; gap: 7px;
  padding: 7px 9px; border-radius: 99px;
  background: rgba(20, 30, 50, .45); backdrop-filter: blur(2px);
}
.hero__dots[hidden] { display: none; }
.hero__dots button {
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .5); cursor: pointer;
  transition: background 200ms var(--ease);
}
.hero__dots button.on { background: var(--paper); }
.hero__blob {
  position: absolute; inset: auto -20% -30% -20%; height: 60%;
  background: radial-gradient(60% 60% at 50% 50%, var(--gold-sf), transparent 70%);
  z-index: -1; pointer-events: none;
}
/* Keep the decorative glow inside the hero so its negative inset does not
   extend the document's horizontal scroll area on narrow viewports. */
.hero { overflow: clip; }

/* ---------- 9. Trust strip ---------- */
.trust {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3);
  padding: var(--s4) 0;
  border-block: 1px solid var(--line);
}
@media (min-width: 760px) { .trust { grid-template-columns: repeat(4, 1fr); gap: var(--s6); } }
.trust__i { display: flex; gap: var(--s3); align-items: flex-start; }
.trust__i svg { flex: none; color: var(--leaf); margin-top: 2px; }
.trust__i b { display: block; font-family: var(--font-display); font-size: 15px; }
.trust__i span { font-size: 13px; color: var(--ink-3); }
@media (max-width: 420px) {
  .trust__i { gap: 10px; }
  .trust__i svg { width: 20px; height: 20px; }
  .trust__i b { font-size: 14px; }
  .trust__i span { font-size: 12.5px; }
}

/* ---------- 10. Sections ---------- */
/* Section rhythm is one step tighter than it was: 72px of empty space between
   two blocks (desktop) is the single biggest reason the flash rail, the reviews
   and the CTA all sit further down the page than they need to, and every one of
   those pixels is distance between a visitor and the buy button. */
.section { padding: var(--s7) 0; }
@media (min-width: 768px) { .section { padding: var(--s9) 0; } }
.section--tight { padding: var(--s6) 0; }
.section__head {
  display: grid; gap: var(--s3);
  margin-bottom: var(--s5);
}
@media (min-width: 700px) {
  .section__head { grid-template-columns: 1fr auto; align-items: end; }
}
.section__head h2 { font-size: clamp(26px, 6.4vw, 40px); }
.section__head p { color: var(--ink-2); max-width: 52ch; }

/* ---------- 11. Category tiles ---------- */
.cats {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3);
}
@media (min-width: 640px) { .cats { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 960px) { .cats { grid-template-columns: repeat(4, 1fr); } }
.cat {
  position: relative; overflow: hidden;
  border-radius: var(--r); background: var(--sand);
  /* 4/3, not square. A square row is ~280px of pure image on a desktop and about
     a third of a phone screen; the phone already used 4/3 for exactly this
     reason, and the crop is the same picture either way. */
  aspect-ratio: 4 / 3;
  display: flex; align-items: flex-end;
  padding: var(--s3) var(--s4);
  box-shadow: var(--sh-rest);
  /* opacity is in this list because this rule now out-ranks :where(.reveal)'s
     transition, so the entrance fade has to come from here. */
  transition: transform 260ms var(--ease), box-shadow 260ms var(--ease), opacity 260ms var(--ease);
}
.cat img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.cat::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(26,18,11,0) 35%, rgba(26,18,11,.72) 100%);
}
.cat__t { position: relative; z-index: 1; color: #fff; }
.cat__t b { display: block; font-family: var(--font-display); font-size: 17px; }
.cat__t span { font-size: 12.5px; opacity: .94; font-weight: 600; }
.cat:hover { transform: translateY(-4px); box-shadow: var(--sh-3); }
.cat:hover img { transform: scale(1.05); }
.cat img { transition: transform 500ms var(--ease); }

/* ---------- 12. Flash sale ---------- */
.flash {
  background: var(--flash-bg);
  color: var(--flash-fg);
  border-radius: var(--r-lg);
  padding: var(--s5) var(--s4);
}
@media (min-width: 768px) { .flash { padding: var(--s6); } }
.flash__head { display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; justify-content: space-between; }
.flash h2 { font-size: clamp(24px, 6vw, 34px); color: var(--flash-fg); }
/* The note above the title. --gold is for text on dark surfaces only (it says so
   where it is defined): on the light band it needs --flash-note, the amber the
   palette keeps for amber text on light. Scoped to .flash so the same class used
   elsewhere — on the dark CTA band, for instance — keeps its gold. */
.flash .eyebrow--gold { color: var(--flash-note); }
.countdown { display: flex; gap: 6px; }
.countdown div {
  min-width: 52px; text-align: center;
  background: var(--flash-chip); border: 1px solid var(--flash-chip-line);
  border-radius: var(--r-sm); padding: 6px 4px;
}
.countdown b { display: block; font-family: var(--font-display); font-size: 24px; font-variant-numeric: tabular-nums; color: var(--flash-fg); }
.countdown span { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; opacity: .9; color: var(--flash-fg); }
.flash .rail { margin-top: var(--s4); }

/* horizontal rail */
.rail {
  display: flex; gap: var(--s3);
  /* overflow-y must be named: with overflow-x: auto it computes to auto by
     itself, and the cards' shadows then give the rail a vertical scroll range.
     The block padding below is the room those shadows need to stay uncut. */
  overflow-x: auto; overflow-y: hidden;
  /* proximity, not mandatory. Mandatory snaps on every scroll change, including
     the ones our own mouse drag makes — you let go mid-card and the rail is
     teleported to the nearest boundary (measured: 140px → 232px in one frame,
     no animation, because a programmatic scrollLeft does not get a smooth
     re-snap). Proximity still lands a fling on a card, but leaves a deliberate
     rest between two cards where the reader put it. */
  scroll-snap-type: x proximity;
  /* The rail bleeds past the page margin (negative margin + matching padding, so
     cards can reach the screen edge). That padding is inside the scroll
     container, so without scroll-padding the snap grid starts 20px in: the
     browser's idea of "aligned" is 20px away from where the layout puts card 1,
     and the first swipe shifts the whole rail sideways to correct it. This tells
     the snap algorithm that the snapport starts inside that padding. */
  scroll-padding-inline: var(--s5);
  /* 6px up for the hover lift, 26px down for the shadow's reach. The clip edge
     sits at the padding box, so without this the shadow is sliced off and the
     cut reads as a line across it. */
  /* The overscroll pull-back. scrollLeft clamps at both ends, so the few pixels
     the rail is dragged PAST an end are a transform instead (see the drag code);
     the spring that returns it — with a jiggle — is .is-bouncing below. The drag
     overrides the transform inline, and the bounce takes it over on release. */
  scrollbar-width: none; padding-block: 6px 26px;
  margin-inline: calc(var(--s5) * -1); padding-inline: var(--s5);
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { flex: 0 0 63%; scroll-snap-align: start; }
/* Mouse-drag only, and deliberately NO `touch-action` claim. Claiming the
   horizontal axis (pan-y) took it away from the browser, so every touch move had
   to be replayed as a main-thread `scrollLeft` write — the one thing a
   compositor-driven scroll never needs, and the reason the rail stuttered. Left
   to the browser, this scroll container pans and snaps off the main thread with
   real momentum. The two known ways to be smooth are exactly these: let the
   compositor scroll (this), or put the whole track on one composited layer and
   drive it with translate3d from a rAF physics loop (how Embla/Swiper do it —
   Embla's docs say its only DOM write is an inline translate3d). Writing
   scrollLeft from a pointer handler is neither of them. The drag below is for
   the mouse, where there is no native pan; app.js starts it on the first
   horizontal move, which is also what keeps a tap's click target on the card
   instead of the rail. */
.rail { cursor: grab; overscroll-behavior-x: contain; }
.rail.is-dragging { cursor: grabbing; scroll-snap-type: none; user-select: none; }
.rail.is-dragging > * { pointer-events: none; }
/* The overscroll bounce-back. A transition can only overshoot once, so the
   jiggle is a short keyframe spring: it goes past the rest point the other way,
   then back again, then home. --from is the offset the rail was pulled to (set
   inline by the drag code), so one curve serves both ends and any pull length;
   the keyframes just scale it. Each segment uses an ease-in-out so the
   oscillation reads as decaying rather than mechanical. */
@keyframes rail-spring {
  0%   { transform: translateX(var(--from, 0px)); }
  55%  { transform: translateX(calc(var(--from, 0px) * -0.22)); }
  76%  { transform: translateX(calc(var(--from, 0px) * 0.11)); }
  90%  { transform: translateX(calc(var(--from, 0px) * -0.05)); }
  100% { transform: none; }
}
.rail.is-bouncing { animation: rail-spring 560ms cubic-bezier(.45, 0, .55, 1); }
@media (min-width: 620px) { .rail > * { flex: 0 0 32%; } }
@media (min-width: 1000px) { .rail > * { flex: 0 0 23.5%; } }

/* ---------- 13. Product grid + card ---------- */
.grid-products {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3);
}
@media (min-width: 700px) { .grid-products { grid-template-columns: repeat(3, 1fr); gap: var(--s5); } }
@media (min-width: 1000px) { .grid-products { grid-template-columns: repeat(4, 1fr); gap: var(--s5); } }

/* Card hover, ported from the reference site (gamer520.com / ripro .post-grid).
   Three things move, and the reference's own numbers are kept for each:
     1. the card lifts  -5px  and drops onto --sh-lift (the soft pool above),
        over 300ms on an ease-out curve;
     2. the cover art brightens (brightness + slight saturation) and zooms to
        1.06 over ~480ms, so the product pops forward as the focus;
     3. the action button rises into the corner over 200ms (see .card__quick).
   The reference's curve is cubic-bezier(.215,.61,.355,1); --ease is
   cubic-bezier(.22,.61,.36,1) — the same ease-out within a rounding error, so it
   is used rather than adding a token that would only ever have one caller.
   The card carries no border. It used to wear a 1px --line-card — a fairly
   saturated tan at 2.1:1 — which read as tape stuck around a white card on
   cream: too heavy to be the edge of a card, too thin to be a design element.
   In its place the resting state carries --sh-rest, the same layered contact
   shadow the other panels sit on, so the card still has a boundary without a
   line drawn around it — and, being layered, it has depth rather than just a
   grey blur under the edge.
   That also puts the two states on one continuum: hover climbs the same ladder
   of shadows two rungs higher (--sh-rest → --sh-lift) instead of switching
   between a border and a shadow, which is the kind of change that snaps
   regardless of how long the transition is. */
.card {
  position: relative;
  background: var(--paper);
  /* The card states its own text colour instead of inheriting one. A card is a
     --paper surface wherever it is dropped, and the flash rail drops cards
     inside .flash, which is a dark band that sets color: var(--on-surface) —
     cream. Inherited, that painted cream titles and cream prices onto white
     cards: 1.03:1, invisible. Anything added to a card should get its colour
     from here, or from a rule that names its own. */
  color: var(--ink);
  box-shadow: var(--sh-rest);
  border-radius: var(--r);
  overflow: hidden;
  display: flex; flex-direction: column;
  /* opacity is in this list because this rule now out-ranks :where(.reveal)'s
     transition, so the entrance fade has to come from here (300ms rather than
     the reveal's 520ms — the card is the thing being looked at, it can arrive
     quicker). box-shadow is here because the shadow has to grow, not snap. */
  transition: transform 300ms var(--ease), box-shadow 300ms var(--ease),
              opacity 300ms var(--ease);
}
.card:hover { transform: translateY(-5px); box-shadow: var(--sh-lift); }
.card__media {
  position: relative; aspect-ratio: 1 / 1; overflow: hidden; background: var(--sand);
}
/* Focus, not fade. The reference fades a full-bleed rgba(0,0,0,.45) scrim over
   the cover art on hover so the photo steps BACK and the buttons step forward.
   That reads as "de-emphasise the product", which is the opposite of what a
   product card hover is for — the product IS the focus. So instead of a scrim
   we brighten the photo itself (brightness + a touch of saturation) and let it
   zoom, so the artwork pops forward on hover. The quick action keeps its own
   contrast from a light ring + dark drop shadow (see .card__quick), so it no
   longer needs the photo darkened behind it. pointer-events stay on the card
   link because there is no overlay left to intercept them. */
.card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 480ms var(--ease), filter 480ms var(--ease); }
.card:hover .card__media img,
.card:focus-within .card__media img { transform: scale(1.06); filter: brightness(1.08) saturate(1.05); }
.card__body { padding: var(--s3) var(--s4) var(--s4); display: grid; gap: 6px; flex: 1; }
.card__cat { font-size: 12.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
.card__title {
  font-family: var(--font-display); font-size: 15.5px; font-weight: 700; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: 2.6em;
}
.card__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin-top: auto; padding-top: 6px; }
.card__price b { font-family: var(--font-display); font-size: 19px; letter-spacing: -.02em; }
.card__price s { font-size: 13px; color: var(--ink-3); }
/* 2-up grid on a 360px phone leaves ~141px of content width — shrink, don't clip.
   Must stay AFTER the base rules above or the cascade cancels it. */
@media (max-width: 420px) {
  .card__body { padding: 10px 12px 12px; gap: 5px; }
  .card__title { font-size: 15px; }
  .card__price b { font-size: 18px; }
  .card__price s { font-size: 12px; }
}
.card__meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.card__add { margin-top: 2px; }
.card__quick {
  position: absolute; right: 10px; top: 10px; z-index: 2;
  width: 38px; height: 38px; border-radius: 50%;
  /* Now that the photo brightens instead of dimming on hover, the button can no
     longer lean on a dark photo for contrast. A light ring + dark drop shadow
     keeps the white chip legible over both bright and dark cover art, in either
     colour scheme. */
  background: var(--paper); box-shadow: 0 2px 8px rgba(26, 18, 11, .28), 0 0 0 1.5px rgba(255, 255, 255, .7);
  display: grid; place-items: center; color: var(--ink);
  opacity: 0; transform: translateY(-6px);
  transition: opacity 200ms var(--ease), transform 200ms var(--ease);
}
.card:hover .card__quick, .card:focus-within .card__quick { opacity: 1; transform: none; }
/* Touch devices get a PRESSED state instead of the hover state, and they get it
   on contact. This is not a nicety — it is the difference between the effect
   existing and not existing on a phone. A touch screen has no pointer, so the
   only hover a browser can offer is a sticky one it applies after the fact:
   iOS Safari paints :hover on the first tap and follows the link on the second
   (which is what "you have to long-press it" is actually describing), and
   Android has already left for the product page before the paint is ever seen.
   :active fires on touchstart, so the card now answers under the finger.
   The lift is most of the feedback on a phone: a 5px lift at the edge of a
   screen is easy to miss under your own thumb, so the brighten + zoom on the
   cover does the work the scrim used to.
   Durations drop to 160ms in here. Out on a desktop the 300/500ms are there to
   be watched; under a finger they read as lag.
   touch-action is deliberately NOT set: removing the double-tap zoom to make a
   press feel a few milliseconds snappier would cost the reader their zoom. */
@media (hover: none) {
  .card__quick { opacity: 1; transform: none; }
  /* The browser's own grey tap flash would land on top of the lift and read as
     a bug rather than as feedback. Removed only here, where the card animates
     instead; every other link on the site keeps it. Inherited, so the cover
     link and the title link are covered by this one declaration. */
  .card, .cat { -webkit-tap-highlight-color: transparent; }
  .card { transition-duration: 160ms; }
  .card__media img { transition-duration: 160ms; }
  .card:active { transform: translateY(-5px); box-shadow: var(--sh-lift); }
  .card:active .card__media img { transform: scale(1.06); filter: brightness(1.08) saturate(1.05); }
  .cat:active { transform: translateY(-4px); box-shadow: var(--sh-3); }
  .cat:active img { transform: scale(1.05); }
}
.card__quick:hover { background: var(--brand); color: var(--on-brand); }

/* When the network image fails (Pexels blocked, offline, slow CDN) the card
   still has to look complete. A category-coloured tile with the first letter
   and the category name sits behind the image; on error we hide the image
   and the fallback takes over. Keeps the page readable when the CDN is down. */
.card__media > img { position: relative; z-index: 1; }
.card__fb {
  position: absolute; inset: 0; z-index: 0;
  display: grid; place-content: center; gap: 4px;
  color: var(--on-surface);
  text-align: center; padding: 12px;
  font-family: var(--font-display);
}
.card__fb span { font-size: 44px; font-weight: 800; line-height: 1; }
.card__fb em { font-size: 13px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; opacity: .92; font-style: normal; }
.card__media > img.is-fail { display: none; }
/* Fixed dark hexes — not theme tokens. var(--sky) and var(--leaf) get
   brighter in dark mode, which would make white text unreadable on the
   fallback tiles. Lock these to a dark shade that keeps >=4.5:1 with
   --on-surface text in both colour schemes. */
.card__fb--electronics { background: #1668a8; }
.card__fb--fashion     { background: #c2410c; }
.card__fb--beauty      { background: #7a3fa0; }
.card__fb--home        { background: #0b6e4f; }
/* category tiles get the same treatment: image is the decoration, the text
   on the gradient overlay already reads without it, but we still want a
   coloured tile behind the image so it does not look broken */
.cat > img.is-fail { visibility: hidden; }

/* stars */
/* --gold on cream is only 2.1:1 — far too faint for a 14px glyph. --star is 3.4:1. */
.stars { display: inline-flex; gap: 1px; color: var(--star); }
.stars svg { width: 15px; height: 15px; }
.rating { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; font-size: 13px; color: var(--ink-3); font-weight: 600; }
@media (max-width: 420px) {
  .rating { font-size: 12px; }
  .stars svg { width: 13px; height: 13px; }
}

/* placeholder media */
.ph {
  position: absolute; inset: 0;
  display: grid; place-content: center; gap: 6px; text-align: center;
  background:
    repeating-linear-gradient(45deg, var(--sand) 0 10px, color-mix(in srgb, var(--sand) 88%, var(--ink)) 10px 20px);
  color: var(--ink-2); padding: 12px;
}
.ph b { font-family: var(--font-display); font-size: 14px; }
.ph span { font-size: 12px; color: var(--ink-3); }

/* ---------- 14. Why-us / benefits ---------- */
.why {
  display: grid; gap: var(--s3);
  background: var(--sand);
  border-radius: var(--r-lg);
  padding: var(--s5) var(--s4);
}
@media (min-width: 760px) { .why { grid-template-columns: repeat(3, 1fr); padding: var(--s6); gap: var(--s5); } }
.why__i { display: grid; gap: 6px; }
.why__n {
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--surface); color: var(--gold);
  display: grid; place-items: center;
  font-family: var(--font-display); font-size: 15px; font-weight: 800;
}
.why__i h3 { font-size: 17.5px; }
.why__i p { color: var(--ink-2); font-size: 15px; }

/* ---------- 15. Reviews ---------- */
.revs { display: grid; gap: var(--s4); }
@media (min-width: 760px) { .revs { grid-template-columns: repeat(3, 1fr); } }
/* A phone gets a rail instead of a column of six: one card per screen with the
   next one peeking in, snapping card by card, bleeding to the screen edge — the
   same sideways gesture the flash rail above already taught. The track is
   scrolled natively rather than through the .rail drag code, because on touch
   the browser's own pan is the path that carries momentum; `proximity` (not
   mandatory) leaves a deliberate rest between two cards alone instead of
   teleporting it onto a boundary. overflow-y has to be named — overflow-x: auto
   computes it to auto by itself, and the cards' shadows then give the rail a
   vertical scroll range — and the block padding is the room those shadows need
   in order not to be cut. */
@media (max-width: 759px) {
  .revs {
    display: flex; gap: var(--s3);
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x proximity;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    padding-block: 6px 20px;
    /* Negative margin + matching padding, so the cards can reach the screen
       edge while the first one still lines up with the page gutter. */
    margin-inline: calc(var(--s4) * -1); padding-inline: var(--s4);
    scroll-padding-left: var(--s4);
  }
  .revs::-webkit-scrollbar { display: none; }
  .revs > .rev {
    flex: 0 0 82%; scroll-snap-align: start;
    /* Uniform cards: the quote row takes the slack, so every byline lands on the
       same line across the rail instead of floating mid-card. */
    grid-template-rows: auto 1fr auto;
    /* The rail reveals its cards by being pushed, so waiting for a scroll
       intersection here would leave the peeking card — the one thing that says
       "there is more to the right" — invisible until it is dragged in. */
    opacity: 1; transform: none; transition: none;
  }
}
/* Panels are bounded by a shadow, not a hairline (see the --sh-rest note above
   for why a 1px tan line reads as tape). But reviews and FAQ sit in long stacked
   grids, where the full --sh-rest three-layer lift reads as heavy — so these use
   a lighter, shallower shadow and recede instead of fighting each other. Still a
   real shadow, just thinner than the product cards. */
.rev {
  background: var(--paper);
  border-radius: var(--r); padding: var(--s4);
  display: grid; gap: 10px;
  box-shadow: 0 1px 2px rgba(58, 36, 12, .06), 0 2px 5px rgba(58, 36, 12, .05);
  /* This is a <figure> and the quote inside it is a <blockquote>, and the
     browser's own stylesheet gives each of them `margin: 1em 40px` — which
     nothing here had reset. So the card sat 80px narrower than its column and
     the text lost another 80px inside it: on a phone the reading column came
     out 166px, about half the screen, with the stars and the byline floating
     outside it. Both margins have to go. */
  margin: 0;
}
.rev blockquote { margin: 0; }
.rev p { color: var(--ink-2); font-size: 15px; }
.rev__who { display: flex; align-items: center; gap: var(--s3); }
.avatar {
  width: 36px; height: 36px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  font-family: var(--font-display); font-weight: 800; color: #fff;
  font-size: 14px;
}
.rev__who b { display: block; font-size: 15px; }
.rev__who span { font-size: 12.5px; color: var(--ink-3); }

/* ---------- 16. FAQ accordion ---------- */
.faq { display: grid; gap: var(--s2); max-width: 780px; }
/* Same treatment as .rev and .card — and because the hairline is gone, the
   shadow has to be here: these are separate panels stacked with a gap, not a
   list divided by rules. */
.faq__i {
  background: var(--paper);
  border-radius: var(--r); overflow: hidden;
  /* Lighter than --sh-rest: FAQ items stack tightly, so the full lift looks
     heavy. Shares .rev's thinner shadow. */
  box-shadow: 0 1px 2px rgba(58, 36, 12, .06), 0 2px 5px rgba(58, 36, 12, .05);
}
.faq__q {
  width: 100%; text-align: left;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  /* 12px + a 15.5px line still clears 44px of tap target — the minimum is kept,
     the padding around it is not. */
  padding: var(--s3) var(--s4);
  font-family: var(--font-display); font-weight: 700; font-size: 15.5px;
}
.faq__q:hover { background: var(--sand); }
.faq__q svg { flex: none; width: 20px; height: 20px; transition: transform 240ms var(--ease); color: var(--brand); }
.faq__i.is-open .faq__q svg { transform: rotate(45deg); }
.faq__a { padding: 0 var(--s4); color: var(--ink-2); font-size: 15px; max-height: 0; overflow: hidden; transition: max-height 280ms var(--ease), padding 280ms var(--ease); }
.faq__i.is-open .faq__a { max-height: 420px; padding: 0 var(--s4) var(--s4); }

/* ---------- 17. CTA band ---------- */
.band {
  background: var(--brand); color: var(--on-brand);
  border-radius: var(--r-lg);
  padding: var(--s6) var(--s4);
  display: grid; gap: var(--s4); justify-items: start;
  position: relative; overflow: hidden;
}
.band::after {
  content: ""; position: absolute; right: -60px; top: -60px;
  width: 260px; height: 260px; border-radius: 50%;
  background: rgba(255,255,255,.09);
}
@media (min-width: 760px) {
  .band { grid-template-columns: 1fr auto; align-items: center; padding: var(--s6); }
}
.band h2 { font-size: clamp(24px, 5.6vw, 36px); color: #fff; }
.band p { max-width: 44ch; }
.btn--onband { --bg: #fff; --fg: #a8250f; }
.btn--onband:hover { box-shadow: 0 8px 24px rgba(0,0,0,.22); }

/* ---------- 18. Footer ---------- */
.footer {
  background: var(--surface);
  color: #d6c4ad;
  margin-top: var(--s8);
  padding: var(--s7) 0 var(--s5);
  font-size: 15px;
}
.footer h4 { color: var(--on-surface); font-size: 15px; margin-bottom: var(--s3); }
.footer a { color: #d6c4ad; }
.footer a:hover { color: var(--on-surface); }
.footer__grid { display: grid; gap: var(--s6); }
@media (min-width: 700px) { .footer__grid { grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: var(--s7); } }
.footer__grid li + li { margin-top: 8px; }
.footer__bottom {
  margin-top: var(--s6); padding-top: var(--s4);
  border-top: 1px solid rgba(255,255,255,.12);
  display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: space-between;
  font-size: 13px;
}
.footer .logo__text { color: var(--on-surface); }
.socials { display: flex; gap: 8px; margin-top: var(--s4); }
.socials a {
  width: 40px; height: 40px; border-radius: 50%;
  background: rgba(255,255,255,.09);
  display: grid; place-items: center; color: #fff;
  transition: background-color var(--t) var(--ease), transform var(--t) var(--ease);
}
.socials a:hover { background: var(--brand); color: var(--on-brand); transform: translateY(-2px); }

/* ---------- 19. Floating widgets ---------- */
.wa-fab {
  position: fixed; right: 16px; bottom: 82px; z-index: 70;
  width: 54px; height: 54px; border-radius: 50%;
  background: #25d366; color: #fff;
  display: grid; place-items: center;
  box-shadow: 0 8px 22px rgba(37, 211, 102, .42);
  transition: transform 200ms var(--ease);
}
.wa-fab:hover { transform: scale(1.07); }
.wa-fab:active { transform: scale(.94); }
@media (min-width: 900px) { .wa-fab { bottom: 24px; } }

/* sticky mobile CTA */
.stickybar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 65;
  background: color-mix(in srgb, var(--paper) 94%, transparent);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
  padding: 10px var(--s4) calc(10px + env(safe-area-inset-bottom));
  display: flex; gap: var(--s3); align-items: center;
  transform: translateY(110%);
  transition: transform 280ms var(--ease);
}
.stickybar.is-on { transform: none; }
@media (min-width: 900px) { .stickybar { display: none; } }
.stickybar .btn { flex: 1; }
.stickybar__p { display: grid; line-height: 1.2; flex: none; }
.stickybar__p b { font-family: var(--font-display); font-size: 19px; }
.stickybar__p span { font-size: 12px; color: var(--ink-3); }

/* toast */
.toast {
  position: fixed; left: 50%; bottom: 96px; z-index: 80;
  transform: translate(-50%, 20px);
  background: var(--surface); color: var(--on-surface);
  padding: 12px 18px; border-radius: var(--r-pill);
  font-size: 15px; font-weight: 600;
  box-shadow: var(--sh-3);
  opacity: 0; pointer-events: none;
  transition: opacity 220ms var(--ease), transform 220ms var(--ease);
  max-width: calc(100vw - 32px); text-align: center;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
@media (min-width: 900px) { .toast { bottom: 28px; } }

/* ---------- 20. Page hero (inner pages) ---------- */
.pagehead {
  padding: var(--s5) 0 var(--s4);
  border-bottom: 1px solid var(--line);
  background: var(--sand);
}
.pagehead h1 { font-size: clamp(28px, 7vw, 44px); }
.crumbs { display: flex; flex-wrap: wrap; gap: 6px; font-size: 13.5px; color: var(--ink-3); margin-bottom: var(--s2); }
.crumbs a:hover { color: var(--brand); }

/* ---------- 21. Shop toolbar / filters ---------- */
.shop__layout { display: grid; gap: var(--s5); padding: var(--s5) 0 var(--s7); }
@media (min-width: 940px) { .shop__layout { grid-template-columns: 240px 1fr; gap: var(--s7); } }
.filters { align-self: start; }
@media (min-width: 940px) { .filters { position: sticky; top: calc(var(--hdr) + 16px); } }
.filters h3 { font-size: 15px; margin-bottom: var(--s3); }
/* Mobile (<940px): the sidebar filter list collapses behind a "筛选" button
   so the product grid starts near the top of the page instead of under a long
   vertical list of category/price/offer options. */
.btn--filters { display: none; }
@media (max-width: 939px) {
  .filters { display: none; }
  .filters.is-open { display: block; margin-bottom: var(--s5); }
  .btn--filters { display: inline-flex; }
}
.filters__group { padding: var(--s3) 0; border-top: 1px solid var(--line); }
.filters__group:first-of-type { border-top: 0; }
.filters__group label {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 0; font-size: 14.5px; cursor: pointer;
}
.filters__group input[type="radio"], .filters__group input[type="checkbox"] {
  width: 18px; height: 18px; accent-color: var(--brand); flex: none;
}
.range { display: flex; gap: 8px; align-items: center; }
.range input {
  width: 100%; height: 44px; padding: 0 12px;
  border: 1.5px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--paper); font-size: 16px;
}
.range input:focus { outline: none; border-color: var(--brand); }
.toolbar {
  display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; justify-content: space-between;
  margin-bottom: var(--s4);
}
.toolbar select {
  height: 42px; padding: 0 34px 0 14px; border-radius: var(--r-pill);
  border: 1.5px solid var(--line-strong); background: var(--paper);
  font-size: 15px; font-weight: 600;
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235c4c3b' stroke-width='2.5' stroke-linecap='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 12px center;
}
.toolbar select:focus { outline: none; border-color: var(--brand); }
.empty { text-align: center; padding: var(--s7) var(--s4); color: var(--ink-3); }
.empty b { display: block; font-family: var(--font-display); font-size: 20px; color: var(--ink); margin-bottom: 6px; }

/* ---------- 22. Product detail ---------- */
.pdp {
  display: grid; gap: var(--s5); padding: var(--s5) 0 var(--s7);
  /* minmax(0,1fr) rather than the implicit auto track — same reasoning as
     .hero__grid: an auto track grows to its item's max-content, and a wide spec
     table (min-width 30rem) then stretched the column past the page gutter. With
     the track pinned, .spec keeps its own width and scrolls its table inside. */
  grid-template-columns: minmax(0, 1fr);
}
.pdp > * { min-width: 0; }
@media (min-width: 900px) { .pdp { grid-template-columns: minmax(0, 1.02fr) minmax(0, .98fr); gap: var(--s7); align-items: start; } }
.gallery { display: grid; gap: var(--s3); }
@media (min-width: 900px) { .gallery { position: sticky; top: calc(var(--hdr) + 16px); } }
.gallery__main {
  position: relative; aspect-ratio: 1/1; border-radius: var(--r-lg);
  overflow: hidden; background: var(--sand); box-shadow: var(--sh-rest);
  /* Swipe owns the horizontal axis; a vertical drag is left to the page. */
  touch-action: pan-y; cursor: grab;
  -webkit-user-select: none; user-select: none;
}
.gallery__main img { width: 100%; height: 100%; object-fit: cover; -webkit-user-drag: none; }
.gallery__main.is-swiping { cursor: grabbing; }
.gallery__main.is-swiping img { will-change: transform; }
/* The incoming photo during a swipe or the autoplay: a second layer stacked over
   the base one, parked off-screen (opacity 0) whenever it is idle. The corner
   .badge keeps z-index 2, so the ribbon stays above both layers. */
.gal__alt {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; pointer-events: none; will-change: transform;
}
.thumbs { display: flex; gap: var(--s2); overflow-x: auto; scrollbar-width: none; }
.thumbs::-webkit-scrollbar { display: none; }
.thumbs button {
  flex: none; width: 62px; height: 62px; border-radius: var(--r-sm);
  overflow: hidden; border: 2px solid transparent; background: var(--sand);
  transition: border-color var(--t) var(--ease);
}
.thumbs button[aria-current="true"] { border-color: var(--brand); }
.thumbs img { width: 100%; height: 100%; object-fit: cover; }

.pdp__info { display: grid; gap: var(--s3); }
.pdp h1 { font-size: clamp(26px, 6.4vw, 38px); }
.pdp__price { display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; }
.pdp__price b { font-family: var(--font-display); font-size: clamp(32px, 8vw, 42px); letter-spacing: -.03em; }
.pdp__price s { color: var(--ink-3); font-size: 17px; }
.pdp__price .chip { align-self: center; }
.opts { display: grid; gap: var(--s3); }
.opts__label { font-size: 13.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); }
.opts__row { display: flex; flex-wrap: wrap; gap: 8px; }
.opt {
  min-height: 44px; padding: 9px 18px; border-radius: var(--r-sm);
  border: 1.5px solid var(--line-strong); background: var(--paper);
  font-size: 14.5px; font-weight: 700;
  transition: border-color var(--t) var(--ease), background-color var(--t) var(--ease), color var(--t) var(--ease);
}
.opt:hover { border-color: var(--ink-3); }
.opt[aria-pressed="true"] { border-color: var(--brand); background: var(--brand-sf); color: var(--brand-dk); }
/* Quantity stepper. The two ends stretch to whatever height the row gives the
   pill, so on the product page the stepper is exactly as tall as the "加入购物车"
   button it sits beside (56px) instead of floating 8px shorter next to it; on
   its own row it keeps its 46px minimum. */
.qty {
  display: inline-flex; align-items: stretch;
  min-height: 46px;
  border: 1.5px solid var(--line-strong); border-radius: var(--r-pill);
  background: var(--paper); overflow: hidden;
}
.qty button {
  display: grid; place-items: center;
  width: 44px; min-height: 46px; padding: 0;
  font-size: 20px; font-weight: 700; line-height: 1; color: var(--ink-2);
  transition: background-color var(--t) var(--ease), color var(--t) var(--ease);
  /* A stepper is tapped in bursts: drop the 300ms double-tap wait, and never let
     a fast second tap select the glyph or pop the iOS text callout. */
  touch-action: manipulation;
  -webkit-user-select: none; user-select: none;
}
.qty button:hover { background: var(--sand); }
.qty button:active { background: var(--brand-sf); color: var(--brand-dk); }
.qty button:disabled { opacity: .4; cursor: not-allowed; }
.qty button:disabled:hover, .qty button:disabled:active { background: none; color: var(--ink-2); }
/* The global :focus-visible ring is drawn outside the button and .qty clips its
   children, so keyboard focus was invisible here. An inset ring is the same
   signal, drawn inside the clip. */
.qty button:focus-visible { outline: none; box-shadow: inset 0 0 0 3px var(--brand); }
.qty span {
  display: grid; place-items: center; min-width: 42px;
  font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums;
}
.pdp__buy { display: grid; gap: var(--s3); }
/* justify-self, not a width: as a grid item the pill was being stretched across
   the whole phone width (measured 366px) with the − 1 + bunched at the left,
   which read as a broken empty capsule. Hug the content instead. */
.pdp__buy .qty { justify-self: start; }
@media (min-width: 520px) {
  .pdp__buy { grid-template-columns: auto 1fr; align-items: center; }
  /* Match the CTA's height in the same row (see .qty above). */
  .pdp__buy .qty { align-self: stretch; }
}
.bullets { display: grid; gap: 8px; margin: var(--s2) 0 0; }
.bullets li { display: flex; gap: 10px; align-items: flex-start; font-size: 15px; color: var(--ink-2); }
.bullets svg { flex: none; width: 18px; height: 18px; margin-top: 3px; color: var(--leaf); }
.deliver {
  background: var(--leaf-sf); border-radius: var(--r);
  padding: var(--s3) var(--s4); display: grid; gap: 5px; font-size: 14px; color: #0a5540;
}
.deliver b { font-family: var(--font-display); }

.tabs { margin-top: var(--s7); }
.tabs__nav {
  display: flex; gap: var(--s2); overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--line);
}
.tabs__nav::-webkit-scrollbar { display: none; }
.tabs__nav button {
  flex: none; padding: 12px 16px; font-weight: 700; font-size: 15.5px; color: var(--ink-3);
  border-bottom: 3px solid transparent; margin-bottom: -1px;
  transition: color var(--t) var(--ease), border-color var(--t) var(--ease);
}
.tabs__nav button[aria-selected="true"] { color: var(--ink); border-color: var(--brand); }
.tabs__panel { padding: var(--s4) 0 0; display: none; color: var(--ink-2); max-width: 74ch; }
.tabs__panel.is-on { display: block; }
.tabs__panel h3 { font-size: 18px; margin-bottom: 8px; color: var(--ink); }
/* Spec sheet: a real table, drawn from the theme tokens so it flips with the
   light/dark palettes like every other surface on the page. The wrapper is the
   scroll container, so a 3–4 column sheet on a phone slides sideways inside its
   own card instead of squeezing every column down to 90px. */
.spec {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line-2); border-radius: var(--r);
  background: var(--paper);
}
.spec__table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 15px; }
.spec__table th, .spec__table td { padding: 10px 14px; text-align: left; vertical-align: top; }
.spec__table thead th {
  background: var(--sand); color: var(--ink);
  font-family: var(--font-display); font-size: 13.5px; font-weight: 700;
  letter-spacing: .02em; white-space: nowrap;
  border-bottom: 2px solid var(--brand);
}
/* Hairlines between columns and between rows, drawn on the cells so the header
   and the body share one grid. */
.spec__table tr > * + * { border-left: 1px solid var(--line-2); }
.spec__table tbody tr + tr > * { border-top: 1px solid var(--line-2); }
/* A two-column sheet is 参数 → 值: the left cell is the label, quietly tinted so
   the eye runs down the values instead of the keys. */
.spec__table tbody th {
  width: 36%; color: var(--ink-3); font-weight: 600;
  background: color-mix(in srgb, var(--sand) 45%, transparent);
}
.spec__table tbody tr:hover > * { background: color-mix(in srgb, var(--brand-sf) 60%, transparent); }
.spec--wide .spec__table { min-width: 30rem; }

/* ---------- 23. Cart ---------- */
.cart__layout { display: grid; gap: var(--s5); padding: var(--s5) 0 var(--s7); }
@media (min-width: 940px) { .cart__layout { grid-template-columns: 1fr 360px; gap: var(--s7); align-items: start; } }
/* A rule between two lines of the same list is a divider, and dividers are
   --line: this was the one place a divider was drawn with --line-card, which is
   heavier than the panel edges it was supposed to separate. */
.line {
  display: grid; grid-template-columns: 72px 1fr; gap: var(--s3);
  padding: var(--s3) 0; border-bottom: 1px solid var(--line);
}
.line__m { width: 72px; height: 72px; border-radius: var(--r-sm); overflow: hidden; background: var(--sand); }
.line__m img { width: 100%; height: 100%; object-fit: cover; }
.line__b { display: grid; gap: 8px; align-content: start; }
.line__b h3 { font-size: 16px; }
.line__r { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; justify-content: space-between; }
.line__del { font-size: 13px; color: var(--ink-3); font-weight: 700; }
.line__del:hover { color: var(--brand); }
.summary {
  background: var(--paper);
  border-radius: var(--r-lg); padding: var(--s4);
  display: grid; gap: var(--s3); box-shadow: var(--sh-rest);
}
@media (min-width: 940px) { .summary { position: sticky; top: calc(var(--hdr) + 16px); } }
.summary__row { display: flex; justify-content: space-between; gap: var(--s4); font-size: 15px; }
.summary__row--total {
  border-top: 1px solid var(--line); padding-top: var(--s3); margin-top: var(--s2);
  font-family: var(--font-display); font-size: 22px; font-weight: 800;
}
/* Coupon field, inline in the summary because that is the number it changes. */
.coupon { display: flex; align-items: center; gap: var(--s2); margin: var(--s3) 0 0; }
.coupon input { flex: 1; min-width: 0; margin: 0; text-transform: uppercase; letter-spacing: .04em; }
.coupon b { font-weight: 800; letter-spacing: .06em; }
.coupon .link { margin-left: auto; }
.coupon__msg { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-3); }
.summary__row--save { color: var(--ok); }
.summary .free { font-size: 13px; color: var(--leaf); font-weight: 700; }

/* ---------- 24. Checkout form ---------- */
.checkout__layout { display: grid; gap: var(--s5); padding: var(--s5) 0 var(--s7); }
@media (min-width: 940px) { .checkout__layout { grid-template-columns: 1fr 380px; gap: var(--s7); align-items: start; } }
.form-card {
  background: var(--paper);
  border-radius: var(--r-lg); padding: var(--s4);
  box-shadow: var(--sh-rest);
}
@media (min-width: 600px) { .form-card { padding: var(--s5); } }
/* 12px between fields, not 16: on a phone the checkout form is six fields long,
   and every step saved here is a step closer to the "提交订单" button. The 48px
   input height is deliberately NOT trimmed — that is the tap target. */
.field { display: grid; gap: 6px; margin-bottom: var(--s3); }
.field label { font-size: 13.5px; font-weight: 700; }
.field label span { color: var(--brand); }
.field input, .field select, .field textarea {
  height: 48px; padding: 0 14px;
  border: 1.5px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--cream);
  font-size: 16px;
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.field textarea { height: auto; min-height: 84px; padding: 12px 14px; resize: vertical; line-height: 1.5; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px var(--brand-sf);
}
.field select { appearance: none; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235c4c3b' stroke-width='2.5' stroke-linecap='round'><path d='M6 9l6 6 6-6'/></svg>"); background-repeat: no-repeat; background-position: right 14px center; }
.field .hint { font-size: 13px; color: var(--ink-3); }
.field .err { font-size: 13px; color: var(--danger); font-weight: 700; display: none; }
.field.is-bad input, .field.is-bad select, .field.is-bad textarea { border-color: var(--danger); }
.field.is-bad .err { display: block; }
.field.is-bad .hint { display: none; }
.two { display: grid; gap: var(--s3); }
@media (min-width: 600px) { .two { grid-template-columns: 1fr 1fr; } }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.payopt {
  display: flex; gap: var(--s3); align-items: flex-start;
  padding: var(--s3) var(--s4); border: 1.5px solid var(--line-strong); border-radius: var(--r);
  cursor: pointer; transition: border-color var(--t) var(--ease), background-color var(--t) var(--ease);
}
.payopt:has(input:checked) { border-color: var(--brand); background: var(--brand-sf); }
.payopt input { margin-top: 3px; width: 18px; height: 18px; accent-color: var(--brand); flex: none; }
.payopt b { display: block; font-family: var(--font-display); }
.payopt span { font-size: 13.5px; color: var(--ink-2); }

/* success */
.success { max-width: 620px; margin: var(--s6) auto; text-align: center; display: grid; gap: var(--s3); justify-items: center; }
.success__tick {
  width: 78px; height: 78px; border-radius: 50%;
  background: var(--leaf-sf); color: var(--leaf);
  display: grid; place-items: center;
  animation: pop .5s var(--ease-out-back);
}
@keyframes pop { from { transform: scale(.4); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.order-no {
  font-family: var(--font-display); font-weight: 800; font-size: 20px;
  background: var(--sand); padding: 10px 20px; border-radius: var(--r-pill);
}

/* ---------- 25. Prose (policy pages) ---------- */
.prose { max-width: 760px; padding: var(--s5) 0 var(--s8); }
.prose h2 { font-size: clamp(21px, 4.6vw, 27px); margin: var(--s5) 0 var(--s2); }
.prose h3 { font-size: 18px; margin: var(--s4) 0 var(--s2); }
.prose p, .prose li { color: var(--ink-2); margin-bottom: var(--s3); }
.prose ul { padding-left: 20px; list-style: disc; }
.prose ul li { margin-bottom: 8px; }
.prose .updated { font-size: 13px; color: var(--ink-3); }

/* ---------- 26. Scroll reveal ----------
   :where() drops both rules to ZERO specificity on purpose — this is a default
   layer, not a rule that components have to fight.
   At (0,2,0) `.reveal.is-in` tied with `.card:hover` and `.cat:hover` (also
   (0,2,0)) and, being later in the file, won every one of those ties. The result
   was invisible unless you went looking: every revealed card and tile took its
   hover shadow but stayed at `transform: none`, so the lift the hover was
   written for never happened, and `.reveal`'s two-property transition list
   replaced the component's own — which left box-shadow and border-color with no
   transition at all, so the shadow snapped on instead of easing in.
   With the entrance at zero, a component that declares its own transform or
   transition always wins, and the entrance still applies to everything that
   declares neither (sections, reviews, tiles) exactly as before. The cost is
   that a card fades in over its own 300ms rather than the 520ms below — which is
   why `opacity` had to be added to those components' transition lists. */
:where(.reveal) { opacity: 0; transform: translateY(18px); transition: opacity 520ms var(--ease), transform 520ms var(--ease); }
:where(.reveal.is-in) { opacity: 1; transform: none; }
/* Left at (0,1,0): it still has to beat the two :where() rules above (it does)
   and it may not beat a component's hover, so a card still lifts under reduced
   motion — just instantly. */
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }

/* ---------- 28. Utility additions ---------- */
.muted { color: var(--ink-3); }
.note-sm { font-size: 14px; color: var(--ink-3); text-align: center; }
.lede-sm { color: var(--ink-2); font-size: 16.5px; }
.pdp__full { grid-column: 1 / -1; }

/* free-delivery meter — width is set through CSSOM, never an inline attribute */
.meter { height: 6px; border-radius: var(--r-pill); background: var(--sand); overflow: hidden; }
.meter i { display: block; height: 100%; width: 0; border-radius: var(--r-pill); background: var(--leaf); transition: width 420ms var(--ease); }

/* spacing helpers */
.u-mb { margin-bottom: 12px; }
.u-mt { margin-top: 24px; }
.row--wrap { flex-wrap: wrap; gap: 12px; }
.row--chips { flex-wrap: wrap; gap: 16px; margin-top: 20px; }
.eyebrow--gold { color: var(--gold); }
.blurb { margin-top: 14px; max-width: 34ch; }
.hours { margin-top: 8px; opacity: .85; }

/* Grid/rail children must be allowed to shrink below their content width.
   Long unbreakable strings (prices, badges, chips) can otherwise force a
   two-up card wider than the viewport on very narrow phones. */
.grid-products > *, .cats > * { min-width: 0; }
.card__title { overflow-wrap: anywhere; }

/* ---------- 26b. Mobile reading pass ----------
   Narrow screens only. Lifts the actual reading copy a notch with a looser
   line height, trims runaway section whitespace so content isn't buried in
   scrolling, and grows every interactive hit area to ≥44px for fat fingers.
   Kept at the end of the file so these win the cascade on phones. */
@media (max-width: 700px) {
  /* main reading copy: lede, how-it-works, reviews, faq answers, prose,
     product tabs/bullets and section intros all get a comfortable size */
  .lede, .why__i p, .rev p, .faq__a, .tabs__panel, .prose p, .prose li,
  .bullets li, .deliver, .section__head p, .lede-sm, .blurb {
    font-size: 15.5px;
  }
  .rev p, .faq__a, .prose p, .why__i p, .bullets li { line-height: 1.7; }

  /* Reviews on a phone: the rail above already gives the card the whole screen;
     this just hands the text a few more px (padding 20 → 16) so a quote lands in
     three lines instead of five. */
  .rev { padding: var(--s4); }

  /* Spec table: same information, tighter cells. The wide (3+ column) variant
     already scrolls sideways inside .spec, so nothing has to be hidden. */
  .spec__table { font-size: 14.5px; }
  .spec__table th, .spec__table td { padding: 8px 11px; }

  /* stop the page turning into a scrollathon: sections breathe less,
    the rails still run edge-to-edge but get their own pull-back */
  .section { padding: var(--s5) 0; }
  .section--tight { padding: var(--s4) 0; }

  /* Category tiles: the caption sits closer to the edge and the tile itself is
    now 4/3 at every width (see .cat), so a row no longer eats a third of the
    first screen. */
  .cat { padding: var(--s3); }

  /* Trim the mobile gutters and vertical rhythm a little further: the side
     padding, the hero, the trust strip, the inner-page header and the shop grid
     each give back a few px, so more of the page fits on one screen. */
  .wrap { padding-inline: var(--s3); }
  .hero { padding: var(--s5) 0 var(--s6); }
  .trust { padding: var(--s4) 0; }
  .pagehead { padding: var(--s4) 0 var(--s3); }
  /* padding-block only: the .wrap gutter must survive, otherwise this shorthand
     zeroes it and the product grid ends up flush to the screen while the page
     title beside it is inset. */
  .shop__layout { padding-block: var(--s4) var(--s7); }

  /* fat-finger friendly controls */
  .btn--sm { min-height: 44px; }
  .catrail a { padding: 5px 13px; font-size: 13px; }
  .catrail__in { padding-block: 6px; }
  .tabs__nav button { padding-block: 13px; }
  .drawer a { padding-block: 14px; }
  .line__del { padding: 6px 4px; }
  .chip { font-size: 13.5px; padding-block: 6px; }
  .card__quick { width: 40px; height: 40px; }
  .qty button { width: 46px; }
  .toolbar { margin-bottom: var(--s4); }
  .toolbar select { height: 44px; }
  /* The trust chip repeats the top announcement and is exactly what shoves the
     sort select and the 筛选 button onto a second row; drop it on phones so the
     toolbar stays one line and the grid starts sooner. */
  .toolbar .chip { display: none; }
  .filters__group label { padding-block: 9px; font-size: 15px; }

  /* footer links are tapped a lot at the bottom of a long scroll */
  .footer__grid li a { display: inline-block; padding: 4px 0; }
  .footer__grid li + li { margin-top: 6px; }
}

/* ---------- 29. Stock / sold-out ---------- */
.card--out .card__media img { filter: grayscale(.55); opacity: .82; }
.line--out { opacity: .72; }
.chip--out { background: var(--ink-3); color: #fff; }
.card__stock { font-size: 13px; font-weight: 700; margin-top: 4px; color: var(--ink-3); }
.card__stock--low { color: var(--danger); }
.btn:disabled, .btn[disabled] { opacity: .55; cursor: not-allowed; box-shadow: none; }

/* ---------- 30. Order lookup (我的订单) ---------- */
.orders__layout { display: grid; gap: var(--s5); padding: var(--s5) 0 var(--s7); }
@media (min-width: 940px) { .orders__layout { grid-template-columns: 400px 1fr; gap: var(--s7); align-items: start; } }
.orders__layout > .form-card { margin: 0; }
.ocard {
  background: var(--paper);
  border-radius: var(--r-lg); padding: var(--s4); display: grid; gap: var(--s3);
  box-shadow: var(--sh-rest);
}
.ocard__top { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.ocard__top .order-no { font-size: 18px; }
.otrack { display: flex; gap: 6px; flex-wrap: wrap; }
.otrack span {
  flex: 1; min-width: 74px; text-align: center;
  padding: 8px 6px; border-radius: var(--r-sm);
  background: var(--sand); color: var(--ink-3);
  font-size: 13.5px; font-weight: 700;
}
.otrack span.is-on { background: var(--leaf-sf); color: var(--leaf); }
.otrack--returned span.is-on { background: var(--brand-sf); color: var(--brand-dk); }
/* Cancelled is deliberately quieter than returned: it is the common, cheap
   ending, and dressing it up like a failure would overstate it. */
.otrack--cancelled span.is-on { background: #eceef2; color: #5d6876; }
.ocard__note { margin: 0; color: var(--ink-3); font-size: 13.5px; }
.ometa { display: grid; gap: 8px; margin: 0; }
.ometa > div { display: flex; justify-content: space-between; gap: var(--s3); border-bottom: 1px solid var(--line); padding-bottom: 6px; }
.ometa dt { color: var(--ink-3); font-size: 13.5px; }
.ometa dd { margin: 0; font-weight: 700; text-align: right; }
.oitems { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.oitems li { display: flex; justify-content: space-between; gap: var(--s3); font-size: 14.5px; }
.oitems li em { color: var(--ink-3); font-style: normal; font-size: 13px; }
.orders__layout .err { min-height: 18px; color: var(--danger); font-weight: 700; font-size: 13px; }

/* order-status pill colours */
.pill { display: inline-block; padding: 3px 10px; border-radius: var(--r-pill); font-size: 12.5px; font-weight: 800; }
.st-pending { background: #fff3d6; color: #8a5e00; }
.st-confirmed { background: #e5f0f9; color: #1668a8; }
.st-shipped { background: #e8e4fb; color: #5a3fa0; }
.st-delivered { background: var(--leaf-sf); color: var(--leaf); }
.st-returned { background: var(--brand-sf); color: var(--brand-dk); }
.st-cancelled { background: #eceef2; color: #5d6876; }

/* ---------- 27. Print ---------- */
@media print {
  .header, .catrail, .stickybar, .wa-fab, .toast, .footer, .announce { display: none !important; }
  body { background: #fff; }
}
