@charset "UTF-8";
/* =============================================================
   ณัฐธนิสชาการค้า.com — Design System
   Palette : white / sky / navy
   Type    : Kanit
   ============================================================= */

/* ---------- 1. Tokens ---------- */
:root {
  /* Brand — navy -> sky ramp */
  --navy-900: #0b1f3a;
  --navy-800: #0f2d54;
  --navy-700: #143d6f;
  --navy-600: #1a4f8f;
  --blue-600: #0369a1;
  --blue-500: #0284c7;
  --sky-400:  #38bdf8;
  --sky-300:  #7dd3fc;
  --sky-200:  #bae6fd;
  --sky-100:  #e0f2fe;
  --sky-50:   #f0f9ff;

  /* Neutrals — never pure black/white */
  --ink:      #0a1626;
  --ink-2:    #33465e;
  --ink-3:    #5b6f89;
  --line:     #dbe6f2;
  --line-2:   #eaf1f8;
  --surface:  #ffffff;
  --page:     #f4f7fb;
  --shell:    #fafcfe;

  /* Semantic */
  --ok:    #0f766e;
  --warn:  #b45309;
  --err:   #b91c1c;
  --ok-bg:   #ecfdf5;
  --warn-bg: #fffbeb;
  --err-bg:  #fef2f2;

  /* Radius — nested: outer > inner */
  --r-shell: 32px;
  --r-xl: 24px;
  --r-lg: 18px;
  --r-md: 12px;
  --r-sm: 8px;
  --r-pill: 999px;

  /* Elevation — blue-tinted, never grey */
  --sh-1: 0 1px 2px rgba(11,31,58,.06), 0 1px 3px rgba(11,31,58,.04);
  --sh-2: 0 4px 12px rgba(11,31,58,.06), 0 2px 4px rgba(11,31,58,.04);
  --sh-3: 0 12px 32px rgba(11,31,58,.10), 0 4px 8px rgba(11,31,58,.04);
  --sh-4: 0 24px 60px rgba(11,31,58,.14), 0 8px 16px rgba(11,31,58,.06);

  /* Motion */
  --e-out:  cubic-bezier(.22,1,.36,1);
  --e-io:   cubic-bezier(.65,0,.35,1);
  --e-soft: cubic-bezier(.4,0,.2,1);
  --t-fast: 160ms;
  --t-med:  260ms;
  --t-slow: 520ms;

  /* Layout */
  --wrap: 1240px;
  --gut: clamp(16px, 4vw, 40px);
  --nav-h: 72px;

  /* Z-scale */
  --z-base: 1;
  --z-sticky: 10;
  --z-nav: 20;
  --z-drawer: 30;
  --z-modal: 50;
  --z-toast: 60;
}

/* ---------- 2. Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
/* The UA rule for [hidden] is `display:none` at author-origin weight zero, so
   any inline `display:flex/grid` on the same element beats it and the element
   stays visible. Several elements here toggle via .hidden AND carry an inline
   display — this makes the attribute actually win. */
[hidden] { display: none !important; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 24px);
}
body {
  font-family: 'Kanit', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-weight: 300;
  font-size: 16px;
  line-height: 1.7;
  color: var(--ink);
  background: var(--page);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img, svg, video { display: block; max-width: 100%; }
img { height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; padding: 0; }
h1, h2, h3, h4 { font-weight: 500; line-height: 1.2; letter-spacing: -.02em; }
:focus-visible {
  outline: 3px solid var(--blue-500);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}
::selection { background: var(--sky-200); color: var(--navy-900); }

/* ---------- 3. Layout ---------- */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gut); }

/* Was a floating "page card": margin 12px + radius + shadow, so the body
   showed through as a band around the whole site. Against the light sections
   that band was invisible, but beside the navy hero and the ticker it read as
   a white strip down both edges — the site looked broken rather than framed.
   Full-bleed instead. `overflow: clip` stays: it still contains the .aura
   blobs, which would otherwise widen the page. */
.shell {
  background: var(--shell);
  margin: 0;
  overflow: clip;
  position: relative;
}

.section { padding-block: clamp(64px, 9vw, 128px); position: relative; }
.section--tight { padding-block: clamp(40px, 6vw, 72px); }

/* ---------- 4. Typography ---------- */
.eyebrow {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--blue-600);
  display: flex;
  align-items: center;
  gap: 10px;
}
.eyebrow::before {
  content: '';
  width: 24px; height: 2px;
  background: var(--sky-400);
  border-radius: 2px;
  flex: none;
}
.eyebrow--center { justify-content: center; }

.display {
  font-size: clamp(2.1rem, 6.4vw, 4.6rem);
  font-weight: 500;
  line-height: 1.08;
  letter-spacing: -.035em;
}
.h2 { font-size: clamp(1.7rem, 4vw, 3rem); line-height: 1.15; }
.h3 { font-size: clamp(1.2rem, 2vw, 1.55rem); font-weight: 500; }
.lede {
  font-size: clamp(1rem, 1.4vw, 1.125rem);
  color: var(--ink-2);
  max-width: 62ch;
  font-weight: 300;
}
.muted { color: var(--ink-3); }
.tnum { font-variant-numeric: tabular-nums; }

