/* ============================================================
   sondervibes baby — stylesheet
   One warm cream theme, matched to the logo. No dark mode.
   Tweak the tokens below and the whole page follows.
   ============================================================ */

/* Cream ground, warm ink, and two accents with a hard role split:
   SAGE IS WHAT A THING IS MADE OF. CLAY IS WHAT IT COSTS AND WHAT YOU DO NEXT.
   A reader never has to ask which family an element belongs to — taxonomy,
   fabric and care are sage; prices, links, focus and every call to action are
   clay.

   There is deliberately no @media (prefers-color-scheme: dark) block. The
   wordmark is an opaque JPEG carrying its own cream rectangle (see --bg), so a
   dark page would show that rectangle as a pale box no matter how the rest of
   the palette were rewritten. The site commits to the one warm look, exactly as
   the parent site does.

   Every colour used for text below was measured with the WCAG 2.1 sRGB
   relative-luminance formula against all four grounds it can land on, and the
   ratios are quoted where they matter. */
:root {
  /* ---- grounds: four, and text may sit on all four ---- */

  /* LOCKED. Sampled from the corners of sondervibes-logo.jpg — the modal pixel
     value in all four, re-measured rather than eyeballed (518,975 of 728,462
     pixels are exactly rgb(232,224,213)). The logo is a JPEG with no
     transparency and no colour profile, so the page background must match its
     ground *exactly* or the image reads as a pasted rectangle. Even one step
     per channel shows as a seam along its edge.

     Nothing the logo sits on may tint that ground either: no gradient behind
     it, and no translucent surface over it. It appears as a surface in exactly
     five places — body, .site-header, .tabbar, .shop-bar, .entry-bar — and
     nowhere else. Both <meta name="theme-color"> tags carry this same literal.
     See .site-header, .hero-figure and #bag-scrim. */
  --bg:        #e8e0d5;

  /* Cards, drawers, panels and fields — and the *ink* on inverted surfaces.
     Carries --text at 13.13, --text-dim 6.09, --clay 6.01, --sage-deep 6.13. */
  --bg-raised: #f5f0e8;

  /* Recessed fill: the #shop band, form fields, the well behind a product photo
     while it loads, table heads, .btn:hover. One step lighter than the parent's
     #ded3c4 on purpose — at that value clay text fell to 4.45 and missed AA by
     five hundredths. Carries --text 10.44, --text-dim 4.85, --clay 4.78. */
  --bg-quiet:  #e0d7c8;

  /* The plate band, #sizes and #about. 1.12 against the page — a real luminance
     step, so it reads as a panel rather than a hue shift. (A same-lightness
     green was tried first and rejected: identical luminance with only a hue
     change reads as a screen cast or a colour-profile fault, not a surface.)
     It is also almost exactly the Meadow Two-Piece's own ground, so the band is
     the colour of a garment on the rail. Carries --text 10.18, --text-dim 4.73,
     --clay 4.66, --sage-deep 4.75. */
  --bg-sage:   #cfd9c2;

  /* ---- ink: two tones, no third ---- */

  /* Body ink. bg 11.39 · raised 13.13 · quiet 10.44 · sage 10.18. Also the
     ground of .btn-primary, carrying --bg-raised. */
  --text:      #33241c;

  /* Every piece of secondary text. bg 5.29 · raised 6.09 · quiet 4.85 · sage
     4.73. A correction of the parent, whose --text-dim measures 4.26 on the
     cream and fails AA for small text while being its most-used colour. There
     is no third, fainter tone: every place that tempted the parent into one is
     small type that needs more contrast, not less. */
  --text-dim:  #6a5648;

  /* ---- accents: two hues, one hard role split ---- */

  /* WARMTH AND ACTION. Links, prices, .btn-primary:hover, the focus ring,
     section eyebrows, the active tab underline, .icon at rest, the bag pip.
     bg 5.21 · raised 6.01 · quiet 4.78 · sage 4.66; carries --bg-raised at
     6.01 and white at 6.82. This is the parent's rose gold walked browner and
     deeper until it passes — brand continuity with the logo's script brown,
     not a fork. */
  --clay:      #874b31;

  /* CALM AND MATERIAL. Category labels, #sizes headings, fabric and care icons,
     the hero kicker. bg 5.32 · raised 6.13 · quiet 4.87 · sage 4.75. */
  --sage-deep: #4c5f41;

  /* The only border colour permitted around an interactive control.
     bg 3.84 · raised 4.43 · quiet 3.52 · sage 3.44 — clears the 3:1 WCAG
     non-text minimum on every ground a control can land on. */
  --edge:      #946449;

  /* WASH AND BORDER ONLY, NEVER TEXT (1.91 on the cream). */
  --clay-soft: #c99a7c;
  /* WASH AND MARK ONLY, NEVER TEXT (1.61 on the cream). */
  --sage-soft: #a8b89b;

  /* ---- hairlines: two weights, and they are not interchangeable ---- */

  /* Dividers *inside* a component: bag rows, drawer rows, card borders,
     swatch rings, the spec list on a product page. */
  --line:      #d2c4b2;
  /* THE structural rule: .band's border-top, .hero-facts, the two sticky bars,
     the footer. Noticeably stronger than --line, which is the point — this line
     runs the full viewport width and is meant to be read. Every section
     transition on this page is one of these. */
  --rule:      #bfae98;

  /* ---- swatch dots ----
     The six real garment tones, lifted from the TONES table in
     scripts/make-placeholders.py so the dots and the photographs agree. Mapped
     by [data-tone], never by an inline style — style-src 'self' has no
     'unsafe-inline', so a style="" attribute would simply be dropped.
     A colour name outside these six renders its text label with NO dot: we do
     not invent a hex for a colour we have not seen. */
  --sw-oat:    #d9cbb7;
  --sw-sage:   #c3d0bb;
  --sw-powder: #bcd0d6;
  --sw-blush:  #e0c4bb;
  --sw-clay:   #dcc0aa;
  --sw-butter: #e3d2ab;

  /* ---- type ----
     CJK sits at the end of each stack. RedNote and Lemon8 captions will bring
     Chinese onto the page eventually, and neither Fraunces nor Instrument Sans
     covers it — without these the browser picks something ugly. These are
     system faces on purpose: a self-hosted Noto SC runs to megabytes. */
  --serif: "Fraunces", ui-serif, Georgia, "Iowan Old Style",
           "Noto Serif SC", "Source Han Serif SC", "Songti SC", serif;
  --sans:  "Instrument Sans", system-ui, -apple-system, "Segoe UI", Roboto,
           "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
  --mono:  ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Ten sizes, replacing the parent's forty-one ad-hoc values below 1.2rem.
     --t-2xs at 0.68rem is the FLOOR: nothing sets a tracked-out mono label
     smaller, because the parent's 0.65rem at 0.14em is genuinely hard to read
     and that is a bug not to inherit. */
  --t-hero:    clamp(2rem, 0.85rem + 4.4vw, 3.5rem);
  --t-sonder:  clamp(2.6rem, 1.1rem + 6vw, 5.5rem);
  --t-display: clamp(1.9rem, 1.3rem + 2.2vw, 3rem);
  --t-quote:   clamp(1.45rem, 1rem + 2.2vw, 2.4rem);
  --t-title:   1.2rem;
  --t-lede:    clamp(1.05rem, 1rem + 0.35vw, 1.25rem);
  --t-body:    clamp(1.0625rem, 1.03rem + 0.17vw, 1.125rem);
  --t-sm:      0.92rem;
  --t-xs:      0.8rem;
  --t-2xs:     0.68rem;

  /* ---- space: six steps, instead of fifteen near-duplicate one-offs ---- */
  --s-1: 0.5rem;
  --s-2: 0.75rem;
  --s-3: 1rem;
  --s-4: 1.5rem;
  --s-5: 2rem;
  --s-6: 3rem;

  /* ---- layout ----
     --header-h and --rail-h must be literals: `position: sticky; top:` cannot
     read an element's height. The two rules that make them true rather than
     approximate are .site-header { height } and .tabbar-inner { height }. */
  --page:     68rem;
  --measure:  62ch;
  --gutter:   1.5rem;
  --header-h: 4.5rem;
  --rail-h:   3.25rem;

  /* ---- radius: five tokens, and no literals except 50% on a circle and the
     3px on the focus ring ---- */
  --r-xs:   6px;
  --r-sm:   10px;
  --r-md:   14px;
  --r-lg:   20px;
  --r-pill: 999px;

  /* ---- shadow: three, brown-tinted off the ink. A neutral shadow reads grey
     and dirty against the cream. ---- */
  --shadow:       0 2px 10px -6px rgba(51, 36, 28, 0.30);
  --shadow-lg:    0 16px 34px -18px rgba(51, 36, 28, 0.45);
  --shadow-panel: 0 12px 32px -12px rgba(51, 36, 28, 0.28);
}

