/* ============================================================================
   Crown Deco QA — prototype application styles

   Consumes tokens.css. No colour, spacing, radius or
   duration literal should appear in this file — everything comes from a token,
   which is what makes the whole brand re-themeable from tokens.json.

   RTL is handled entirely with CSS logical properties. There is no
   [dir="rtl"] override block and there must never be one.

   ANIMATION CONTRACT
   Every transition and animation in this file is inside
   @media (prefers-reduced-motion: no-preference), or is neutralised by the
   global reduce block at the bottom. Motion is opt-in, never assumed.
   ========================================================================= */

/* ---------------------------------------------------------------- the reset
   tokens.css sets typographic and colour defaults but deliberately does not
   include a box model reset, and a plain HTML page gets no reset for free.
   Without this, `.container { width:100%; padding-inline: var(--gutter) }`
   overflows the viewport by exactly the gutter and the body scrolls sideways.
   ------------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

/* A body must never scroll horizontally. `clip` rather than `hidden` so this
   cannot silently create a scroll container that breaks position:sticky. */
html, body { overflow-x: clip; max-inline-size: 100%; }

img, svg, video { max-inline-size: 100%; block-size: auto; }

/* ---------------------------------------------------------------- keyframes */
@keyframes cd-fade-up      { from { opacity:0; transform: translateY(18px); } to { opacity:1; transform:none; } }
@keyframes cd-fade-in      { from { opacity:0; } to { opacity:1; } }
@keyframes cd-scale-in     { from { opacity:0; transform: scale(.94); } to { opacity:1; transform:none; } }
@keyframes cd-float        { 0%,100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-14px) rotate(2deg); } }
@keyframes cd-float-slow   { 0%,100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-22px) rotate(-3deg); } }
@keyframes cd-shimmer      { from { background-position: -200% 0; } to { background-position: 200% 0; } }
@keyframes cd-pop          { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }
@keyframes cd-spin         { to { transform: rotate(360deg); } }
@keyframes cd-toast-in     { from { opacity:0; transform: translateY(14px) scale(.97); } to { opacity:1; transform:none; } }
@keyframes cd-drawer-in    { from { transform: translateX(100%); } to { transform:none; } }
@keyframes cd-sheen        { from { transform: translateX(-120%) skewX(-18deg); } to { transform: translateX(320%) skewX(-18deg); } }
@keyframes cd-check        { from { stroke-dashoffset: 32; } to { stroke-dashoffset: 0; } }
@keyframes cd-ring         { 0% { box-shadow: 0 0 0 0 var(--c-primary-500); opacity:.6; } 100% { box-shadow: 0 0 0 18px var(--c-primary-500); opacity:0; } }
@keyframes cd-pattern-drift { to { background-position: 56px 32px, -56px 32px, 28px 16px; } }

/* ------------------------------------------------------------------- layout */
.container { width:100%; max-width: var(--container-xl); margin-inline:auto; padding-inline: var(--gutter); }
.stack > * + * { margin-block-start: var(--s-4); }
.row { display:flex; align-items:center; gap: var(--s-3); }
.row-wrap { flex-wrap: wrap; }
.push { margin-inline-start:auto; }
.center { text-align:center; }
.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;
}

/* Skip link — first tab stop, per WCAG 2.4.1 */
.skip {
  position:absolute; inset-block-start:-100%; inset-inline-start: var(--s-4);
  z-index: var(--z-toast); background: var(--c-primary-600); color: var(--c-on-primary);
  padding: var(--s-3) var(--s-5); border-radius: 0 0 var(--r-md) var(--r-md); font-weight:600;
}
.skip:focus { inset-block-start:0; }

/* ------------------------------------------------------------------- header */
.hdr {
  position:sticky; inset-block-start:0; z-index: var(--z-header);
  background: color-mix(in srgb, var(--c-base-000) 86%, transparent);
  backdrop-filter: blur(14px) saturate(1.4);
  border-block-end:1px solid var(--c-border-200);
}
.hdr-in {
  display:flex; align-items:center; gap: var(--s-4);
  min-height: var(--header-h);
  transition: min-height var(--d-base) var(--e-standard);
}
.hdr.is-scrolled { box-shadow: var(--shadow-md); }
.hdr.is-scrolled .hdr-in { min-height: 56px; }
.hdr.is-scrolled .brand-sub { opacity:0; max-height:0; }

.brand { display:flex; align-items:center; gap: var(--s-3); text-decoration:none; color:inherit; flex:0 0 auto; }
.crest {
  /* Up from 40px. At that size an uploaded wordmark — which is what most
     brands supply, not a single glyph — was reduced to something you had to
     lean in to read. */
  width:48px; height:48px; border-radius: var(--r-md); flex:0 0 auto;
  background: linear-gradient(145deg, var(--c-primary-600), var(--c-primary-700));
  border:1px solid var(--c-accent-500);
  display:grid; place-items:center;
  color: var(--c-accent-500); font-family: var(--font-display); font-weight:700;
  position:relative; overflow:hidden;
}
/* Gold sheen sweeping the crest — the one deliberately decorative flourish. */
.crest::after {
  content:''; position:absolute; inset-block:0; inline-size:40%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent);
}

/* ---- a real logo has been uploaded ---------------------------------------
   Everything above exists to make the FALLBACK — a single letter — look
   deliberate: a coloured tile, a gold rule, a sweep of light across it. Behind
   an actual logo all three are competing decoration, and the effect is a small
   picture sitting in a frame rather than a brand mark.

   So when branding.js finds a logo it adds `has-logo`, and the tile gets out of
   the way: no gradient, no border, no sheen, and the image is allowed to fill
   the box. The mark is then as large as the space allows instead of being
   inset inside a smaller one.
   ------------------------------------------------------------------------- */
.crest.has-logo {
  /* No tile, no rule, no sweep, no rounded corner — nothing but the logo.
     Every one of those exists to give the fallback LETTER a shape of its own;
     around a real mark they read as a frame somebody put there by mistake. */
  background: none;
  border: 0;
  border-radius: 0;
  overflow: visible;

  /* AUTO width. The logo is an <img> child (see branding.js) so it carries its
     own aspect ratio: a landscape wordmark comes out wide, a round emblem comes
     out square, and neither is squeezed into a box sized for the other. Only
     the height is fixed, which is what keeps the header bar a consistent
     height whatever a brand uploads. */
  width: auto;
  height: 46px;
  /* A ceiling, because nothing stops somebody uploading a 3000×200 banner and
     pushing the navigation off the screen. */
  max-width: 200px;
  display: block;
}
.crest.has-logo::after { display:none; }

.crest.has-logo img {
  height: 100%;
  width: auto;
  max-width: 100%;
  /* contain rather than cover: a logo must never be cropped, even at the cost
     of empty space beside it. */
  object-fit: contain;
  display: block;
}

/* The header shrinks on scroll; the mark follows rather than staying oversized
   against a shorter bar. */
.hdr.is-scrolled .crest { width:40px; height:40px; }
.hdr.is-scrolled .crest.has-logo { width:auto; height:36px; }

/* The footer is not a compressed bar, so the mark can be bigger there — it is
   the last thing on the page and the one worth remembering. */
.ftr-brand .crest { width:56px; height:56px; }
/* Up from 58px. The footer has no bar height to respect and the brand column
   is the widest one in the grid, so the mark can be the size it deserves here
   — this is the last thing on the page and the one worth remembering.
   max-width raised in step, otherwise a wide wordmark hits the ceiling and
   stops getting taller. */
.ftr-brand .crest.has-logo { width:auto; height:84px; max-width:320px; }

@media (max-width: 560px) {
  /* On a phone the mark competes with the search field and the basket for a
     narrow bar, so it gives way first. */
  .crest { width:42px; height:42px; }
  .crest.has-logo { height:38px; max-width:150px; }
  /* The footer mark stays generous on a phone too — it is not competing with
     anything down here — but capped so a landscape wordmark cannot run past
     the gutters. */
  .ftr-brand .crest.has-logo { height:64px; max-width:calc(100vw - (var(--gutter) * 4)); }
}
.brand-name { font-family: var(--font-display); font-size:1.0625rem; line-height:1.1; display:block; }
.brand-sub {
  font-size: var(--fs-caption); color: var(--c-ink-600); display:block;
  overflow:hidden; transition: opacity var(--d-base) var(--e-standard), max-height var(--d-base) var(--e-standard);
  /* One line, clipped. This exists so the header can collapse the tagline to
     zero height on scroll — it is an animation constraint, not a design one. */
  max-height:1.5em;
}
/* The footer is not a bar that shrinks, so the constraint above has no purpose
   there and would clip a tagline that wraps — which the Arabic one does at this
   width. */
.ftr-brand .brand-sub { max-height:none; overflow:visible; transition:none; }

/* flex:0 0 auto so the nav keeps its natural width. Without it the nav is a
   flex item that will happily shrink below its content, which is what forced
   "Stage design" and "Track order" onto two lines each — the search field was
   taking the space instead. */
.nav { display:flex; gap: var(--s-1); flex: 0 0 auto; }
.nav a {
  padding: var(--s-2) var(--s-3); border-radius: var(--r-md);
  color: var(--c-ink-700); text-decoration:none; font-size: var(--fs-small); font-weight:500;
  position:relative; min-height: var(--touch-min); display:inline-flex; align-items:center;
  /* A menu item is a label, not prose. Two words on two lines reads as two
     separate links and makes the whole bar taller. */
  white-space: nowrap;
}
.nav a::after {
  content:''; position:absolute; inset-block-end:6px; inset-inline:var(--s-3);
  block-size:2px; background: var(--c-primary-600); border-radius:2px;
  scale: 0 1; transform-origin: center;
  transition: scale var(--d-base) var(--e-spring);
}
.nav a:hover::after, .nav a[aria-current="page"]::after { scale: 1 1; }
.nav a[aria-current="page"] { color: var(--c-primary-600); font-weight:600; }

/* The search gives way before the navigation does.
   `flex: 1 1 180px` with a smaller basis and a lower ceiling: it still grows
   into whatever room is left on a wide screen, but on a laptop it shrinks
   first rather than squeezing the menu labels. */
.search {
  flex: 1 1 180px; max-inline-size: 300px; min-inline-size: 0; position:relative;
}
.search input {
  inline-size:100%; font:inherit; font-size: var(--fs-small);
  padding-block: var(--s-3); padding-inline: var(--s-10) var(--s-4);
  min-height: var(--touch-min);
  background: var(--c-surface-200); color: var(--c-ink-900);
  border:1px solid transparent; border-radius: var(--r-full);
  transition: background var(--d-fast) var(--e-standard), border-color var(--d-fast), box-shadow var(--d-fast);
}
.search input:focus {
  background: var(--c-surface-100); border-color: var(--c-primary-300);
  box-shadow: 0 0 0 4px var(--c-primary-100);
}
.search .icon {
  position:absolute; inset-block-start:50%; inset-inline-start: var(--s-4);
  translate: 0 -50%; color: var(--c-ink-400); pointer-events:none; font-size:.95rem;
}

.seg { display:inline-flex; border:1px solid var(--c-border-300); border-radius: var(--r-full); overflow:hidden; background: var(--c-surface-100); flex:0 0 auto; }
.seg button {
  appearance:none; border:0; background:transparent; color: var(--c-ink-600);
  font:inherit; font-size: var(--fs-caption); font-weight:600;
  padding: var(--s-2) var(--s-3); min-height: var(--touch-min); cursor:pointer;
  transition: background var(--d-fast) var(--e-standard), color var(--d-fast);
}
.seg button[aria-pressed="true"] { background: var(--c-primary-600); color: var(--c-on-primary); }

.cart-btn {
  position:relative; appearance:none; cursor:pointer;
  background: var(--c-primary-600); color: var(--c-on-primary);
  border:0; border-radius: var(--r-full);
  padding: var(--s-2) var(--s-4); min-height: var(--touch-min);
  font:inherit; font-size: var(--fs-small); font-weight:600;
  display:inline-flex; align-items:center; gap: var(--s-2); flex:0 0 auto;
  transition: background var(--d-fast) var(--e-standard), transform var(--d-fast);
}
.cart-btn:hover { background: var(--c-primary-700); }
.cart-btn:active { transform: translateY(1px); }
.cart-count {
  min-inline-size:1.35em; padding-inline:.3em; border-radius: var(--r-full);
  background: var(--c-accent-500); color: var(--c-on-accent);
  font-size: var(--fs-caption); font-weight:700; font-variant-numeric: tabular-nums;
}
.cart-count.is-bumped { animation: cd-pop 420ms var(--e-spring); }