/* Gradient ink for the emphasised clause */
.grad {
  background: linear-gradient(100deg, var(--blue-600), var(--sky-400) 70%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* De-emphasised trailing clause (reference does this) */
.ghost { color: var(--ink-3); opacity: .5; }

/* ---------- 5. Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 0 26px;
  border-radius: var(--r-pill);
  font-size: 15px;
  font-weight: 400;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-med) var(--e-soft),
              color var(--t-med) var(--e-soft),
              box-shadow var(--t-med) var(--e-soft),
              border-color var(--t-med) var(--e-soft);
}
.btn svg { width: 18px; height: 18px; flex: none; }

.btn--primary {
  background: var(--navy-800);
  color: #fff;
  box-shadow: var(--sh-2);
}
.btn--primary:hover { background: var(--blue-600); box-shadow: var(--sh-3); }

.btn--accent { background: var(--blue-600); color: #fff; box-shadow: var(--sh-2); }
.btn--accent:hover { background: var(--navy-800); box-shadow: var(--sh-3); }

.btn--ghost {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
}
.btn--ghost:hover { border-color: var(--blue-500); color: var(--blue-600); }

.btn--line { background: transparent; color: var(--ink-2); border: 1px solid var(--line); }
.btn--line:hover { background: var(--sky-50); color: var(--blue-600); border-color: var(--sky-300); }

/* 44, not the 40 this was. `.btn--sm` is the header CTA and the pair of
   buttons under the hero — small in weight, not in importance. */
.btn--sm { min-height: 44px; padding: 0 18px; font-size: 14px; }
.btn--block { width: 100%; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* Icon-only button — meets 44px target */
.icon-btn {
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  transition: color var(--t-fast) var(--e-soft),
              border-color var(--t-fast) var(--e-soft),
              background var(--t-fast) var(--e-soft);
}
.icon-btn:hover { color: var(--blue-600); border-color: var(--sky-300); background: var(--sky-50); }
.icon-btn svg { width: 19px; height: 19px; }

/* ---------- 6. Header — floating glass pill ----------
   The bar is a rounded, translucent pill that floats over the page rather
   than a full-width band. It sits on both dark heroes and plain light
   sections, so the surface stays opaque enough (.86) to keep text at
   contrast on either, and gains a visible border + deeper shadow once
   scrolled (`is-stuck`) so it never dissolves into a white section.
--------------------------------------------------------- */
.nav {
  position: sticky;
  top: 12px;
  z-index: var(--z-nav);
  padding-inline: clamp(10px, 2.4vw, 22px);
  margin-bottom: 12px;
}
.nav__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  max-width: var(--wrap);
  min-height: 64px;
  margin-inline: auto;
  padding: 8px 10px 8px 20px;
  border-radius: var(--r-pill);
  background: rgba(255,255,255,.86);
  backdrop-filter: blur(16px) saturate(1.6);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  border: 1px solid rgba(219,230,242,.9);
  box-shadow: 0 10px 30px rgba(11,31,58,.12);
  transition: background var(--t-med) var(--e-soft),
              border-color var(--t-med) var(--e-soft),
              box-shadow var(--t-med) var(--e-soft);
}
.nav.is-stuck .nav__in {
  background: rgba(255,255,255,.94);
  border-color: var(--line);
  box-shadow: 0 16px 40px rgba(11,31,58,.16);
}

/* `flex: 0 1 auto` + `min-width: 0`, not the `flex: none` this had. Sweeping
   320–400px in 5px steps showed the header pill overflowing every page below
   360, by 40px at 320 — and clearing 360 itself by a single pixel. One pixel
   is not a margin; if Kanit fails to load and a wider fallback steps in, the
   whole site scrolls sideways. Allowing the brand to shrink, and truncating
   its text rather than pushing the page open, is the guard no media query can
   provide because it does not depend on knowing the font in advance.
   min-height is the tap target: the emblem is 40px, and the link around it
   was 40px tall with it. */
.brand {
  display: flex;
  align-items: center;
  gap: 11px;
  flex: 0 1 auto;
  min-width: 0;
  min-height: 44px;
}
.brand__name, .brand__sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Bigger than the placeholder tile it replaced (34px). The real emblem is an
   outline, not a filled square, so at 34px it reads lighter than the text
   beside it and looks like an afterthought. */
.brand__mark { width: 40px; height: 40px; flex: none; object-fit: contain; }
.brand__mark--lg { width: 46px; height: 46px; }
.brand__txt { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.brand__name { font-size: 15px; font-weight: 500; letter-spacing: -.01em; color: var(--navy-900); }
.brand__sub { font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); font-weight: 400; }

.nav__links { display: flex; align-items: center; gap: 4px; }
.nav__link {
  position: relative;
  /* 10px, not 9 — at 9 the link measured 43px tall, one pixel short. */
  padding: 10px 14px;
  border-radius: var(--r-pill);
  font-size: 14.5px;
  font-weight: 300;
  color: var(--ink-2);
  transition: color var(--t-fast) var(--e-soft), background var(--t-fast) var(--e-soft);
}
.nav__link:hover { color: var(--navy-900); background: var(--sky-100); }
/* Active page reads as a solid navy pill — the strongest anchor in the bar,
   and legible whether the pill floats over a dark hero or a white section. */
.nav__link[aria-current="page"] { color: #fff; font-weight: 400; background: var(--navy-800); }
.nav__link[aria-current="page"]:hover { color: #fff; background: var(--navy-700); }
.nav__actions { display: flex; align-items: center; gap: 8px; flex: none; }
.nav .icon-btn { background: transparent; }
.nav .icon-btn:hover { background: var(--sky-50); }

.nav__burger { display: none; }

@media (max-width: 980px) {
  .nav__links { display: none; }
  .nav__burger { display: grid; }
  .nav__cta { display: none; }
  .nav__in { padding-left: 14px; }
}

/* Small phones. At 320px the pill needed 359px and got 288, so every page
   scrolled sideways by 40px — the single worst thing on the site, and it was
   on all seven pages at once.
   Room is taken back from decoration, never from a target: the emblem drops
   to 34px, the gaps tighten, and the English tagline goes. "WHOLESALE
   TRADING" is a flourish; the shop's actual name is not, so that stays.
   The three round buttons keep their full 44px — they are the phone, the
   order list and the menu, which is the entire header on a phone.
   Measured after: 320px fits with 11px to spare. 380 rather than 360 as the
   cut-off so 375px phones (iPhone SE, 6/7/8 — still everywhere) get real
   slack instead of clearing by a hair. */
@media (max-width: 379.98px) {
  .nav { padding-inline: 8px; }
  .nav__in { gap: 8px; padding: 8px 6px 8px 10px; }
  .brand { gap: 8px; }
  .brand__mark { width: 34px; height: 34px; }
  .brand__name { font-size: 13.5px; }
  .brand__sub { display: none; }
  .nav__actions { gap: 4px; }
}

/* Mobile drawer — dark navigation rail.
   Slides in from the right on mobile with the same structure the desktop
   sidebar concept used: icon + label rows, then a persistent call button
   and opening hours pinned to the bottom (what wholesale buyers open the
   menu looking for). Dark panel so it reads as a separate layer over the
   page instead of a white sheet on white. */
.drawer {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  visibility: hidden;
  pointer-events: none;
}
.drawer.is-open { visibility: visible; pointer-events: auto; }
.drawer__scrim {
  position: absolute;
  inset: 0;
  background: rgba(11,31,58,.42);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--t-med) var(--e-soft);
}
.drawer.is-open .drawer__scrim { opacity: 1; }
.drawer__panel {
  position: absolute;
  inset: 0 0 0 auto;
  width: min(86vw, 320px);
  background: var(--navy-900);
  color: #dce9f7;
  padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 4px;
  transform: translateX(100%);
  transition: transform var(--t-slow) var(--e-out);
  overflow-y: auto;
}
.drawer.is-open .drawer__panel { transform: none; }
.drawer__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 16px;
}
.drawer__head .brand__name { color: #fff; font-size: 15.5px; }
.drawer__head .brand__sub { color: #8fb2d6; }
.drawer__panel .icon-btn {
  background: rgba(255,255,255,.06);
  border-color: rgba(255,255,255,.16);
  color: #cfe3f7;
}
.drawer__panel .icon-btn:hover {
  background: rgba(125,211,252,.16);
  border-color: var(--sky-300);
  color: #fff;
}
.drawer__link {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 48px;
  padding: 12px 14px;
  border-radius: var(--r-md);
  font-size: 15.5px;
  color: #b9d2ec;
  transition: background var(--t-fast) var(--e-soft), color var(--t-fast) var(--e-soft);
}
.drawer__link svg { width: 19px; height: 19px; flex: none; opacity: .85; }
.drawer__link:hover { background: rgba(255,255,255,.07); color: #fff; }
.drawer__link[aria-current="page"] { background: var(--blue-600); color: #fff; font-weight: 400; }
.drawer__link[aria-current="page"] svg { opacity: 1; }
.drawer__foot {
  margin-top: auto;
  padding-top: 20px;
  display: grid;
  gap: 10px;
  border-top: 1px solid rgba(255,255,255,.12);
}
/* Ghost/line buttons live on a dark panel here — re-tint so they stay legible */
.drawer__foot .btn--ghost {
  background: var(--sky-400);
  border-color: transparent;
  color: var(--navy-900);
  font-weight: 500;
}
.drawer__foot .btn--ghost:hover { background: var(--sky-300); color: var(--navy-900); }
.drawer__hours {
  text-align: center;
  font-size: 12.5px;
  color: #8fb2d6;
  padding-bottom: 2px;
}

/* ---------- 7. Footer ---------- */
.foot { background: var(--navy-900); color: #cdddef; padding-block: clamp(48px, 6vw, 76px) 28px; }
.foot a { transition: color var(--t-fast) var(--e-soft); }
.foot a:hover { color: var(--sky-300); }
.foot__grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr 1.3fr;
  gap: clamp(28px, 4vw, 56px);
}
@media (max-width: 900px) { .foot__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .foot__grid { grid-template-columns: 1fr; } }
.foot__brand .brand__name { color: #fff; }
.foot__brand .brand__sub { color: #7f9cbd; }
.foot__h {
  font-size: 11px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: #6f8dae;
  font-weight: 500;
  margin-bottom: 16px;
}
/* The gap came down from 11px to pay for the taller links below, so the
   footer grows by about 13px per row rather than 26. */
.foot__list { display: grid; gap: 4px; font-size: 14.5px; font-weight: 300; }
/* "ชา" and "นม" measured 15x22 and 17x22 — two-character Thai words, the
   smallest targets on the whole site. Making the link fill its row fixes the
   width for free (a short category becomes as easy to hit as a long one) and
   min-height fixes the rest. In the contact column the link sits beside an
   icon, so it stays content-width there and only gains the height. */
.foot__list a { display: flex; align-items: center; min-height: 44px; }
.foot__bar {
  margin-top: clamp(36px, 5vw, 56px);
  padding-top: 22px;
  border-top: 1px solid rgba(255,255,255,.09);
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
  color: #7f9cbd;
}
.foot__social { display: flex; gap: 9px; }
.foot__social a {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255,255,255,.14);
  transition: background var(--t-fast) var(--e-soft), border-color var(--t-fast) var(--e-soft);
}
.foot__social a:hover { background: rgba(255,255,255,.09); border-color: var(--sky-400); }
.foot__social svg { width: 18px; height: 18px; }

/* Legal warning block — required for controlled goods */
.legal-note {
  margin-top: 30px;
  padding: 16px 18px;
  border: 1px solid rgba(255,255,255,.14);
  border-radius: var(--r-md);
  background: rgba(255,255,255,.03);
  font-size: 12.5px;
  line-height: 1.65;
  color: #a8c2dd;
  display: flex;
  gap: 12px;
}
.legal-note svg { width: 18px; height: 18px; flex: none; margin-top: 3px; color: var(--sky-400); }

/* ---------- 8. Kinetic type reveal ---------- */
.reveal { display: inline-block; overflow: hidden; vertical-align: bottom; }
.reveal > span {
  display: inline-block;
  transform: translateY(105%);
  transition: transform 760ms var(--e-out);
  transition-delay: calc(var(--i, 0) * 42ms);
}
.is-in .reveal > span { transform: none; }

/* Word-by-word fade (used on the long paragraph statement) */
.wordfade span {
  opacity: .16;
  transition: opacity 420ms var(--e-soft);
}
.wordfade span.lit { opacity: 1; }

/* Generic scroll-in */
[data-anim] { opacity: 0; transition: opacity 700ms var(--e-out), transform 700ms var(--e-out); }
[data-anim="up"]    { transform: translateY(26px); }
[data-anim="fade"]  { transform: none; }
[data-anim="left"]  { transform: translateX(-26px); }
[data-anim="right"] { transform: translateX(26px); }
[data-anim="zoom"]  { transform: scale(.94); }
[data-anim].is-in { opacity: 1; transform: none; }
[data-anim][data-delay] { transition-delay: var(--d); }


/* ---------- Dark hero (homepage) ----------
   The floating pill nav was designed to sit over a dark surface, so the
   homepage opens on one. The nav is pulled down over the hero by lifting
   .shell up by the nav's flow height (64px pill + 12px margin = 76px);
   the hero re-adds that as padding so nothing hides underneath.

   The neon strip directly below is already sky-300 — dark navy → bright
   sky → light page reads as one deliberate gradient down the page, which
   is why the rest of the site needs no re-toning.
--------------------------------------------------------- */
.has-dark-hero .shell { margin-top: -76px; }

.hero--dark {
  position: relative;
  z-index: 0;                 /* own stacking context — stays under the nav */
  overflow: clip;
  color: #eaf3fc;
  background: radial-gradient(120% 100% at 80% 0%, #164a86 0%, var(--navy-800) 42%, #081a30 100%);
  padding-block: calc(76px + clamp(40px, 6vw, 78px)) clamp(48px, 6vw, 84px);
}
/* faint blueprint grid, masked so it fades out before the edges */
.hero--dark::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255,255,255,.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.045) 1px, transparent 1px);
  background-size: 56px 56px;
  -webkit-mask-image: radial-gradient(80% 70% at 50% 20%, #000, transparent 75%);
  mask-image: radial-gradient(80% 70% at 50% 20%, #000, transparent 75%);
  pointer-events: none;
}
.hero__glow {
  position: absolute;
  width: 620px; height: 620px;
  border-radius: 50%;
  filter: blur(110px);
  background: rgba(56,189,248,.26);
  top: -260px; right: -120px;
  pointer-events: none;
  animation: heroDrift 16s var(--e-soft) infinite;
}
.hero__glow--2 {
  width: 520px; height: 520px;
  background: rgba(3,105,161,.34);
  top: auto; bottom: -320px; left: -160px; right: auto;
  animation-delay: 5s;
}
@keyframes heroDrift {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(-34px, 26px); }
}

.hero--dark .eyebrow { color: var(--sky-200); }
.hero--dark .display { color: #fff; }
.hero--dark .lede { color: #bcd4ec; }
.hero--dark .ghost { color: var(--sky-200); opacity: .45; }
.hero--dark .grad {
  background: linear-gradient(100deg, var(--sky-300), #a5f3fc 45%, var(--sky-400));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
/* On navy, sky reads as the primary action and white-on-blue would not */
.hero--dark .btn--primary {
  background: var(--sky-400);
  color: var(--navy-900);
  box-shadow: 0 10px 26px rgba(56,189,248,.3);
}
.hero--dark .btn--primary:hover { background: var(--sky-300); color: var(--navy-900); }
.hero--dark .btn--ghost {
  background: rgba(255,255,255,.09);
  border-color: rgba(255,255,255,.24);
  color: #fff;
}
.hero--dark .btn--ghost:hover {
  background: rgba(255,255,255,.16);
  border-color: var(--sky-300);
  color: #fff;
}

.hero__in { position: relative; z-index: 1; }
.hero__cols {
  display: grid;
  grid-template-columns: 1.1fr .9fr;
  gap: clamp(28px, 4vw, 56px);
  align-items: center;
  margin-top: 30px;
}
.hero__stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-top: 34px;
  max-width: 520px;
}
.hero__stat {
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: var(--r-lg);
  padding: 16px 14px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.hero__stat b { display: block; font-size: 1.6rem; font-weight: 500; color: #fff; letter-spacing: -.02em; }
.hero__stat span { font-size: 12.5px; color: #a9c6e4; line-height: 1.4; }

/* Tilted glass product stack — decorative, hidden from the a11y tree */
.hero__stack { position: relative; perspective: 1200px; min-height: 360px; }
.hero__stack .stack__card {
  position: absolute;
  width: 186px;
  padding: 12px;
  border-radius: var(--r-lg);
  background: rgba(255,255,255,.1);
  border: 1px solid rgba(255,255,255,.18);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 24px 60px rgba(4,16,32,.4);
  transition: transform 380ms var(--e-out);
}
.hero__stack .stack__card:nth-child(1) { top: 2%;  left: 0;   transform: rotateY(16deg) rotateZ(-6deg); }
.hero__stack .stack__card:nth-child(2) { top: 28%; left: 32%; transform: rotateY(10deg) rotateZ(3deg); z-index: 2; }
.hero__stack .stack__card:nth-child(3) { top: 56%; left: 5%;  transform: rotateY(14deg) rotateZ(-3deg); }
.hero__stack .stack__card:hover { transform: translateY(-10px) rotateY(0deg) rotateZ(0deg); }
.stack__media {
  display: block;
  aspect-ratio: 1;
  border-radius: var(--r-md);
  overflow: hidden;
}
.stack__media .pcard__art { width: 100%; height: 100%; }
.stack__media .pcard__art > svg { width: 38%; }
.stack__media > img { width: 100%; height: 100%; object-fit: cover; display: block; }
.stack__name { display: block; font-size: 13px; color: #fff; margin-top: 9px; line-height: 1.35; }
.stack__unit { display: block; font-size: 11.5px; color: var(--sky-200); }

@media (max-width: 900px) {
  .hero__cols { grid-template-columns: 1fr; }
  .hero__stack { min-height: 290px; }
  .hero__stack .stack__card { width: 152px; }
}
@media (max-width: 560px) {
  .hero__stats { grid-template-columns: 1fr; gap: 10px; }
  .hero__stat { display: flex; align-items: baseline; gap: 10px; }
  .hero__stat b { font-size: 1.3rem; }
}

/* Proof items on the blue panel — what the shop demonstrably does, used
   where a testimonial would go until real ones exist. */
.proof-item {
  display: flex;
  gap: 13px;
  align-items: flex-start;
  padding: 15px 17px;
  border-radius: var(--r-lg);
  background: rgba(255,255,255,.1);
  border: 1px solid rgba(255,255,255,.16);
}
.proof-item > svg { width: 19px; height: 19px; color: var(--sky-300); flex: none; margin-top: 2px; }
.proof-item b { display: block; font-size: 14.5px; font-weight: 500; }
.proof-item span span { font-size: 12.5px; color: #bfe0f5; }

/* ---------- FAQ ----------
   <details>/<summary> on purpose: open/close works with zero JS, keyboard
   and screen readers get it for free, and Ctrl+F still finds text inside a
   closed panel in Chrome. Do not "upgrade" this to a JS accordion.

   Only questions with a confirmed answer belong here. Anything the shop
   owner has not answered yet stays in the commented block in index.html —
   an invented answer in an FAQ is worse than a missing one, because the
   FAQPage schema hands it to Google as fact.
------------------------------------------------------------------ */
.faq { display: grid; gap: 10px; max-width: 780px; margin-inline: auto; }
.faq details {
  background: var(--surface);
  /* --line not --line-2: a white card on the near-white page (#fafcfe) with a
     #eaf1f8 edge is invisible, so short cards read as holes in the grid. */
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  transition: border-color var(--t-med) var(--e-soft), box-shadow var(--t-med) var(--e-soft);
}
.faq details[open] { border-color: var(--sky-300); box-shadow: var(--sh-2); }
.faq summary {
  padding: 17px 22px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 400;
  display: flex;
  align-items: center;
  gap: 12px;
  list-style: none;
  min-height: 44px;          /* WCAG 2.5.5 touch target */
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:focus-visible { outline: 2px solid var(--blue-500); outline-offset: -2px; }
.faq summary::after {
  content: "";
  width: 9px; height: 9px;
  border-right: 2px solid var(--ink-3);
  border-bottom: 2px solid var(--ink-3);
  transform: rotate(45deg);
  margin-left: auto;
  flex: none;
  transition: transform var(--t-med) var(--e-soft);
}
.faq details[open] summary::after { transform: rotate(-135deg); }
.faq__a {
  padding: 0 22px 19px;
  font-size: 14px;
  line-height: 1.8;
  color: var(--ink-2);
  font-weight: 300;
}
.faq__a a { color: var(--blue-600); }
.faq__a strong { font-weight: 500; color: var(--ink); }

/* ---------- Catalogue page header ----------
   The homepage hero can be tall — visitors there are reading. This page is
   a tool: the shop owner opens it on a phone to check "do they stock this,
   what's a case". So the heading stays small and search / categories / the
   item count all sit above the fold. Do not grow this block.
------------------------------------------------------------------ */
.plist-head__top {
  display: flex;
  gap: 24px;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
}
.plist-head__h {
  font-size: clamp(1.6rem, 3vw, 2.2rem);
  font-weight: 300;
  letter-spacing: -.02em;
  line-height: 1.2;
}
.plist-head__h b { font-weight: 500; }
.plist-head__meta {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--ink-3);
  margin-top: 10px;
}
.plist-head__meta b { color: var(--ink); font-weight: 500; }
.plist-head__tools {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: flex-end;
  flex: 1 1 420px;
  justify-content: flex-end;
}
.plist-head__tools .field--q { flex: 1 1 240px; max-width: 320px; }
.plist-head__tools .field--sort { flex: 0 1 180px; }

/* Under 860px the tools wrap onto their own row. `justify-content: flex-end`
   is right while they sit beside the heading, but once wrapped it strands the
   fields against the right edge: measured at 390px the heading and the
   category bar start at 16px while the search field started at 54px and the
   sort field at 194px — four different left edges down one column. It was
   worst on a tablet, where 768px put the search field 223px in from the
   heading, floating in the middle of nowhere.
   So below the wrap point, stop right-aligning and let each field own a full
   row, which lines them up with the heading and the category bar above and
   below them. 860 is where the measurement showed the row breaking, not a
   round number picked by feel. */
@media (max-width: 859px) {
  .plist-head__tools { justify-content: flex-start; }
  .plist-head__tools .field--q,
  .plist-head__tools .field--sort { flex: 1 1 100%; max-width: none; }
}
/* ---------- Catalogue: filter column + product column ----------
   Categories were a wrapping chip row. At 1440px eleven chips wrapped onto a
   second line carrying a single orphan ("บุหรี่"), and the row grows every
   time a category is added. A vertical list cannot run out of horizontal
   room, so this holds up if the catalogue outgrows 44 items.
------------------------------------------------------------------ */
.plist-split {
  display: grid;
  grid-template-columns: 232px 1fr;
  gap: 28px;
  align-items: start;
}
.plist-main { min-width: 0; }

.pfilter { position: relative; }

/* The filter column had no surface of its own: heading, category rows and
   the price note sat straight on the page background while the product grid
   beside it was a wall of cards. The column read as loose text that had
   come unstuck rather than as a control panel.

   Same fix, and the same reasoning, as the privacy-policy document card —
   --line rather than --line-2, because a white panel on the near-white page
   (#fafcfe) with a #eaf1f8 edge has no visible boundary at all. Matches
   .vcard so the page holds together instead of growing a third box style.

   Sticky: the column is far shorter than the grid, so it would otherwise
   scroll out of sight and leave no way back to the categories without
   scrolling all the way up. 88px clears the floating navbar, same offset
   the privacy-policy index uses. */
@media (min-width: 901px) {
  .pfilter__body {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-xl);
    padding: 20px 18px 18px;
    position: sticky;
    top: 88px;
  }
}
.pfilter__h {
  font-size: 11.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 10px;
}
.pfilter__rule { height: 1px; background: var(--line); margin: 16px 0 14px; }

/* One category row. A <button>, not a link: it filters in place, it does not
   navigate. 40px tall rather than the chip's 44px — in a vertical list the
   whole row is the target, so the width does the work the height used to. */
.pcat {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  padding: 8px 11px;
  border: none;
  border-radius: var(--r-sm);
  background: none;
  font: inherit;
  font-size: 14px;
  font-weight: 300;
  color: var(--ink-2);
  text-align: left;
  cursor: pointer;
  transition: background var(--t-fast) var(--e-soft), color var(--t-fast) var(--e-soft);
}
.pcat:hover { background: var(--sky-50); color: var(--blue-600); }
.pcat[aria-pressed="true"] { background: var(--navy-800); color: #fff; }
.pcat > svg { width: 13px; height: 13px; flex: none; }
.pcat__n { margin-left: auto; font-size: 12px; color: var(--ink-3); }
.pcat[aria-pressed="true"] .pcat__n { color: var(--sky-200); }
.pcat:focus-visible { outline: 2px solid var(--blue-500); outline-offset: 2px; }

/* Desktop: the panel is simply open, and the toggle does not exist. */
.pfilter__toggle { display: none; }

@media (max-width: 900px) {
  .plist-split { grid-template-columns: 1fr; gap: 18px; }
  /* Stacked, an open category column pushes the first product ~848px down —
     worse than the chip row it replaced. Collapsed to a button instead, so
     the products stay near the top where a phone user expects them. */
  .pfilter__toggle {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    min-height: 46px;
    padding: 11px 15px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
    font: inherit;
    font-size: 14.5px;
    font-weight: 300;
    color: var(--ink);
    cursor: pointer;
  }
  .pfilter__toggle > svg:first-child { width: 17px; height: 17px; color: var(--blue-600); flex: none; }
  .pfilter__caret { width: 16px; height: 16px; margin-left: auto; color: var(--ink-3); transition: transform var(--t-med) var(--e-soft); }
  .pfilter__toggle[aria-expanded="true"] .pfilter__caret { transform: rotate(180deg); }
  /* Below 900px the panel is already inside the toggle's own bordered box,
     so it takes the card treatment's padding but not a second border. */
  .pfilter__body {
    display: none;
    margin-top: 10px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    padding: 14px 15px 15px;
  }
  .pfilter[data-open="true"] .pfilter__body { display: block; }
  .pfilter__h { display: none; }
}

/* Explains up front why some rows carry no price. Without it the first
   reaction to a price-less card is "the site is broken" rather than "this
   shop follows the law".

   Collapsed to a single pill: the full legal wording ran three lines across
   the page, directly between the filters and the first product. The summary
   line answers the "is this broken?" question on its own; the statute text
   stays one click away, and in full at the footer of every page. It is
   shortened here, never dropped. */
.price-note {
  display: inline-flex;
  gap: 9px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 18px;
  padding: 9px 15px;
  border-radius: var(--r-pill);
  background: var(--sky-50);
  border: 1px solid var(--sky-200);
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-2);
  font-weight: 300;
}
.price-note > svg { width: 17px; height: 17px; color: var(--blue-600); flex: none; }
.price-note b { font-weight: 500; color: var(--ink); }
.price-note__more {
  position: relative;
  font: inherit;
  font-size: 12.5px;
  color: var(--blue-600);
  background: none;
  border: none;
  border-bottom: 1px solid currentColor;
  padding: 0;
  cursor: pointer;
  white-space: nowrap;
}
.price-note__more:hover { color: var(--navy-700); }
/* 22px tall as drawn, and it sits inside a sentence, so padding would open a
   gap in the line. Invisible overlay to 44 instead. */
.price-note__more::before { content: ''; position: absolute; inset: -12px -6px; }
.price-note__full {
  margin-top: 12px;
  max-width: 82ch;
  font-size: 13px;
  line-height: 1.75;
  color: var(--ink-2);
  font-weight: 300;
}

/* In the 232px filter column there is no room for a pill, and no need for
   one: it is no longer sitting between the filters and the products, so it
   does not have to announce itself. Plain small print under the categories. */
.pfilter .price-note {
  display: block;
  margin-top: 0;
  padding: 0;
  border: none;
  background: none;
  border-radius: 0;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--ink-3);
}
.pfilter .price-note > svg { display: none; }
.pfilter .price-note b { color: var(--ink-2); }
.pfilter .price-note__more { margin-top: 6px; display: inline-block; }
.pfilter .price-note__full { margin-top: 9px; font-size: 12.5px; }

/* ---------- Verifiable-fact card ----------
   A claim a visitor can check by phone, by map, or by walking in. Built for
   /reviews/ to stand in for testimonials, and reusable anywhere a page needs
   to earn trust without quoting someone. Nothing goes in one of these that
   cannot survive being checked.
------------------------------------------------------------------ */
.vgrid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.vcard {
  background: var(--surface);
  /* --line not --line-2: a white card on the near-white page (#fafcfe) with a
     #eaf1f8 edge is invisible, so short cards read as holes in the grid. */
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 24px;
  transition: border-color var(--t-med) var(--e-soft), box-shadow var(--t-med) var(--e-soft);
}
.vcard:hover { border-color: var(--sky-300); box-shadow: var(--sh-3); }
.vcard__ic {
  width: 44px; height: 44px; border-radius: var(--r-md);
  display: grid; place-items: center; margin-bottom: 15px;
  background: var(--sky-100); color: var(--blue-600);
}
.vcard__ic > svg { width: 21px; height: 21px; }
.vcard h3 { font-size: 15.5px; font-weight: 500; }
.vcard p { font-size: 13.5px; line-height: 1.7; color: var(--ink-2); margin-top: 7px; font-weight: 300; }
.vcard a { color: var(--blue-600); }

/* Fact-forward variant. The original card was a heading plus a paragraph, so
   the phone number a visitor actually wants was buried mid-sentence in 13.5px
   text. Here the checkable value is the largest thing on the card and the
   sentence explains it underneath — same information, opposite order.
   .vcard--fact is additive: cards without it keep the old treatment. */
.vcard--fact { display: flex; flex-direction: column; }
.vcard--fact .vcard__ic { width: 38px; height: 38px; margin-bottom: 16px; }
.vcard--fact .vcard__ic > svg { width: 18px; height: 18px; }
.vcard__lab {
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  display: block;
}
.vcard__big {
  display: block;
  font-size: 21px;
  font-weight: 500;
  color: var(--navy-900);
  line-height: 1.25;
  letter-spacing: -.01em;
  margin-top: 6px;
}
.vcard__big a { color: inherit; text-decoration: none; }
.vcard__big a:hover { color: var(--blue-600); }
.vcard--fact p { font-size: 13px; margin-top: 10px; }
/* Pushed to the bottom so the four cards line their links up with each other
   even though the sentences above them differ in length. */
.vcard__go {
  margin-top: auto;
  padding-top: 14px;
  font-size: 13px;
  color: var(--blue-600);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* Last row of a card, so the extra height falls into the card's own
     padding and nothing below it moves. It measured 36px. */
  min-height: 44px;
}
.vcard__go > svg { width: 13px; height: 13px; transition: transform var(--t-med) var(--e-soft); }
.vcard:hover .vcard__go > svg { transform: translateX(3px); }

/* Full-bleed brand band for a page header. /reviews/ opened with black text
   on white and nothing else — 16% of the page carried any colour at all, and
   the first thing a visitor could click sat below the fold. */
.phead {
  background: linear-gradient(135deg, var(--navy-900), var(--navy-700));
  color: #fff;
  padding: clamp(28px, 4vw, 40px) 0 clamp(30px, 4vw, 42px);
}
.phead__crumb { font-size: 12.5px; color: #9fc4e8; margin-bottom: 15px; }
.phead__crumb a { color: var(--sky-300); }
.phead__eyebrow {
  font-size: 12px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--sky-300); display: flex; align-items: center; gap: 10px;
}
.phead__eyebrow::before { content: ""; width: 22px; height: 1px; background: var(--sky-300); }
.phead__h {
  font-size: clamp(1.9rem, 3.6vw, 2.7rem);
  font-weight: 300; line-height: 1.18; letter-spacing: -.02em;
  margin-top: 15px; max-width: 17ch;
}
.phead__h b { font-weight: 500; color: var(--sky-300); display: block; }
.phead__p {
  font-size: 14.5px; line-height: 1.75; color: #cfe3f6;
  font-weight: 300; margin-top: 16px; max-width: 48ch;
}
.phead__acts { display: flex; gap: 11px; flex-wrap: wrap; margin-top: 24px; }

/* Numbered steps on a dark surface (how to send us a review). Light-surface
   steps use .flow / .step instead — these two are not interchangeable. */
.rsteps { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin-top: 26px; }
.rstep { display: flex; gap: 13px; align-items: flex-start; }
.rstep__n {
  width: 30px; height: 30px; flex: none; border-radius: var(--r-pill);
  display: grid; place-items: center; font-size: 13px; font-weight: 500;
  background: rgba(255,255,255,.14); color: #fff;
  border: 1px solid rgba(255,255,255,.22);
}
.rstep b { display: block; font-size: 14.5px; font-weight: 500; }
.rstep span span { font-size: 12.5px; color: #bfe0f5; line-height: 1.65; display: block; margin-top: 3px; }

/* ---------- "Send us a review" aside ----------
   Was a full navy panel 620px tall — the largest block on /reviews/, given to
   the one message on the page addressed to people who have ALREADY bought.
   Someone reading a reviews page is deciding whether to buy at all, so this
   is a footnote to them, and it now looks like one. It also leaves the
   closing CTA as the page's only dark panel; two navy slabs stacked read as
   one shapeless 1,109px block.
------------------------------------------------------------------ */
.rask {
  display: flex;
  gap: 26px;
  align-items: center;
  flex-wrap: wrap;
  padding: 26px 28px;
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--surface);
}
.rask__body { flex: 1 1 380px; min-width: 0; }
.rask__h { font-size: 18px; font-weight: 400; color: var(--ink); line-height: 1.45; }
.rask__p { font-size: 14px; line-height: 1.75; color: var(--ink-2); font-weight: 300; margin-top: 7px; max-width: 56ch; }
/* The three steps were 30px numbered circles stacked as cards. At this size
   they are a single sentence with separators — same information, one line. */
.rask__steps {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 13px;
  font-size: 12.5px;
  color: var(--ink-3);
  font-weight: 300;
}
.rask__steps b { font-weight: 400; color: var(--ink-2); }
.rask__steps span:not(:last-child)::after { content: "→"; margin-left: 8px; color: var(--line); }
.rask__acts { display: flex; gap: 10px; flex-wrap: wrap; flex: none; }

/* Status pill — tells the visitor an empty section is waiting on something,
   not abandoned. */
.statusline {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 22px;
  background: var(--sky-50);
  border: 1px solid var(--sky-200);
  border-radius: var(--r-pill);
  padding: 8px 16px;
  font-size: 13px;
  color: var(--ink-2);
  line-height: 1.5;
}
.statusline b { font-weight: 500; color: var(--ink); }
.statusline::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--sky-400);
  flex: none;
}

/* ---------- Review cards ----------
   Masonry-ish columns so short and long reviews can sit together without
   leaving holes. The section renders nothing until real reviews exist —
   see assets/js/reviews.js.
------------------------------------------------------------------ */
.rgrid { columns: 3 290px; column-gap: 16px; }
.rcard {
  break-inside: avoid;
  margin-bottom: 16px;
  background: var(--surface);
  /* --line not --line-2: a white card on the near-white page (#fafcfe) with a
     #eaf1f8 edge is invisible, so short cards read as holes in the grid. */
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 24px;
  transition: border-color var(--t-med) var(--e-soft), box-shadow var(--t-med) var(--e-soft);
}
.rcard:hover { border-color: var(--sky-300); box-shadow: var(--sh-3); }
/* Every third card inverts so a long page does not read as one grey block. */
.rcard--navy { background: var(--navy-900); border-color: var(--navy-800); color: #fff; }
.rcard--navy .rcard__who { color: #9db8d4; }
.rcard__stars { display: flex; gap: 3px; margin-bottom: 12px; color: var(--blue-500); }
.rcard--navy .rcard__stars { color: var(--sky-300); }
.rcard__stars svg { width: 15px; height: 15px; }
.rcard__txt { font-size: 14.5px; line-height: 1.75; font-weight: 300; }
.rcard__foot { display: flex; align-items: center; gap: 11px; margin-top: 18px; }
.rcard__ava {
  width: 38px; height: 38px; border-radius: var(--r-pill); flex: none;
  display: grid; place-items: center;
  font-size: 14px; font-weight: 500; color: #fff;
}
.rcard__name { font-size: 14px; font-weight: 400; display: block; }
.rcard__who { font-size: 12.5px; color: var(--ink-3); }

/* ---------- Closing CTA card ----------
   The last block before the footer. It carries the contact facts (hours,
   address, own delivery) instead of a speed promise, because none of the
   delivery terms have been confirmed by the shop owner — see README.
------------------------------------------------------------------ */
.cta-card {
  background: var(--navy-900);
  color: #fff;
  border-radius: var(--r-xl);   /* NOT --r-2xl: no such token exists */
  padding: clamp(30px, 4vw, 52px);
  position: relative;
  overflow: hidden;
}
.cta-card::before {
  content: "";
  position: absolute;
  inset: -40% 40% 40% -20%;
  background: radial-gradient(circle, rgba(56,189,248,.22), transparent 65%);
  pointer-events: none;
}
.cta-card > * { position: relative; }
.cta-card__grid {
  display: grid;
  gap: 34px;
  grid-template-columns: 1.15fr .85fr;
  align-items: center;
}
.cta-facts { display: grid; gap: 10px; }
.cta-fact {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 13px 16px;
  border-radius: var(--r-lg);
  background: rgba(255,255,255,.09);
  border: 1px solid rgba(255,255,255,.15);
}
.cta-fact > svg { width: 18px; height: 18px; color: var(--sky-300); flex: none; margin-top: 2px; }
.cta-fact b { display: block; font-size: 14px; font-weight: 500; }
.cta-fact span span { font-size: 12.5px; color: #bfe0f5; }
@media (max-width: 860px) { .cta-card__grid { grid-template-columns: 1fr; } }

/* ---------- Promo banners ----------
   Scroll-snap strip. Deliberately no autoplay and no JS-driven transform:
   the browser's own scrolling is the animation, so it behaves identically
   with prefers-reduced-motion on, and a half-read slide never slides away
   mid-sentence. See the comment above the markup in index.html for the
   rules about what may and may not go on a slide.
--------------------------------------------------------- */
.promo { position: relative; }
.promo__track {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  border-radius: var(--r-xl);
}
.promo__track::-webkit-scrollbar { display: none; }
.promo__track:focus-visible { outline: 2px solid var(--blue-500); outline-offset: 4px; }

.promo__slide {
  scroll-snap-align: start;
  flex: 0 0 100%;
  min-height: 178px;
  border-radius: var(--r-xl);
  padding: 26px 28px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  color: #fff;
  position: relative;
  overflow: hidden;
}
/* Two at a time once there is room; three would shrink the text column
   below a comfortable measure. */
@media (min-width: 760px) { .promo__slide { flex-basis: calc(50% - 8px); } }

/* Each slide gets its own colour so swiping registers as a change even
   before the text is read. All four are drawn from the existing navy/blue
   ramp rather than new hues. */
.promo__slide--a { background: linear-gradient(135deg, var(--navy-800), var(--blue-600)); }
.promo__slide--b { background: linear-gradient(135deg, #0e7490, #0891b2); }
.promo__slide--c { background: linear-gradient(135deg, #0f766e, #10917f); }
.promo__slide--d { background: linear-gradient(135deg, var(--navy-900), var(--navy-700)); }

.promo__ic {
  width: 44px; height: 44px;
  border-radius: 13px;
  display: grid;
  place-items: center;
  background: rgba(255,255,255,.16);
  margin-bottom: 4px;
}
.promo__ic svg { width: 22px; height: 22px; }
.promo__h { font-size: clamp(17px, 2.3vw, 20px); font-weight: 500; line-height: 1.3; }
.promo__p { font-size: 13.5px; line-height: 1.6; color: rgba(255,255,255,.84); max-width: 40ch; }
.promo__cta {
  position: relative;
  margin-top: auto;
  padding-top: 12px;
  font-size: 13.5px;
  font-weight: 400;
  color: #fff;
  border-bottom: 1px solid rgba(255,255,255,.45);
  padding-bottom: 2px;
  transition: border-color var(--t-fast) var(--e-soft);
}
/* An underlined text link, so it cannot be padded to 44px without dragging
   the underline away from the words. The target grows invisibly instead —
   same trick as .promo__dot. It measured 38px tall; -3px each way clears 44.
   Nothing else in the slide is clickable, so the overlay cannot steal a tap
   from a neighbour. */
.promo__cta::before { content: ''; position: absolute; inset: -4px -4px; }
.promo__cta:hover { border-bottom-color: #fff; }

/* Arrows are desktop-only: on a phone the strip is swiped, and two more
   tap targets over a 100%-wide slide would sit on top of the text. */
.promo__nav {
  display: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  box-shadow: var(--sh-2);
  place-items: center;
  transition: background var(--t-fast), color var(--t-fast), opacity var(--t-fast);
}
.promo__nav svg { width: 18px; height: 18px; }
.promo__nav:hover { background: var(--sky-50); color: var(--blue-600); }
/* Disabled at the ends rather than hidden, so the control does not jump
   in and out of the layout as the strip scrolls. */
.promo__nav[disabled] { opacity: .32; cursor: default; }
.promo__nav[disabled]:hover { background: var(--surface); color: var(--ink-2); }
/* -16px keeps the circle's centre exactly where it sat at 38px, so growing
   the button to 44 does not shift the arrow relative to the strip. */
.promo__nav--prev { left: -16px; }
.promo__nav--next { right: -16px; }
@media (min-width: 900px) { .promo__nav { display: grid; } }

.promo__dots {
  display: flex;
  justify-content: center;
  /* 18px, not the 7px this started with. The gap is what the invisible hit
     area below is allowed to spread into, so widening it is what makes the
     dots tappable at all — see .promo__dot::before. */
  gap: 18px;
  margin-top: 14px;
}
.promo__dot {
  position: relative;
  width: 7px; height: 7px;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--line);
  cursor: pointer;
  transition: width var(--t-med) var(--e-out), background var(--t-med) var(--e-soft);
}
/* A 7px dot is a 7px target. Rather than draw a bigger dot — the row of small
   dots is the point, a row of big ones reads as buttons — the target grows
   invisibly to 35x25px, five times the area, without moving a single pixel of
   layout.
   The horizontal -9px exactly fills half the 18px gap on each side, so
   neighbouring hit areas meet edge to edge and never overlap: every tap
   resolves to one unambiguous dot. The vertical -14px is bounded by the 14px
   `margin-top` above and the margin of whatever follows, so the area stops
   exactly at the strip's bottom edge and cannot swallow a swipe.
   That lands on WCAG 2.5.8 AA (24x24 with non-overlapping spacing) rather
   than Apple's 44x44: reaching 44 needs 23px of extra height, and measured on
   a 360x640 phone that pushes the /products/ search field below the fold —
   which is the exact thing the cover's padding was tuned to prevent. */
.promo__dot::before {
  content: '';
  position: absolute;
  inset: -14px -9px;
}
/* The homepage promo strip only. Its dots are the last thing in their section,
   so the section that follows paints over the lower half of the hit area —
   probing it row by row, the target died 3px below the dot's centre and came
   out 21px tall instead of 35. 14px of padding keeps the area inside the
   strip's own box and it measures full height again.
   Deliberately not `.promo__dots` across the board: the brand strip below it
   and the /products/ cover already probe at the full 36px, and on the cover
   14px more would push the search field under the fold on a 360x640 phone —
   the one thing that section's padding was tuned to avoid. */
.promo > .promo__dots { padding-bottom: 14px; }
.promo__dot[aria-selected="true"] { width: 22px; background: var(--blue-600); }

/* ---------- Brand strip ----------
   Manufacturer banners + a row of brand marks. Shares .promo__nav and
   .promo__dots with the promo strip above rather than cloning them: both
   are the same control over the same scroll-snap mechanism, and two sets
   would drift apart the first time one is restyled.

   One slide at a time at every width. The promo strip shows two on desktop
   because its slides are text cards that stay readable at half width;
   these are 2.7:1 artwork whose type is baked in, and halving them makes
   the brand's own copy unreadable. */
.bstrip { position: relative; }
.bstrip__track {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  border-radius: var(--r-xl);
}
.bstrip__track::-webkit-scrollbar { display: none; }
.bstrip__track:focus-visible { outline: 2px solid var(--blue-500); outline-offset: 4px; }

.bstrip__slide {
  scroll-snap-align: start;
  flex: 0 0 100%;
  display: block;
  border-radius: var(--r-xl);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--surface);
  /* Follows the wider of the two files (1200x447). The other is 2.628:1 and
     loses ~2% top and bottom to the cover — margin, not artwork. */
  aspect-ratio: 1200 / 447;
  transition: border-color var(--t-fast) var(--e-soft), box-shadow var(--t-med) var(--e-out);
}
.bstrip__slide img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bstrip__slide:hover { border-color: var(--sky-300); box-shadow: var(--sh-3); }

.bstrip .promo__dots { margin-top: 14px; }

/* Cover variant — the same strip used as the top of /products/. Everything
   here is about giving back vertical space: that page puts its search field
   directly under the heading so a phone can reach it without scrolling, and
   every pixel this cover spends is a pixel the search field moves down.
   The artwork itself is not shortened; cropping it would cut the brands'
   own legal small print out of the edges. */
.bstrip--cover .promo__dots { margin-top: 10px; }
/* General sibling, not adjacent: the row of brand marks sits between the
   strip and the note. */
.bstrip--cover ~ .blogos { margin-top: 14px; }
.bstrip--cover ~ .bstrip__note { margin-top: 12px; font-size: 11.5px; }

/* Chip layout for the mark row when it rides on the cover: the name sits
   beside its disc instead of under it. That removes a whole text line —
   94px of row becomes 46px — and it reads as what the row now is on this
   page: a filter, not a badge.
   Only above 430px, though. Three chips need ~380px of run and a 360px
   phone gives 328, so below that they wrap to two lines and the layout
   ends up TALLER than the stacked one it was meant to shorten. Measured,
   not guessed: 106px wrapped vs 94px stacked. */
@media (min-width: 430px) {
.blogos--inline { gap: 10px; }
.blogos--inline .blogo {
  flex-direction: row;
  gap: 9px;
  padding: 5px 15px 5px 5px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
  transition: border-color var(--t-fast) var(--e-soft), box-shadow var(--t-med) var(--e-out);
}
.blogos--inline .blogo__disc { width: 34px; height: 34px; }
.blogos--inline .blogo__name { font-size: 13px; }
.blogos--inline .blogo:hover { border-color: var(--sky-300); box-shadow: var(--sh-3); }
/* The stacked row lifts its disc on hover; inside a chip that would slide
   the disc out of its own frame. */
.blogos--inline .blogo:hover .blogo__disc { transform: none; }
.blogos--inline .blogo:focus-visible .blogo__disc { outline: none; }
.blogos--inline .blogo:focus-visible { outline: 2px solid var(--blue-500); outline-offset: 2px; }
}

/* Trade-mark line. Small, but it belongs on the page rather than only in
   the README: it is the sentence that separates "we stock this brand" from
   "this brand endorses us". */
.bstrip__note {
  margin-top: 20px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink-3);
  text-align: center;
}

.blogos {
  list-style: none;
  padding: 0;
  margin: 22px 0 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 14px 26px;
}
.blogo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  color: inherit;
}
.blogo__disc {
  width: 64px; height: 64px;
  border-radius: 50%;
  overflow: hidden;
  background: #fff;
  border: 1px solid var(--line);
  display: grid;
  place-items: center;
  transition: transform var(--t-med) var(--e-out), border-color var(--t-fast) var(--e-soft), box-shadow var(--t-med) var(--e-out);
}
/* The marks are round and fill their own frame, so cover crops nothing —
   contain would leave a hairline of background inside the disc. */
.blogo__disc img { width: 100%; height: 100%; object-fit: cover; display: block; }
.blogo__name { font-size: 12.5px; color: var(--ink-2); }
.blogo:hover .blogo__disc { transform: translateY(-3px); border-color: var(--sky-300); box-shadow: var(--sh-3); }
.blogo:hover .blogo__name { color: var(--blue-600); }
.blogo:focus-visible { outline: none; }
.blogo:focus-visible .blogo__disc { outline: 2px solid var(--blue-500); outline-offset: 3px; }

/* ---------- Category rail ----------
   A swipeable row of category cards. Each carries its own hue from
   CATEGORIES, an icon, the real product count, and example brand names —
   "do you stock Singha / Nescafé" is the first thing a shop owner wants
   answered, and answering it before the click saves a page load.

   Deliberately icons, not product photos: no real photography exists yet,
   and a drawn bottle pretending to be a photo looks like a mistake. When
   real photos arrive, swap .cat-card__ic for an <img> of the same size.

   This used to drift on its own (a marquee, with the row cloned so the
   loop never showed a gap). It no longer does, for two reasons: a target
   that slides out from under the tap is hard to hit, and under
   prefers-reduced-motion the drift was suppressed anyway — leaving a row
   that was built to move sitting still. A row the visitor swipes behaves
   the same for everyone. The clone loop went with it: nothing wraps now,
   so there is no gap to paper over.
--------------------------------------------------------- */
.cat-rail {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x proximity;
  /* Room for the hover lift and the focus ring, which would otherwise be
     clipped by the scroll container. */
  padding-block: 8px;
  /* .cat-rail sits outside .wrap (it was full-bleed as a marquee), so the
     gutter is padding only — a negative margin here would push it past the
     viewport edge and give the whole page a horizontal scrollbar. */
  padding-inline: var(--gut);
  scrollbar-width: none;
}
.cat-rail::-webkit-scrollbar { display: none; }
.cat-rail__row {
  flex: none;
  display: flex;
  align-items: stretch;
  gap: 16px;
}
.cat-card { scroll-snap-align: start; }

/* Six open categories fit a desktop row, so centre them rather than
   left-packing with dead space on the right. */
@media (min-width: 1040px) {
  .cat-rail { overflow-x: visible; justify-content: center; }
}

.cat-card {
  flex: none;
  width: clamp(158px, 19vw, 214px);
  padding: 16px 15px 15px;
  text-align: center;
  border-radius: var(--r-xl);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--sh-1);
  transition: transform 280ms var(--e-out),
              box-shadow 280ms var(--e-out),
              border-color 220ms var(--e-soft);
}
.cat-card:hover {
  transform: translateY(-7px);
  box-shadow: var(--sh-3);
  border-color: var(--sky-300);
}
/* Fallback only — see home.js. Kept for a category added later that has no
   product photos yet. */
.cat-card__ic {
  width: 60px; height: 60px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  margin: 0 auto 11px;
}
/* Photo frame. Full card width rather than a centred tile: a 96px stamp
   floating in a 158px card reads as a badge, and the point of the change was
   to show stock, not to decorate.
   104px tall, not a 1:1 square like .pcard__media — square here would make
   each rail card about 250px tall, and this is a horizontal rail that has to
   stay swipeable on a phone.
   White background with `contain`, matching .pcard__media exactly: the files
   in assets/images/products/ are 800x800 packshots pre-padded on white, so
   contain shows no letterbox and cover would crop the cap off a bottle. The
   category's hue survives on the border, since it cannot be the background
   with a white-backed photo sitting on it. */
.cat-card__ph {
  display: grid;
  place-items: center;
  width: 100%;
  height: 104px;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: #fff;
  overflow: hidden;
}
.cat-card__ph > img {
  width: 100%; height: 100%;
  object-fit: contain;
  transition: transform var(--t-slow) var(--e-out);
}
.cat-card:hover .cat-card__ph > img { transform: scale(1.05); }
.cat-card__ic svg { width: 26px; height: 26px; }
.cat-card h3 { font-size: 15px; font-weight: 500; }
/* Brand line is a single line by design — the full list lives on /products/ */
.cat-card__brands {
  font-size: 12px;
  color: var(--ink-3);
  margin-top: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cat-card__n {
  display: inline-block;
  margin-top: 11px;
  font-size: 11.5px;
  color: var(--blue-600);
  background: var(--sky-50);
  border-radius: var(--r-pill);
  padding: 4px 11px;
}

/* ---------- How-to-order flow ----------
   Sits where the decorative icon row used to be. The dashed rule joins the
   three numbered markers on desktop; on mobile the row becomes a stack and
   the rule is dropped rather than redrawn vertically — a stacked list
   already reads as a sequence without it.
--------------------------------------------------------- */
.flow {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin-top: clamp(34px, 4vw, 48px);
  position: relative;
}
.flow::before {
  content: '';
  position: absolute;
  top: 31px;
  left: 16%;
  right: 16%;
  height: 2px;
  background: repeating-linear-gradient(90deg, var(--sky-300) 0 8px, transparent 8px 16px);
}
.step { text-align: center; padding-inline: 18px; position: relative; }
.step__n {
  width: 62px; height: 62px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  margin: 0 auto 18px;
  background: var(--surface);
  border: 2px solid var(--sky-300);
  color: var(--navy-800);
  font-size: 20px;
  font-weight: 500;
  box-shadow: 0 6px 18px rgba(56,189,248,.22);
  position: relative;
  z-index: 1;
}
.step--on .step__n { background: var(--navy-800); border-color: var(--navy-800); color: #fff; }
.step h3 { font-size: 17px; font-weight: 500; }
.step p {
  font-size: 14.5px;
  color: var(--ink-2);
  margin-top: 8px;
  line-height: 1.7;
  max-width: 30ch;
  margin-inline: auto;
}
.step__meta {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--blue-600);
  background: var(--sky-50);
  border-radius: var(--r-pill);
  padding: 5px 12px;
}
.step__meta svg { width: 14px; height: 14px; flex: none; }
.flow__cta {
  display: flex;
  gap: 11px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: clamp(32px, 4vw, 46px);
}
@media (max-width: 900px) {
  .flow { grid-template-columns: 1fr; }
  .flow::before { display: none; }
  .step { padding-block: 22px; border-bottom: 1px solid var(--line-2); }
  .step:last-child { border-bottom: 0; }
}

/* ---------- Utility ticker ----------
   Sits directly under the dark hero. The navy → blue → sky gradient is
   doing structural work: it carries the eye from the hero's dark surface
   down to the light page, which is why the rest of the site needs no
   re-toning after the hero went dark.

   Content is facts a shop owner wants before phoning, not decoration —
   so unlike the old strip it is NOT aria-hidden, and the phone number is
   a real tel: link. Only the duplicated copy (added by JS for the
   seamless loop) is hidden from assistive tech.

   It scrolls on its own AND by hand: native overflow scrolling for
   touch/trackpad, pointer-drag for mouse. Auto-advance yields the moment
   the visitor touches it and resumes when they stop.
--------------------------------------------------------- */
.ticker {
  background: linear-gradient(90deg, var(--navy-800), var(--blue-600) 55%, var(--sky-400));
  padding-block: 12px;
  display: flex;              /* track + its JS-added twin sit side by side */
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  /* html sets scroll-behavior:smooth for anchor jumps — the drift writes
     scrollLeft every frame and must never be animated on top of that. */
  scroll-behavior: auto;
  cursor: grab;
  /* Removed: mask-image faded the bar to transparent over the outer 5% at each
     end, so the page background showed through as a white glow on the left and
     right. It was meant to soften the scroll edges, but on a full-bleed bar it
     just reads as the bar being washed out. Solid gradient to both edges now. */
}
.ticker::-webkit-scrollbar { display: none; }
.ticker.is-dragging { cursor: grabbing; }
/* Half the gap as padding on each side, so the seam between the track and
   its twin is spaced exactly like every other gap and the loop is invisible. */
.ticker__track {
  display: flex;
  align-items: center;
  gap: 38px;
  width: max-content;
  flex: none;
  padding-inline: 19px;
}
.ticker__it {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 14.5px;
  font-weight: 300;
  color: #eaf6ff;
  white-space: nowrap;
  user-select: none;
}
.ticker__it b { font-weight: 500; color: #fff; }
.ticker__it > svg { width: 17px; height: 17px; color: var(--sky-300); flex: none; }
/* The tel: link stays tappable while the rest of the bar drags. Padding
   grows the hit area to a thumb-sized target; the matching negative margin
   keeps the bar itself the same height. */
a.ticker__it {
  padding-block: 10px;
  margin-block: -10px;
  transition: color var(--t-fast) var(--e-soft);
}
a.ticker__it:hover { color: #fff; }
a.ticker__it:hover b { text-decoration: underline; text-underline-offset: 3px; }
.ticker__live {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #4ade80;
  position: relative;
  flex: none;
}
.ticker__live::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid #4ade80;
  opacity: .6;
  animation: tickerPing 2.2s var(--e-out) infinite;
}
@keyframes tickerPing {
  0%        { transform: scale(.6); opacity: .7; }
  70%, 100% { transform: scale(1.5); opacity: 0; }
}

/* Avatar / tile rail */
.tile {
  width: clamp(64px, 8vw, 92px);
  aspect-ratio: 1;
  border-radius: var(--r-lg);
  overflow: hidden;
  flex: none;
  box-shadow: var(--sh-2);
  background: var(--sky-100);
  transition: transform var(--t-med) var(--e-out);
}
.tile:hover { transform: translateY(-6px) scale(1.05); }
.tile > * { width: 100%; height: 100%; object-fit: cover; }

/* ---------- 11. Bento ---------- */
.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}
.bento__cell {
  background: var(--surface);
  /* --line not --line-2: a white card on the near-white page (#fafcfe) with a
     #eaf1f8 edge is invisible, so short cards read as holes in the grid. */
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 26px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  position: relative;
  overflow: hidden;
  transition: border-color var(--t-med) var(--e-soft), box-shadow var(--t-med) var(--e-soft);
}
.bento__cell:hover { border-color: var(--sky-300); box-shadow: var(--sh-3); }
.bento__cell--w2 { grid-column: span 2; }
.bento__cell--w3 { grid-column: span 3; }
.bento__cell--h2 { grid-row: span 2; }
.bento__cell--navy { background: var(--navy-900); border-color: var(--navy-800); color: #fff; }
.bento__cell--navy .muted { color: #9db8d4; }
.bento__cell--blue {
  background: linear-gradient(150deg, var(--blue-600), var(--navy-800));
  border-color: var(--navy-700);
  color: #fff;
}
.bento__cell--blue .muted { color: #bfe0f5; }
@media (max-width: 900px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
  /* Both wide cells fill the two-column row; without this the LINE card
     sits alone and leaves a hole beside it. */
  .bento__cell--w3,
  .bento__cell--navy { grid-column: span 2; }
}
@media (max-width: 560px) {
  .bento { grid-template-columns: 1fr; }
  /* Single column — every span must reset, including the one widened
     for tablets above, or the cell spills into an implicit second column. */
  .bento__cell--w2,
  .bento__cell--w3,
  .bento__cell--navy { grid-column: span 1; }
  .bento__cell--h2 { grid-row: span 1; }
}
.bento__icon {
  width: 42px; height: 42px;
  border-radius: var(--r-md);
  display: grid; place-items: center;
  background: var(--sky-100);
  color: var(--blue-600);
  flex: none;
}
.bento__cell--navy .bento__icon,
.bento__cell--blue .bento__icon { background: rgba(255,255,255,.12); color: #fff; }
.bento__icon svg { width: 21px; height: 21px; }
.bento__num {
  font-size: clamp(1.9rem, 3.4vw, 2.9rem);
  font-weight: 500;
  letter-spacing: -.04em;
  line-height: 1;
}

/* ---------- 12. Product card ---------- */
.grid-products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 18px;
}

/* Catalogue row mode — the same .pcard markup laid out as a list row.
   Deliberately CSS-only: window.productCard() is shared with the homepage
   teaser, so restyling here leaves the homepage untouched.

   NO LONGER APPLIED — /products/ uses plain .grid-products tiles. Kept
   because the switch is one class on #grid in products/index.html and the
   trade-off could go either way again.

   The original argument for rows was that the catalogue had no photography,
   and 44 empty frames announce an unfinished site while a 62px thumbnail
   does not. That held while every card in a category drew the same repeated
   icon. It no longer does: productMedia() now draws the product's actual
   packaging (bottle / can / carton / pouch, read from `unit`), so the frame
   carries real information and reads as artwork rather than as a missing
   image. Photographs are still the goal — a product with `img` set skips
   the drawing entirely. */
/* auto-fit, not a fixed count: the filter column eats 260px, so the product
   column is ~940px at 1440 (two rows fit) but a single row below that. A
   hard `repeat(2, 1fr)` ran the names into their ellipsis on mid-size
   screens; a hard `1fr` made the page 1,400px longer on a desktop that had
   room to spare. 360px is the width at which a name like
   "น้ำดื่มเนสท์เล่ เพียวไลฟ์ 600 ml" still fits beside its price. */
.grid-products--rows {
  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
  gap: 11px;
}

.grid-products--rows .pcard {
  flex-direction: row;
  align-items: center;
  gap: 14px;
  padding: 13px 15px 13px 19px;
  border-radius: var(--r-lg);
}

/* The thumbnail is a category icon, so every water product carried the same
   bottle and every coffee the same cup — it never said how two products
   differ, it only made the card look like it had a photo when it does not.
   Reduced to a colour spine: the category is still readable at a glance,
   without pretending to be product photography. It also hands ~71px back to
   the product name, which is what pays for the enquiry button below.
   When real photos arrive this whole block goes away with the --rows class. */
.grid-products--rows .pcard__media {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 4px;
  height: auto;
  aspect-ratio: auto;
  border-radius: 0;
}
.grid-products--rows .pcard__art > svg { display: none; }
/* The 20+ badge is sized for a full-width tile and cannot sit on a 4px
   spine. The lock icon in the category list and the price-less foot already
   carry the message here. */
.grid-products--rows .badge--ctrl { display: none; }

/* `display: contents` dissolves the foot so that .pcard__price / .pcard__ask
   and .pcard__go become grid items of the body directly. That is what lets
   the price and the button occupy separate tracks without touching
   window.productCard(), which the homepage teaser also renders. */
.grid-products--rows .pcard__foot { display: contents; }

.grid-products--rows .pcard__body {
  display: grid;
  grid-template-columns: 1fr auto auto;
  grid-template-areas:
    "cat   price go"
    "name  price go"
    "meta  each  go";
  align-items: center;
  column-gap: 16px;
  row-gap: 1px;
  padding: 0;
  min-width: 0;
}
.grid-products--rows .pcard__cat   { grid-area: cat; }
.grid-products--rows .pcard__name  { grid-area: name; font-size: 15px; }
.grid-products--rows .pcard__meta  { grid-area: meta; }
.grid-products--rows .pcard__price,
.grid-products--rows .pcard__ask   { grid-area: price; justify-self: end; text-align: right; }
.grid-products--rows .pcard__each  { grid-area: each; justify-self: end; }

/* Was plain blue text with an arrow. The whole card has always been
   clickable, but nothing on screen said so — a filled pill does. 40px tall
   so it is a reliable finger target on a phone. */
.grid-products--rows .pcard__go {
  grid-area: go;
  align-self: center;
  min-height: 40px;
  padding: 0 17px;
  border-radius: var(--r-pill);
  background: var(--navy-800);
  color: #fff;
  font-size: 13px;
  justify-content: center;
  transition: background var(--t-fast) var(--e-soft);
}
.grid-products--rows .pcard__go > svg { display: none; }
.grid-products--rows .pcard:hover .pcard__go { background: var(--navy-700); }

/* Long brand names must not push the price column off the row. */
.grid-products--rows .pcard__name,
.grid-products--rows .pcard__meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Below 430px the three tracks are too tight for a one-line name. Giving the
   button its own full-width row fixes that but adds ~42px to all 28 cards —
   about 1,170px of extra scrolling on the page that most needs to be short.
   Cheaper: let the name wrap to two lines and keep everything on one row.
   The button loses its side padding, not its 40px height, so it stays a
   reliable finger target. */
@media (max-width: 430px) {
  .grid-products--rows .pcard { gap: 10px; }
  .grid-products--rows .pcard__name {
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.35;
    font-size: 14px;
  }
  .grid-products--rows .pcard__go { padding: 0 12px; font-size: 12.5px; }
  .grid-products--rows .pcard__cat { font-size: 10px; }
}

/* Homepage teaser: a fixed column count per breakpoint, with the overflow
   cards hidden, so every row is full — an auto-fill grid leaves a ragged
   gap whenever the card count doesn't divide evenly. Also stops the phone
   layout from running 3,400px deep for what is only a sample; the full
   catalogue is one tap away. */
.grid-products--teaser { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1100px) {
  .grid-products--teaser { grid-template-columns: repeat(3, 1fr); }
  .grid-products--teaser > :nth-child(n+7) { display: none; }
}
@media (max-width: 820px) {
  .grid-products--teaser { grid-template-columns: repeat(2, 1fr); }
  .grid-products--teaser > :nth-child(n+7) { display: none; }
}
@media (max-width: 560px) {
  .grid-products--teaser { grid-template-columns: 1fr; }
  .grid-products--teaser > :nth-child(n+5) { display: none; }
}
.pcard {
  background: var(--surface);
  /* --line not --line-2: a white card on the near-white page (#fafcfe) with a
     #eaf1f8 edge is invisible, so short cards read as holes in the grid. */
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  position: relative;          /* anchors the card-wide link overlay below */
  transition: border-color var(--t-med) var(--e-soft), box-shadow var(--t-med) var(--e-soft);
}
.pcard:hover { border-color: var(--sky-300); box-shadow: var(--sh-3); }
.pcard__media {
  /* Square, to match the product files: every image in
     assets/images/products/ is 800×800 with the packshot trimmed to its own
     edges and re-padded, so a square frame lets the product fill the width.
     A 4:3 frame here letterboxed each square image and shrank the bottle to
     about three quarters of the space it could have had. Was 16:9 originally,
     back when the artwork was one repeated category icon per category and did
     not deserve the room. */
  aspect-ratio: 1 / 1;
  position: relative;
  overflow: hidden;
  background: var(--sky-50);
  display: grid;
  place-items: center;
}
/* A real photo fills the frame; the icon placeholder sits centred on its
   category colour instead — an icon stretched to cover would look broken. */
.pcard__media > img {
  width: 100%; height: 100%;
  /* contain, not cover. These are white-background product cutouts, not
     scenic photography: cover would crop the cap off a tall bottle and the
     edges off a wide pouch. The files are pre-padded to a square on white,
     so contain leaves no visible letterboxing. Revisit if real in-situ
     photographs replace them — those do want cover. */
  object-fit: contain;
  background: #fff;
  transition: transform var(--t-slow) var(--e-out);
}
.pcard__art {
  width: 100%; height: 100%;
  display: grid;
  place-items: center;
}
.pcard__art > svg {
  width: 32%;
  height: auto;
  color: #fff;
  filter: drop-shadow(0 4px 10px rgba(0,0,0,.18));
  transition: transform var(--t-slow) var(--e-out);
}
.pcard:hover .pcard__media > img,
.pcard:hover .pcard__art > svg { transform: scale(1.06); }
.pcard__body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 7px; flex: 1; }
.pcard__cat { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--blue-600); font-weight: 400; }
.pcard__name { font-size: 15.5px; font-weight: 400; line-height: 1.4; color: var(--ink); }

/* Card-wide tap target. The <a> stays on the product name so assistive tech
   announces one link with a sensible name; only its ::after is stretched.
   Wrapping the whole <article> in an <a> instead would read the category,
   name, unit, SKU and price aloud as a single link label. */
.pcard__name a { color: inherit; text-decoration: none; }
.pcard__name a::after { content: ""; position: absolute; inset: 0; }
.pcard:hover .pcard__name a { color: var(--blue-600); }
.pcard__name a:focus-visible { outline: none; }
.pcard:focus-within { border-color: var(--sky-300); box-shadow: var(--sh-3); }
.pcard:focus-within .pcard__name a { outline: 2px solid var(--blue-500); outline-offset: 3px; }

.pcard__meta { font-size: 12.5px; color: var(--ink-3); }
/* Internal stock code — kept because search matches it, de-emphasised
   because no shop owner shops by SKU. */
.pcard__sku { font-size: 11px; opacity: .7; }
/* The division a wholesale buyer would otherwise do in their head. */
.pcard__each { font-size: 12.5px; color: var(--blue-600); font-weight: 400; }
.pcard__foot {
  margin-top: auto;
  padding-top: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.pcard__go {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  color: var(--blue-600);
  font-weight: 400;
  white-space: nowrap;
}
.pcard__go > svg { width: 14px; height: 14px; transition: transform var(--t-med) var(--e-soft); }
.pcard:hover .pcard__go > svg { transform: translateX(3px); }
.pcard__price { font-size: 17px; font-weight: 500; color: var(--navy-900); }
.pcard__price small { font-size: 12px; font-weight: 300; color: var(--ink-3); }
.pcard__ask { font-size: 13px; color: var(--ink-3); font-weight: 300; }

.badge {
  position: absolute;
  top: 10px; left: 10px;
  padding: 4px 11px;
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: 400;
  background: rgba(255,255,255,.94);
  color: var(--navy-800);
  backdrop-filter: blur(6px);
  box-shadow: var(--sh-1);
  z-index: 2;
}
.badge--ctrl { background: var(--navy-900); color: #fff; }

/* Filter chips */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  padding: 9px 17px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  font-size: 14px;
  font-weight: 300;
  color: var(--ink-2);
  cursor: pointer;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  transition: background var(--t-fast) var(--e-soft),
              color var(--t-fast) var(--e-soft),
              border-color var(--t-fast) var(--e-soft);
}
.chip:hover { border-color: var(--sky-300); color: var(--blue-600); background: var(--sky-50); }
.chip[aria-pressed="true"] { background: var(--navy-800); border-color: var(--navy-800); color: #fff; }
.chip__n {
  font-size: 11px;
  padding: 1px 7px;
  border-radius: var(--r-pill);
  background: var(--sky-100);
  color: var(--blue-600);
  font-variant-numeric: tabular-nums;
}
.chip[aria-pressed="true"] .chip__n { background: rgba(255,255,255,.18); color: #fff; }

/* ---------- 13. Forms ---------- */
.field { display: grid; gap: 7px; }
.label { font-size: 13.5px; font-weight: 400; color: var(--ink-2); }
.label .req { color: var(--err); }
.input, .select, .textarea {
  width: 100%;
  min-height: 48px;
  padding: 12px 15px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--surface);
  /* 16px is a hard floor, not a design choice. Safari on iOS zooms the whole
     page in when a focused field's text is smaller than 16px, and it does not
     zoom back out — the customer taps the search box and has to pinch their
     way out of a magnified page. This was 15px, one pixel into that trap, on
     the /products/ search and every field of the contact form. */
  font-size: 16px;
  font-weight: 300;
  color: var(--ink);
  transition: border-color var(--t-fast) var(--e-soft), box-shadow var(--t-fast) var(--e-soft);
}
.textarea { min-height: 128px; resize: vertical; line-height: 1.65; }
.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235b6f89' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 17px;
  padding-right: 42px;
  cursor: pointer;
}
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--blue-500);
  box-shadow: 0 0 0 4px rgba(2,132,199,.13);
}
.input[aria-invalid="true"], .textarea[aria-invalid="true"], .select[aria-invalid="true"] {
  border-color: var(--err);
  box-shadow: 0 0 0 4px rgba(185,28,28,.1);
}
.hint { font-size: 12.5px; color: var(--ink-3); }
.err-msg {
  font-size: 12.5px;
  color: var(--err);
  display: none;
  align-items: center;
  gap: 5px;
}
.err-msg::before { content: '⚠'; font-size: 11px; }
.field.has-err .err-msg { display: flex; }

/* The whole label is the target, not just the box — that is what a <label>
   is for, and it measures well past 44px on every width. The box itself goes
   from 19 to 22 so it is not the thing your thumb aims at and misses. */
.check { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; font-size: 14px; font-weight: 300; min-height: 44px; }
.check input { width: 22px; height: 22px; margin-top: 2px; accent-color: var(--blue-600); flex: none; cursor: pointer; }

/* ---------- 14. Modal / Age gate ---------- */
.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(11,31,58,.62);
  backdrop-filter: blur(8px);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--t-med) var(--e-soft), visibility var(--t-med);
}
.modal.is-open { opacity: 1; visibility: visible; }
.modal__box {
  background: var(--surface);
  border-radius: var(--r-xl);
  padding: clamp(28px, 4vw, 44px);
  max-width: 460px;
  width: 100%;
  text-align: center;
  box-shadow: var(--sh-4);
  transform: scale(.94) translateY(12px);
  transition: transform var(--t-slow) var(--e-out);
}
.modal.is-open .modal__box { transform: none; }
.modal__mark {
  width: 62px; height: 62px;
  margin: 0 auto 18px;
  border-radius: var(--r-pill);
  background: var(--sky-100);
  color: var(--blue-600);
  display: grid;
  place-items: center;
  font-size: 21px;
  font-weight: 500;
}
.modal__actions { display: grid; gap: 10px; margin-top: 24px; }

/* ---------- 15. Toast ---------- */
.toast-wrap {
  position: fixed;
  right: 16px; bottom: 16px;
  z-index: var(--z-toast);
  display: grid;
  gap: 9px;
  pointer-events: none;
}
.toast {
  padding: 13px 18px;
  border-radius: var(--r-md);
  background: var(--navy-900);
  color: #fff;
  font-size: 14px;
  font-weight: 300;
  box-shadow: var(--sh-4);
  display: flex;
  align-items: center;
  gap: 9px;
  animation: toast-in 380ms var(--e-out);
  max-width: 340px;
}
.toast--ok  { background: var(--ok); }
.toast--err { background: var(--err); }
@keyframes toast-in { from { opacity: 0; transform: translateY(14px); } }

/* ---------- 16. Misc ---------- */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--r-pill);
  background: var(--sky-100);
  color: var(--blue-600);
  font-size: 12px;
  font-weight: 400;
}
/* ---------- Order-list cart ----------
   Not a checkout. See the header comment in assets/js/cart.js for why this
   ends in a LINE message rather than a payment step, and why controlled
   goods have no add button at all.
--------------------------------------------------------- */

/* Add button on the product card. Positioned above .pcard__name a::after,
   which is stretched across the whole card — without the z-index the tap
   would fall through to the card link and navigate away instead of adding. */
.pcard__add {
  position: relative;
  z-index: 1;
  flex: none;
  /* 44, not 42. This is the one button that puts something in the order
     list — the button the whole catalogue exists for. */
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: var(--blue-600);
  color: #fff;
  cursor: pointer;
  box-shadow: 0 4px 12px hsl(201 90% 30% / .28);
  transition: background var(--t-fast) var(--e-soft),
              transform var(--t-fast) var(--e-out);
}
.pcard__add svg { width: 19px; height: 19px; }
.pcard__add:hover { background: var(--navy-700); transform: scale(1.06); }
.pcard__add:active { transform: scale(.94); }
/* Already in the list. Colour alone would not carry this — the button also
   re-labels itself with the current quantity (see render() in cart.js). */
.pcard__add.is-added { background: #0f766e; }

/* Header button + count badge */
.nav__cart { position: relative; }
.nav__cart-n {
  position: absolute;
  top: -3px; right: -3px;
  min-width: 18px; height: 18px;
  padding: 0 5px;
  border-radius: var(--r-pill);
  background: #dc2626;
  color: #fff;
  font-size: 10.5px;
  font-weight: 500;
  line-height: 18px;
  text-align: center;
  border: 2px solid var(--surface);
}

/* Slide-out panel — same visibility/scrim mechanics as .drawer so both
   behave identically, but light instead of navy: this one is a working
   surface with editable quantities, not a navigation menu. */
.cart {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  visibility: hidden;
  pointer-events: none;
}
.cart.is-open { visibility: visible; pointer-events: auto; }
.cart__scrim {
  position: absolute;
  inset: 0;
  background: rgba(11,31,58,.42);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--t-med) var(--e-soft);
}
.cart.is-open .cart__scrim { opacity: 1; }
.cart__panel {
  position: absolute;
  inset: 0 0 0 auto;
  width: min(94vw, 420px);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  padding: 18px 18px calc(18px + env(safe-area-inset-bottom));
  box-shadow: var(--sh-4);
  transform: translateX(100%);
  transition: transform var(--t-med) var(--e-out);
}
.cart.is-open .cart__panel { transform: none; }
.cart__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.cart__h { font-size: 19px; font-weight: 500; }
/* States plainly that nothing has been ordered. Without this the panel
   reads like a checkout and the LINE step comes as a surprise. */
.cart__note {
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-3);
  background: var(--sky-50);
  border-radius: var(--r-md);
  padding: 9px 12px;
}
.cart__list {
  flex: 1;
  overflow-y: auto;
  margin: 14px -6px 0;
  padding: 0 6px;
}
.cart__empty { flex: 1; display: grid; align-content: center; text-align: center; padding: 20px; }
.cart__empty p { font-size: 15px; color: var(--ink-2); }
.cart__empty-sub { font-size: 13px !important; color: var(--ink-3) !important; margin-top: 8px; max-width: 30ch; margin-inline: auto; }
.cart__foot { display: grid; gap: 9px; padding-top: 14px; border-top: 1px solid var(--line); }
.cart__clear {
  border: 0;
  background: none;
  color: var(--ink-3);
  font: inherit;
  font-size: 12.5px;
  padding: 4px;
  cursor: pointer;
  text-decoration: underline;
}
.cart__clear:hover { color: #b91c1c; }

/* One line in the list */
.cline {
  display: flex;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line-2);
}
.cline:last-child { border-bottom: 0; }
.cline__media {
  flex: none;
  width: 58px; height: 58px;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--sky-50);
  display: grid;
  place-items: center;
}
.cline__media img { width: 100%; height: 100%; object-fit: cover; }
.cline__media .pcard__art > svg { width: 44%; }
.cline__body { flex: 1; min-width: 0; }
.cline__name { font-size: 14px; line-height: 1.4; }
.cline__unit { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; }
.cline__qty { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
.cline__qty button {
  width: 28px; height: 28px;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: #fff;
  color: var(--ink-2);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.cline__qty button:hover { border-color: var(--sky-300); background: var(--sky-50); }
.cline__qty input {
  width: 52px;
  height: 28px;
  border: 1px solid var(--line);
  border-radius: 9px;
  text-align: center;
  font: inherit;
  font-size: 13.5px;
  color: var(--ink);
  background: #fff;
}
/* The spinners are redundant next to the −/+ buttons and make the field
   too narrow to read a three-digit quantity. */
.cline__qty input::-webkit-outer-spin-button,
.cline__qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cline__qty input { -moz-appearance: textfield; appearance: textfield; }
.cline__unitword { font-size: 12px; color: var(--ink-3); }
.cline__side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; }
.cline__price { font-size: 14px; font-weight: 500; }
.cline__del {
  border: 0;
  background: none;
  color: var(--ink-3);
  padding: 4px;
  cursor: pointer;
  border-radius: 8px;
}
.cline__del svg { width: 17px; height: 17px; }
.cline__del:hover { color: #b91c1c; background: #fef2f2; }

.divider { height: 1px; background: var(--line); border: 0; }
.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;
}
/* ---------- Touch targets that belong to no single component ----------
   Four leftovers from the 44px sweep. They are grouped here rather than
   scattered because none of them owns a block of its own, and a later reader
   needs to find all four together to understand the rule being applied.

   The rule: a *control* gets 44x44. A link sitting inside a sentence does
   not — WCAG 2.5.8 exempts it, and padding it would tear a hole in the line.
   That exemption is why the phone number and LINE address inside the privacy
   policy's prose are deliberately left alone, while the same page's table of
   contents — a navigation list, not prose — is fixed.
-------------------------------------------------------------------------- */

/* Breadcrumbs. Selected by aria-label because two of the four pages write the
   nav with an inline style and no class; matching the label reaches all four
   without editing HTML. An overlay rather than padding, so the crumb line
   keeps its height and the page below it does not shift. */
nav[aria-label="เส้นทางนำทาง"] a { position: relative; display: inline-block; }
nav[aria-label="เส้นทางนำทาง"] a::before { content: ''; position: absolute; inset: -13px -8px; }

/* Privacy policy table of contents — eleven links, 24px tall each. They fill
   their row, so a short entry is as easy to hit as a long one. */
.toc ol a { display: flex; align-items: center; min-height: 44px; }
.toc ol { display: grid; gap: 2px; }

/* Footer bottom bar. "ติดต่อเรา" measured 48x22 down there. */
.foot__bar a { display: inline-flex; align-items: center; min-height: 44px; }

/* The phone number on /reviews/, set large as a display element but still a
   tel: link someone taps to call. */
.vcard__big a { display: inline-flex; align-items: center; min-height: 44px; }

.skip {
  position: absolute;
  left: 12px; top: -60px;
  z-index: var(--z-toast);
  padding: 11px 20px;
  background: var(--navy-900);
  color: #fff;
  border-radius: var(--r-sm);
  font-size: 14px;
  transition: top var(--t-med) var(--e-out);
}
.skip:focus { top: 12px; }

/* A search that returns nothing is not a failure state — it is the one
   moment a visitor has told us, in their own words, exactly what they want
   to buy. The old treatment (faded magnifier, centred grey text) read as a
   dead end. This reads as a reply, and puts the ways to reach the shop in
   reach of the cursor that just typed. */
.empty {
  display: flex;
  gap: 22px;
  align-items: flex-start;
  padding: 32px 30px;
  border: 1px solid var(--sky-200);
  background: var(--sky-50);
  border-radius: var(--r-xl);
  color: var(--ink-2);
}
@media (max-width: 640px) { .empty { flex-direction: column; padding: 26px 22px; } }
.empty__ic {
  width: 46px; height: 46px; flex: none;
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--sky-200);
  display: grid; place-items: center;
}
.empty__ic > svg { width: 21px; height: 21px; color: var(--blue-600); }
.empty__h { font-size: 18px; font-weight: 400; color: var(--ink); line-height: 1.45; }
.empty__p { font-size: 14px; line-height: 1.75; font-weight: 300; margin-top: 8px; max-width: 54ch; }
.empty__acts { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }

/* Soft aurora blobs behind hero */
/* Decorative blurred blobs, positioned to bleed past the left and right edges.
   Over the navy hero the pale blue read as a white glow down both sides and
   looked like a rendering fault rather than decoration. Off by default.
   To bring them back: delete the `display: none` line below. */
.aura, .hero__glow { display: none; }
.aura { position: absolute; border-radius: 50%; filter: blur(88px); opacity: .5; pointer-events: none; z-index: 0; }
.aura--1 { width: 460px; height: 460px; background: var(--sky-200); top: -140px; right: -110px; }
.aura--2 { width: 380px; height: 380px; background: #d9e9fb; bottom: -130px; left: -100px; }

/* Skeleton */
.skel {
  background: linear-gradient(90deg, var(--line-2) 25%, #f6fafd 50%, var(--line-2) 75%);
  background-size: 200% 100%;
  animation: skel 1.4s ease-in-out infinite;
  border-radius: var(--r-md);
}
@keyframes skel { to { background-position: -200% 0; } }

/* ---------- 17. Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .reveal > span { transform: none; }
  [data-anim] { opacity: 1; transform: none; }
  .hero__glow { animation: none; }
  .hero__stack .stack__card:hover { transform: none; }
  .tile:hover { transform: none; }
}

/* ---------- 18. Print ---------- */
@media print {
  .nav, .foot, .drawer, .modal, .ticker { display: none !important; }
  .shell { box-shadow: none; margin: 0; }
  body { background: #fff; }
}