/* ---------- fonts ----------
   Self-hosted so no third-party origin is involved: nothing to add to the CSP,
   no extra DNS round-trip, and no font CDN seeing every visitor.

   These are variable fonts, instanced down from what Google ships. Declaring a
   weight *range* is what tells the browser the file is variable — the old
   format('woff2-variations') syntax is a dead draft that Safari and Firefox
   reject, so plain format('woff2') is correct here.

   Axes kept: Fraunces has opsz 14–144 (optical sizing, applied automatically by
   font-size), wght 300–600, and WONK 0–1. SOFT is pinned at 40 — the warmth
   setting we want, and pinning it halved the file. WONK stays variable because
   removing it saved nothing, and it lets display type be characterful while
   small type stays steady.

   Never list opsz in font-variation-settings anywhere below: naming the axis
   pins it and kills the automatic optical sizing. Only "WONK" is ever named. */

@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/fraunces-var.woff2") format("woff2");
  font-weight: 300 600;
  font-style: normal;
  font-display: swap;
}

/* A separately drawn italic (italicAngle -16), not a mechanical slant. */
@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/fraunces-italic-var.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Instrument Sans";
  src: url("/assets/fonts/instrument-sans-var.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* ---------- base ---------- */

/* Cross-document view transitions. The browser cross-fades between pages
   instead of the usual white flash, and any two elements sharing a
   view-transition-name morph from one position to the other. / to
   /shop/<slug>/ is exactly the two-page case this was written for. Chrome, Edge
   and Safari 18+ honour it; everywhere else simply navigates as normal. */
@view-transition { navigation: auto; }

/* Named elements persist across the navigation rather than fading, so the
   header and logo appear to stay put while the content beneath changes. A name
   must be unique per document, which is why brand-logo sits on the header
   wordmark only and never on .hero-logo. */
.site-header { view-transition-name: site-header; }
.wordmark img { view-transition-name: brand-logo; }
.site-footer { view-transition-name: site-footer; }

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; animation: none !important; }
  /* Also stop the page-to-page animation, which is not a CSS transition and so
     is not covered by the rule above. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  /* 17px climbing to 18px. Dark-on-cream reads heavier than dark-on-white, so
     body stays at 400 rather than the 450–500 a white page would want. */
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  /* WONK off by default. Fraunces' quirk axis is charming on display type and
     reads as wobbly at small sizes, so it is switched on for exactly six
     display selectors rather than globally. Instrument Sans ignores this. */
  font-variation-settings: "WONK" 0;
}

img { max-width: 100%; }

/* The sprite only defines symbols; nothing in it should ever render. */
.sprite { display: none; }

/* ---------- links & buttons ---------- */

a { color: var(--clay); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--text); }

/* --clay measures 5.21 on the cream and 4.66 on the sage bands, well past the
   3:1 a focus indicator needs, on every ground it can land on. */
:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
  border-radius: 3px;
}

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: 1rem; top: 1rem;
  z-index: 50;
  padding: 0.6rem 1rem;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  text-decoration: none;
}

/* Visually hidden but still announced. Used for the enquiry count, where the
   visible pip is a bare number and a live region reading out "3" with no noun
   would be useless. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Animated link.
   The "arrow" is not a glyph — it is the underline itself. A clip-path holds
   the 1px rule flat at rest, then on hover reveals a version with a peak at the
   midpoint, so the line appears to fold up into a chevron. The label lifts 2px
   at the same time. Both halves are needed; the underline alone reads as a
   glitch. The inner <span> is mandatory in the markup. */
.styled-link {
  position: relative;
  display: inline-block;
  padding-bottom: 0.3rem;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.styled-link::before {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  height: 1px;
  background: var(--clay);
  transform-origin: 50% 100%;
  pointer-events: none;
  transition: clip-path 0.3s, transform 0.3s cubic-bezier(0.2, 1, 0.8, 1);
  clip-path: polygon(0% 0%, 0% 100%, 0 100%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%, 100% 100%, 100% 0%);
}
.styled-link:hover::before,
.styled-link:focus-visible::before {
  transform: translate3d(0, 2px, 0) scale3d(1, 2, 1);
  clip-path: polygon(0% 0%, 0% 100%, 100% 100%, 50% 0, 50% 0, 50% 100%, 50% 100%, 0 100%, 100% 100%, 100% 0%);
}

.styled-link span {
  display: inline-block;
  transition: transform 0.5s cubic-bezier(0.2, 1, 0.8, 1), color 0.3s ease;
}
.styled-link:hover span,
.styled-link:focus-visible span {
  transform: translate3d(0, -2px, 0);
  color: var(--clay);
}

/* Same effect, sized for a "see everything" link at the end of a section. */
.more-link {
  margin-top: var(--s-4);
  font-family: var(--serif);
  font-size: 1.05rem;
}

/* Secondary button. --line is the right colour for a divider but only reaches
   1.31:1 against the cream, so as a button edge it was nearly invisible. These
   use a raised fill plus --edge, which clears the 3:1 that WCAG asks of
   interface edges on every one of the four grounds. */
.btn {
  display: inline-block;
  padding: 0.7rem 1.35rem;
  background: var(--bg-raised);
  border: 1px solid var(--edge);
  border-radius: var(--r-pill);
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 500;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}
.btn:hover { background: var(--bg-quiet); border-color: var(--text); color: var(--text); }

/* Ink on cream rather than clay on cream — clay is too light to carry button
   text at an accessible ratio at this size. */
.btn-primary {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg-raised);
  font-weight: 550;
}
/* Hover deepens rather than lightens: clay carries white at 6.82, so the label
   is easier to read on hover than at rest, not harder. */
.btn-primary:hover {
  background: var(--clay);
  border-color: var(--clay);
  color: #fff;
}

.btn-sm {
  padding: 0.5rem 1rem;
  font-size: 0.87rem;
}
/* Below the drawer breakpoint every .btn-sm is a thumb target, and 34px is not
   one. The extra 10px is padding: the label keeps its 0.87rem and the button
   keeps its proportions. */
@media (max-width: 52rem) {
  .btn-sm { padding-block: 0.82rem; }
}

/* Buttons carrying an icon need to be flex; .btn is inline-block for the
   text-only case, so this only applies where an icon is actually present. */
.btn:has(.btn-icon) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
}

/* Most of the interface glyphs are drawn as outlines, with their own
   fill="none" stroke="currentColor" on the path. Those presentation attributes
   beat an inherited value, so this rule only actually paints a symbol that
   carries no attributes of its own — i.e. a SOLID glyph. */
.btn-icon {
  width: 1rem;
  height: 1rem;
  flex: none;
  fill: none;
  stroke: currentColor;
}

/* THE TWO SOLID GLYPHS, and the only two exceptions in this stylesheet.
   icon-mail and icon-external are filled shapes; stroking them draws a second
   outline around the envelope and the arrow. Every other symbol in the sprite
   is stroke-only and needs nothing here.

   RULE FOR THE FUTURE: any new SOLID glyph placed inside a .btn-icon or a
   .feature-tile needs its own :has() line beside these, or it will be outlined
   twice. Do not add one silently. */
.btn-icon:has(use[href="#icon-mail"]),
.btn-icon:has(use[href="#icon-external"]) { fill: currentColor; stroke: none; }

/* ---------- icons ---------- */

/* Base icon: sized, and clay at rest because an icon beside a label is part of
   the action. Only `fill` is set — never `stroke` — so a stroke-only symbol
   keeps its own attributes and a solid one is filled. */
.icon {
  width: 1.35rem;
  height: 1.35rem;
  flex: none;
  fill: currentColor;
  color: var(--clay);
  transition: color 0.18s ease;
}

/* Variants. Anything describing MATERIAL — fabric, care, fit, size — is sage;
   anything beside money or an action stays clay. That is the role split doing
   its job at icon scale. */