/* Header responsive ladder. Six controls (brand, nav, search, locale, theme,
   cart) do not fit a narrow viewport, so they drop in order of how easily the
   user can reach the same thing elsewhere. The cart and the locale/theme
   toggles never drop — they have no alternative route. */
@media (max-width: 1040px) { .hdr-in { gap: var(--s-3); } .search { flex-basis: 200px; } }
@media (max-width: 900px)  { .search { display: none; } }   /* search is a shortcut; the nav still reaches everything */
@media (max-width: 620px) {
  /* Wrap onto two rows rather than squeezing six controls into one. Squeezing
     overflowed the header by ~47px, and because the body clips horizontal
     overflow that silently cut the cart button off the edge — a clipped
     control is worse than a taller header.
     Row 1: crest, locale, theme, cart.   Row 2: the nav, centred.
     Wrapping also survives the Arabic labels, which are a different width. */
  .hdr-in {
    flex-wrap: wrap;
    gap: var(--s-2);
    row-gap: var(--s-2);
    padding-block: var(--s-2);
    min-height: 0;
  }
  /* Scoped to .hdr, both of them.
     Unscoped, these reached the FOOTER brand too — so on a phone the footer
     showed a logo with no name and no tagline beside it, and the block read as
     a broken image rather than a signature. Only the header bar is short of
     room; the footer has a column to itself. */
  .hdr .brand { margin-inline-end: auto; }
  .hdr .brand-sub, .hdr .brand-name { display: none; }
  .nav {
    order: 3;
    /* flex-basis:100% plus `flex: 1 1 auto` to override the `0 0 auto` set
       above. That rule stops the nav shrinking on desktop so the labels keep
       one line; here it would pin the row to the labels' full width, and with
       five items the last one ran off the edge — unreachable, because the body
       clips horizontal overflow rather than scrolling. */
    flex: 1 1 auto;
    flex-basis: 100%;
    justify-content: flex-start;
    border-block-start: 1px solid var(--c-border-200);
    padding-block-start: var(--s-2);

    /* Scrolls sideways within its own row. The labels stay on one line each,
       and every item stays reachable however many are added later. */
    overflow-x: auto;
    scrollbar-width: none;
    /* Snaps to a whole item so a swipe never leaves a label half off-screen. */
    scroll-snap-type: inline mandatory;
    /* The gutter, so the first and last items are not flush to the edge. */
    padding-inline: var(--gutter);
    margin-inline: calc(var(--gutter) * -1);
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav a { scroll-snap-align: start; flex: 0 0 auto; }
  .seg button { padding-inline: var(--s-2); }
  .cart-btn { padding-inline: var(--s-3); }
  /* The sticky offsets are derived from the header height, which is now taller. */
  /* .filters is no longer sticky below 900px at all — see the note on the
     base rule. .summary (the cart total) still needs releasing here. */
  .summary { position: static; }
}

/* --------------------------------------------------------------------- hero */
.hero { position:relative; overflow:hidden; padding-block: clamp(5rem,10vw,9rem) var(--s-16); }
.hero::before {
  content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(60ch 40ch at 15% 20%, var(--c-primary-100), transparent 70%),
    radial-gradient(50ch 34ch at 85% 30%, var(--c-accent-100), transparent 70%);
  opacity:.9;
}
.hero-in { position:relative; z-index:1; display:grid; gap: clamp(3rem,7vw,7rem); align-items:center; }

/* ---- cover photograph ------------------------------------------------------
   Set from the Branding screen. Absent by default, in which case the gradient
   above is the whole background and nothing below applies.

   TEXT LEGIBILITY IS THE WHOLE PROBLEM HERE. An admin can upload any
   photograph, including a bright one, and the headline has to stay readable
   over all of them. Three things together do that:

     - a scrim that is opaque where the text is and clear where it is not, so
       the picture is still visible without the words fighting it;
     - the scrim direction flips for RTL, because in Arabic the copy sits on
       the other side;
     - the fallback colour under the image, so the moment before it loads is
       the brand background rather than white.

   The image is NOT `background-attachment: fixed`. That is smooth on a desktop
   and janky-to-broken on iOS Safari, which is most of this audience.
   ------------------------------------------------------------------------- */
.hero.has-cover {
  background-image: var(--hero-cover);
  background-size: cover;
  /* Both set by branding.js from the Branding screen. The fallbacks are what
     applies before the settings arrive and if they never do. */
  background-position: var(--hero-pos, center);
  background-color: var(--c-surface-200);
  --hero-scrim: .75;
}
/* The decorative gradient would sit ON TOP of the photo and wash it out. */
.hero.has-cover::before { display: none; }

/* The scrim.
   Each stop is the base colour at a percentage SCALED BY --hero-scrim, so one
   slider moves the whole ramp and keeps its shape: at 0 the photograph is
   untouched, at 100 it is fully covered, and every value between keeps the
   headline end darker than the far end.

   calc() inside color-mix is why this is a custom property rather than a class
   per strength — an admin wanting 40% should get 40%, not "medium". */
/* --hero-scrim-dir, not `to inline-end`.
 *
 * This rule previously read `linear-gradient(to inline-end, …)`. There is no
 * such thing: linear-gradient accepts an angle or a PHYSICAL side keyword, and
 * logical keywords are not part of the grammar. One invalid token invalidates
 * the whole declaration, so the scrim was simply never painted — the cover
 * photograph rendered at full strength behind the headline and the body copy
 * became unreadable over anything but the plainest image. It failed silently,
 * which is why it looked like the image "not showing properly" rather than a
 * missing overlay.
 *
 * Everything else in this stylesheet uses logical properties and needs no
 * direction override. Gradients are the one genuine exception, so the direction
 * is a custom property flipped once for RTL rather than a duplicated rule. */
:root { --hero-scrim-dir: to right; }
[dir="rtl"] { --hero-scrim-dir: to left; }

.hero.has-cover::after {
  content:''; position:absolute; inset:0; z-index:0; pointer-events:none;
  background: linear-gradient(
    var(--hero-scrim-dir),
    color-mix(in srgb, var(--c-base-000) calc(var(--hero-scrim) * 100%), transparent) 0%,
    color-mix(in srgb, var(--c-base-000) calc(var(--hero-scrim) * 88%), transparent) 45%,
    color-mix(in srgb, var(--c-base-000) calc(var(--hero-scrim) * 34%), transparent) 80%,
    transparent 100%
  );
}
/* On a phone the copy sits across the whole width, so the scrim has to as
   well — the side-weighted version leaves the last lines over bare photo. */
@media (max-width: 899px) {
  .hero.has-cover::after {
    background: linear-gradient(
      to bottom,
      color-mix(in srgb, var(--c-base-000) calc(var(--hero-scrim) * 96%), transparent) 0%,
      color-mix(in srgb, var(--c-base-000) calc(var(--hero-scrim) * 90%), transparent) 60%,
      color-mix(in srgb, var(--c-base-000) calc(var(--hero-scrim) * 62%), transparent) 100%
    );
  }
}
/* The showcase card is a decorative stand-in for imagery. With a real
   photograph behind the section it is redundant and covers the subject. */
.hero.has-cover .hero-showcase { display:none; }
@media (min-width:900px) { .hero.has-cover .hero-in { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); } }

@media (min-width:900px) { .hero-in { grid-template-columns: 1.05fr .95fr; } }

/* Scaled down from clamp(3.4rem,7.2vw,7.4rem)/.9. At the old size the headline
   filled most of a laptop viewport on its own and pushed the buttons and the
   stats row below the fold, so the first thing a visitor saw carried no way to
   act on it. `max-inline-size` stays in `ch` so the line breaks land in the
   same places at the smaller size. */
.hero h1 { font-size: clamp(2.6rem,5.2vw,5.1rem); max-inline-size: 11ch; letter-spacing:-.04em; line-height:.86; margin-block-end: var(--s-5); }
/* `.hl`, not `.line` — `.line` is the cart line-item class further down this
   file, and reusing it here leaked a 1px top border and 16px of padding into
   the headline. Generic class names in a single stylesheet collide. */
.hero h1 .hl { display:block; }
[lang="ar"] .hero h1 {
  /* Arabic glyphs extend farther above and below the Latin em box. The larger
     leading keeps dots and the long lam/alef strokes from being cropped. */
  font-size:clamp(2.5rem,4.9vw,4.75rem);
  /* Deliberately NOT reduced alongside the Latin line-height. Arabic glyphs
     extend well past the Latin em box, and Reem Kufi's ascenders and the
     descender on the final ya' are clipped below roughly 1.1. */
  line-height:1.14;
  letter-spacing:0;
  max-inline-size:12ch;
}
[lang="ar"] .hero h1 .hl {
  /* background-clip:text clips to the element paint box; give the gold line
     enough paint area for Reem Kufi's Arabic ascenders and descenders. */
  padding-block:.06em .12em;
  margin-block:-.06em -.12em;
}
.hero .gold {
  background: linear-gradient(100deg, var(--c-accent-600), var(--c-accent-500) 45%, var(--c-accent-700));
  -webkit-background-clip:text; background-clip:text; color:transparent;
  /* Fallback for engines that ignore background-clip:text — accent-700 is the
     only gold that clears AA for body-weight text on the ivory base. */
  -webkit-text-fill-color:transparent;
}
@supports not ((-webkit-background-clip:text) or (background-clip:text)) {
  .hero .gold { color: var(--c-accent-700); -webkit-text-fill-color: currentColor; }
}
.hero p.lede { font-size: calc(var(--fs-body-lg) * var(--font-scale)); color: var(--c-ink-600); max-inline-size:56ch; }
.hero-copy { position:relative; z-index:2; }
.hero-copy::before {
  content:'CROWN / DECO'; position:absolute; inset-block-start:-3.75rem; inset-inline-start:0;
  color:var(--c-primary-600); font-size:.68rem; font-weight:700; letter-spacing:.28em;
}

/* Floating decorative emoji. aria-hidden in the markup. */
.hero-art { position:relative; min-block-size:320px; }
.hero-art span {
  position:absolute; font-size: clamp(2.5rem, 6vw, 4.5rem);
  filter: drop-shadow(0 8px 18px rgba(26,16,19,.18));
}
.hero-art .a1 { inset-block-start:4%;  inset-inline-start:12%; }
.hero-art .a2 { inset-block-start:34%; inset-inline-end:14%; }
.hero-art .a3 { inset-block-end:12%;   inset-inline-start:26%; }
.hero-art .a4 { inset-block-start:8%;  inset-inline-end:32%; font-size: clamp(1.8rem,4vw,2.8rem); }
.hero-art .a5 { inset-block-end:26%;   inset-inline-end:6%;  font-size: clamp(1.8rem,4vw,2.8rem); }

.hero-showcase {
  container-type:inline-size;
  position:relative; min-block-size:clamp(430px,48vw,600px);
  border-radius:clamp(2rem,4vw,4.5rem);
  /* The three stops were hardcoded maroon, so this panel stayed maroon when the
     brand colour changed — the one element on the page that most obviously
     should follow it. They are now variables that branding.js sets from the
     chosen showcase colour, with the original values as the fallback so the
     page is unchanged when no branding has been saved. */
  background:
    radial-gradient(circle at 88% 8%,rgba(255,255,255,.1),transparent 28%),
    linear-gradient(145deg,
      var(--c-showcase-1,#78102f) 0%,
      var(--c-showcase-2,#4c0c20) 48%,
      var(--c-showcase-3,#270e18) 100%);
  box-shadow:0 42px 90px rgba(47,18,29,.24); overflow:hidden; isolation:isolate;
}
.hero-showcase::before {
  content:''; position:absolute; inset:1px; border-radius:inherit;
  border:1px solid color-mix(in srgb,var(--c-accent-500) 55%,transparent);
  background:
    radial-gradient(circle at 20% 10%,rgba(255,255,255,.08),transparent 27%),
    radial-gradient(circle at 78% 72%,rgba(205,164,86,.06),transparent 32%);
  pointer-events:none;
}
.showcase-atmosphere {
  position:absolute; inset:0; z-index:1; overflow:hidden; pointer-events:none;
}
.showcase-atmosphere::before {
  content:''; position:absolute; inset:-12%;
  background-image:
    linear-gradient(30deg,transparent 49%,rgba(255,255,255,.055) 49.5%,rgba(255,255,255,.055) 50.5%,transparent 51%),
    linear-gradient(150deg,transparent 49%,rgba(205,164,86,.05) 49.5%,rgba(205,164,86,.05) 50.5%,transparent 51%),
    radial-gradient(circle at 50% 50%,rgba(255,255,255,.075) 0 1.2px,transparent 1.5px);
  background-position:0 0,0 0,28px 16px;
  background-size:56px 32px,56px 32px,56px 32px;
  mask-image:linear-gradient(to bottom,rgba(0,0,0,.45),#000 24%,#000 78%,rgba(0,0,0,.5));
  -webkit-mask-image:linear-gradient(to bottom,rgba(0,0,0,.45),#000 24%,#000 78%,rgba(0,0,0,.5));
  opacity:.58;
}
.deco-item {
  position:absolute; display:block; color:rgba(238,204,134,.52);
  filter:drop-shadow(0 5px 9px rgba(15,2,8,.22)); transform-origin:center;
}
.deco-balloon {
  inline-size:22px; block-size:28px;
  border:1.5px solid currentColor; border-radius:52% 52% 48% 48%;
}
.balloon-one { inset-block-start:34%; inset-inline-start:15%; }
.balloon-two { inset-block-start:45%; inset-inline-start:46%; scale:.72; opacity:.7; }
.balloon-three { inset-block-start:29%; inset-inline-end:26%; scale:.86; opacity:.8; }
.balloon-four { inset-block-start:13%; inset-inline-start:12%; scale:.68; opacity:.72; }
.balloon-five { inset-block-start:22%; inset-inline-start:37%; scale:.82; opacity:.76; }
.deco-balloon::before {
  content:''; position:absolute; inset-inline-start:50%; inset-block-end:-6px;
  inline-size:6px; block-size:6px; border-inline-start:1.5px solid currentColor;
  border-block-end:1.5px solid currentColor; transform:translateX(-50%) rotate(-45deg);
}
.deco-balloon::after {
  content:''; position:absolute; inset-inline-start:50%; inset-block-start:calc(100% + 5px);
  inline-size:18px; block-size:29px; border-inline-start:1px solid currentColor;
  border-radius:50%; transform:translateX(-1px) rotate(15deg);
}
.deco-flower {
  inline-size:7px; block-size:7px;
  border-radius:50%; background:var(--c-accent-500);
  box-shadow:
    0 -9px 0 2px rgba(255,255,255,.32),
    9px 0 0 2px rgba(255,255,255,.25),
    0 9px 0 2px rgba(255,255,255,.32),
    -9px 0 0 2px rgba(255,255,255,.25);
}
.flower-one { inset-block-start:43%; inset-inline-end:15%; }
.flower-two { inset-block-start:31%; inset-inline-start:31%; scale:.72; opacity:.72; }
.flower-three { inset-block-start:53%; inset-inline-end:34%; scale:.86; opacity:.82; }
.flower-four { inset-block-start:15%; inset-inline-start:45%; scale:.65; opacity:.72; }
.flower-five { inset-block-start:24%; inset-inline-start:20%; scale:.58; opacity:.66; }
.deco-spark {
  inset-block-start:54%; inset-inline-start:36%; inline-size:18px; aspect-ratio:1;
  background:var(--c-accent-500);
  clip-path:polygon(50% 0,59% 40%,100% 50%,59% 60%,50% 100%,41% 60%,0 50%,41% 40%);
}
.spark-two { inset-block-start:10%; inset-inline-start:31%; scale:.62; opacity:.66; }
.deco-ring {
  inset-block-start:32%; inset-inline-start:50%; inline-size:31px; aspect-ratio:1;
  border:1px solid rgba(255,255,255,.42); border-radius:50%;
  box-shadow:inset 0 0 0 5px rgba(255,255,255,.025);
}
.deco-ribbon {
  inset-block-start:53%; inset-inline-end:10%; inline-size:38px; block-size:23px;
  border-block-start:1.5px solid currentColor; border-radius:50%;
}
.deco-ribbon::after {
  content:''; position:absolute; inset:5px -2px auto 6px; block-size:16px;
  border-block-start:1.5px solid rgba(255,255,255,.38); border-radius:50%;
  transform:rotate(-22deg);
}
.deco-dots {
  inset-block-start:28%; inset-inline-start:31%; inline-size:5px; block-size:5px;
  border-radius:50%; background:rgba(255,255,255,.5);
  box-shadow:15px 8px 0 rgba(205,164,86,.55),28px -3px 0 rgba(255,255,255,.3);
}
.dots-two { inset-block-start:19%; inset-inline-start:53%; scale:.78; opacity:.7; }
.showcase-frame {
  position:absolute; z-index:2; inset:clamp(1rem,4cqi,1.75rem);
  border:1px solid rgba(255,255,255,.08); border-radius:calc(clamp(2rem,4vw,4.5rem) - 1rem);
  pointer-events:none;
}
.showcase-frame::after {
  content:'DOHA  ·  2026'; position:absolute; inset-block-end:1rem; inset-inline-start:1.1rem;
  color:rgba(255,255,255,.58); font-size:.6rem; font-weight:600; letter-spacing:.18em;
}
.showcase-orbit {
  position:absolute; z-index:1; inline-size:66%; aspect-ratio:1; inset-inline-end:-13%; inset-block-start:-9%;
  border:1px solid color-mix(in srgb,var(--c-accent-500) 48%,transparent); border-radius:50%;
}
.showcase-orbit::before,.showcase-orbit::after {
  content:''; position:absolute; border-radius:50%; background:var(--c-accent-500);
  box-shadow:0 0 34px color-mix(in srgb,var(--c-accent-500) 65%,transparent);
}
.showcase-orbit::before { inline-size:13px; block-size:13px; inset-inline-start:12%; inset-block-start:14%; }
.showcase-orbit::after { inline-size:7px; block-size:7px; inset-inline-end:5%; inset-block-end:24%; }
.showcase-card { position:absolute; color:#fff; z-index:3; }
.showcase-card--main { inset-inline:8%; inset-block-end:12%; inline-size:84%; }
.showcase-kicker,.showcase-meta {
  display:block; font-size:clamp(.58rem,2.4cqi,.72rem); line-height:1.5;
  letter-spacing:.18em; text-transform:uppercase; color:rgba(255,255,255,.68);
}
.showcase-title {
  display:block; inline-size:100%; margin-block:clamp(.55rem,2.2cqi,.9rem);
  padding-inline:.035em;
  /* Down from clamp(2.8rem,15cqi,4.65rem)/.94. The card is sized in container
     units, so this shrinks with the card rather than against it. */
  font-family:var(--font-display); font-size:clamp(2rem,11cqi,3.3rem);
  line-height:.9; font-weight:500; letter-spacing:-.022em;
}
.showcase-title span { display:block; white-space:nowrap; }
[lang="ar"] .showcase-title {
  /* Same reasoning as the Arabic hero: size comes down, leading does not. */
  font-size:clamp(1.9rem,10cqi,3.05rem); line-height:1.14;
  letter-spacing:0; padding-block:.04em .1em;
}
.showcase-line {
  display:block; inline-size:clamp(54px,16cqi,76px); block-size:2px;
  margin-block:clamp(1rem,4cqi,1.5rem); background:var(--c-accent-500);
}
.showcase-card--float {
  inset-inline-end:6%; inset-block-start:13%; inline-size:min(194px,43%); padding:clamp(.9rem,4cqi,1.25rem);
  background:linear-gradient(145deg,rgba(255,255,255,.16),rgba(255,255,255,.08));
  backdrop-filter:blur(16px) saturate(1.2);
  border:1px solid rgba(255,255,255,.22); border-radius:clamp(.9rem,4cqi,1.25rem);
  box-shadow:0 18px 34px rgba(24,4,12,.18);
}
.showcase-card--float span { color:var(--c-accent-500); font-size:.66rem; letter-spacing:.18em; }
.showcase-card--float strong,.showcase-card--float small { display:block; }
.showcase-card--float strong {
  margin-block:.65rem .5rem; font-family:var(--font-display);
  font-size:clamp(.95rem,4cqi,1.18rem); line-height:1.18;
}
[lang="ar"] .showcase-card--float strong { line-height:1.45; }
.showcase-card--float small { color:rgba(255,255,255,.66); font-size:clamp(.65rem,2.6cqi,.76rem); line-height:1.5; }
.showcase-chip {
  position:absolute; z-index:4; padding:clamp(.48rem,2.1cqi,.62rem) clamp(.72rem,3.1cqi,1rem);
  border-radius:var(--r-full); background:rgba(255,255,255,.96); color:#72102c;
  border:1px solid rgba(255,255,255,.7); box-shadow:0 10px 24px rgba(24,4,12,.15);
  font-size:clamp(.58rem,2.5cqi,.72rem); line-height:1; font-weight:700;
  letter-spacing:.05em; text-transform:uppercase;
}
.chip-b { inset-inline-end:7%; inset-block-end:8%; }
@media (max-width:899px) {
  .hero { padding-block-start:6rem; }
  .hero-showcase { min-block-size:clamp(420px,132vw,570px); inline-size:min(100%,620px); justify-self:center; }
}
@media (max-width:480px) {
  .hero-showcase { min-block-size:460px; border-radius:2rem; }
  .showcase-frame { inset:.8rem; border-radius:1.35rem; }
  .showcase-card--main { inset-inline:8%; inset-block-end:13%; }
  .showcase-card--float { inset-block-start:14%; }
}

/* ------------------------------------------------------------------- stats */
.stats { display:grid; grid-template-columns: repeat(3,1fr); gap: var(--s-4); margin-block-start: var(--s-8); max-inline-size:520px; }
.stat-n { font-family: var(--font-display); font-size: calc(var(--fs-h2) * var(--font-scale)); line-height:1; font-variant-numeric: tabular-nums; color: var(--c-primary-600); }
.stat-l { font-size: var(--fs-caption); color: var(--c-ink-600); margin-block-start: var(--s-1); }

/* ---------------------------------------------------------------- sections */
.section { padding-block: var(--s-16); }
.section-head { margin-block-end: var(--s-8); }
.eyebrow {
  font-size: var(--fs-caption); font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color: var(--c-accent-700); margin-block-end: var(--s-2);
}
[lang="ar"] .eyebrow { letter-spacing:0; text-transform:none; }
.section-head p { color: var(--c-ink-600); }

.grid { display:grid; gap: var(--s-5); }
.g-cat  { grid-template-columns: repeat(auto-fill, minmax(150px,1fr)); }
.g-prod { grid-template-columns: repeat(auto-fill, minmax(240px,1fr)); }
.g-svc  { grid-template-columns: repeat(auto-fill, minmax(300px,1fr)); }
.g-how  { grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); }

/* ----------------------------------------------------------- category tile */
.cat {
  display:flex; flex-direction:column; align-items:center; gap: var(--s-2);
  padding: var(--s-5) var(--s-3); text-decoration:none; color:inherit;
  background: var(--c-surface-100); border:1px solid var(--c-border-200);
  border-radius: var(--r-lg);
  transition: transform var(--d-base) var(--e-spring), box-shadow var(--d-base), border-color var(--d-base);
}
.cat:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--c-accent-500); }
.cat-emoji {
  font-size:2rem; inline-size:60px; block-size:60px; display:grid; place-items:center;
  border-radius: var(--r-full); background: var(--cat-bg, var(--c-surface-200));
  transition: transform var(--d-base) var(--e-spring);
}
.cat:hover .cat-emoji { transform: scale(1.12) rotate(-6deg); }
.cat-name { font-size: var(--fs-small); font-weight:600; text-align:center; }

/* ------------------------------------------------------------- product card */
.card {
  background: var(--c-surface-100); border:1px solid var(--c-border-200);
  border-radius: var(--r-lg); overflow:hidden; display:flex; flex-direction:column;
  transition: transform var(--d-base) var(--e-standard), box-shadow var(--d-base), border-color var(--d-base);
}
.card:hover { transform: translateY(-5px); box-shadow: var(--shadow-xl); border-color: var(--c-border-300); }
.card-media {
  aspect-ratio: 4/3; position:relative; overflow:hidden;
  display:grid; place-items:center; font-size:3.2rem;
  background:
    radial-gradient(circle at 30% 28%, var(--c-primary-100), transparent 62%),
    radial-gradient(circle at 72% 68%, var(--c-accent-100), transparent 62%),
    var(--c-surface-200);
}
.card-media .emo { transition: transform var(--d-slow) var(--e-spring); }
.card:hover .card-media .emo { transform: scale(1.16) rotate(4deg); }
/* Sheen sweep across the media on hover. */
.card-media::after {
  content:''; position:absolute; inset-block:0; inline-size:45%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
  transform: translateX(-140%) skewX(-18deg); opacity:0;
}
.card-body { padding: var(--s-4); display:flex; flex-direction:column; gap: var(--s-2); flex:1; }
.card-vendor { font-size: var(--fs-caption); color: var(--c-ink-600); }
.card-title {
  font-family: var(--font-sans); font-weight:600; font-size: var(--fs-small);
  line-height:1.45; margin:0; flex:1;
}
.card-title a { color:inherit; text-decoration:none; }
.card-title a:hover { color: var(--c-primary-600); }
.card-foot {
  display:flex; align-items:center; gap: var(--s-3); margin-block-start: var(--s-1);
  /* Reserves the height of the tallest case — a price that has wrapped to two
     lines — so a card with a sale price and one without stand exactly the same
     height and the row of cards keeps a straight baseline. */
  min-block-size: var(--touch-min);
}

/* The action never wraps.
   "Add to cart" broke onto two lines in exactly the cards whose price carried
   a struck-through original — three words, three different card heights in one
   row, and the grid looked broken. The button keeps its single line and the
   price gives way instead: the price can wrap, the button cannot. */
.card-foot > .push { flex: 0 0 auto; }
.card-foot .btn { white-space: nowrap; }
/* min-inline-size:0 lets this actually shrink — a flex item's default
   min-width:auto refuses to go below its content and nothing gives. */
.card-foot > :first-child { min-inline-size: 0; }

.price { font-weight:700; font-size:1.0625rem; }
.price .cur { font-weight:500; font-size:.72em; color: var(--c-ink-600); margin-inline-end:.28em; }
.price del { color: var(--c-ink-400); font-weight:500; font-size:.74em; margin-inline-start:.4em; }
.price-from { font-size: var(--fs-caption); color: var(--c-ink-600); font-weight:500; display:block; }

.badge {
  position:absolute; inset-block-start: var(--s-3); inset-inline-start: var(--s-3);
  font-size: var(--fs-caption); font-weight:700; padding:.28em .7em;
  border-radius: var(--r-full); z-index:1;
}
.badge-bestseller { background: var(--c-success-100); color: var(--c-success-600); }
.badge-new        { background: var(--c-info-100);    color: var(--c-info-600); }
.badge-low_stock  { background: var(--c-warning-100); color: var(--c-warning-600); }

/* ------------------------------------------------------------------ buttons */
.btn {
  appearance:none; font:inherit; font-weight:600; cursor:pointer; text-decoration:none;
  border-radius: var(--r-md); padding: var(--s-3) var(--s-5);
  min-height: var(--touch-min); border:1px solid transparent;
  display:inline-flex; align-items:center; justify-content:center; gap: var(--s-2);
  position:relative; overflow:hidden;
  transition: background var(--d-fast) var(--e-standard), color var(--d-fast),
              border-color var(--d-fast), transform var(--d-fast), box-shadow var(--d-fast);
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--c-primary-600); color: var(--c-on-primary); }
.btn-primary:hover { background: var(--c-primary-700); box-shadow: var(--shadow-md); }
/* Dark theme flips which primary step is the fill — 500 there carries ink text. */
:root[data-theme="dark"] .btn-primary { background: var(--c-primary-500); color: var(--c-on-primary); }
.btn-gold { background: var(--c-accent-500); color: var(--c-on-accent); }
.btn-gold:hover { background: var(--c-accent-300); box-shadow: var(--shadow-md); }
.btn-outline { background:transparent; color: var(--c-primary-600); border-color: var(--c-primary-600); }
.btn-outline:hover { background: var(--c-primary-100); }
.btn-ghost { background:transparent; color: var(--c-ink-700); }
.btn-ghost:hover { background: var(--c-surface-200); }
.btn-sm { padding: var(--s-2) var(--s-4); font-size: var(--fs-small); }
.btn-block { inline-size:100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity:.5; cursor:not-allowed; }
.btn.is-ok { background: var(--c-success-600); color:#fff; }

/* Sheen on primary buttons */
.btn-primary::after, .btn-gold::after {
  content:''; position:absolute; inset-block:0; inline-size:38%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.28), transparent);
  transform: translateX(-140%) skewX(-18deg); opacity:0;
}

/* ------------------------------------------------------------------ service */
.svc {
  display:flex; flex-direction:column; gap: var(--s-3);
  padding: var(--s-6); border-radius: var(--r-xl);
  background: var(--c-surface-100); border:1px solid var(--c-border-200);
  position:relative; overflow:hidden;
  transition: transform var(--d-base) var(--e-standard), box-shadow var(--d-base);
}
.svc:hover { transform: translateY(-5px); box-shadow: var(--shadow-xl); }
.svc::before {
  content:''; position:absolute; inset-block-start:0; inset-inline:0; block-size:3px;
  background: linear-gradient(90deg, var(--c-primary-600), var(--c-accent-500));
  scale: 0 1; transform-origin: left center;
  transition: scale var(--d-slow) var(--e-standard);
}
[dir="rtl"] .svc::before { transform-origin: right center; }
.svc:hover::before { scale: 1 1; }
.svc-emoji { font-size:2.4rem; }
.svc h3 { margin:0; font-size: calc(var(--fs-h3) * var(--font-scale)); }
.svc p { color: var(--c-ink-600); font-size: var(--fs-small); margin:0; }
.svc ul { margin:0; padding-inline-start: var(--s-5); color: var(--c-ink-600); font-size: var(--fs-small); }
.svc ul li + li { margin-block-start: var(--s-1); }

/* ---------------------------------------------------------------- how it works */
.how { text-align:center; padding: var(--s-6); }
.how-n {
  inline-size:52px; block-size:52px; margin-inline:auto; margin-block-end: var(--s-4);
  display:grid; place-items:center; border-radius: var(--r-full);
  background: var(--c-primary-100); color: var(--c-primary-600);
  font-family: var(--font-display); font-size:1.35rem; font-weight:700;
  position:relative;
}
.how-n::after {
  content:''; position:absolute; inset:0; border-radius:inherit;
}
.how h4 { margin-block:0 var(--s-2); }
.how p { color: var(--c-ink-600); font-size: var(--fs-small); margin:0; max-inline-size:34ch; margin-inline:auto; }

/* ------------------------------------------------------------------ catalog */
.catalog { display:grid; gap: var(--s-8); align-items:start; padding-block: var(--s-10); }
@media (min-width:900px) { .catalog { grid-template-columns: 250px 1fr; } }

.filters {
  background: var(--c-surface-100); border:1px solid var(--c-border-200);
  border-radius: var(--r-lg); padding: var(--s-5);
}

/* The summary is the mobile toggle. Hidden from ≥900px, where the panel is a
   permanent sidebar and a "Filters" button would be noise. */
.filters > summary {
  cursor: pointer; font-weight: 600; font-size: var(--fs-small);
  list-style: none; display: flex; align-items: center; gap: var(--s-2);
  min-height: var(--touch-min);
}
.filters > summary::-webkit-details-marker { display: none; }
.filters > summary::after {
  content: '▾'; margin-inline-start: auto; color: var(--c-ink-600);
  transition: rotate var(--d-fast) var(--e-standard);
}
.filters[open] > summary::after { rotate: 180deg; }
.filters[open] > summary { margin-block-end: var(--s-4); padding-block-end: var(--s-3); border-block-end: 1px solid var(--c-border-200); }

/* STICKY ONLY WHERE THERE IS A SIDEBAR.
 *
 * `position: sticky` used to be on the base rule, with a `position: static`
 * override inside the mobile media query EARLIER in this file. Media queries
 * carry no specificity of their own, so the later base rule won and the panel
 * stayed sticky on a phone — where it is ~900px tall, one column wide, and
 * therefore stuck to the top of the viewport covering the products behind it
 * as you scrolled. That is the "filters overlaying the products" report.
 *
 * Declaring it only inside the ≥900px query makes the two states impossible to
 * get the wrong way round: below that width there is no sidebar to stick. */
@media (min-width:900px) {
  .filters {
    position: sticky;
    inset-block-start: calc(var(--header-h) + var(--s-4));
    /* Never taller than the space it is stuck in — otherwise the bottom of a
       long filter list is unreachable, which is what happens once a shop has
       enough vendors. */
    max-block-size: calc(100vh - var(--header-h) - var(--s-8));
    overflow-y: auto;
  }
  /* Always open on desktop: the panel is the sidebar there, and a collapsed
     sidebar with a toggle nobody can see would be unopenable. */
  .filters > summary { display: none; }
  .filters[open] > summary { margin-block-end: 0; padding-block-end: 0; border: 0; }
}
.filter-grp + .filter-grp { margin-block-start: var(--s-6); padding-block-start: var(--s-5); border-block-start:1px solid var(--c-border-200); }
.filter-grp h4 { font-size: var(--fs-small); margin-block-end: var(--s-3); }
.check { display:flex; align-items:center; gap: var(--s-3); padding-block: var(--s-2); cursor:pointer; font-size: var(--fs-small); min-height: var(--touch-min); }
.check input { inline-size:18px; block-size:18px; accent-color: var(--c-primary-600); cursor:pointer; flex:0 0 auto; }
.check span { color: var(--c-ink-700); }
.check small { color: var(--c-ink-400); margin-inline-start:auto; font-variant-numeric: tabular-nums; }
input[type="range"] { inline-size:100%; accent-color: var(--c-primary-600); }

.toolbar { display:flex; align-items:center; gap: var(--s-3); flex-wrap:wrap; margin-block-end: var(--s-5); }
.toolbar select {
  font:inherit; font-size: var(--fs-small); padding: var(--s-2) var(--s-3);
  min-height: var(--touch-min); border-radius: var(--r-md);
  border:1px solid var(--c-border-300); background: var(--c-surface-100); color: var(--c-ink-900);
}
.count { color: var(--c-ink-600); font-size: var(--fs-small); }

.empty { text-align:center; padding: var(--s-16) var(--s-4); }
.empty-emoji { font-size:3rem; margin-block-end: var(--s-4); display:block; }

/* ------------------------------------------------------------------- detail */
.detail { display:grid; gap: var(--s-10); padding-block: var(--s-10); align-items:start; }
@media (min-width:900px) { .detail { grid-template-columns: 1fr 1fr; } }

/* Grid children may shrink below their contents.
 *
 * `min-width: auto` is the default on a grid item, and it means "never narrower
 * than the widest thing inside me". The booking date strip is twenty-one
 * 66px tiles in a flex row — about 1550px — so the column holding it refused to
 * be 600px wide and grew to 1546px inside a 1200px container. Everything past
 * the fold was simply cut off, and `overflow-x: auto` on the strip did nothing
 * at all: a box that got the width it asked for has nothing to scroll.
 *
 * With a floor of zero the column takes its share of the grid and the strip
 * scrolls inside it, which is what it was always written to do. This is why the
 * page broke on every width rather than below some breakpoint — the single
 * column at narrow widths has exactly the same default. */
.detail > * { min-inline-size: 0; }
.detail-media {
  aspect-ratio:1; border-radius: var(--r-xl); display:grid; place-items:center;
  font-size: clamp(5rem, 18vw, 9rem); position:relative; overflow:hidden;
  background:
    radial-gradient(circle at 28% 26%, var(--c-primary-100), transparent 60%),
    radial-gradient(circle at 74% 70%, var(--c-accent-100), transparent 60%),
    var(--c-surface-200);
  border:1px solid var(--c-border-200);
}
.crumbs { font-size: var(--fs-caption); color: var(--c-ink-600); margin-block-end: var(--s-3); display:flex; gap:.5ch; flex-wrap:wrap; }
.crumbs a { color: var(--c-ink-600); text-decoration:none; }
.crumbs a:hover { color: var(--c-primary-600); text-decoration:underline; }
.detail h1 { font-size: calc(var(--fs-h1) * var(--font-scale)); margin-block-end: var(--s-3); }
.detail .price { font-size:1.6rem; }
.vendor-chip {
  display:inline-flex; align-items:center; gap: var(--s-2);
  padding: var(--s-2) var(--s-3); border-radius: var(--r-full);
  background: var(--c-surface-200); font-size: var(--fs-caption); text-decoration:none; color: var(--c-ink-700);
}
.vendor-chip:hover { background: var(--c-primary-100); }
.stars { color: var(--c-accent-600); letter-spacing:-.05em; }

.qty { display:inline-flex; align-items:center; border:1px solid var(--c-border-300); border-radius: var(--r-md); overflow:hidden; }
.qty button {
  appearance:none; border:0; background: var(--c-surface-200); color: var(--c-ink-900);
  font:inherit; font-size:1.1rem; font-weight:600; cursor:pointer;
  inline-size: var(--touch-min); min-height: var(--touch-min);
  transition: background var(--d-fast);
}
.qty button:hover { background: var(--c-primary-100); }
.qty output { min-inline-size:3ch; text-align:center; font-weight:600; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- booking UI */
/* --------------------------------------------------------- scheduling field

   The calendar folds into a single line until somebody wants it. Open, a month
   grid and a row of times is most of a screen given to a control used once —
   and it pushed the price, the description and the reviews below the fold.
   Closed, it reads as a field with an answer written in it. */
.sched {
  border: 1px solid var(--c-border-300); border-radius: var(--r-lg);
  background: var(--c-surface-100); overflow: hidden;
  max-inline-size: 28rem;
  transition: border-color var(--d-fast), box-shadow var(--d-fast);
}
.sched.is-open { border-color: var(--c-primary-500); box-shadow: var(--shadow-md); }

.sched-trigger {
  appearance: none; font: inherit; cursor: pointer; inline-size: 100%;
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 0 var(--s-4); text-align: start;
  padding: var(--s-4) var(--s-5); min-block-size: var(--touch-min);
  background: none; border: 0; color: var(--c-ink-900);
}
.sched-trigger:hover { background: var(--c-surface-200); }
.sched-trigger:focus-visible { outline: 2px solid var(--c-accent-500); outline-offset: -2px; }

.sched-label {
  grid-column: 1; font-size: var(--fs-caption); color: var(--c-ink-600);
  letter-spacing: .02em;
}

/* The answer. Muted while it is still a question, full strength once it is a
   fact — so a filled-in field looks different from an empty one at a glance. */
.sched-value { grid-column: 1; font-size: var(--fs-body); color: var(--c-ink-600); }
.sched-value.is-set { color: var(--c-ink-900); font-weight: 600; }

.sched-caret {
  grid-column: 2; grid-row: 1 / span 2;
  font-size: 1.1rem; color: var(--c-ink-600); line-height: 1;
}

.sched-panel {
  padding: 0 var(--s-5) var(--s-5);
  border-block-start: 1px solid var(--c-border-200);
  padding-block-start: var(--s-5);
}

.sched-h {
  margin: 0 0 var(--s-3); font-size: var(--fs-caption);
  color: var(--c-ink-600); font-weight: 600;
}
.sched-times { margin-block-start: var(--s-5); }

/* Inside the field the calendar has no border of its own — two nested boxes
   with the same edge is a seam, not a structure. */
.sched-panel .cal {
  border: 0; background: none; padding: 0; max-inline-size: none;
}

/* On the narrowest screens the panel gives its padding back to the grid,
   because a wider cell is worth more than a wider margin. */
@media (max-width: 420px) {
  .sched-panel { padding-inline: var(--s-3); }
  .cal-grid { gap: 3px; }
}

@media (prefers-reduced-motion: no-preference) {
  .sched-panel { animation: sched-open var(--d-base) var(--e-decelerate) both; }
  @keyframes sched-open { from { opacity: 0; transform: translateY(-4px); } }
}

/* ------------------------------------------------------------ booking calendar

   Was a horizontal scroller of twenty-one tiles: three weeks, one direction,
   and no way to reach another month. A stage build for a wedding is booked
   further out than three weeks, and the only clue the strip scrolled at all
   was a thin scrollbar underneath it. */
.cal {
  border: 1px solid var(--c-border-200); border-radius: var(--r-lg);
  background: var(--c-surface-100); padding: var(--s-4);
  max-inline-size: 26rem;
}

.cal-head { display: flex; align-items: center; gap: var(--s-2); margin-block-end: var(--s-3); }
.cal-title { flex: 1; text-align: center; font-size: var(--fs-body); }

.cal-nav {
  appearance: none; cursor: pointer; font: inherit; font-size: 1.25rem; line-height: 1;
  inline-size: 40px; block-size: 40px; flex: 0 0 auto;
  border: 1px solid var(--c-border-300); border-radius: var(--r-md);
  background: var(--c-base-000); color: var(--c-ink-900);
  transition: background var(--d-fast), border-color var(--d-fast);
}
.cal-nav:hover:not([disabled]) { border-color: var(--c-primary-500); background: var(--c-surface-200); }
.cal-nav[disabled] { opacity: .35; cursor: not-allowed; }

.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }

.cal-dow {
  text-align: center; font-size: var(--fs-caption); font-weight: 600;
  color: var(--c-ink-600); padding-block-end: var(--s-1);
}

/* Square cells, so the grid reads as a calendar at any width rather than
   becoming seven tall columns on a phone. */
/* The global `button { min-width: 44px }` is right for a lone button and
   impossible for a calendar: seven of them need 308px before any padding or
   gaps, which no 320px screen has. These cells are allowed to shrink and take
   a floor of 34px instead — comfortably past the 24px WCAG 2.5.8 target size,
   with the gap doing the rest of the work of keeping them separable. */
.cal-cell {
  appearance: none; font: inherit; padding: 0;
  min-inline-size: 0; min-block-size: 34px;
  aspect-ratio: 1; display: grid; place-items: center;
  border: 1px solid transparent; border-radius: var(--r-md);
  background: none; color: var(--c-ink-900);
  transition: background var(--d-fast), border-color var(--d-fast), transform var(--d-fast) var(--e-spring);
}
.cal-n { font-size: var(--fs-small); font-variant-numeric: tabular-nums; }

.cal-cell.is-blank { visibility: hidden; }

/* Bookable. The only cells that look like buttons, because they are the only
   ones that do anything. */
.cal-cell.is-free {
  cursor: pointer;
  background: var(--c-base-000);
  border-color: var(--c-border-300);
  font-weight: 600;
}
.cal-cell.is-free:hover { border-color: var(--c-primary-500); transform: translateY(-1px); }
.cal-cell.is-free:focus-visible { outline: 2px solid var(--c-accent-500); outline-offset: 2px; }

.cal-cell.is-today { border-color: var(--c-accent-500); }

.cal-cell.is-on {
  background: var(--c-primary-600); border-color: var(--c-primary-600);
  color: var(--c-on-primary);
}

/* Not bookable: still drawn, so the month keeps its shape and somebody can see
   that the studio works Sundays but not Fridays.
   ink-600, not ink-400 — a date is information, and the muted tier put it at
   4.06:1 on this panel, under the AA floor. Nothing is lost by darkening it:
   the bookable days are told apart by a background, a border and bold weight,
   not by being the darker text. */
.cal-cell.is-shut { color: var(--c-ink-600); cursor: not-allowed; }

.cal-hint {
  margin: var(--s-3) 0 0; font-size: var(--fs-caption);
  color: var(--c-ink-600); text-align: center;
}

.slots { display:flex; flex-wrap:wrap; gap: var(--s-2); }
.slot {
  appearance:none; cursor:pointer; font:inherit; font-size: var(--fs-small); font-weight:600;
  padding: var(--s-2) var(--s-4); min-height: var(--touch-min);
  border:1px solid var(--c-border-300); border-radius: var(--r-md);
  background: var(--c-surface-100); color: var(--c-ink-900);
  font-variant-numeric: tabular-nums;
  transition: background var(--d-fast), border-color var(--d-fast), transform var(--d-fast) var(--e-spring);
}
.slot:hover:not([disabled]) { border-color: var(--c-primary-500); transform: translateY(-2px); }
.slot[aria-pressed="true"] { background: var(--c-primary-600); color: var(--c-on-primary); border-color: var(--c-primary-600); }
.slot[disabled] { background: var(--c-surface-200); color: var(--c-ink-400); cursor:not-allowed; text-decoration:line-through; }

.note {
  border-inline-start:3px solid var(--c-accent-500);
  background: var(--c-accent-100); color: var(--c-ink-900);
  padding: var(--s-4); border-radius: var(--r-md); font-size: var(--fs-small);
}
.note p { margin:0; max-inline-size:none; }
.note.info { border-inline-start-color: var(--c-info-600); background: var(--c-info-100); }

/* ---------------------------------------------------------------- cart page */
.cart-wrap { display:grid; gap: var(--s-8); padding-block: var(--s-10); align-items:start; }
@media (min-width:900px) { .cart-wrap { grid-template-columns: 1fr 340px; } }

.vgroup { border:1px solid var(--c-border-200); border-radius: var(--r-lg); overflow:hidden; background: var(--c-surface-100); }
.vgroup + .vgroup { margin-block-start: var(--s-4); }
.vgroup-head {
  background: var(--c-surface-200); padding: var(--s-3) var(--s-4);
  display:flex; align-items:center; gap: var(--s-3); flex-wrap:wrap;
  font-size: var(--fs-small); font-weight:600;
}
.chip {
  display:inline-flex; align-items:center; gap:.4em;
  font-size: var(--fs-caption); font-weight:600; padding:.3em .7em; border-radius: var(--r-full);
}
.chip::before { content:''; inline-size:.5em; block-size:.5em; border-radius:50%; background:currentColor; flex:0 0 auto; }
.chip-success { background: var(--c-success-100); color: var(--c-success-600); }
.chip-warning { background: var(--c-warning-100); color: var(--c-warning-600); }
.chip-info    { background: var(--c-info-100);    color: var(--c-info-600); }
.chip-neutral { background: var(--c-surface-200); color: var(--c-ink-600); }

.line { display:flex; align-items:center; gap: var(--s-4); padding: var(--s-4); border-block-start:1px solid var(--c-border-200); }
.line-emo {
  inline-size:56px; block-size:56px; flex:0 0 auto; border-radius: var(--r-md);
  display:grid; place-items:center; font-size:1.7rem; background: var(--c-surface-200);
  overflow:hidden;
}
.line-emo img {
  inline-size:100%; block-size:100%; object-fit:cover; display:block;
  border-radius:inherit;
}
.line-main { flex:1; min-inline-size:0; }
.line-title { font-size: var(--fs-small); font-weight:600; margin:0 0 2px; }
.line-meta { font-size: var(--fs-caption); color: var(--c-ink-600); }
.line-end { display:flex; flex-direction:column; align-items:flex-end; gap: var(--s-2); }
.link-danger {
  appearance:none; border:0; background:none; cursor:pointer;
  color: var(--c-danger-600); font:inherit; font-size: var(--fs-caption);
  text-decoration:underline; padding: var(--s-1);
}
.line.is-leaving { opacity:0; transform: translateX(-24px); }
[dir="rtl"] .line.is-leaving { transform: translateX(24px); }

.summary {
  background: var(--c-surface-100); border:1px solid var(--c-border-200);
  border-radius: var(--r-lg); padding: var(--s-5);
  position:sticky; inset-block-start: calc(var(--header-h) + var(--s-4));
}
.srow { display:flex; gap: var(--s-4); font-size: var(--fs-small); padding-block: var(--s-2); }
.srow span:last-child { margin-inline-start:auto; font-variant-numeric: tabular-nums; }
.srow.total {
  font-size:1.1rem; font-weight:700; padding-block-start: var(--s-4);
  margin-block-start: var(--s-2); border-block-start:1px solid var(--c-border-200);
}
.srow .free { color: var(--c-success-600); font-weight:600; }
.coupon { display:flex; gap: var(--s-2); margin-block: var(--s-4); }
.coupon input {
  flex:1; min-inline-size:0; font:inherit; font-size: var(--fs-small);
  padding: var(--s-3); min-height: var(--touch-min);
  border:1px solid var(--c-border-300); border-radius: var(--r-md);
  background: var(--c-surface-100); color: var(--c-ink-900);
}

/* ------------------------------------------------------------------ stepper */
.stepper { display:flex; align-items:center; gap: var(--s-2); margin-block-end: var(--s-8); }
.step { display:flex; align-items:center; gap: var(--s-2); font-size: var(--fs-small); color: var(--c-ink-400); }
.step-n {
  inline-size:28px; block-size:28px; border-radius: var(--r-full);
  display:grid; place-items:center; font-size: var(--fs-caption); font-weight:700;
  background: var(--c-surface-200); color: var(--c-ink-400);
  transition: background var(--d-base) var(--e-standard), color var(--d-base), scale var(--d-base) var(--e-spring);
}
.step.is-on { color: var(--c-ink-900); font-weight:600; }
.step.is-on .step-n { background: var(--c-primary-600); color: var(--c-on-primary); scale:1.1; }
.step.is-done .step-n { background: var(--c-success-600); color:#fff; }
.step-bar { flex:1; block-size:2px; background: var(--c-border-200); border-radius:2px; position:relative; overflow:hidden; }
.step-bar::after {
  content:''; position:absolute; inset-block:0; inset-inline-start:0; inline-size:0;
  background: var(--c-primary-600); transition: inline-size var(--d-slow) var(--e-standard);
}
.step-bar.is-full::after { inline-size:100%; }

/* -------------------------------------------------------------------- forms */
.field { display:grid; gap: var(--s-2); margin-block-end: var(--s-4); }
.field label { font-size: var(--fs-small); font-weight:600; }
.field .hint { font-size: var(--fs-caption); color: var(--c-ink-600); }
/* :not() on the type, and it is doing two jobs.
   It stops a checkbox or radio being given text-input dimensions — 100% wide,
   44px tall, padded — which is what turned "Same as my phone number" into a
   giant square with its label wrapping one word per line. It also lifts this
   rule's specificity clear of `.check input`, which previously tied with it and
   lost only because it appears earlier in the file. A tie decided by source
   order is a bug waiting for the next person who reorders this stylesheet. */
.field input:not([type="checkbox"]):not([type="radio"]), .field select, .field textarea {
  font:inherit; font-size: var(--fs-body); padding: var(--s-3);
  min-height: var(--touch-min); inline-size:100%;
  background: var(--c-surface-100); color: var(--c-ink-900);
  border:1px solid var(--c-border-300); border-radius: var(--r-md);
  transition: border-color var(--d-fast), box-shadow var(--d-fast);
}
.field input:not([type="checkbox"]):not([type="radio"]):focus { border-color: var(--c-primary-500); box-shadow: 0 0 0 4px var(--c-primary-100); }
/* A checkbox keeps a focus ring, just not the text-input border treatment. */
.field input[type="checkbox"]:focus-visible, .field input[type="radio"]:focus-visible { outline: 2px solid var(--c-primary-500); outline-offset: 2px; }
.field.is-bad input { border-color: var(--c-danger-600); border-width:2px; }
.field .err { font-size: var(--fs-caption); color: var(--c-danger-600); font-weight:500; }
/* align-items:start is load-bearing.
   A grid row stretches its cells to the tallest one by default, and .field is
   itself a grid — so in a row where one field carries a hint ("Drivers rely on
   landmarks…") and its neighbour does not, the neighbour's INPUT grew to
   absorb the extra height. Unit/floor rendered half again as tall as Landmark
   beside it, which read as a broken layout rather than a taller row. */
.field-row { display:grid; gap: var(--s-4); align-items:start; }
@media (min-width:560px) { .field-row.two { grid-template-columns:1fr 1fr; } .field-row.three { grid-template-columns:1fr 1fr 1fr; } }
fieldset { border:0; padding:0; margin:0 0 var(--s-6); }
legend { font-family: var(--font-display); font-size: calc(var(--fs-h3) * var(--font-scale)); margin-block-end: var(--s-4); padding:0; }

/* ------------------------------------------------------------ cart drawer */
.backdrop {
  position:fixed; inset:0; z-index: var(--z-drawer);
  background: rgba(26,16,19,.5); backdrop-filter: blur(2px);
  opacity:0; pointer-events:none; transition: opacity var(--d-base) var(--e-standard);
}
.backdrop.is-open { opacity:1; pointer-events:auto; }
.drawer {
  position:fixed; inset-block:0; inset-inline-end:0; z-index: var(--z-modal);
  inline-size: min(420px, 100vw);
  background: var(--c-base-000); border-inline-start:1px solid var(--c-border-200);
  box-shadow: var(--shadow-xl);
  display:flex; flex-direction:column;
  transform: translateX(100%);
  transition: transform var(--d-slow) var(--e-standard);
}
[dir="rtl"] .drawer { transform: translateX(-100%); }
.drawer.is-open { transform:none; }
.drawer-head { padding: var(--s-4) var(--s-5); border-block-end:1px solid var(--c-border-200); display:flex; align-items:center; gap: var(--s-3); }
.drawer-head h2 { margin:0; font-size: calc(var(--fs-h3) * var(--font-scale)); }
.drawer-body { flex:1; overflow-y:auto; padding: var(--s-4) var(--s-5); }
.drawer-foot { padding: var(--s-5); border-block-start:1px solid var(--c-border-200); background: var(--c-surface-100); }
.icon-btn {
  appearance:none; border:0; background:transparent; cursor:pointer;
  inline-size: var(--touch-min); block-size: var(--touch-min);
  border-radius: var(--r-full); color: var(--c-ink-600); font-size:1.2rem;
  display:grid; place-items:center; transition: background var(--d-fast);
}
.icon-btn:hover { background: var(--c-surface-200); color: var(--c-ink-900); }

/* -------------------------------------------------------------------- toast */
.toasts {
  position:fixed; inset-block-end: var(--s-5); inset-inline-start:50%;
  translate:-50% 0; z-index: var(--z-toast);
  display:flex; flex-direction:column; gap: var(--s-2); align-items:center;
  pointer-events:none; inline-size: min(420px, calc(100vw - 2rem));
}
.toast {
  background: var(--c-ink-900); color: var(--c-base-000);
  padding: var(--s-3) var(--s-5); border-radius: var(--r-full);
  font-size: var(--fs-small); font-weight:500; box-shadow: var(--shadow-lg);
  display:flex; align-items:center; gap: var(--s-3); pointer-events:auto;
  max-inline-size:100%;
}
/* The tick sits on the pill, which is dark in BOTH themes — so it needs the
   light-on-dark status colours in both, not the ones tuned for a white page.
   --c-success-600 and --c-danger-600 are the white-page pair, and on the pill
   they measured 3.55:1 and 2.85:1. These are the same two colours the dark
   theme uses, stated literally because the pill does not follow the theme. */
.toast { --toast-ok: #4CAF8B; --toast-bad: #F2685F; }
.toast .tick { color: var(--toast-ok); font-weight: 700; }
.toast.is-bad .tick { color: var(--toast-bad); }

/* Dark mode: the pill stops inverting and becomes a raised surface instead.
   Inverting is how it lifts off a light page; on a dark page the same trick
   produces a white slab, and a border plus a lighter surface reads as
   "in front of" without shouting. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .toast {
    background: var(--c-surface-200);
    color: var(--c-ink-900);
    border: 1px solid var(--c-border-300);
  }
}
:root[data-theme="dark"] .toast {
  background: var(--c-surface-200);
  color: var(--c-ink-900);
  border: 1px solid var(--c-border-300);
}

/* ----------------------------------------------------------------- skeleton */
.skel {
  border-radius: var(--r-md);
  background: linear-gradient(90deg, var(--c-surface-200) 25%, var(--c-border-200) 37%, var(--c-surface-200) 63%);
  background-size: 200% 100%;
}
.skel-card { block-size:300px; border-radius: var(--r-lg); }

.spinner {
  inline-size:16px; block-size:16px; border-radius:50%; flex:0 0 auto;
  border:2px solid currentColor; border-block-start-color:transparent;
}

/* ------------------------------------------------------------------- footer */
/* -------------------------------------------------------------------- footer
   Four columns on desktop, stacking to one on a phone, with a separate bottom
   bar for the legal line.

   The old version was three columns with the copyright glued underneath the
   first one, so on a wide screen the small print sat alone under the blurb with
   two empty columns beside it. Splitting the bar out is what lets it span.

   Headings are uppercase micro-labels rather than headline text: in a footer
   they are signposts between link lists, and at body size they compete with the
   links they are supposed to be labelling.
   ------------------------------------------------------------------------- */
.ftr {
  border-block-start:1px solid var(--c-border-200);
  background: var(--c-surface-100);
  padding-block: var(--s-12) var(--s-6);
  margin-block-start: var(--s-16);
}
.ftr-grid { display:grid; gap: var(--s-8); }
@media (min-width:700px)  { .ftr-grid { grid-template-columns: 1.6fr 1fr 1fr; } }
/* The fourth column only appears once there is genuinely room for it; below
   this it wraps under the brand rather than squeezing all four. */
@media (min-width:980px)  { .ftr-grid { grid-template-columns: 2fr 1fr 1fr 1fr; gap: var(--s-10); } }

.ftr-brand .brand { margin-block-end: var(--s-4); }
.ftr-blurb {
  color: var(--c-ink-600); font-size: var(--fs-small); line-height:1.65;
  /* Measure. A blurb running the full width of a 2fr column is a paragraph
     nobody finishes. */
  max-inline-size: 42ch; margin:0;
}

.ftr-trust {
  list-style:none; margin: var(--s-6) 0 0; padding:0;
  display:flex; flex-wrap:wrap; gap: var(--s-2) var(--s-4);
}
.ftr-trust li {
  display:inline-flex; align-items:center; gap: var(--s-2);
  font-size: var(--fs-caption); color: var(--c-ink-600);
  margin:0; white-space:nowrap;
}

.ftr h4 {
  font-size: var(--fs-caption); font-weight:700;
  letter-spacing:.08em; text-transform:uppercase;
  color: var(--c-ink-900); margin: 0 0 var(--s-4);
}
.ftr ul { list-style:none; margin:0; padding:0; }
.ftr-col li { margin-block-end: var(--s-3); }
.ftr a {
  color: var(--c-ink-600); text-decoration:none; font-size: var(--fs-small);
  /* Underline on hover only, and offset so it does not collide with the
     descenders — a footer of permanently underlined links reads as noise. */
  transition: color var(--d-fast) var(--e-standard);
}
.ftr a:hover { color: var(--c-primary-600); text-decoration:underline; text-underline-offset:3px; }
.ftr a:focus-visible { outline:2px solid var(--c-focus-ring); outline-offset:3px; border-radius: var(--r-sm); }

.ftr-bar {
  margin-block-start: var(--s-10);
  padding-block-start: var(--s-5);
  border-block-start:1px solid var(--c-border-200);
  display:flex; align-items:center; gap: var(--s-4); flex-wrap:wrap;
}
.ftr-note { margin:0; color: var(--c-ink-600); font-size: var(--fs-caption); }
.ftr-legal {
  display:flex; gap: var(--s-5); margin-inline-start:auto; flex-wrap:wrap;
}
.ftr-legal li { margin:0; }
.ftr-legal a { font-size: var(--fs-caption); }

@media (max-width:520px) {
  /* Stacked and left-aligned rather than centred: centred small print under a
     left-aligned footer looks like a mistake. */
  .ftr-bar { flex-direction:column; align-items:flex-start; gap: var(--s-3); }
  .ftr-legal { margin-inline-start:0; }
}

/* ============================ MOTION =======================================
   Everything below only applies when the user has not asked for less motion.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {

  /* Scroll reveal. Elements start hidden and animate in via IntersectionObserver. */
  .reveal { opacity:0; transform: translateY(22px); }
  .reveal.is-in {
    animation: cd-fade-up var(--d-slower) var(--e-decelerate) forwards;
    animation-delay: var(--reveal-delay, 0ms);
  }

  /* Route transition. Applied to the <main> content on each navigation. */
  .view-enter { animation: cd-fade-up 340ms var(--e-decelerate) both; }

  /* Hero entrance, staggered by inline --i */
  .hero-anim { opacity:0; animation: cd-fade-up 620ms var(--e-decelerate) both; animation-delay: calc(var(--i, 0) * 90ms); }
  .hero-art span { animation: cd-float 6s ease-in-out infinite; animation-delay: calc(var(--i, 0) * -1.4s); }
  .hero-art .a3, .hero-art .a5 { animation-name: cd-float-slow; animation-duration: 8s; }
  .showcase-atmosphere::before { animation:cd-pattern-drift 22s linear infinite; }

  .crest::after { animation: cd-sheen 4.5s var(--e-standard) infinite; animation-delay:1.5s; }

  .card-media::after { transition: transform var(--d-slower) var(--e-standard), opacity var(--d-fast); }
  .card:hover .card-media::after { transform: translateX(320%) skewX(-18deg); opacity:1; }

  .btn-primary::after, .btn-gold::after { transition: transform 620ms var(--e-standard), opacity var(--d-fast); }
  .btn-primary:hover::after, .btn-gold:hover::after { transform: translateX(320%) skewX(-18deg); opacity:1; }

  /* Pulse ring behind the "how it works" numerals while in view */
  .how.is-in .how-n::after { animation: cd-ring 2.4s var(--e-decelerate) infinite; animation-delay: calc(var(--i, 0) * 800ms); }

  .skel { animation: cd-shimmer 1.4s linear infinite; }
  .spinner { animation: cd-spin 720ms linear infinite; }
  .toast { animation: cd-toast-in var(--d-slow) var(--e-spring) both; }
  .line { transition: opacity var(--d-base) var(--e-standard), transform var(--d-base) var(--e-standard); }
  .vgroup { animation: cd-scale-in var(--d-slow) var(--e-decelerate) both; }
  .drawer-body .line { animation: cd-fade-up var(--d-slow) var(--e-decelerate) both; animation-delay: calc(var(--i, 0) * 60ms); }

  .cat, .card, .svc { will-change: transform; }
}

/* The global brake. Kills every animation and transition for users who ask,
   including the ones declared above. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:.01ms !important; animation-iteration-count:1 !important;
    transition-duration:.01ms !important; scroll-behavior:auto !important;
  }
  .reveal { opacity:1 !important; transform:none !important; }
  .hero-anim { opacity:1 !important; }
}

/* Print — nobody needs the chrome on paper. */
@media print {
  .hdr, .drawer, .backdrop, .toasts, .filters, .ftr { display:none !important; }
  .reveal { opacity:1 !important; transform:none !important; }
}

/* Uploaded product image. Fills the tile's media box in place of the emoji
   placeholder; object-fit:cover so a portrait or landscape upload both crop
   sensibly rather than distorting. */
.card-media .card-img,
.detail-media .card-img {
  inline-size:100%; block-size:100%;
  object-fit:cover; display:block;
  border-radius:inherit;
}

/* ==========================================================================
   Checkout — payment method chooser

   Big tappable rows rather than a select. This is the last decision before
   money moves, on a page that is mostly phone traffic, and a native select
   hides every option but one behind a tap.
   ========================================================================== */

.pay-methods { display: grid; gap: var(--s-3); }

.pay-method {
  display: flex; align-items: center; gap: var(--s-4);
  padding: var(--s-4); cursor: pointer;
  border: 1.5px solid var(--c-border-300); border-radius: var(--r-md);
  background: var(--c-base-000);
  transition: border-color var(--d-fast) var(--e-standard), background var(--d-fast) var(--e-standard);
}
.pay-method:hover { border-color: var(--c-primary-300); }
.pay-method.is-on { border-color: var(--c-primary-600); background: var(--c-primary-050); }
.pay-method:focus-within { outline: 2px solid var(--c-focus-ring); outline-offset: 2px; }

/* The radio stays in the accessibility tree and keeps arrow-key navigation;
   it is simply not what the eye follows. */
.pay-method input { position: absolute; opacity: 0; pointer-events: none; }

.pay-method-mark {
  inline-size: 40px; block-size: 40px; flex: 0 0 auto;
  display: grid; place-items: center; font-size: 1.25rem;
  background: var(--c-surface-200); border-radius: var(--r-sm);
}
.pay-method-body { display: grid; gap: 2px; min-inline-size: 0; }
.pay-method-name { font-weight: 600; font-size: var(--fs-small); }
.pay-method-note { font-size: var(--fs-caption); color: var(--c-ink-600); }
.pay-method-tick {
  margin-inline-start: auto; flex: 0 0 auto;
  inline-size: 22px; block-size: 22px; border-radius: 50%;
  display: grid; place-items: center; font-size: 12px; font-weight: 700;
  background: var(--c-surface-200); color: transparent;
  transition: background var(--d-fast) var(--e-standard), color var(--d-fast) var(--e-standard);
}
.pay-method.is-on .pay-method-tick { background: var(--c-primary-600); color: var(--c-on-primary); }

#co-error {
  display: block; margin-block-start: var(--s-4);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-md);
  background: var(--c-danger-100); color: var(--c-danger-600);
  font-size: var(--fs-small);
}
#co-error[hidden] { display: none; }

.note.warn { border-inline-start-color: var(--c-warning-600); }

/* ==========================================================================
   Search suggestions
   ========================================================================== */

/* Anchored to .search, which is position:relative, so the list hangs beneath
   the field without affecting the header's height. */
.sug {
  position: absolute;
  /* Anchored to the END of the field and grown backwards, NOT stretched across
     it. The search input is deliberately narrow so the menu labels keep their
     single line, and a list constrained to that width squeezed the product
     titles out of existence — the one thing the row is there to show. */
  inset-inline-end: 0; inset-inline-start: auto;
  inset-block-start: calc(100% + 6px);
  min-inline-size: 340px;
  /* Never wider than the viewport minus the gutters. */
  max-inline-size: calc(100vw - (var(--gutter) * 2));
  z-index: var(--z-header);
  background: var(--c-base-000);
  border: 1px solid var(--c-border-200); border-radius: var(--r-md);
  box-shadow: var(--shadow-xl);
  overflow: hidden;
  /* A long list must not run off the bottom of a phone. */
  max-block-size: min(70vh, 480px); overflow-y: auto;
}

.sug-row {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  cursor: pointer; border-block-end: 1px solid var(--c-border-200);
}
.sug-row:last-of-type { border-block-end: 0; }
/* One rule for both, so keyboard and mouse highlighting cannot drift apart. */
.sug-row:hover, .sug-row.is-on { background: var(--c-primary-100); }

.sug-emo {
  inline-size: 38px; block-size: 38px; flex: 0 0 auto;
  display: grid; place-items: center; font-size: 1.15rem;
  background: var(--c-surface-200); border-radius: var(--r-sm); overflow: hidden;
}
.sug-emo img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }

.sug-txt { min-inline-size: 0; flex: 1; }
.sug-t {
  display: block; font-size: var(--fs-small); font-weight: 600; line-height: 1.35;
  /* One line, clipped. A three-line product title turns the list into a wall. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sug-v { display: block; font-size: var(--fs-caption); color: var(--c-ink-600); }
/* flex:0 0 auto so the price takes only what it needs; the title column has
   flex:1 and min-inline-size:0, which is what lets it shrink and ellipsis
   instead of pushing the price out or collapsing to nothing. */
.sug-p { font-size: var(--fs-small); font-weight: 700; white-space: nowrap; flex: 0 0 auto; }

.sug-all, .sug-none {
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-caption); color: var(--c-ink-600);
  background: var(--c-surface-100); text-align: center;
}
.sug-all { font-weight: 600; color: var(--c-primary-600); }

@media (max-width: 720px) {
  /* Full width of the viewport rather than of a narrow field. */
  .sug { position: fixed; inset-inline: var(--gutter); inset-block-start: 64px; }
}

/* ===========================================================================
   Mobile Safari's focus zoom
   ---------------------------------------------------------------------------
   iOS Safari zooms the whole page in when a text field smaller than 16px takes
   focus — and it does NOT zoom back out when the field is blurred. Tapping the
   search box, or the order number field on the tracking page, leaves the reader
   on a page they now have to pan sideways to read, having done nothing wrong.

   16px is the threshold; the 14px used around the interface is a deliberate
   desktop density choice. So the floor is raised only at widths where the
   behaviour exists, rather than coarsening the whole interface to fix a phone.

   The stacked :not() guards are load-bearing, not noise. Several components
   style their own fields at class-plus-element specificity (.trk-form input,
   .field select, .search input), and some of those live in a page's own
   <style> block, which is parsed after this file. A bare `input` selector would
   lose to every one of them. The guards raise this rule above them without
   reaching for !important, and each one is also true of every control that can
   actually be focused and typed into.
   ======================================================================== */