.chevron            { width: 1rem;    height: 1rem; transition: transform 0.18s ease; }
.section-more-icon  { width: 1rem;    height: 1rem; }
.product-badge-icon { width: 0.85rem; height: 0.85rem; flex: none; }
.fit-icon           { width: 0.95rem; height: 0.95rem; flex: none; color: var(--sage-deep); }
.care-icon          { width: 1rem;    height: 1rem; flex: none; color: var(--sage-deep); }
.spec-icon          { width: 0.95rem; height: 0.95rem; flex: none; color: var(--sage-deep); }
/* The one variant that needs fill: the third hero fact carries icon-mail, which
   is a solid glyph and would otherwise render as nothing. */
.hero-fact-icon     { width: 1rem;    height: 1rem; flex: none; fill: currentColor; color: var(--sage-deep); }

/* ---------- wind-down notice ----------
   Sits above the sticky header and scrolls away with the page. Clay rather than
   sage: on this site sage means "what a thing is made of" and clay means "what
   it costs and what you do next", and a change to how you can buy is squarely
   the second. It is a statement, not a warning, so it stays in the palette
   rather than reaching for a red nobody else on the page uses.

   No border-radius and full bleed on purpose — a rounded card would read as one
   more component in the page, and this has to read as a condition of the whole
   shop. */
.site-notice {
  background: var(--clay);
  color: #fdf8f2;                /* off-white, not pure white: 6.45:1 on --clay */
  padding: 0.85rem var(--s-4);
}

.site-notice-text {
  margin: 0 auto;
  max-width: var(--page);
  font-family: var(--sans);
  font-size: var(--t-sm);
  line-height: 1.5;
  text-wrap: pretty;
}

/* The lead is the part someone reads if they read nothing else, so it carries
   the weight on its own — no colour change, which at this size on this ground
   would only muddy it. */
.site-notice-lead { font-weight: 600; }

@media (max-width: 52rem) {
  .site-notice { padding: 0.8rem var(--s-3); }
}

/* ---------- header ---------- */

/* Sticky band across the full width, so content scrolls *under* it rather than
   past it.

   OPAQUE --bg, not the translucent-and-blurred bar the reference uses. The
   wordmark inside it is a JPEG with its own baked-in cream rectangle, so the
   band it sits on has to be exactly --bg. A translucent header is whatever is
   scrolling underneath it — the sage plate band, a product photograph — and
   against any of those the logo's rectangle appears as a pale box. No
   backdrop-filter, no alpha, ever. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  height: var(--header-h);
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

.header-inner {
  max-width: var(--page);
  height: 100%;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex;
  gap: 1rem;
  align-items: center;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  color: var(--text);
  font-family: var(--serif);
  font-size: 1.05rem;
  text-decoration: none;
}
/* 2.9rem plus 0.8rem of air either side is --header-h. No border-radius and no
   clipping ancestor: rounding an opaque JPEG cuts the corners off its own cream
   ground, which turns an invisible rectangle into a visible rounded one. */
.wordmark img { display: block; height: 2.9rem; width: auto; }

/* Nav and the bag button sit together at the right; the wordmark holds the
   left. Below 52rem the nav becomes a drawer and this margin stops mattering,
   so .bag-button takes it over there instead. */
.site-header nav {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  margin-left: auto;
  font-size: 0.94rem;
  font-weight: 500;
}
.site-header nav a { color: var(--text-dim); text-decoration: none; }
.site-header nav a:hover { color: var(--text); }

/* Styled UNQUALIFIED on purpose. main.js writes aria-current="true" from the
   scroll position and build-shop.py writes aria-current="page" on a generated
   page's nav link. The parent's stylesheet only matched "true", which left the
   generated pages' marker invisible — a live bug, not inherited here. */
.site-header nav a[aria-current],
.popover-link[aria-current] { color: var(--text); }

/* ---------- mobile nav ---------- */

/* 44px minimums on both header controls, per WCAG 2.5.8: the hamburger drew a
   37px box and the bag trigger a 34x30 one, which is a thumb-sized miss on the
   two controls a phone reaches for first. The size comes from the box, not from
   the glyph, so nothing about the header's weight changes. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0.4rem;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: var(--text);
  cursor: pointer;
}
.nav-toggle svg { display: block; width: 1.5rem; height: 1.5rem; }

/* The icon swap is driven purely by the ARIA state, so there is one source of
   truth for whether the menu is open. */
.nav-toggle .nav-toggle-close { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle-open { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle-close { display: block; }

@media (max-width: 52rem) {
  /* This number is duplicated in main.js as matchMedia('(max-width: 52rem)').
     The media query is the single source of truth for which mode we are in, so
     the two must stay identical. */
  .nav-toggle { display: inline-flex; order: 3; }
  .bag-button { order: 2; margin-left: auto; }

  /* The drawer drops out of the band rather than growing it, so --header-h
     stays true and the section rail below does not jump. */
  .site-header nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 29;               /* under the 30 header it hangs from */
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-left: 0;
    padding: 0.5rem var(--gutter) 1.25rem;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-panel);
    font-size: 1.05rem;
  }
  /* `hidden` alone loses to display: flex, so restore it. */
  .site-header nav[hidden] { display: none; }

  .site-header nav > *,
  .site-header nav .popover { padding: 0.7rem 0; border-bottom: 1px solid var(--line); }
  .site-header nav > *:last-child { border-bottom: 0; }

  .site-header nav .popover { display: flex; justify-content: space-between; flex-wrap: wrap; }
  .site-header nav .popover-link { flex: 1; }
}

/* ---------- nav popover / megamenu ---------- */

/* The link still works on its own, and the adjacent button only opens the
   panel. Keeping them separate means the menu is reachable by keyboard without
   hijacking the link itself. */
.popover { position: relative; display: inline-flex; align-items: center; gap: 0.15rem; }

.popover-link { color: var(--text-dim); text-decoration: none; }
.popover-link:hover { color: var(--text); }

.popover-button {
  display: inline-grid;
  place-items: center;
  padding: 0.25rem;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: var(--text-dim);
  cursor: pointer;
}
.popover-button:hover { color: var(--text); }
.popover-button[aria-expanded="true"] .chevron { transform: rotate(180deg); }

.megamenu {
  position: absolute;
  top: calc(100% + 0.6rem);
  left: 50%;
  translate: -50% 0;
  z-index: 20;
  width: max-content;
  max-width: min(32rem, calc(100vw - 2rem));
  padding: 0.6rem;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow-panel);
}
/* `hidden` alone loses to the display: grid further down, so restore it. */
.megamenu[hidden] { display: none; }

.megamenu-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.25rem; }

.megamenu-item {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0.7rem;
  padding: 0.7rem 0.8rem;
  border-radius: 8px;
  color: var(--text);
  text-decoration: none;
}
.megamenu-item:hover { background: var(--bg-quiet); color: var(--text); }
.megamenu-item .icon { margin-top: 0.15rem; }

.megamenu-text { display: block; }
.megamenu-title { display: block; font-family: var(--serif); font-size: 1rem; line-height: 1.3; }
.megamenu-description {
  display: block;
  margin-top: 0.15rem;
  color: var(--text-dim);
  font-size: 0.82rem;
  line-height: 1.35;
}

@media (max-width: 52rem) {
  /* Inside the drawer the panel takes its own line rather than floating. */
  .site-header nav .megamenu {
    position: static;
    translate: none;
    flex: 0 0 100%;
    width: auto;
    max-width: none;
    margin-top: 0.5rem;
    box-shadow: none;
  }
}
@media (max-width: 34rem) {
  .megamenu-grid { grid-template-columns: 1fr; }
}

/* ---------- enquiry bag: the header trigger ---------- */

.bag-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  gap: 0.45rem;
  flex: none;
  padding: 0.35rem 0.5rem;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: var(--text);
  font: inherit;
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}
.bag-button .icon { width: 1.15rem; height: 1.15rem; }
.bag-button:hover .icon { color: var(--text); }

/* The pip is aria-hidden; the announcement lives in the separate .sr-only
   status element beside it, because a bare live region reading out "3" with no
   noun is useless. main.js rewrites both, and the button's own aria-label. */