@media (max-width: 767px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select:not([hidden]):not([disabled]),
  textarea:not([hidden]):not([disabled]) {
    /* max(), not a flat 16px: a reader who has enlarged their root font must
       keep the larger size, and 1rem alone would drop back under the threshold
       for anyone who has made it smaller. */
    font-size: max(16px, 1rem);
  }
}

/* ===========================================================================
   Icons in page headings
   ---------------------------------------------------------------------------
   The shop heading used a bare emoji. Emoji inherit the surrounding font, and
   this heading is Playfair Display — a serif with no emoji glyphs — so the
   browser fell back to a monochrome symbol font and drew a black shape next to
   the word.

   The shop icon is now an SVG, which has no font to fall back to. The category
   headings keep their emoji, because those are the shop's visual shorthand and
   appear on the category tiles too, but they are given an explicit emoji stack
   so they render in colour rather than as serif fallbacks.
   ======================================================================== */

.h-icon {
  inline-size: .82em; block-size: .82em;
  margin-inline-end: .38em;
  vertical-align: -0.06em;
  /* Slightly lighter than the words, so it reads as an ornament and not as a
     letter competing with them. */
  opacity: .72;
  flex: 0 0 auto;
}

.h-emoji {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
               "Twemoji Mozilla", "EmojiOne Color", sans-serif;
  font-size: .78em;
  margin-inline-end: .38em;
  /* Emoji fonts sit on a different baseline from the serif around them. */
  vertical-align: .02em;
}
/* ===========================================================================
   Approximate prices for overseas shoppers
   ---------------------------------------------------------------------------
   Deliberately quieter than the real price: smaller, lighter, and never on the
   same line weight. A shopper must never be in doubt about which number is the
   one being charged — the ≈ and the styling both say "estimate".

   aria-hidden in the markup, too. A screen reader announcing two prices for
   one product is confusing, and the real price is the one that matters.
   ======================================================================== */

.price-approx {
  display: block;
  font-size: .8125rem;
  font-weight: 400;
  color: var(--c-ink-600);
  margin-block-start: 2px;
  letter-spacing: 0;
}

/* In the basket drawer and the checkout summary the price sits in a row, so
   the estimate goes underneath rather than pushing the layout sideways. */
.srow .price-approx,
.drawer .price-approx { text-align: end; }

/* ---- the picker -----------------------------------------------------------
   In the footer, not the header. It is a convenience for a minority of
   visitors, and putting a currency selector next to the basket implies you can
   pay in that currency, which you cannot. */
.cur-pick { display: inline-flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.cur-pick label { font-size: .8125rem; color: var(--c-ink-600); }
.cur-pick select {
  font: inherit; font-size: .8125rem;
  padding: var(--s-1) var(--s-2);
  border: 1px solid var(--c-border-300); border-radius: var(--r-sm);
  background: var(--c-surface-100); color: var(--c-ink-900);
  min-height: var(--touch-min);
}
.cur-pick .hint { display: block; font-size: .75rem; color: var(--c-ink-600); margin-block-start: 4px; }
/* The one place the difference is spelled out in words rather than implied by
   a symbol: at the total, where somebody is about to pay. */
.charge-note {
  margin-block: var(--s-3) 0;
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--c-surface-100);
  border: 1px solid var(--c-border-200);
  font-size: .8125rem;
  line-height: 1.55;
  color: var(--c-ink-700);
}
/* ===========================================================================
   Payment method
   ---------------------------------------------------------------------------
   The last thing a customer looks at before handing over a card, so it is
   worth more than a radio button. Each method is a card: mark, name, what it
   accepts, and an unmistakable selected state.

   The accepted-card marks are drawn here rather than loaded as brand logos.
   Visa and Mastercard artwork carries licensing conditions and would be a
   third-party request on the checkout page; the network NAMES are stated as
   plain text, which is accurate and needs nobody's permission.
   ======================================================================== */