.bag-pip {
  position: absolute;
  top: -0.15rem;
  right: -0.25rem;
  display: grid;
  place-items: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.3rem;
  background: var(--clay);
  border-radius: var(--r-pill);
  color: var(--bg-raised);
  font-size: var(--t-2xs);
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.bag-pip[hidden] { display: none; }

@media (max-width: 40rem) {
  .bag-label { display: none; }
}

/* ---------- hero ---------- */

.hero {
  max-width: var(--page);
  margin-inline: auto;
  padding: clamp(2.25rem, 5vw, 4rem) var(--gutter) clamp(2rem, 4vw, 3rem);
}

.hero-split {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  padding-top: clamp(2rem, 5vw, 4rem);
}

/* The reference puts a blurred colour blob behind its hero. There is
   deliberately none here: the logo lockup sits in this area, and any wash under
   it tints the ground the JPEG's own cream rectangle has to match. The hero
   keeps the flat --bg so the lockup has no visible edge. */
.hero-figure { display: flex; justify-content: center; }

/* No border-radius. Rounding an opaque JPEG clips the corners off its own cream
   ground, which turns an invisible rectangle into a visible rounded one. */
.hero-logo {
  display: block;
  width: min(20rem, 78vw);
  height: auto;
}

/* The wordmark beside the logo is a picture; this line is the machine-readable
   name. Sage, because it is an identity label rather than an action. */
.hero-kicker {
  margin: 0 0 0.8rem;
  color: var(--sage-deep);
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  width: max-content;
  margin: 0 0 1.35rem;
  padding: 0.35rem 0.9rem 0.35rem 0.75rem;
  background: color-mix(in srgb, var(--clay-soft) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--edge) 35%, transparent);
  border-radius: var(--r-pill);
  color: var(--clay);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.status-dot {
  width: 0.42rem;
  height: 0.42rem;
  flex: none;
  border-radius: 50%;
  background: var(--clay);
  animation: status-pulse 2.4s ease-in-out infinite;
}
@keyframes status-pulse { 50% { opacity: 0.3; } }

/* The parent's white-space: nowrap is deliberately dropped. That trick was
   tuned for the fifteen characters of "thesondervibes."; this headline is
   thirty-eight and would either overflow the gutters or shrink to nothing.
   text-wrap: balance across two or three lines instead, with max-width forcing
   the break where we want it. */
.hero-title {
  margin: 0;
  max-width: 15ch;
  font-family: var(--serif);
  font-size: var(--t-hero);
  font-weight: 500;
  font-variation-settings: "WONK" 1;
  line-height: 1.06;
  letter-spacing: -0.025em;
  text-wrap: balance;
}
.hero-title-accent { color: var(--clay); font-style: italic; font-weight: 400; }

.lede {
  margin: 1.4rem 0 0;
  max-width: 48ch;
  color: var(--text-dim);
  font-size: var(--t-lede);
  line-height: 1.6;
}
.hero-copy .lede { max-width: 34ch; }

.actions {
  margin-top: var(--s-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

/* The first long rule on the page. The third fact is doing real work: saying
   "no checkout" here, in the same quiet register as the fabric, makes it a fact
   about how the shop works rather than an apology at the end. */
.hero-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem clamp(1.5rem, 4vw, 3rem);
  margin: clamp(2rem, 4vw, 3rem) 0 0;
  padding: 0.9rem 0 0;
  border-top: 1px solid var(--rule);
  list-style: none;
}
.hero-fact {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

@media (max-width: 52rem) {
  .hero-split { grid-template-columns: 1fr; text-align: center; }
  .hero-figure { order: -1; }                 /* logo first on a phone */
  .hero-copy .lede,
  .hero-copy .hero-title { margin-inline: auto; }
  .hero-copy .actions { justify-content: center; }
  .status-pill { margin-inline: auto; }
  .hero-facts { justify-content: center; }
}

/* ---------- plate band ---------- */

/* The sonder definition. Live text and one drawn line, never a photograph: this
   band has to look finished on a fresh checkout with an empty products folder,
   it makes no request, and it can never 404.

   Flat --bg-sage — no image, no scrim, no gradient. Nothing here uses a
   negative z-index, so no isolation: isolate is needed; the band's own
   overflow: hidden and the mark's z-index: 0 are load-bearing. */
.plate {
  position: relative;
  overflow: hidden;
  padding: clamp(3.5rem, 9vw, 6rem) var(--gutter);
  background: var(--bg-sage);
  color: var(--text);
}

/* The mount. A drawn frame rather than a border on the band itself, so it
   insets from the full-bleed edge. */
.plate::after {
  content: "";
  position: absolute;
  inset: 1.25rem;
  border: 1px solid var(--sage-soft);
  border-radius: var(--r-lg);
  pointer-events: none;
}

.plate-inner {
  position: relative;
  z-index: 1;
  max-width: var(--page);
  margin-inline: auto;
}
.plate-inner > * { max-width: 22ch; }

/* One unbroken hand-drawn line reading as a swaddled sleeping shape. Same
   drawing discipline as the interface icons, costs one sprite symbol, and
   cannot 404. */
.plate-mark {
  position: absolute;
  z-index: 0;
  right: -2%;
  top: 50%;
  translate: 0 -50%;
  width: min(26rem, 45%);
  height: auto;
  color: var(--sage-soft);
  opacity: 0.5;
}

/* The largest thing on the site — larger than the hero — and set LIGHT rather
   than bold. A huge word set light reads as drawn; a huge word set bold reads
   as an advertisement. The parent gave the definition a photograph to sit on;
   without one, the only honest way to give it real estate is to let the
   typography carry it alone. */
.sonder-word {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-sonder);
  font-weight: 300;
  font-variation-settings: "WONK" 1;
  line-height: 0.95;
  letter-spacing: -0.02em;
}
.sonder-word .pos {
  font-size: 0.5em;
  font-style: italic;
  letter-spacing: 0;
  color: var(--text-dim);
}

.plate-pronounce {
  margin: 0.35rem 0 var(--s-4);
  color: var(--text-dim);
  font-family: var(--sans);
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
}

.plate blockquote {
  margin: 0;
  max-width: 26ch;
  font-family: var(--serif);
  font-size: var(--t-quote);
  font-weight: 400;
  font-variation-settings: "WONK" 1;
  line-height: 1.18;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
/* 4.66:1 on the sage ground — clay passes here as text. */
.plate blockquote em { color: var(--clay); font-style: italic; }

/* The repointing at the new arrival. An aside spoken after the definition, so
   it is the one whole line on the page set in italic, and it earns its place by
   being specific and a little dry rather than sentimental. */
.plate-coda {
  position: relative;
  max-width: 30ch;
  margin: var(--s-4) 0 0;
  padding-top: 1.1rem;
  color: var(--text-dim);
  font-family: var(--serif);
  font-size: var(--t-lede);
  font-style: italic;
  line-height: 1.5;
}
.plate-coda::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 2.5rem;
  height: 1px;
  background: var(--sage-soft);
}

/* The optional photograph strip. build-shop.py emits it between the plate
   markers only when the catalogue holds three or more products, and adds
   .has-strip to the band at the same time. Below three the markers are empty
   and the composition is complete without it — the mark already occupies that
   side. */
.plate-strip {
  position: absolute;
  z-index: 1;
  right: clamp(1.5rem, 5vw, 4rem);
  bottom: clamp(1.5rem, 5vw, 3rem);
  display: flex;
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.plate-shot { display: block; }
.plate-shot img {
  display: block;
  width: 7rem;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--bg-quiet);
  box-shadow: var(--shadow-panel);
}
.plate-strip li:nth-child(1) .plate-shot { rotate: -2deg; }
.plate-strip li:nth-child(2) .plate-shot { rotate: 1.5deg; }
.plate-strip li:nth-child(3) .plate-shot { rotate: -1deg; }

/* With the strip present the mark steps back so the two do not fight. */
.plate.has-strip .plate-mark { opacity: 0.32; }

/* Between 52rem and the full measure the strip and the coda reach for the same
   pixels, so the band buys the text some room. Applied unconditionally rather
   than behind a sixth breakpoint: above the measure it is three rem of extra
   air under a band that is already generously padded, which nobody sees. */
.plate.has-strip .plate-inner { padding-bottom: 3rem; }

@media (max-width: 52rem) {
  .plate-strip { display: none; }
  .plate.has-strip .plate-inner { padding-bottom: 0; }
  .plate-mark { right: -15%; opacity: 0.3; }
}

/* ---------- section rail ---------- */

/* Opaque for the same reason as the header directly above it: two stacked bars
   where one is translucent and the other is not reads as a mismatch, and this
   one also passes over the sage plate band. */
.tabbar {
  position: sticky;
  top: var(--header-h);
  z-index: 20;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

/* Scrolls sideways on a phone rather than wrapping, so the rail stays one line
   tall and --rail-h stays true. */
.tabbar-inner {
  display: flex;
  height: var(--rail-h);
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--gutter);
  overflow-x: auto;
  scrollbar-width: none;
}
.tabbar-inner::-webkit-scrollbar { display: none; }

.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
  padding: 0 1.15rem;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 0.18s ease;
}
.tab:hover { color: var(--text); }

/* Drawn on every tab and scaled to nothing, so it grows from the centre. */
.tab::after {
  content: "";
  position: absolute;
  left: 0.9rem;
  right: 0.9rem;
  bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--clay);
  transform: scaleX(0);
  transition: transform 0.22s ease;
}
/* Unqualified [aria-current], as above: main.js says "true", the build script
   says "page", and both must render. */