.pay-methods { display: grid; gap: var(--s-3); }

.pay-method {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4);
  border: 1px solid var(--c-border-300);
  border-radius: var(--r-lg);
  background: var(--c-surface-0);
  cursor: pointer;
  transition: border-color var(--d-fast), background var(--d-fast), box-shadow var(--d-fast);
}
.pay-method:hover { border-color: var(--c-primary-300); }

/* The selected state was a flat pink fill that swamped the text. A ring plus a
   tint reads as "chosen" without fighting the label for contrast. */
.pay-method.is-on {
  border-color: var(--c-primary-600);
  background: var(--c-primary-50, var(--c-surface-100));
  box-shadow: 0 0 0 3px var(--c-primary-100);
}

/* Focus follows the radio, which is visually hidden inside the label. */
.pay-method:focus-within { box-shadow: 0 0 0 4px var(--c-primary-100); }

.pay-method-mark {
  display: grid; place-items: center;
  inline-size: 48px; block-size: 34px;
  border-radius: var(--r-sm);
  background: var(--c-surface-100);
  border: 1px solid var(--c-border-200);
  font-size: 1.25rem; line-height: 1;
}
.pay-method-mark svg { inline-size: 26px; block-size: 26px; }

.pay-method-body { display: grid; gap: 3px; min-inline-size: 0; }
.pay-method-name { font-weight: 600; color: var(--c-ink-900); }
.pay-method-note { font-size: .8125rem; color: var(--c-ink-600); }

/* The row of accepted networks. Text, deliberately — see the note above. */
.pay-accepts { display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 2px; }
.pay-accepts span {
  font-size: .6875rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase;
  padding: 2px 6px;
  border: 1px solid var(--c-border-300); border-radius: 4px;
  color: var(--c-ink-600); background: var(--c-surface-0);
}

.pay-method-tick {
  inline-size: 26px; block-size: 26px;
  display: grid; place-items: center;
  border-radius: 999px;
  border: 1px solid var(--c-border-300);
  color: transparent;
  transition: background var(--d-fast), color var(--d-fast), border-color var(--d-fast);
}
.pay-method.is-on .pay-method-tick {
  background: var(--c-primary-600);
  border-color: var(--c-primary-600);
  color: #fff;
}

/* The radio itself is hidden but still focusable and still the thing that
   carries the value — the label is a presentation layer over it, never a
   replacement for it. */