.tab[aria-current] { color: var(--text); }
.tab[aria-current]::after { transform: scaleX(1); }

/* Clears both sticky bars when a fragment link lands. */
:target,
main section[id] { scroll-margin-top: calc(var(--header-h) + var(--rail-h)); }

/* ---------- bands ---------- */

/* The measure is held by the band's own inline padding, not by centring each
   child — this is the single most important layout rule on the page and the
   reason a grid and a heading share one left edge for free.

   The border-top in --rule means every section transition IS a long horizontal
   line running the full viewport width. That is the structural signature of
   this design; do not soften it to --line. */
.band {
  border-top: 1px solid var(--rule);
  padding-block: clamp(2.5rem, 5vw, 4rem);
  padding-inline: max(var(--gutter), calc((100% - var(--page)) / 2));
}
.band > * { max-width: var(--page); }

.band-quiet { background: var(--bg-quiet); }
.band-sage  { background: var(--bg-sage); }

/* ---------- section headings ---------- */

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: 1.75rem;
}

.section-eyebrow {
  margin: 0 0 0.4rem;
  color: var(--clay);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.band h2.section-title,
.section-title {
  margin: 0;
  color: var(--text);
  font-family: var(--serif);
  font-size: var(--t-display);
  font-weight: 500;
  font-variation-settings: "WONK" 1;
  letter-spacing: -0.02em;
  line-height: 1.05;
  text-transform: none;
  text-wrap: balance;
}

.section-subtitle {
  max-width: 46ch;
  margin: 0 0 1.75rem;
  font-family: var(--serif);
  font-size: clamp(1.1rem, 1rem + 0.5vw, 1.45rem);
  line-height: 1.35;
  text-wrap: balance;
}

.section-more {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex: none;
  color: var(--text-dim);
  font-size: var(--t-sm);
  font-weight: 500;
  text-decoration: none;
  transition: color 0.18s ease, gap 0.18s ease;
}
/* The gap animates — a cheap, legible affordance. */
.section-more:hover { color: var(--clay); gap: 0.6rem; }

@media (max-width: 34rem) {
  .section-head { flex-direction: column; align-items: flex-start; gap: var(--s-2); }
}

/* ---------- prose, panels and empty states ---------- */

.prose { max-width: var(--measure); }
.prose p { margin: 0 0 1.1rem; text-wrap: pretty; }
.prose p:last-child { margin-bottom: 0; }
/* Emphasis inside prose is <strong> at Fraunces 600, never italic. */
.prose strong { color: var(--text); font-family: var(--serif); font-weight: 600; }

.panel-title {
  margin: 0 0 var(--s-3);
  color: var(--sage-deep);
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* A serif-italic empty state reads as a written note rather than an error. */
.shop-empty,
.bag-empty {
  margin: 0;
  padding: 2.5rem 1.5rem;
  border: 1px dashed var(--line);
  border-radius: var(--r-md);
  color: var(--text-dim);
  font-family: var(--serif);
  font-size: 1.05rem;
  font-style: italic;
  text-align: center;
}
.shop-empty[hidden],
.bag-empty[hidden] { display: none; }

/* ============================================================
   SHARED SURFACES

   One card treatment underneath everything below — products, features and
   the social tiles all sit on it, which is what keeps the page reading as one
   thing rather than several stacked designs.
   ============================================================ */

.card {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
}

/* Only cards that lead somewhere get the lift. */
a.card { display: block; color: var(--text); text-decoration: none; }
a.card:hover {
  border-color: color-mix(in srgb, var(--clay) 55%, var(--line));
  box-shadow: var(--shadow-lg);
}

/* ============================================================
   SHOP — the product grid and the product card

   The card's one structural insistence: the anchor wraps the media and the
   body, and the size <select> and the add button sit OUTSIDE it. A form control
   inside an <a> is invalid HTML and hostile to a keyboard. So .product is an
   <article>, not an anchor, and it does not compose a.card — there is exactly
   one anchor per tile, one tab stop for the destination, and no
   stretched-pseudo-element trick.
   ============================================================ */

.product-grid {
  display: grid;
  gap: 1.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
  /* The min(100%, …) guard is what lets auto-fill collapse to a single column
     on a phone with no media query. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15.5rem), 1fr));
}
/* Larger tiles on the homepage teaser, tighter on /shop/ — same component,
   driven by an id prefix. At --page the wide grid is three columns and the
   narrow one four, so six products never strand a lone card on its own row. */
#shop .product-grid { grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr)); }

/* A flex li rather than a bare one, so the card inside stretches to the tallest
   in its row and every footer in the grid sits on the same line.

   The second rule is MANDATORY and easy to forget: an item's own display
   outranks the UA stylesheet's [hidden] { display: none }, so without it the
   shop filter hides nothing and simply looks broken. */
.product-item { display: flex; }
.product-item[hidden] { display: none; }

.product {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  flex: 1;
  transition: box-shadow 0.25s ease, border-color 0.25s ease;
}
/* :focus-within as well as :hover, so a keyboard user tabbing to the add button
   sees the card respond. */
.product:hover,
.product:focus-within {
  box-shadow: var(--shadow-lg);
  border-color: color-mix(in srgb, var(--clay) 45%, var(--line));
}

/* A flex COLUMN that grows, not a block. The anchor is a flex item of the
   card, and only a growing one hands the card's spare height down to
   .product-body's flex: 1 and the swatches' margin-top: auto below. As
   display: block it absorbed nothing, the spare height piled up under
   .product-foot instead, and the Add button on any card with a two-line blurb
   floated 22px above its row-mates. */
.product-link {
  display: flex;
  flex-direction: column;
  flex: 1;
  color: inherit;
  text-decoration: none;
}
.product-link:hover .product-name { text-decoration: underline; text-underline-offset: 3px; }

/* aspect-ratio, not the parent's fixed height: every photograph is 1000×1250,
   exactly 4:5, so object-fit: cover crops nothing at all. No radius of its own
   — the card's overflow: hidden shapes the top corners. */
.product-media {
  position: relative;
  display: block;
  /* flex: none because the anchor above is now a flex column: without it the
     photograph would be the thing that shrinks when the body grows, and the
     4:5 crop would stop being 4:5. */
  flex: none;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--bg-quiet);
}

/* height: 100% is load-bearing. Each img carries width/height attributes, which
   the browser applies as a presentational height hint; without this every tile
   renders at the file's full 1250px.

   There is deliberately NO dark hover overlay. The parent's scrim reads "View
   photo" over the image; on a garment that hides the one thing the shopper is
   trying to see. */
.product-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.45s ease;
}
.product:hover .product-photo,
.product:focus-within .product-photo { transform: scale(1.04); }

/* Top-LEFT, not the parent's top-right: the right corner is where the eye looks
   for a price, and a badge there collides with it. Quieter than the parent's
   inverted pill, because a badge on every third card stops meaning anything if
   it shouts. The default treatment below is what an unrecognised badge string
   gets, so the owner can invent one. */
.product-badge {
  position: absolute;
  top: 0.8rem;
  left: 0.8rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.22rem 0.6rem;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.product-badge[data-badge="new"] {
  background: color-mix(in srgb, var(--sage-soft) 70%, var(--bg-raised));
  border-color: var(--sage-soft);
  color: var(--sage-deep);
}
.product-badge[data-badge="last-few"] {
  background: color-mix(in srgb, var(--clay-soft) 55%, var(--bg-raised));
  border-color: var(--clay-soft);
  color: var(--clay);
}

.product-body {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  flex: 1;
  padding: 1.15rem 1.15rem 0;
}

/* Taxonomy is material, so it is sage. */
.product-category {
  display: block;
  color: var(--sage-deep);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.product-name {
  display: block;
  font-family: var(--serif);
  font-size: var(--t-title);
  font-weight: 500;
  line-height: 1.2;
  text-wrap: balance;
}

.product-prices { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }

.product-price {
  font-family: var(--serif);
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--clay);
  font-variant-numeric: tabular-nums;
}
/* No price in product.txt: say so, rather than print $0 or a guess. */
.product-price.is-ask {
  font-family: var(--sans);
  font-size: var(--t-sm);
  font-weight: 400;
  color: var(--text-dim);
}

/* A former price, stated. There is no "SAVE 18%" pill and no percentage
   anywhere on this site — the shop stated a price it used to charge, it did not
   make a discount claim. */
.product-was {
  color: var(--text-dim);
  font-size: var(--t-sm);
  text-decoration-thickness: 1px;
  font-variant-numeric: tabular-nums;
}

/* The first sentence of the description; the full text lives on the product
   page. Clamped so a long sentence cannot push the swatches out of line with
   the card beside it. */
.product-blurb {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  color: var(--text-dim);
  font-size: var(--t-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

/* margin-top: auto pins the swatches to the bottom of the body regardless of
   how long the blurb ran. */
.product-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: auto;
  padding-top: 0.2rem;
}
.swatch {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.12rem 0.5rem 0.12rem 0.35rem;
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
}
/* Every dot takes a ring, so butter (1.32 on the raised ground) does not
   vanish. */
.swatch-dot {
  display: block;
  width: 0.68rem;
  height: 0.68rem;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 50%;
}
.swatch[data-tone="oat"]    .swatch-dot { background: var(--sw-oat); }
.swatch[data-tone="sage"]   .swatch-dot { background: var(--sw-sage); }
.swatch[data-tone="powder"] .swatch-dot { background: var(--sw-powder); }
.swatch[data-tone="blush"]  .swatch-dot { background: var(--sw-blush); }
.swatch[data-tone="clay"]   .swatch-dot { background: var(--sw-clay); }
.swatch[data-tone="butter"] .swatch-dot { background: var(--sw-butter); }
/* A colour name outside the six real garment tones gets its text label and no
   dot. We do not invent a hex for a colour we have not seen — that is the same
   failure as inventing a review. */
.swatch:not([data-tone]) .swatch-dot { display: none; }

.product-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.6rem;
  padding: 0.9rem 1.15rem 1.15rem;
  margin-top: 0.9rem;
  border-top: 1px solid var(--line);
}
/* No sizes on this product, so the button takes the whole footer. */
.product-foot.is-single { justify-content: stretch; }
.product-foot.is-single .bag-add { flex: 1; }

.product-size { flex: 1; display: flex; flex-direction: column; gap: 0.25rem; min-width: 6rem; }
/* A real, visible label inside the wrapping <label>, not an aria-label standing
   in for one. Because the label IS the wrapper, no for= is needed. That rule
   holds for every form control on this site. */
.product-size-label {
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.bag-add { flex: none; }
/* main.js swaps this text between Add / Added / In your enquiry. Holding it on
   one line means the button grows sideways rather than reflowing the footer to
   two rows and nudging the card's height mid-browse. */
.bag-add-label { white-space: nowrap; }

/* Native <select>, only the closed state styled: it is keyboard-complete for
   free and renders as the platform's own picker on a phone, which no
   hand-built dropdown matches. The chevron is drawn in the background so no
   extra element is needed — img-src 'self' data: in _headers is what permits
   the data URI, and it is the only one on the site. */
.bag-size,
.shop-filter select {
  appearance: none;
  width: 100%;
  padding: 0.38rem 1.9rem 0.38rem 0.7rem;
  background-color: var(--bg-quiet);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236a5648' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m19.5 8.25l-7.5 7.5l-7.5-7.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 0.9rem;
  border: 1px solid var(--edge);
  border-radius: var(--r-pill);
  color: var(--text);
  font: inherit;
  font-family: var(--mono);
  font-size: var(--t-xs);
  cursor: pointer;
  transition: border-color 0.18s ease;
}
/* Same 44px floor as .btn-sm on a phone. A select's height is padding plus one
   line, so padding-block is the only lever that does not change the type. */
@media (max-width: 52rem) {
  .bag-size,
  .shop-filter select { padding-block: 0.65rem; }
}
.bag-size:hover,
.shop-filter select:hover { border-color: var(--text); }
/* Tighter than the global 3px so the ring hugs the control. */
.bag-size:focus-visible,
.shop-filter select:focus-visible { outline-offset: 1px; }

/* ---------- product states ---------- */

/* status: sold-out in product.txt. The footer loses the select and the button
   entirely and carries one honest line instead. No waitlist form, no "notify
   me", no restock date, no counter — none of those exist. */
.product.is-sold-out .product-media { filter: saturate(0.5) opacity(0.72); }
.product-badge.is-out {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg-raised);
}
.product-gone { margin: 0; color: var(--text-dim); font-size: var(--t-sm); }

/* ---------- callout ---------- */

.callout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s-4);
  margin-top: 1.75rem;
  padding: 1.85rem;
  background: color-mix(in srgb, var(--clay-soft) 16%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, var(--edge) 35%, transparent);
  border-radius: var(--r-md);
}
.callout-title {
  margin: 0;
  font-family: var(--serif);
  font-size: 1.45rem;
  font-weight: 500;
  font-variation-settings: "WONK" 1;
}
.callout-text { margin: 0.35rem 0 0; max-width: 52ch; color: var(--text-dim); font-size: var(--t-sm); }
.callout .btn { flex: none; }

/* ============================================================
   /shop/ — the generated index

   Everything here is written by scripts/build-shop.py and appears on no other
   page, so none of it can be found by grepping index.html.
   ============================================================ */

/* Its own ground for the same reason as the two bars above it: product
   photographs scroll under this bar and must not show through. */
.shop-bar {
  position: sticky;
  top: var(--header-h);
  z-index: 25;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-4);
  padding: 0.7rem 0;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.shop-filter { display: inline-flex; align-items: center; gap: var(--s-1); }
.shop-filter select { width: auto; }
.shop-filter-label {
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.shop-reset {
  margin-left: auto;
  padding: 0.3rem 0.7rem;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  color: var(--text-dim);
  font: inherit;
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.shop-reset:hover { border-color: var(--edge); color: var(--text); }
.shop-reset[hidden] { display: none; }

/* A live region, so the count is announced when a filter changes. */
.shop-count {
  margin: 0 0 0 auto;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* :not([hidden]) matters: the reset is in the DOM even when the filters are
   clear, and a bare sibling selector would strip the count's auto margin and
   pull it back beside the selects. */
.shop-reset:not([hidden]) ~ .shop-count { margin-left: 0; }

/* A whole category, hidden by the filter's second pass when nothing in it
   matches — otherwise a filtered view leaves an orphan heading over an empty
   grid. */
.shop-group { margin-bottom: var(--s-6); }
.shop-group[hidden] { display: none; }
.shop-group:last-of-type { margin-bottom: 0; }

/* The hairline runs from the end of the words to the right edge, which is the
   same editorial device as .band's border-top at a smaller scale. */
.shop-group-title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin: 0 0 var(--s-3);
  font-family: var(--serif);
  font-size: 1.35rem;
  font-weight: 500;
  scroll-margin-top: calc(var(--header-h) + 4rem);
}
.shop-group-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--sage-soft);
}

@media (max-width: 34rem) {
  /* Two selects side by side on the first row, the count and reset beneath. */
  .shop-filter { flex: 1 1 45%; flex-direction: column; align-items: stretch; gap: 0.25rem; }
  .shop-filter select { width: 100%; }
  .shop-count { flex: 1 1 100%; margin: 0; }
  .shop-reset { margin-left: 0; }
}