.pay-method input[type="radio"] {
  position: absolute; opacity: 0; pointer-events: none;
}

.pay-secure {
  display: flex; align-items: flex-start; gap: var(--s-2);
  margin-block-start: var(--s-4);
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--c-surface-100);
  border: 1px solid var(--c-border-200);
  font-size: .8125rem; line-height: 1.55; color: var(--c-ink-700);
}
.pay-secure svg { inline-size: 16px; block-size: 16px; flex: 0 0 auto; margin-block-start: 2px; color: var(--c-success-600, var(--c-ink-600)); }

@media (max-width: 480px) {
  .pay-method { grid-template-columns: auto 1fr; gap: var(--s-3); }
  .pay-method-tick { grid-column: 2; justify-self: end; margin-block-start: 4px; }
}
/* ===========================================================================
   The hidden attribute
   ---------------------------------------------------------------------------
   The browser's own [hidden] rule is `display: none`, and it lives in the
   user-agent stylesheet — so ANY author rule that sets display beats it. The
   recipient fields carry both `hidden` and `.field-row { display: grid }`, and
   grid won: the fields sat open on every checkout with the checkbox unticked.

   Declared once, globally, because the next component to hit this will not
   look related to this one. The other standalone pages already carry their own
   copy of this rule; app.css was the one that did not.
   ======================================================================== */
[hidden] { display: none !important; }

/* The sub-line under the method name, and the row of acceptance marks. */
.pay-method-sub {
  font-size: .8125rem;
  color: var(--c-ink-600);
  line-height: 1.4;
}

.pay-accepts {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px;
  margin-block-start: 6px;
}
.pay-accepts img {
  inline-size: 38px; block-size: 24px;
  border-radius: 3px;
  /* A hairline, because several of these marks are white-on-white and vanish
     into the card behind them otherwise. */
  box-shadow: 0 0 0 1px var(--c-border-200);
  background: #fff;
  object-fit: contain;
}
/* ===========================================================================
   "Same as my phone number"
   ---------------------------------------------------------------------------
   When the box is ticked the WhatsApp field is not merely disabled, it is gone.
   A greyed-out empty input above a ticked checkbox is a control that visibly
   does nothing, and people try to type in it anyway.
   ======================================================================== */

.field.is-mirrored > input { display: none; }
.field.is-mirrored > .check { margin-block-start: 0; }
/* ------------------------------------------------------- layout stability */

/* The shell ships with <main> empty and app.js fills it in.
 *
 * Without a reserved height the first paint puts the footer just under the
 * header, at the top of the screen — and the instant the page renders, the
 * footer drops out of sight. Cumulative Layout Shift measured that at 1.61,
 * where anything above 0.1 counts as poor, and almost all of it was that one
 * jump.
 *
 * A screenful of reserved space puts the footer below the fold from the
 * beginning. It still moves when the real content arrives, but it moves while
 * off screen, and CLS only counts elements that were visible when they shifted.
 *
 * `:empty` stops matching the moment app.js writes anything, so this costs
 * nothing after the first paint. Comments do not count as content, which is
 * what makes it match the `<!-- rendered by app.js -->` placeholder.
 */
#main:empty { min-block-size: 100vh; }

/* ---- how much of the photograph you actually see --------------------------

   The hero's height was entirely content-driven — padding, plus however tall
   the headline happened to be. With a cover photo set, `background-size: cover`
   then cropped whatever was uploaded down to that strip: you could see the
   balloon arch but not the room it was standing in, and the photograph read as
   a texture behind the words rather than as a picture of the work.

   A floor gives it room to be a scene. The copy is centred inside that height
   rather than pinned under the top padding, so the extra space is shared above
   and below it instead of opening a gap underneath.

   Capped in two units deliberately: vh on its own becomes a wall on a tall
   desktop monitor, and a fixed pixel height on its own ignores short laptop
   screens. min() takes whichever is smaller, so both stay sensible.

   Only `.has-cover`. Without a photograph there is nothing to show, and a
   half-empty screen of gradient is worse than a compact hero.
   ------------------------------------------------------------------------- */
.hero.has-cover {
  min-block-size: min(78vh, 640px);
  /* .hero holds exactly one child (.hero-in), so this centres the copy in the
     taller box. Without it the text sits at the top and the extra height reads
     as dead space. */
  display: grid;
  align-content: center;
}

@media (min-width: 900px) {
  .hero.has-cover { min-block-size: min(88vh, 920px); }
}

/* A phone in landscape is only a few hundred pixels tall, so 78vh there leaves
   no room for the headline it is meant to sit behind. */
@media (max-height: 520px) and (orientation: landscape) {
  .hero.has-cover { min-block-size: 0; }
}

/* ---- the hero's bottom edge -----------------------------------------------

   Both bottom corners are rounded so the photograph reads as a panel sitting on
   the page rather than a band bleeding off it. The top stays square because it
   meets the header, and a gap of page colour between the two would look like a
   mistake.

   `overflow: hidden` is already on .hero, so the background image, the scrim
   and the ::after highlight are all clipped by this radius together — without
   it the scrim would keep its square corners and show as two dark triangles
   under the curve.

   Only `.has-cover`: with no photograph there is no panel to round, just page
   background meeting page background.
   ------------------------------------------------------------------------- */
.hero.has-cover {
  border-end-start-radius: clamp(1.25rem, 4vw, 2.75rem);
  border-end-end-radius: clamp(1.25rem, 4vw, 2.75rem);
}

/* ---- customer reviews, on the product page --------------------------------

   Only approved reviews ever reach this — the server decides that, not the
   page — so everything rendered here has been read by somebody at the shop.

   Nothing renders at all for a product with no reviews. An empty star row or a
   "0 reviews" line reads as a verdict on the product rather than as an absence
   of one.
   ------------------------------------------------------------------------ */

.rvp-stars { color: var(--c-accent-600); letter-spacing: 1px; }
.rvp-stars .off { color: var(--c-border-200); }

/* The summary beside the title, which is what a shopper looks for first. */
.rvp-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: 999px;
  background: var(--c-surface-200);
  color: inherit;
  text-decoration: none;
  font-size: var(--fs-caption);
}
.rvp-chip:hover { background: var(--c-primary-100); }

.rvp { margin-block-start: var(--s-8); }
.rvp:empty { margin-block-start: 0; }

/* Score and breakdown side by side on a desktop, stacked on a phone. The
   breakdown is the half that makes a large average mean anything, so it is
   never the half that gets dropped. */
/* Under the photographs now, so it is framed as its own card rather than
   introducing a section. */
.rvp-panel:empty { display: none; }
.rvp-panel {
  margin-block-start: var(--s-2);
  padding: var(--s-4);
  border: 1px solid var(--c-border-200);
  border-radius: var(--r-lg, 1rem);
  background: var(--c-surface-100);
}

.rvp-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-5);
  align-items: center;
}
/* The panel is narrow inside the gallery column on a tablet, and full width
   on a phone. Stack whenever the two halves would be squeezed. */
@media (max-width: 900px) {
  .rvp-head { grid-template-columns: 1fr; gap: var(--s-4); }
}

.rvp-score { text-align: center; }
.rvp-avg { display: block; font-size: 2.6rem; font-weight: 700; line-height: 1; font-family: var(--font-display); }
.rvp-count { display: block; margin-block-start: 2px; font-size: var(--fs-caption); color: var(--c-ink-600); }

/* One row per star. Bars are a PERCENTAGE of the total, so the shape reads
   the same whether there are twelve reviews or twelve hundred; the count
   beside it supplies the number a bar cannot. */
.rvp-spread { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.rvp-spread li { display: grid; grid-template-columns: 3ch minmax(0, 1fr) 4ch; align-items: center; gap: var(--s-3); }
.rvp-spread-star { font-size: var(--fs-caption); color: var(--c-ink-600); white-space: nowrap; }
.rvp-spread-n { font-size: var(--fs-caption); color: var(--c-ink-600); text-align: end; font-variant-numeric: tabular-nums; }
.rvp-bar { block-size: 8px; border-radius: 999px; background: var(--c-surface-200); overflow: hidden; }
.rvp-bar > span { display: block; block-size: 100%; background: var(--c-accent-600); border-radius: inherit; }

.rvp-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-5); }
.rvp-item { padding-block-end: var(--s-5); border-block-end: 1px solid var(--c-border-200); }
.rvp-item:last-child { border-block-end: 0; padding-block-end: 0; }

.rvp-item-head { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; margin-block-end: var(--s-2); }
.rvp-author { font-weight: 600; font-size: var(--fs-small); }

/* A statement of fact — every review here is tied to a real order — rather
   than a badge the shop awarded itself. */
.rvp-verified {
  font-size: var(--fs-caption);
  color: var(--c-success-600);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.rvp-verified::before { content: '✓'; font-weight: 700; }

.rvp-date { font-size: var(--fs-caption); color: var(--c-ink-600); margin-inline-start: auto; }

.rvp-title { margin: 0 0 var(--s-1); font-weight: 600; }
/* pre-wrap keeps a customer's line breaks; anywhere stops one long unbroken
   string stretching the page sideways. */
.rvp-body { margin: 0; color: var(--c-ink-600); white-space: pre-wrap; overflow-wrap: anywhere; }

/* "Show more reviews". Centred and full-width on a phone, where a small button
   at the end of a long list is easy to miss and hard to hit. */
.rvp-more { margin-block-start: var(--s-5); }
.rvp-more:empty { margin-block-start: 0; }
.rvp-more .btn { inline-size: 100%; }
@media (min-width: 560px) {
  .rvp-more .btn { inline-size: auto; }
}
.rvp-left { opacity: .7; font-variant-numeric: tabular-nums; }

/* The date sits on its own line on a narrow screen rather than squeezing the
   name — margin-inline-start:auto pushes it to the edge on wide ones. */
@media (max-width: 480px) {
  .rvp-item-head { gap: var(--s-2); }
  .rvp-date { margin-inline-start: 0; inline-size: 100%; }
}

/* ---- product gallery ------------------------------------------------------

   One large photograph with a strip of thumbnails beneath it. The strip is
   only rendered when there is more than one photo — a row containing a single
   thumbnail is a control that cannot do anything.
   ------------------------------------------------------------------------ */

.detail-gallery { display: grid; gap: var(--s-3); align-content: start; }

.pg-thumbs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Auto-fit rather than a fixed count: four photos fill the row, two sit at
     a sensible size instead of stretching to half the page each. */
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--s-2);
}

.pg-thumb {
  all: unset;
  cursor: pointer;
  display: block;
  aspect-ratio: 1;
  border-radius: var(--r-md);
  overflow: hidden;
  /* Transparent rather than none, so the box does not resize by two pixels
     when it becomes the selected one. */
  border: 2px solid transparent;
  background: var(--c-surface-200);
  transition: border-color var(--d-fast) ease, opacity var(--d-fast) ease;
  opacity: .72;
}
.pg-thumb img { inline-size: 100%; block-size: 100%; object-fit: cover; display: block; }

.pg-thumb:hover { opacity: 1; }
.pg-thumb:focus-visible { outline: 2px solid var(--c-primary-500); outline-offset: 2px; }
.pg-thumb.on { border-color: var(--c-primary-600); opacity: 1; }

/* On a phone the strip scrolls sideways instead of wrapping to three rows and
   pushing the price below the fold. */
@media (max-width: 560px) {
  .pg-thumbs {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Room for the focus ring, which would otherwise be clipped by the
       scroll container. */
    padding-block-end: 4px;
    -webkit-overflow-scrolling: touch;
  }
  .pg-thumbs li { flex: 0 0 72px; scroll-snap-align: start; }
}

@media (prefers-reduced-motion: reduce) {
  .pg-thumb { transition: none; }
}

/* Past the preview the list scrolls inside itself instead of pushing the
   footer further away with every press. Capped in vh so it is never taller
   than the screen it is being read on. */
.rvp-list.is-scrollable {
  max-block-size: min(70vh, 620px);
  overflow-y: auto;
  /* Room for the scrollbar so text does not sit under it. */
  padding-inline-end: var(--s-3);
  /* A scroll container clips focus rings on its first and last items. */
  scroll-padding-block: var(--s-2);
  overscroll-behavior: contain;
}

/* A hairline that says the panel scrolls, without a scrollbar being the only
   clue on platforms that hide them until you touch the surface. */
.rvp-list.is-scrollable {
  border-block-start: 1px solid var(--c-border-200);
  padding-block-start: var(--s-4);
}

/* The chip beside the title repeats what the panel now says. On a phone the
   two are within a screen of each other, so only one of them is useful. */
@media (max-width: 900px) {
  .rvp-chip { display: none; }
}