/* ============================================================
   /shop/<slug>/ — the generated product page
   ============================================================ */

/* Sticks to the top of the viewport once the header scrolls away, so the way
   back to the rail is always one click. The band is cream too, so the bar needs
   its own ground to stop photographs showing through as they scroll under it. */
.entry-bar {
  position: sticky;
  top: var(--header-h);
  z-index: 25;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  max-width: var(--page);
  margin: 0 auto 1.75rem;
  padding: 0.6rem 0;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.entry-back,
.entry-step {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.4rem 0.7rem;
  border-radius: var(--r-pill);
  color: var(--text);
  font-size: var(--t-sm);
  text-decoration: none;
  transition: background 0.18s ease, color 0.18s ease;
}
/* Tag-qualified so the disabled <span> at the end of the run does not light
   up. */
.entry-back:hover,
a.entry-step:hover { background: var(--bg-quiet); color: var(--text); }

.entry-back { font-family: var(--serif); font-size: 1rem; }
.entry-steps { display: inline-flex; align-items: center; gap: 0.2rem; }

.entry-step-label {
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Ends of the run are kept in place rather than removed, so the bar does not
   change shape as you move through the catalogue. */
.entry-step.is-disabled { opacity: 0.3; cursor: default; }

/* One chevron in the sprite, turned to point whichever way is needed. */
.entry-chevron { width: 1rem; height: 1rem; flex: none; color: var(--clay); }
.entry-chevron.is-left  { transform: rotate(90deg); }
.entry-chevron.is-right { transform: rotate(-90deg); }
.entry-step.is-disabled .entry-chevron { color: var(--text-dim); }

.entry-split {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: clamp(2rem, 5vw, 3.5rem);
  align-items: start;
}

/* Every image from the product's folder, in filename order, all at the same
   crop. There is no album/single special case — product photographs share a
   ratio by construction, which is the opposite of the parent gallery's rule. */
.entry-photos { display: grid; gap: var(--s-2); }
.entry-photos img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--r-lg);
  background: var(--bg-quiet);
}

.entry-detail { display: flex; flex-direction: column; gap: var(--s-3); }
.entry-detail .product-category { margin: 0; }

.entry-title {
  margin: 0;
  font-family: var(--serif);
  font-size: var(--t-display);
  font-weight: 500;
  font-variation-settings: "WONK" 1;
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.entry-detail .product-prices { margin: 0; }

/* A row is omitted entirely by the build script when its key is absent from
   product.txt, so this list never shows an empty term. */
.spec-list { margin: 0; }
.spec-row {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: var(--s-3);
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--line);
}
.spec-row:last-child { border-bottom: 0; }
.spec-row dt {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--sage-deep);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.spec-row dd { margin: 0; font-size: var(--t-sm); }

.entry-add { display: flex; align-items: flex-end; gap: 0.6rem; flex-wrap: wrap; }
.entry-add .bag-add { flex: 1; }

/* The line under the add button that says what adding actually does. */
.field-note { margin: 0; max-width: 46ch; color: var(--text-dim); font-size: 0.78rem; }

@media (min-width: 52.0625rem) {
  /* The photo strip is twice the height of the panel beside it, so at rest the
     right-hand column was blank for its bottom 685px and the price and the Add
     control scrolled away while the shopper was still looking at the garment.
     Sticky keeps them alongside whichever photograph is on screen.

     --rail-h is deliberately NOT in this sum: that is the homepage tabbar,
     which this page does not have. The number here is .entry-bar, which is
     3.8rem tall and already sticks at --header-h. */
  .entry-detail {
    position: sticky;
    top: calc(var(--header-h) + 3.8rem + var(--s-3));
  }
}

@media (max-width: 52rem) {
  /* One column, and the DETAIL comes first. Two full-width 4:5 photographs
     stack 1,800px tall at tablet width, which put the name, the price and the
     Add control two and a half screens below the fold. The images are not
     focusable, so reordering them visually leaves the tab order alone. */
  .entry-split { grid-template-columns: 1fr; }
  .entry-detail { order: -1; }
}
@media (max-width: 34rem) {
  .spec-row { grid-template-columns: 1fr; gap: 0.3rem; }
}
@media (max-width: 26rem) {
  /* Just the chevrons once there is no room for words. */
  .entry-step-label { display: none; }
}

/* ============================================================
   SIZES & FABRIC

   Everything factual between the sizes markers is generated from the products'
   own product.txt files, so the sizes we claim to cut and the fabrics we claim
   to use cannot contradict a product page, and both grow when the catalogue
   does. The age table below them describes CHILDREN, not garments.
   ============================================================ */

.sizes-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: var(--s-4) var(--s-6);
}
/* min-width: 0 so a long fabric string cannot push a grid column past its
   track and make the whole band scroll sideways. */
.size-panel,
.care-panel { min-width: 0; }

.size-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0 0 var(--s-4);
  padding: 0;
  list-style: none;
}

/* Real links, so they work with JavaScript off; with JS on, /shop/ reads
   ?size= on load and arrives already filtered. This is what turns a decorative
   list into the site's size-first entry point. */
.size-chip {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  background: var(--bg-raised);
  border: 1px solid var(--edge);
  border-radius: var(--r-xs);
  color: var(--text);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.1em;
  text-decoration: none;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.size-chip:hover { background: var(--text); border-color: var(--text); color: var(--bg-raised); }
/* These ten chips are the size-first entry point into /shop/, so on a phone
   they are the most-tapped links on the page and 30px will not do. */
@media (max-width: 52rem) {
  .size-chip { padding-block: 0.75rem; }
}

.fit-notes { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.6rem; }
.fit-notes li {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0.55rem;
  color: var(--text-dim);
  font-size: var(--t-sm);
  line-height: 1.5;
}
.fit-notes .fit-icon { margin-top: 0.35rem; }

.care-list { margin: 0; padding: 0; list-style: none; }
.care-row {
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--sage-soft);
}
.care-row:first-child { padding-top: 0; }
.care-row:last-child { border-bottom: 0; padding-bottom: 0; }

.care-fabric,
.care-wash {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0.5rem;
  margin: 0;
  font-size: var(--t-sm);
  line-height: 1.45;
}
.care-fabric { color: var(--text); }
.care-wash { margin-top: 0.3rem; color: var(--text-dim); }
.care-fabric .care-icon,
.care-wash .care-icon { margin-top: 0.28rem; }

.care-for {
  margin: 0.35rem 0 0 1.5rem;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Wide on a phone, so it gets its own scroll container rather than making the
   whole page scroll sideways. */
.size-table-wrap {
  margin-top: var(--s-5);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.size-table {
  width: 100%;
  min-width: 32rem;
  border-collapse: collapse;
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
}
.size-table-caption {
  max-width: 60ch;
  margin-bottom: var(--s-3);
  color: var(--text-dim);
  font-size: var(--t-sm);
  line-height: 1.5;
  text-align: left;
}
.size-table th,
.size-table td {
  padding: 0.6rem 0.9rem 0.6rem 0;
  border-bottom: 1px solid var(--sage-soft);
  text-align: left;
}
.size-table thead th {
  color: var(--sage-deep);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.size-table tbody th {
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  white-space: nowrap;
}
.size-table tbody td { color: var(--text-dim); }
.size-table tbody tr:last-child th,
.size-table tbody tr:last-child td { border-bottom: 0; }

.sizes-fine {
  max-width: 52ch;
  margin: var(--s-4) 0 0;
  color: var(--text-dim);
  font-size: var(--t-sm);
}

/* ============================================================
   CONNECT
   ============================================================ */

/* Hard two-up rather than auto-fill: four cards over auto-fill columns strand
   one on a second row at several widths. */
.social-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }

/* Tag-qualified on purpose, to out-specify a.card { display: block }. */
a.social-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem;
}

.social-tile {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  background: var(--bg-quiet);
  border-radius: var(--r-sm);
  color: var(--clay);
  transition: background 0.18s ease;
}
.social-card:hover .social-tile { background: color-mix(in srgb, var(--clay-soft) 40%, transparent); }
.social-tile .icon { width: 1.45rem; height: 1.45rem; color: inherit; }

.social-text { min-width: 0; }
.social-name { display: block; font-weight: 600; font-size: 0.95rem; }
/* min-width: 0 on .social-text above lets the middle track shrink; nowrap plus
   the ellipsis is what stops the handle from being the thing that decides how
   wide a card has to be at 360px. */
.social-handle {
  display: block;
  margin-top: 0.1rem;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: 0.76rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.social-meta {
  flex: none;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  line-height: 1.45;
  text-align: right;
}

/* auto-FIT here rather than auto-fill, so two features always fill the row. */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: 1.25rem;
  margin-top: var(--s-4);
}

.feature {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.85rem;
  padding: 1.85rem;
}
.feature-tile {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  background: color-mix(in srgb, var(--sage-soft) 45%, transparent);
  border-radius: var(--r-sm);
  color: var(--sage-deep);
}
.feature-tile svg { width: 1.2rem; height: 1.2rem; fill: none; stroke: currentColor; }
/* The same two solid glyphs as the .btn-icon carve-out above, for the same
   reason. Any new solid glyph dropped in here needs its own line. */
.feature-tile:has(use[href="#icon-mail"]) svg { fill: currentColor; stroke: none; }

.feature-title {
  margin: 0;
  font-family: var(--serif);
  font-size: 1.3rem;
  font-weight: 500;
}
.feature-text { margin: 0; flex: 1; color: var(--text-dim); font-size: 0.93rem; line-height: 1.6; text-wrap: pretty; }

@media (max-width: 44rem) {
  .social-grid { grid-template-columns: 1fr; }
}
/* The third track is auto, so the note took its full max-content width and the
   handle — which has an ellipsis and therefore a min-content of zero — paid for
   it: "@thesondervi…" on the Instagram card at 360px, and three of the four
   handles clipped at 320px. A 7rem floor on the middle track is the whole fix:
   "@thesondervibes" measures 110px at the handle's 0.76rem mono, so the handle
   now states its claim first and the note takes an extra line when it must. */
@media (max-width: 40rem) {
  a.social-card { grid-template-columns: auto minmax(7rem, 1fr) auto; }
}

/* ---------- contact icons ---------- */

.social-icons { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.social-icons a {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--text-dim);
  transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease, transform 0.18s ease;
}
.social-icons a:hover { border-color: var(--edge); color: var(--text); transform: translateY(-2px); }
.social-icons svg { width: 1.45rem; height: 1.45rem; fill: currentColor; }

/* ---------- footer ---------- */

.site-footer {
  border-top: 1px solid var(--rule);
  color: var(--text-dim);
  font-size: var(--t-sm);
}
.site-footer p { margin: 0; }

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  max-width: var(--page);
  margin: 0 auto;
  padding: 2.5rem var(--gutter) 3.5rem;
}

.footer-brand { flex: none; }

.footer-wordmark {
  display: block;
  color: var(--text);
  font-family: var(--serif);
  font-size: 1.15rem;
  font-style: italic;
}
.footer-domain { margin-top: 0.2rem; font-family: var(--mono); font-size: 0.78rem; }
.footer-legal { font-family: var(--mono); font-size: 0.78rem; }

/* Smaller and borderless in the footer — the same component, quieter. */
.site-footer .social-icons a { width: 2.1rem; height: 2.1rem; background: none; border: 0; }
.site-footer .social-icons svg { width: 1.15rem; height: 1.15rem; }

@media (max-width: 40rem) {
  .footer-inner { flex-direction: column; text-align: center; }
}

/* ============================================================
   ENQUIRY BAG — the off-canvas panel

   There is no server and no payment processor, so the bag is a list that
   becomes an email. The panel says so twice, and the hero says it a third time.
   ============================================================ */

/* The scrim starts at var(--header-h), not 0. That is a seam decision rather
   than an aesthetic one — a translucent layer must never sit over the logo —
   and it leaves the header fully usable while the drawer is open. */
#bag-scrim {
  position: fixed;
  inset: var(--header-h) 0 0 0;
  z-index: 27;
  background: rgba(51, 36, 28, 0.28);
}
#bag-scrim[hidden] { display: none; }

.bag-panel {
  position: fixed;
  top: var(--header-h);
  right: 0;
  z-index: 28;
  width: min(24rem, 92vw);
  height: calc(100dvh - var(--header-h));
  display: flex;
  flex-direction: column;
  padding: var(--s-4);
  background: var(--bg-raised);      /* opaque; no backdrop-filter anywhere */
  border-left: 1px solid var(--line);
  box-shadow: var(--shadow-panel);
}
/* Must beat display: flex. */
.bag-panel[hidden] { display: none; }

.bag-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.bag-title { margin: 0; font-family: var(--serif); font-size: var(--t-title); font-weight: 500; }

.bag-close {
  display: inline-grid;
  place-items: center;
  flex: none;
  padding: 0.35rem;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: var(--text-dim);
  cursor: pointer;
}
.bag-close .icon { width: 1.2rem; height: 1.2rem; color: inherit; }
.bag-close:hover { color: var(--text); }

.bag-note { margin: 0.6rem 0 0; color: var(--text-dim); font-size: var(--t-xs); line-height: 1.5; }

.bag-list { flex: 1; overflow-y: auto; margin: var(--s-3) 0; padding: 0; list-style: none; }

/* Five children — thumbnail, text, stepper, line price, remove — so named areas
   over two rows rather than one impossible row of three columns. The thumbnail
   spans both; the stepper and the price sit under the name. Grid placement is
   independent of source order, so main.js can keep appending in the order that
   reads best. */
.bag-row {
  display: grid;
  grid-template-columns: 3rem 1fr auto;
  grid-template-areas:
    "thumb text  remove"
    "thumb qty   price";
  align-items: center;
  gap: 0.3rem 0.6rem;
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--line);
}
.bag-row:last-child { border-bottom: 0; }

.bag-thumb { grid-area: thumb; display: block; align-self: start; }
.bag-thumb img {
  display: block;
  width: 3rem;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--r-xs);
  background: var(--bg-quiet);
}

.bag-text { grid-area: text; min-width: 0; }
.bag-name { display: block; font-size: var(--t-sm); font-weight: 600; }
.bag-size-note {
  display: block;
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.08em;
}

.bag-qty { grid-area: qty; display: inline-flex; align-items: center; gap: 0.15rem; }
.bag-qty button {
  display: inline-grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  background: none;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--text-dim);
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.bag-qty button:hover { border-color: var(--edge); color: var(--text); }
.bag-qty svg { width: 0.8rem; height: 0.8rem; }
.bag-qty-n {
  min-width: 1.4rem;
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.bag-line-price {
  grid-area: price;
  text-align: right;
  color: var(--clay);
  font-family: var(--serif);
  font-size: var(--t-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.bag-remove {
  grid-area: remove;
  justify-self: end;
  display: inline-grid;
  place-items: center;
  padding: 0.25rem;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: var(--text-dim);
  cursor: pointer;
  transition: color 0.18s ease;
}
.bag-remove:hover { color: var(--clay); }
.bag-remove svg { width: 0.95rem; height: 0.95rem; }

/* The panel's empty state is a note, not a dashed box the width of the drawer. */
.bag-empty { padding: var(--s-5) 0.5rem; border: 0; font-size: 1rem; }

.bag-foot { flex: none; padding-top: var(--s-3); border-top: 1px solid var(--line); }

/* "Roughly", never "Total". It is arithmetic on published prices with no
   shipping and no tax, and it promises nothing. */
.bag-sum {
  margin: 0 0 var(--s-2);
  font-family: var(--serif);
  font-size: 1.15rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.bag-sum[hidden] { display: none; }

.bag-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); }
.bag-send { flex: 1; }
/* Disabled rather than removed, so the panel does not change shape when the
   last line is taken out. */
.bag-send.is-disabled { opacity: 0.45; pointer-events: none; }

#bag-clear { flex: none; }
/* Two taps to empty: no dialog, no confirm(), no dependency. */
#bag-clear.is-confirming { border-color: var(--clay); color: var(--clay); }

.bag-fine { margin: var(--s-2) 0 0; color: var(--text-dim); font-size: 0.72rem; line-height: 1.45; }

@media (max-width: 34rem) {
  /* A bottom sheet, where a thumb can reach it. */
  .bag-panel {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    height: min(80dvh, 34rem);
    border-left: 0;
    border-top: 1px solid var(--line);
    border-radius: var(--r-md) var(--r-md) 0 0;
  }
}
