/* DEGROSS — generated bundle. Edit assets/css/*, not this file. */

/* ── fonts.css ── */
/* =============================================================================
   Self-hosted typefaces — Madras Weave.

   Bricolage Grotesque carries the display voice: a contemporary grotesque with
   enough irregularity in the letterforms to read as chosen rather than
   defaulted. Work Sans handles everything functional — product names, filters,
   sizes and every price — because its open apertures keep 36 / 38 / 40 / 42
   unambiguous at 11px.

   Both are VARIABLE fonts requested on the weight axis only. Bricolage also
   ships opsz and wdth axes; dropping them cut the pair from 180 KB to 58 KB and
   nothing in this design animates width.

   ─────────────────────────────────────────────────────────────────────────────
   THE latin-ext SUBSET IS NOT OPTIONAL.

   The rupee sign ₹ (U+20B9) lives in U+20AD–20C0, which is latin-ext. Both
   files below were verified by parsing their cmap tables — a unicode-range
   declaration only tells the browser which file to fetch, it does not promise
   the glyph is inside. Without latin-ext every price falls back mid-string.
   ─────────────────────────────────────────────────────────────────────────────
   ========================================================================== */

/* ── Bricolage Grotesque — display ──────────────────────────────────────── */
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 800;          /* variable */
  font-display: swap;
  src: url('../fonts/bricolage-grotesque-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                 U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 400 800;          /* variable */
  font-display: swap;
  src: url('../fonts/bricolage-grotesque-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}

/* ── Work Sans — UI, prices, everything functional ──────────────────────── */
@font-face {
  font-family: 'Work Sans';
  font-style: normal;
  font-weight: 300 700;          /* variable */
  font-display: swap;
  src: url('../fonts/work-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                 U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Work Sans';
  font-style: normal;
  font-weight: 300 700;          /* variable */
  font-display: swap;
  src: url('../fonts/work-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                 U+FEFF, U+FFFD;
}


/* ── tokens.css ── */
/* =============================================================================
   DEGROSS — Design tokens · MADRAS WEAVE
   The single source of truth. Nothing downstream hard-codes a colour, a size
   or a duration; everything reads from here.

   ─────────────────────────────────────────────────────────────────────────────
   THE PALETTE IS THE PLAID.

   Every colour below is drawn from a madras check — madder, cobalt, jade and
   saffron on unbleached cotton. That is not decoration: it is the same colour
   set the product is woven from, which is why the interface can carry real
   colour without fighting a catalogue of 45 patterned shirts.

   The rule the palette obeys: SATURATION LIVES IN SMALL PLACES. Grounds stay
   near-white, type stays near-black, and the four plaid colours appear only
   where they carry meaning — sale, link, stock, offer. A page that is 6% madder
   reads as confident. A page that is 40% madder reads as a warning.

   Every pairing here was computed against WCAG 2.1 AA before it shipped; the
   measured ratio is noted beside anything that came close.
   ─────────────────────────────────────────────────────────────────────────────
   ========================================================================== */

:root {
  /* ── Surface ─────────────────────────────────────────────────────────── */
  /* Paired with THEME_COLOR in inc/config.php, which sets the browser chrome
     colour. A <meta> tag cannot read a custom property, so that one literal
     lives there — change both together. Everything else re-themes from here. */
  --bone:        #faf8f1;   /* unbleached cotton — the page ground */
  --bone-2:      #efede3;   /* alternating band */
  --paper:       #ffffff;   /* cards, raised surfaces — true white, so a
                               printed shirt reads against it honestly */

  /**
   * The dark surface is a WARM NEAR-BLACK, not navy and not #000.
   *
   * It was a deep cobalt, which tied the dark bands to the plaid but read as
   * blue against the bone and fought the sepia of the workroom photography.
   * #17130f is black to the eye — R23 G19 B15 — but its few points of warmth
   * are what let it sit beside a #faf8f1 ground without the seam between them
   * going cold. Pure #000 next to a warm cream reads as a hole in the page.
   *
   * Cobalt survives as a THREAD (links, the checked pattern, the selvedge),
   * which is where the plaid actually wants it. It is no longer a surface.
   */
  --ink:         #17130f;   /* dark sections, primary buttons · 17.39:1 on bone */
  --ink-2:       #241e18;   /* raised surface on dark sections */

  /* ── Text ────────────────────────────────────────────────────────────── */
  --text:        #191813;   /* 16.73:1 on bone */
  --text-soft:   #4c4a41;   /*  8.36:1 */
  --text-muted:  #6b685c;   /*  5.26:1 */
  --text-on-ink: #f2ece2;   /* 15.73:1 on ink — warm off-white, not pure chalk */
  --text-on-ink-soft: #b3a898;  /* 7.89:1 */

  /* ── Line ────────────────────────────────────────────────────────────── */
  /* --line is decorative and deliberately faint. --line-strong is the token
     for anything WCAG treats as a UI boundary (input borders, focus targets)
     and clears 3:1 at 3.35:1. Do not use --line on a form field. */
  --line:        #e4ded0;
  --line-strong: #8b887a;
  --line-on-ink: #342c24;

  /* ── The plaid ───────────────────────────────────────────────────────── */
  /* Four threads. Each has a text-safe value for type and rules, and where the
     hue is too light to carry text on a pale ground it also has a -fill value
     used only as a background with ink type on top. */
  --madder:      #a82a20;   /* sale, price drops, focus accents · 6.55:1 */
  --madder-soft: #c4453a;
  --cobalt:      #12459b;   /* links, in-page navigation · 8.41:1 */
  --cobalt-soft: #eaeff8;
  --jade:        #10634b;   /* trust: in stock, COD, secure payment · 6.79:1 */
  --jade-soft:   #e7f0ec;
  --saffron:     #7d5610;   /* saffron as TYPE · 6.16:1 */
  --saffron-fill:#d99a16;   /* saffron as a FILL — ink label only, 7.27:1.
                               Never put pale type on this. */

  /* Legacy aliases. The component layer was written against --clay/--pine;
     repointing here keeps ~20 files working without a rename sweep, which is
     churn with real risk and no user-visible benefit. */
  --clay:        var(--madder);
  --clay-soft:   var(--madder-soft);
  --pine:        var(--jade);
  --pine-soft:   var(--jade-soft);

  /* ── Feedback ────────────────────────────────────────────────────────── */
  --danger:      #9b2116;
  --danger-soft: #f9eae8;

  /* ── Weave ───────────────────────────────────────────────────────────── */
  /* The warp/weft rule system. Intervals are deliberately uneven — a real
     madras is not a uniform grid, and evenly spaced rules read as graph paper.
     Consumed by components/weave.css. */
  --weave-warp:  rgb(18 69 155 / 0.09);
  --weave-weft:  rgb(168 42 32 / 0.07);
  --weave-slub:  rgb(217 154 22 / 0.10);
  --weave-a:     23px;
  --weave-b:     61px;
  --weave-c:     97px;

  /* ── Typography ──────────────────────────────────────────────────────── */
  --font-display: 'Bricolage Grotesque', 'Segoe UI', system-ui, sans-serif;
  --font-ui: 'Work Sans', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* Fluid scale. Mobile-first: the small end is the phone, not a shrunk desktop. */
  --fs-xs:   0.75rem;                              /* 12 */
  --fs-sm:   0.8125rem;                            /* 13 */
  --fs-base: 0.9375rem;                            /* 15 */
  --fs-md:   1rem;                                 /* 16 */
  --fs-lg:   clamp(1.0625rem, 0.3vw + 1rem, 1.25rem);
  --fs-xl:   clamp(1.25rem, 1vw + 1rem, 1.625rem);
  --fs-2xl:  clamp(1.625rem, 2.4vw + 1rem, 2.5rem);
  --fs-3xl:  clamp(2.125rem, 4.5vw + 1rem, 3.75rem);
  --fs-display: clamp(2.6rem, 10vw, 6.5rem);

  --lh-tight:  1.05;
  --lh-snug:   1.25;
  --lh-normal: 1.6;

  /* A grotesque at display size needs more negative tracking than the serif it
     replaced — Bricolage's counters are open enough to take -0.03em without
     the headline closing up. */
  --ls-tight:  -0.03em;
  --ls-normal: 0;
  --ls-wide:   0.08em;
  --ls-eyebrow: 0.2em;

  /* ── Space — 4px base ────────────────────────────────────────────────── */
  --s1:  0.25rem;
  --s2:  0.5rem;
  --s3:  0.75rem;
  --s4:  1rem;
  --s5:  1.5rem;
  --s6:  2rem;
  --s7:  2.5rem;
  --s8:  3rem;
  --s9:  4rem;
  --s10: 5rem;
  --s11: 6.5rem;
  --s12: 8rem;

  /* Gutter — grows with the viewport, never cramped on a phone */
  --pad: clamp(1.25rem, 5vw, 5rem);
  --section-y: clamp(3.5rem, 9vw, 8rem);
  --maxw: 1560px;
  --maxw-prose: 68ch;

  /* ── Radius ──────────────────────────────────────────────────────────── */
  --r-sharp: 2px;    /* the editorial default */
  --r-card:  4px;
  --r-sheet: 14px;   /* mobile bottom sheets only */
  --r-pill:  999px;

  /* ── Elevation — restrained; this brand does not float ───────────────── */
  /* Tinted toward the warm ink rather than neutral black: a shadow that shares
     the ground's warmth sits on the page instead of hovering over it. */
  --shadow-sm: 0 1px 2px rgb(23 19 15 / 0.07);
  --shadow-md: 0 6px 24px -8px rgb(23 19 15 / 0.15);
  --shadow-lg: 0 24px 60px -20px rgb(23 19 15 / 0.26);

  /* ── Motion ──────────────────────────────────────────────────────────── */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --t-fast: 180ms;
  --t-mid:  280ms;
  --t-slow: 520ms;


  /* ── Layout ──────────────────────────────────────────────────────────── */
  --header-h: 60px;
  --announce-h: 36px;
  --tap: 44px;              /* minimum interactive target */
  --z-header: 100;
  --z-overlay: 200;
  --z-drawer: 210;
  --z-toast: 300;

  /* ── Aspect ratios — locked so real photography drops in cleanly ─────── */
  --ar-product: 3 / 4;
  --ar-banner: 16 / 9;
  --ar-lifestyle: 4 / 5;
}

@media (min-width: 768px) {
  :root {
    --header-h: 72px;
  }
}

/* Honour the OS setting everywhere, with no exceptions. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --t-fast: 1ms;
    --t-mid: 1ms;
    --t-slow: 1ms;
  }
}


/* ── base.css ── */
/* =============================================================================
   Base — reset, typography, and the primitives every page depends on.
   Mobile-first: base styles ARE the phone; every media query is min-width.
   ========================================================================== */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: smooth;
  /* Anchor targets clear the fixed header */
  scroll-padding-top: calc(var(--header-h) + var(--s4));
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  min-height: 100svh;
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  font-weight: 400;
  line-height: var(--lh-normal);
  color: var(--text);
  background: var(--bone);
  /**
   * NO overflow constraint on <body>. This is deliberate.
   *
   * `overflow-x: hidden` makes the body a scroll container, which silently
   * breaks `position: sticky` for every descendant — the hero's pinned stage
   * scrolled away instead of holding. `overflow-x: clip` is no better: when one
   * axis is clip, the other computes from `visible` to `auto`, so the body is
   * still a scrollport.
   *
   * It was only ever a safety net, and horizontal overflow is verified absent
   * (document scrollWidth equals the viewport at 390/768/1440). If something
   * overflows in future, fix it at the offending element — do not reintroduce
   * a body-level constraint, or sticky positioning breaks site-wide.
   */
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

/* Every image declares its ratio, so nothing ever shifts on load. */
img[width][height] { height: auto; }

input, button, textarea, select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

a {
  color: inherit;
  text-decoration: none;
}

ul, ol { list-style: none; padding: 0; }

p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }

table { border-collapse: collapse; width: 100%; }

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

/* ── Focus — visible, always, and never removed ─────────────────────────── */
:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
  border-radius: var(--r-sharp);
}

/* Only suppress the ring for pointer users who have a :focus-visible-capable
   browser — keyboard focus is never hidden. */
:focus:not(:focus-visible) { outline: none; }

/* ── Typography ─────────────────────────────────────────────────────────── */
h1, h2, h3, h4 {
  font-weight: 500;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  text-wrap: balance;
}

.display,
.section-title {
  font-family: var(--font-display);
  /* A grotesque needs real weight to hold a display line; the serif this
     replaced carried the same size at 500 on its stroke contrast alone. */
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: var(--ls-tight);
}

.display { font-size: var(--fs-display); }
.section-title { font-size: var(--fs-3xl); }

/* Kept from the serif era and still correct: wherever the display face sets
   numerals, pin them to lining figures so a price or a count never drops a
   descending 1 beside caps. */
.display,
.section-title,
.stats dd,
.hero__title,
.sectionhead__count strong,
.brandmark {
  font-variant-numeric: lining-nums;
  font-feature-settings: 'lnum' 1;
}

.eyebrow {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
  line-height: 1;
}

.lede {
  font-size: var(--fs-lg);
  line-height: 1.5;
  color: var(--text-soft);
  max-width: var(--maxw-prose);
}

.prose { max-width: var(--maxw-prose); }
.prose > * + * { margin-top: var(--s4); }
.prose h2 { font-family: var(--font-display); font-size: var(--fs-2xl); margin-top: var(--s7); }
.prose h3 { font-size: var(--fs-lg); margin-top: var(--s6); }
.prose a { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line-strong); }
.prose a:hover { text-decoration-color: var(--clay); }
.prose ul { list-style: disc; padding-left: 1.25em; }
.prose ol { list-style: decimal; padding-left: 1.25em; }
.prose li + li { margin-top: var(--s2); }

/* Prices always use tabular figures so columns align and digits don't jitter. */
.num,
.price {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* Numerals that animate (see modules/motion.js) need BOTH features at once:
   lining so the display face doesn't fall back to old-style figures, tabular
   so the box cannot change width while the digits run.

   These need their own rule because the two above collide — `.stats dd` and
   `.sectionhead__count strong` match the lining selector at (0,1,1) and the
   tabular one at (0,1,0), so tabular silently lost. Harmless while the numbers
   were static; visible as a width jitter the moment they count. */
.stats dd,
.sectionhead__count strong {
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: 'lnum' 1, 'tnum' 1;
}

/* ── Utilities ──────────────────────────────────────────────────────────── */
.container {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--pad);
}

.section { padding-block: var(--section-y); }

.stack > * + * { margin-top: var(--s4); }

.center { text-align: center; }

.muted { color: var(--text-muted); }
.soft  { color: var(--text-soft); }

/* Screen-reader-only, but focusable when it needs to be (skip link). */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: 0; left: 50%;
  transform: translate(-50%, -120%);
  z-index: calc(var(--z-toast) + 1);
  background: var(--ink);
  color: var(--text-on-ink);
  padding: var(--s3) var(--s5);
  font-size: var(--fs-sm);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  transition: transform var(--t-fast) var(--ease);
}

.skip-link:focus { transform: translate(-50%, 0); }

/* ── Ink sections — the light/dark rhythm that carries the premium read ─── */
.on-ink {
  background: var(--ink);
  color: var(--text-on-ink);
}

.on-ink .eyebrow { color: var(--text-on-ink-soft); }
.on-ink .lede,
.on-ink .soft { color: var(--text-on-ink-soft); }
.on-ink ::selection { background: var(--bone); color: var(--ink); }

/* ── Scroll-in reveal ───────────────────────────────────────────────────── */
/* Default state is the FINAL state. JS only ever adds the "not yet" class, so
   with JS disabled or blocked, everything is simply visible. */
.js [data-reveal]:not(.is-revealed) {
  opacity: 0;
  transform: translateY(18px);
}

[data-reveal] {
  transition:
    opacity var(--t-slow) var(--ease),
    transform var(--t-slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal]:not(.is-revealed) {
    opacity: 1;
    transform: none;
  }
}

/* ── Rails — horizontal scroll that is a RAIL, never a scroll hijack ────── */
/* A rail bleeds to the viewport edge, so it is a direct child of its section
   and carries the gutter itself. It must NOT sit inside .container — doubling
   the padding there is what used to push the document 20px wider than the
   viewport on a phone. */
.rail {
  display: flex;
  gap: var(--s4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  padding-inline: var(--pad);
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; flex: 0 0 auto; }

/* Content dissolves at the bleed edge instead of being guillotined by it. The
   mask stops exactly at the gutter, so the first and last card are never
   faded while they are the ones being read. */
.rail {
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--pad),
    #000 calc(100% - var(--pad)),
    transparent 100%
  );
}

@media (hover: hover) {
  .rail { cursor: grab; }
  .rail:active { cursor: grabbing; }
}

/* ── Rail position indicator ────────────────────────────────────────────── */
.railwrap { position: relative; }

.railbar {
  position: relative;
  height: 1px;
  margin-inline: var(--pad);
  margin-top: var(--s5);
  background: var(--line);
  /* Hidden until JS confirms there is actually something to scroll to. */
  opacity: 0;
  transition: opacity var(--t-mid) var(--ease);
}

.railwrap.is-scrollable .railbar { opacity: 1; }

.on-ink .railbar { background: var(--line-on-ink); }

.railbar__fill {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: calc(var(--rw, 1) * 100%);
  background: var(--clay);
  /* --rt is the offset in units of this element's own width, so a percentage
     translate lands it correctly without CSS having to divide. */
  translate: calc(var(--rt, 0) * 100%) 0;
}

/* ── Marquee — pauses on hover and respects reduced motion ──────────────── */
.marquee {
  overflow: hidden;
  /* No border: the marquee is now bound top and bottom by a .selvedge in the
     markup, which is the woven equivalent of the hairline it used to carry. */
  padding-block: var(--s3);
  background: var(--bone-2);
  /* Same edge treatment as the rails — phrases dissolve at the gutter rather
     than being cut off mid-word, which visually ties the page's two
     horizontal elements together. */
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--pad),
    #000 calc(100% - var(--pad)),
    transparent 100%
  );
}

.marquee__track {
  display: flex;
  width: max-content;
  gap: var(--s6);
  animation: marquee 38s linear infinite;
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-soft);
}

/* Every other phrase in madder — the ticker reads as a woven stripe rather
   than a run of grey text. */
.marquee__track > span:nth-child(odd) { color: var(--madder); }

.marquee:hover .marquee__track { animation-play-state: paused; }

@keyframes marquee {
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
}

/* ── Media wrapper — the CLS guarantee ──────────────────────────────────── */
.media {
  position: relative;
  overflow: hidden;
  background: var(--bone-2);
  aspect-ratio: var(--ar, var(--ar-product));
}

.media > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.media--banner    { --ar: var(--ar-banner); }
.media--lifestyle { --ar: var(--ar-lifestyle); }

/* Gentle scale on hover.
   Uses the independent `scale` property, not `transform`. Three effects now
   animate this image — the Wipe counter-move (transform), this hover zoom
   (scale) and scroll parallax (translate) — and each owns exactly one
   property so they compose instead of overwriting one another. See
   components/motion.css. */
.media--zoom > img {
  transition: scale var(--t-slow) var(--ease);
}

@media (hover: hover) {
  .media--zoom:hover > img,
  a:hover .media--zoom > img { scale: 1.03; }
}

/* ── Rules ──────────────────────────────────────────────────────────────── */
.rule {
  border: 0;
  border-top: 1px solid var(--line);
}

.on-ink .rule { border-color: var(--line-on-ink); }


/* ── layout.css ── */
/* =============================================================================
   Page-level layout scaffolding.
   ========================================================================== */

main { display: block; }

/* Interior pages get breathing room under the sticky header; the homepage hero
   supplies its own. */
.page-home main { padding-top: 0; }

/* ── Two-column shells (PDP, checkout, cart) ────────────────────────────── */
.split {
  display: grid;
  gap: var(--s7);
  align-items: start;
}

@media (min-width: 1024px) {
  .split {
    grid-template-columns: 1.15fr 1fr;
    gap: clamp(var(--s7), 4vw, var(--s10));
  }

  .split--narrow { grid-template-columns: 1fr minmax(320px, 400px); }

  /* Sticky side column — the order summary and buy panel stay in view. */
  .split__sticky {
    position: sticky;
    top: calc(var(--header-h) + var(--s5));
  }
}

/* ── Section separators ─────────────────────────────────────────────────── */
.section--tight { padding-block: clamp(2.5rem, 6vw, 4.5rem); }
.section--flush-top { padding-top: 0; }

.section--bone2 { background: var(--bone-2); }

/* ── Notices ────────────────────────────────────────────────────────────── */
.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-left: 2px solid var(--text-muted);
  border-radius: var(--r-sharp);
  background: var(--paper);
  font-size: var(--fs-sm);
  color: var(--text-soft);
}

.notice svg { flex: none; margin-top: 2px; }

.notice--info   { border-left-color: var(--clay); }
.notice--trust  { border-left-color: var(--pine); background: var(--pine-soft); color: var(--pine); }
.notice--error  { border-left-color: var(--danger); background: var(--danger-soft); color: var(--danger); }

/* A deliberately plain, honest note for data we do not have yet. */
.notice--pending {
  border-style: dashed;
  border-left-style: solid;
  background: transparent;
  color: var(--text-muted);
}

/* ── Accordion ──────────────────────────────────────────────────────────── */
.acc { border-top: 1px solid var(--line); }

.acc__item { border-bottom: 1px solid var(--line); }

.acc__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  width: 100%;
  min-height: var(--tap);
  padding-block: var(--s4);
  text-align: left;
  font-size: var(--fs-base);
  font-weight: 450;
  color: var(--text);
  transition: color var(--t-fast) var(--ease);
}

.acc__trigger:hover { color: var(--clay); }

.acc__icon {
  flex: none;
  position: relative;
  width: 12px;
  height: 12px;
}

.acc__icon::before,
.acc__icon::after {
  content: '';
  position: absolute;
  background: currentColor;
  transition: transform var(--t-mid) var(--ease), opacity var(--t-fast) var(--ease);
}

.acc__icon::before { top: 50%; left: 0; width: 100%; height: 1px; margin-top: -0.5px; }
.acc__icon::after  { left: 50%; top: 0; height: 100%; width: 1px; margin-left: -0.5px; }

[aria-expanded="true"] .acc__icon::after { transform: scaleY(0); opacity: 0; }

.acc__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--t-mid) var(--ease);
}

.acc__panel[data-open="true"] { grid-template-rows: 1fr; }

.acc__panel > div { overflow: hidden; }

.acc__body {
  padding-bottom: var(--s5);
  font-size: var(--fs-sm);
  color: var(--text-soft);
  line-height: 1.6;
  display: grid;
  gap: var(--s3);
}

/* No-JS: panels are simply open. */
html:not(.js) .acc__panel { grid-template-rows: 1fr; }
html:not(.js) .acc__trigger { pointer-events: none; }
html:not(.js) .acc__icon { display: none; }

/* ── Definition rows ────────────────────────────────────────────────────── */
.deflist { display: grid; gap: var(--s2); font-size: var(--fs-sm); }

.deflist__row {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
}

.deflist__row dt { color: var(--text-muted); }
.deflist__row dd { font-variant-numeric: tabular-nums; }

/* ── Simple data table ──────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.table {
  min-width: 26rem;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.table th,
.table td {
  text-align: left;
  padding: var(--s3) var(--s4) var(--s3) 0;
  border-bottom: 1px solid var(--line);
}

.table th {
  font-weight: 500;
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ── Pagination / load more ─────────────────────────────────────────────── */
.loadmore {
  display: grid;
  justify-items: center;
  gap: var(--s3);
  padding-top: var(--s8);
}

.loadmore__count {
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}


/* ── weave.css ── */
/* =============================================================================
   The weave — warp and weft as the page's structural grid.

   ─────────────────────────────────────────────────────────────────────────────
   WHY THIS EXISTS

   A madras check is not decoration applied to cloth; it IS the cloth's
   structure, produced by coloured threads crossing at uneven intervals. This
   component does the same thing to the page: a set of vertical (warp) and
   horizontal (weft) rules that products sit between, rather than a background
   image laid behind them.

   Two rules keep it from becoming wallpaper:

     1. It is opt-in per section, never global. A page fully covered in plaid
        is a page nobody can read a product on.
     2. The intervals are UNEVEN (--weave-a/b/c). Evenly spaced rules read as
        graph paper or a CSS demo; real madras is irregular, and the irregular
        spacing is most of why this reads as cloth.

   Everything here is a background layer on an element that already exists, so
   it adds no DOM, no script, and nothing for a screen reader to announce.
   ─────────────────────────────────────────────────────────────────────────────
   ========================================================================== */

/* ── The base weave ─────────────────────────────────────────────────────── */
/**
 * Overlapping translucent gradients, exactly the way a real gingham builds
 * colour: where warp crosses weft the two alphas compound and the intersection
 * goes darker on its own. That compounding is why these use rgb(… / a) rather
 * than solid colours — a solid grid cannot produce the crossing.
 */
.weave {
  position: relative;
  background-image:
    /* warp — vertical threads */
    repeating-linear-gradient(90deg,
      transparent 0 var(--weave-a),
      var(--weave-warp) var(--weave-a) calc(var(--weave-a) + 1px),
      transparent calc(var(--weave-a) + 1px) var(--weave-b),
      var(--weave-warp) var(--weave-b) calc(var(--weave-b) + 2px),
      transparent calc(var(--weave-b) + 2px) var(--weave-c)),
    /* weft — horizontal threads */
    repeating-linear-gradient(0deg,
      transparent 0 var(--weave-a),
      var(--weave-weft) var(--weave-a) calc(var(--weave-a) + 1px),
      transparent calc(var(--weave-a) + 1px) var(--weave-b),
      var(--weave-weft) var(--weave-b) calc(var(--weave-b) + 2px),
      transparent calc(var(--weave-b) + 2px) var(--weave-c));
}

/* A single saffron slub every few repeats — the irregular bright thread that
   stops a plaid looking machine-perfect. */
.weave--slub {
  background-image:
    repeating-linear-gradient(90deg,
      transparent 0 calc(var(--weave-c) * 2),
      var(--weave-slub) calc(var(--weave-c) * 2) calc(var(--weave-c) * 2 + 3px),
      transparent calc(var(--weave-c) * 2 + 3px) calc(var(--weave-c) * 5)),
    repeating-linear-gradient(90deg,
      transparent 0 var(--weave-a),
      var(--weave-warp) var(--weave-a) calc(var(--weave-a) + 1px),
      transparent calc(var(--weave-a) + 1px) var(--weave-b),
      var(--weave-warp) var(--weave-b) calc(var(--weave-b) + 2px),
      transparent calc(var(--weave-b) + 2px) var(--weave-c)),
    repeating-linear-gradient(0deg,
      transparent 0 var(--weave-a),
      var(--weave-weft) var(--weave-a) calc(var(--weave-a) + 1px),
      transparent calc(var(--weave-a) + 1px) var(--weave-b),
      var(--weave-weft) var(--weave-b) calc(var(--weave-b) + 2px),
      transparent calc(var(--weave-b) + 2px) var(--weave-c));
}

/* Fade the weave out at the top and bottom edges so a section does not begin
   and end on a hard seam. */
.weave--fade {
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
}

/* On dark sections the threads have to lighten rather than darken, or they
   disappear into the ground. Warm off-white, matching --text-on-ink, so the
   weave reads as the same cloth rather than a cool grid laid over it. */
.on-ink.weave,
.on-ink .weave {
  --weave-warp: rgb(242 236 226 / 0.13);
  --weave-weft: rgb(242 236 226 / 0.09);
  --weave-slub: rgb(217 154 22 / 0.20);
}

/* ── Selvedge rule ──────────────────────────────────────────────────────── */
/**
 * A four-thread band used to open and close a section — the striped edge of a
 * woven length. Replaces the plain 1px hairline the old build used between
 * sections, and is the one place the full plaid appears at full saturation.
 */
.selvedge {
  height: 3px;
  border: 0;
  margin: 0;
  background: linear-gradient(90deg,
    var(--madder) 0 22%,
    var(--saffron-fill) 22% 34%,
    var(--bone-2) 34% 42%,
    var(--cobalt) 42% 74%,
    var(--jade) 74% 100%);
}

.selvedge--thin { height: 2px; }

/* Vertical variant for the page edge. */
.selvedge--v {
  height: auto;
  width: 3px;
  background: linear-gradient(180deg,
    var(--madder) 0 22%,
    var(--saffron-fill) 22% 34%,
    var(--bone-2) 34% 42%,
    var(--cobalt) 42% 74%,
    var(--jade) 74% 100%);
}

/* ── Pattern swatch ─────────────────────────────────────────────────────── */
/**
 * A real woven check, built from two colours crossing. Used by the hero bands
 * and anywhere a pattern needs representing before photography exists.
 * Set --t1 / --t2 per instance.
 */
.swatch {
  --t1: rgb(18 69 155 / 0.62);
  --t2: rgb(217 154 22 / 0.5);
  background-color: var(--bone);
  background-image:
    repeating-linear-gradient(90deg, transparent 0 15px, var(--t1) 15px 27px),
    repeating-linear-gradient(0deg,  transparent 0 15px, var(--t1) 15px 27px),
    repeating-linear-gradient(90deg, transparent 0 31px, var(--t2) 31px 38px),
    repeating-linear-gradient(0deg,  transparent 0 31px, var(--t2) 31px 38px);
}

/* Stripes run one way only — that is the whole difference from a check. */
.swatch--striped {
  background-image:
    repeating-linear-gradient(90deg, transparent 0 9px, var(--t1) 9px 15px),
    repeating-linear-gradient(90deg, transparent 0 26px, var(--t2) 26px 30px);
}

/* A print is not a weave: irregular, no grid. Radial blooms rather than rules. */
.swatch--printed {
  background-image:
    radial-gradient(circle at 22% 28%, var(--t1) 0 7px, transparent 8px),
    radial-gradient(circle at 68% 12%, var(--t2) 0 5px, transparent 6px),
    radial-gradient(circle at 84% 62%, var(--t1) 0 6px, transparent 7px),
    radial-gradient(circle at 38% 74%, var(--t2) 0 8px, transparent 9px),
    radial-gradient(circle at 12% 88%, var(--t1) 0 4px, transparent 5px);
  background-size: 96px 96px;
}

/* Solid is the absence of pattern — a flat ground with only the weave's own
   texture showing through. */
.swatch--solid {
  background-image:
    repeating-linear-gradient(90deg, transparent 0 3px, rgb(0 0 0 / 0.04) 3px 4px),
    repeating-linear-gradient(0deg,  transparent 0 3px, rgb(0 0 0 / 0.03) 3px 4px);
}

@media (prefers-reduced-motion: reduce) {
  .weave, .weave--slub { background-attachment: scroll; }
}


/* ── button.css ── */
/* =============================================================================
   Buttons, badges, pills.
   Every variant is ≥44px tall on touch. No action depends on hover.
   ========================================================================== */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  --btn-bd: var(--ink);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: 0 var(--s5);
  border: 1px solid var(--btn-bd);
  border-radius: var(--r-sharp);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  line-height: 1;
  text-align: center;
  cursor: pointer;
  transition:
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease),
    opacity var(--t-fast) var(--ease);
}

/* ── Variants ───────────────────────────────────────────────────────────── */
.btn--ink {
  --btn-bg: var(--ink);
  --btn-fg: var(--text-on-ink);
  --btn-bd: var(--ink);
}

.btn--ink:hover {
  --btn-bg: var(--clay);
  --btn-bd: var(--clay);
}

.btn--outline:hover {
  --btn-bg: var(--ink);
  --btn-fg: var(--text-on-ink);
}

.btn--clay {
  --btn-bg: var(--clay);
  --btn-fg: #fff;
  --btn-bd: var(--clay);
}

.btn--clay:hover {
  --btn-bg: var(--ink);
  --btn-bd: var(--ink);
}

/* On an ink background the polarity inverts. */
.on-ink .btn--outline,
.btn--light {
  --btn-bd: var(--text-on-ink);
  --btn-fg: var(--text-on-ink);
}

.on-ink .btn--outline:hover,
.btn--light:hover {
  --btn-bg: var(--bone);
  --btn-fg: var(--ink);
  --btn-bd: var(--bone);
}

.on-ink .btn--ink {
  --btn-bg: var(--bone);
  --btn-fg: var(--ink);
  --btn-bd: var(--bone);
}

.on-ink .btn--ink:hover {
  --btn-bg: var(--clay);
  --btn-fg: #fff;
  --btn-bd: var(--clay);
}

/**
 * Fill-on-hover — ink flooding a printed box.
 *
 * The sweep is a layer that scales from the leading edge, not a background
 * cross-fade, so the surface is never a flat in-between colour mid-hover.
 * `overflow: hidden` clips it to the 2px radius; `isolation: isolate` keeps
 * the z-index:-1 layer above the button's own background but behind its label.
 */
.btn--fill {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.btn--fill::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--clay);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t-mid) var(--ease);
}

@media (hover: hover) {
  .btn--fill:hover::before { transform: scaleX(1); }

  /* Hold the base fill so the sweep is the only thing that moves. Without
     this, .btn--ink:hover would cross-fade the whole surface to clay
     underneath and there would be nothing left for the sweep to reveal. */
  .btn--ink.btn--fill:hover { --btn-bg: var(--ink); --btn-bd: var(--clay); }
}

/* ── Sizes ──────────────────────────────────────────────────────────────── */
.btn--sm {
  min-height: 40px;
  padding: 0 var(--s4);
  font-size: var(--fs-xs);
}

.btn--lg {
  min-height: 54px;
  padding: 0 var(--s7);
  font-size: var(--fs-base);
}

.btn--block { width: 100%; }

/* ── States ─────────────────────────────────────────────────────────────── */
.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.42;
  cursor: not-allowed;
  pointer-events: none;
}

/* Busy state — the label is swapped for a spinner without the button resizing. */
.btn.is-busy {
  color: transparent;
  pointer-events: none;
  position: relative;
}

.btn.is-busy::after {
  content: '';
  position: absolute;
  width: 15px;
  height: 15px;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--btn-fg);
  animation: spin 620ms linear infinite;
}

.btn--ink.is-busy::after { border-color: var(--text-on-ink); border-top-color: transparent; }

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .btn.is-busy::after { animation-duration: 1.6s; }
}

/* ── Text link with an animated rule ────────────────────────────────────── */
.link {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--fs-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding-bottom: 3px;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 100% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size var(--t-mid) var(--ease), color var(--t-fast) var(--ease);
}

.link:hover {
  color: var(--clay);
  background-size: 0% 1px;
  background-position: 100% 100%;
}

/* ── Badges ─────────────────────────────────────────────────────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px var(--s2);
  border-radius: var(--r-sharp);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
}

/* Sale is stated quietly — a hairline outline, not a shouting red block.
   Permanent loud discounting is what made the old site read cheap. */
.badge--sale {
  border: 1px solid var(--clay);
  color: var(--clay);
  background: color-mix(in srgb, var(--clay) 7%, transparent);
}

.badge--trust {
  border: 1px solid color-mix(in srgb, var(--pine) 40%, transparent);
  color: var(--pine);
  background: var(--pine-soft);
}

.badge--ink {
  background: var(--ink);
  color: var(--text-on-ink);
}

/**
 * Sold out.
 *
 * Solid rather than outlined, because unlike a discount this is not a nicety —
 * it changes whether the card is worth clicking, and it has to survive being
 * seen at a glance over a photograph.
 */
.badge--out {
  background: var(--ink);
  color: var(--text-on-ink);
  border: 1px solid var(--ink);
}

/* Client-set labels. Quieter than sale or sold out — they are emphasis, not
   information the customer needs before deciding to click. */
.badge--new,
.badge--bestseller,
.badge--limited,
.badge--restocked {
  border: 1px solid var(--line-strong);
  color: var(--text-soft);
  background: var(--paper);
}

.card__badges {
  position: absolute;
  left: var(--s3);
  bottom: var(--s3);
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  z-index: 1;
}

/* A styled span standing in for a button that would have nothing to do. */
.btn.is-disabled {
  pointer-events: none;
  color: var(--text-muted);
  border-color: var(--line);
  background: var(--bone-2);
}

/* ── Quantity stepper ───────────────────────────────────────────────────── */
.qty {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sharp);
  background: var(--paper);
}

.qty__btn {
  width: var(--tap);
  height: var(--tap);
  display: grid;
  place-items: center;
  font-size: var(--fs-md);
  color: var(--text-soft);
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.qty__btn:hover:not([disabled]) { color: var(--text); background: var(--bone-2); }
.qty__btn[disabled] { opacity: 0.35; cursor: not-allowed; }

.qty__value {
  min-width: 2.25rem;
  text-align: center;
  font-size: var(--fs-base);
  font-variant-numeric: tabular-nums;
  border: 0;
  background: none;
  -moz-appearance: textfield;
  appearance: textfield;
}

.qty__value::-webkit-outer-spin-button,
.qty__value::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ── Price ──────────────────────────────────────────────────────────────── */
.price {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s2);
}

.price__now { font-weight: 600; }
.price__now--sale { color: var(--madder); }

/* The strike is a madder thread, the same gesture as .pricefig__was in the
   offer band — just at a weight that suits 13px instead of 9rem. */
.price__was {
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-color: var(--madder);
  text-decoration-thickness: 1.5px;
  font-size: 0.875em;
}

/* ── Star rating ────────────────────────────────────────────────────────── */
.stars {
  display: inline-flex;
  gap: 2px;
  color: var(--clay);
  line-height: 1;
}

.stars svg { width: 13px; height: 13px; }
.stars__empty { color: var(--line-strong); }


/* ── overlay.css ── */
/* =============================================================================
   Overlay primitive — shared by the nav drawer, cart drawer, search sheet,
   size sheet and filter sheet.

   One set of behaviours, defined once:
     • scrim + panel, panel slides from an edge
     • [hidden] is the closed state, so no-JS never traps a user behind a scrim
     • focus trap / Esc / scroll lock are handled in JS (modules/overlay.js)
   ========================================================================== */

.overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
}

.overlay[hidden] { display: none; }

.overlay__scrim {
  position: absolute;
  inset: 0;
  background: rgb(20 18 16 / 0.42);
  opacity: 0;
  transition: opacity var(--t-mid) var(--ease);
  cursor: pointer;
}

.overlay.is-open .overlay__scrim { opacity: 1; }

.overlay__panel {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  background: var(--bone);
  max-height: 100dvh;
  transition: transform var(--t-mid) var(--ease);
  will-change: transform;
}

/* ── Edges ──────────────────────────────────────────────────────────────── */
.overlay__panel--left,
.overlay__panel--right {
  width: min(100%, 400px);
  height: 100dvh;
  margin-left: auto;
}

.overlay__panel--left  { margin: 0 auto 0 0; transform: translateX(-100%); }
.overlay__panel--right { transform: translateX(100%); }

.overlay__panel--top {
  width: 100%;
  margin-bottom: auto;
  transform: translateY(-100%);
  max-height: 92dvh;
}

.overlay.is-open .overlay__panel { transform: none; }

/* ── Mobile bottom sheets ───────────────────────────────────────────────── */
/* On a phone, a sheet rising from the bottom sits in the thumb zone. */
.overlay--sheet .overlay__panel {
  width: 100%;
  margin-top: auto;
  max-height: 88dvh;
  border-radius: var(--r-sheet) var(--r-sheet) 0 0;
  transform: translateY(100%);
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.overlay--sheet .overlay__panel::before {
  content: '';
  width: 36px;
  height: 4px;
  margin: var(--s3) auto var(--s1);
  background: var(--line-strong);
  border-radius: var(--r-pill);
  flex: none;
}

@media (min-width: 768px) {
  /* Above the phone, the sheet becomes a centred dialog. */
  .overlay--sheet { align-items: center; justify-content: center; }

  .overlay--sheet .overlay__panel {
    width: min(100%, 560px);
    margin: auto;
    max-height: 84dvh;
    border-radius: var(--r-card);
    transform: translateY(16px) scale(0.98);
    opacity: 0;
    transition:
      transform var(--t-mid) var(--ease),
      opacity var(--t-mid) var(--ease);
  }

  .overlay--sheet .overlay__panel::before { display: none; }

  .overlay--sheet.is-open .overlay__panel { transform: none; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .overlay__panel { transition: none; }
}

/* ── Drawer chrome ──────────────────────────────────────────────────────── */
.drawer__head,
.cartdrawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
  flex: none;
}

.drawer__close {
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  margin-right: calc(var(--s2) * -1);
  color: var(--text);
  transition: color var(--t-fast) var(--ease);
}

.drawer__close:hover { color: var(--clay); }

.drawer__body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s5);
  -webkit-overflow-scrolling: touch;
}

.drawer__foot {
  flex: none;
  padding: var(--s5);
  border-top: 1px solid var(--line);
  display: grid;
  gap: var(--s1);
  font-size: var(--fs-sm);
}

.drawer__contact { color: var(--text-soft); }
.drawer__contact:hover { color: var(--clay); }

.drawer__note {
  margin-top: var(--s2);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ── Big numbered nav list ──────────────────────────────────────────────── */
.navlist { display: grid; }

.navlist__link {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding-block: var(--s3);
  min-height: var(--tap);
  border-bottom: 1px solid var(--line);
  transition: color var(--t-fast) var(--ease);
}

.navlist__link:hover { color: var(--clay); }

.navlist__num {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 1.6em;
}

.navlist__label {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  line-height: 1.05;
  flex: 1;
}

.navlist__meta {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.drawer__links {
  display: grid;
  gap: var(--s1);
  margin-top: var(--s5);
  font-size: var(--fs-base);
}

.drawer__links a {
  display: block;
  padding-block: var(--s2);
  color: var(--text-soft);
  min-height: var(--tap);
  display: flex;
  align-items: center;
}

.drawer__links a:hover { color: var(--clay); }

/* ── Search sheet ───────────────────────────────────────────────────────── */
.searchsheet {
  padding: var(--s4) var(--pad) var(--s6);
  gap: var(--s3);
  background: var(--paper);
}

.searchsheet__form {
  display: flex;
  align-items: center;
  gap: var(--s2);
  border-bottom: 1px solid var(--ink);
  padding-block: var(--s2);
}

.searchsheet__icon { flex: none; color: var(--text-muted); }

.searchsheet__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  font-size: var(--fs-lg);
  padding-block: var(--s2);
  outline: none;
}

.searchsheet__input::placeholder { color: var(--text-muted); }
.searchsheet__input::-webkit-search-cancel-button { -webkit-appearance: none; }

.searchsheet__submit { flex: none; }

.searchsheet__close {
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  flex: none;
  color: var(--text-soft);
}

.searchsheet__hint {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  letter-spacing: 0.05em;
}

.searchsheet__results {
  overflow-y: auto;
  overscroll-behavior: contain;
  display: grid;
  gap: var(--s1);
}

.searchsheet__results:empty { display: none; }

.sresult {
  display: grid;
  grid-template-columns: 48px 1fr auto;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2);
  border-radius: var(--r-sharp);
  min-height: var(--tap);
  transition: background var(--t-fast) var(--ease);
}

.sresult:hover,
.sresult:focus-visible { background: var(--bone-2); }

.sresult__img {
  width: 48px;
  aspect-ratio: var(--ar-product);
  object-fit: cover;
  background: var(--bone-2);
}

.sresult__name { font-size: var(--fs-sm); line-height: 1.3; }
.sresult__meta { font-size: var(--fs-xs); color: var(--text-muted); }
.sresult__price { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

.searchsheet__empty {
  padding: var(--s5) 0;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

.chips { display: flex; flex-wrap: wrap; gap: var(--s2); }

.chip {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 0 var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-soft);
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}

.chip:hover { border-color: var(--ink); color: var(--text); }

.searchsheet__quick { display: grid; gap: var(--s2); }

/* Hide the suggestion block once a query is active. */
.searchsheet.is-searching .searchsheet__quick { display: none; }

/* ── Toast ──────────────────────────────────────────────────────────────── */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px));
  transform: translate(-50%, 120%);
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  gap: var(--s4);
  width: min(calc(100% - var(--s6)), 420px);
  padding: var(--s3) var(--s4);
  background: var(--ink);
  color: var(--text-on-ink);
  border-radius: var(--r-sharp);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-sm);
  transition: transform var(--t-mid) var(--ease);
}

.toast[hidden] { display: none; }
.toast.is-open { transform: translate(-50%, 0); }

.toast__text { flex: 1; }

.toast__action {
  flex: none;
  text-decoration: underline;
  text-underline-offset: 3px;
  font-weight: 500;
}

/* When the sticky buy bar is present, lift the toast above it. */
body.has-buybar .toast { bottom: calc(var(--s5) + 68px + env(safe-area-inset-bottom, 0px)); }

@media (min-width: 768px) {
  body.has-buybar .toast { bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px)); }
}


/* ── header.css ── */
/* =============================================================================
   Header + announcement bar — mobile-first.
   Phone: menu · brand · search · bag.  From 1024px the inline nav appears.
   ========================================================================== */

/* ── Announcement bar ───────────────────────────────────────────────────── */
.announce {
  background: var(--ink);
  color: var(--text-on-ink);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
}

.announce[hidden] { display: none; }

.announce__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  min-height: var(--announce-h);
  padding: var(--s2) var(--pad);
  position: relative;
}

.announce__text {
  text-align: center;
  line-height: 1.35;
  /* Keep clear of the absolutely-positioned close button — without this the
     text wraps underneath it on a narrow phone. */
  padding-inline: var(--s6);
}

.announce__text strong { font-weight: 600; }

.announce__sep { opacity: 0.45; margin-inline: var(--s1); }

.announce__close {
  position: absolute;
  right: max(0px, calc(var(--pad) - var(--s5)));
  display: grid;
  place-items: center;
  /* Full 44px target even though the glyph is 14px. */
  width: var(--tap);
  height: var(--tap);
  color: inherit;
  opacity: 0.6;
  transition: opacity var(--t-fast) var(--ease);
}

.announce__close:hover { opacity: 1; }

/* ── Header ─────────────────────────────────────────────────────────────── */
.header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: color-mix(in srgb, var(--bone) 88%, transparent);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
  border-bottom: 1px solid transparent;
  transition:
    border-color var(--t-mid) var(--ease),
    background var(--t-mid) var(--ease),
    transform var(--t-mid) var(--ease);
}

/* A hairline appears only once the page has scrolled — cleaner at rest. */
.header.is-stuck {
  border-bottom-color: var(--line);
  background: color-mix(in srgb, var(--bone) 96%, transparent);
}

/* Hide on scroll-down, reveal on scroll-up — reclaims vertical space on a phone
   without ever making the bag unreachable. */
.header.is-hidden { transform: translateY(-100%); }

/**
 * Scroll progress.
 *
 * The one element that spans every section of a long homepage. Riding on the
 * header's existing hairline means it costs no extra chrome — the rule is
 * already there, this just fills it — and it turns "how much more is there?"
 * into an instrument reading rather than a scroll-and-see.
 *
 * --progress is written by modules/header.js inside the rAF it already runs,
 * so this adds no listener.
 */
.header::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  bottom: -1px;
  height: 1px;
  background: var(--clay);
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
  transition: opacity var(--t-mid) var(--ease);
}

/* An open overlay owns the screen; the page behind it is not being read. */
body:has(.overlay.is-open) .header::after { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .header.is-hidden { transform: none; }
}

.header__inner {
  display: grid;
  grid-template-columns: var(--tap) 1fr auto;
  align-items: center;
  gap: var(--s2);
  min-height: var(--header-h);
  padding-inline: clamp(0.5rem, 3vw, 2.5rem);
  max-width: var(--maxw);
  margin-inline: auto;
}

/* ── Brand ──────────────────────────────────────────────────────────────── */
.header__brand {
  justify-self: center;
  display: inline-flex;
  align-items: center;
  padding-block: var(--s2);
  min-height: var(--tap);   /* the logo is a primary link; give it a real target */
}

/**
 * The wordmark is live text, never an image — it inherits currentColor, so
 * re-theming the brand is a tokens.css edit with nothing to re-export.
 * No colour is declared here on purpose.
 *
 * DEGROSS is a single all-caps word. Caps need more tracking than mixed case
 * to avoid reading as a solid block, hence the wider letter-spacing.
 */
.brandmark {
  font-family: var(--font-display);
  font-size: clamp(1.0625rem, 3.4vw, 1.375rem);
  font-weight: 600;
  letter-spacing: 0.06em;
  line-height: 1;
  white-space: nowrap;
}

/* ── Icon buttons — always ≥44px, never hover-only ──────────────────────── */
.header__icon {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  color: var(--text);
  border-radius: var(--r-sharp);
  transition: color var(--t-fast) var(--ease);
}

.header__icon:hover { color: var(--clay); }

.header__actions {
  display: flex;
  align-items: center;
  justify-self: end;
}

/* Bag count */
.header__count {
  position: absolute;
  top: 4px;
  right: 2px;
  min-width: 17px;
  height: 17px;
  padding-inline: 4px;
  display: none;
  place-items: center;
  background: var(--clay);
  color: #fff;
  border-radius: var(--r-pill);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.header__count.is-visible { display: grid; }

/* A short pulse when an item lands in the bag. */
.header__count.is-bumped { animation: countBump 420ms var(--ease); }

@keyframes countBump {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.42); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .header__count.is-bumped { animation: none; }
}

/* ── Burger ─────────────────────────────────────────────────────────────── */
.burger {
  position: relative;
  display: block;
  width: 19px;
  height: 11px;
}

.burger span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--t-mid) var(--ease), opacity var(--t-fast) var(--ease);
}

.burger span:first-child { top: 0; }
.burger span:last-child  { bottom: 0; }

[aria-expanded="true"] .burger span:first-child { transform: translateY(4.75px) rotate(45deg); }
[aria-expanded="true"] .burger span:last-child  { transform: translateY(-4.75px) rotate(-45deg); }

/* ── Inline nav — desktop only ──────────────────────────────────────────── */
.header__nav { display: none; }

@media (min-width: 1024px) {
  .header__inner {
    grid-template-columns: auto 1fr auto;
    gap: var(--s6);
  }

  .header__menu { display: none; }

  .header__brand { justify-self: start; }

  .header__nav {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: clamp(var(--s4), 2vw, var(--s6));
  }

  .header__link {
    position: relative;
    font-size: var(--fs-sm);
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-soft);
    padding-block: var(--s2);
    transition: color var(--t-fast) var(--ease);
  }

  /* Underline grows from the left — the only nav flourish. */
  .header__link::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 2px;
    width: 100%;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--t-mid) var(--ease);
  }

  .header__link:hover,
  .header__link.is-active { color: var(--text); }

  .header__link:hover::after,
  .header__link.is-active::after { transform: scaleX(1); }
}


/* ── footer.css ── */
/* =============================================================================
   Footer — the closing ink section.
   ========================================================================== */

.footer {
  padding-top: var(--section-y);
  margin-top: var(--section-y);
}

/* ── Closing CTA ────────────────────────────────────────────────────────── */
.footer__cta {
  text-align: center;
  padding-bottom: var(--s10);
  border-bottom: 1px solid var(--line-on-ink);
}

.footer__headline {
  font-size: clamp(2.25rem, 8vw, 5rem);
  margin-block: var(--s4) var(--s6);
}

/* ── Newsletter ─────────────────────────────────────────────────────────── */
.footer__news {
  padding-block: var(--s8);
  border-bottom: 1px solid var(--line-on-ink);
}

.footer__news-inner {
  display: grid;
  gap: var(--s5);
}

.footer__news-title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  margin-bottom: var(--s1);
}

.footer__news-copy {
  font-size: var(--fs-sm);
  color: var(--text-on-ink-soft);
  max-width: 42ch;
}

.newsletter__row {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}

.newsletter__input {
  flex: 1 1 12rem;
  min-width: 0;
  min-height: var(--tap);
  padding: 0 var(--s4);
  background: transparent;
  border: 1px solid var(--line-on-ink);
  border-radius: var(--r-sharp);
  color: var(--text-on-ink);
  font-size: var(--fs-base);
  transition: border-color var(--t-fast) var(--ease);
}

.newsletter__input::placeholder { color: var(--text-on-ink-soft); opacity: 0.7; }
.newsletter__input:focus { border-color: var(--text-on-ink); outline: none; }
.newsletter__input:focus-visible { outline: 2px solid var(--clay); outline-offset: 2px; }

/**
 * A clay rule draws along the bottom edge on focus.
 *
 * The wrapper carries the line rather than the input, because an input cannot
 * host a pseudo-element. Sits on top of the existing 1px border so the field's
 * height and box are untouched — focus must never move the layout.
 */
.newsletter__field {
  position: relative;
  flex: 1 1 12rem;
  min-width: 0;
  display: flex;
}

.newsletter__field .newsletter__input { flex: 1 1 auto; }

.newsletter__field::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 1px;
  background: var(--clay);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t-mid) var(--ease);
}

.newsletter__field:focus-within::after { transform: scaleX(1); }

.newsletter__btn { flex: 0 0 auto; }

.newsletter__msg {
  margin-top: var(--s2);
  font-size: var(--fs-xs);
  min-height: 1.2em;
  color: var(--text-on-ink-soft);
}

.newsletter__msg.is-error { color: #e2a08c; }
.newsletter__msg.is-ok    { color: #9dc7ae; }

@media (min-width: 860px) {
  .footer__news-inner {
    grid-template-columns: 1fr minmax(0, 30rem);
    align-items: center;
    gap: var(--s8);
  }
}

/* ── Link grid ──────────────────────────────────────────────────────────── */
.footer__grid {
  display: grid;
  gap: var(--s7);
  padding-block: var(--s8);
  grid-template-columns: 1fr 1fr;
}

.footer__col--brand { grid-column: 1 / -1; }

.footer__blurb {
  margin-block: var(--s3) var(--s4);
  font-size: var(--fs-sm);
  color: var(--text-on-ink-soft);
  max-width: 40ch;
}

.footer__h {
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-on-ink-soft);
  margin-bottom: var(--s3);
}

.footer__col ul { display: grid; gap: var(--s1); }

.footer__col li a,
.footer__social a {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  font-size: var(--fs-sm);
  color: var(--text-on-ink);
  opacity: 0.78;
  transition: opacity var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}

.footer__col li a:hover,
.footer__social a:hover { opacity: 1; color: #fff; }

.footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
}

.footer__address {
  display: grid;
  gap: 2px;
  margin-top: var(--s3);
  font-style: normal;
  font-size: var(--fs-xs);
  color: var(--text-on-ink-soft);
}

@media (min-width: 640px) {
  .footer__grid { grid-template-columns: repeat(3, 1fr); }
  .footer__col--brand { grid-column: 1 / -1; }
}

@media (min-width: 1024px) {
  .footer__grid {
    grid-template-columns: 1.6fr repeat(4, 1fr);
    gap: var(--s6);
  }
  .footer__col--brand { grid-column: auto; }
}

/* ── Trust row ──────────────────────────────────────────────────────────── */
.footer__trust {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  padding-block: var(--s5);
  border-top: 1px solid var(--line-on-ink);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-on-ink-soft);
}

.footer__trust li { display: flex; align-items: center; gap: var(--s2); }

.footer__trust li::before {
  content: '';
  width: 4px;
  height: 4px;
  background: var(--clay);
  border-radius: 50%;
  flex: none;
}

/* ── Legal bar ──────────────────────────────────────────────────────────── */
.footer__bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s2);
  padding-block: var(--s5) var(--s6);
  border-top: 1px solid var(--line-on-ink);
  font-size: var(--fs-xs);
  color: var(--text-on-ink-soft);
}

/* Clear the mobile sticky buy bar so the legal line is never covered. */
body.has-buybar .footer__bar { padding-bottom: calc(var(--s6) + 72px); }

@media (min-width: 768px) {
  body.has-buybar .footer__bar { padding-bottom: var(--s6); }
}


/* ── product-card.css ── */
/* =============================================================================
   Product card + grid.

   Grid is 2-up on a phone — fashion shoppers scan, and one-up wastes half the
   screen. 3-up at 768, 4-up at 1280.
   ========================================================================== */

.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s5) var(--s3);
}

@media (min-width: 768px) {
  .grid { grid-template-columns: repeat(3, 1fr); gap: var(--s7) var(--s4); }
}

@media (min-width: 1280px) {
  .grid { grid-template-columns: repeat(4, 1fr); }
}

/* ── Card ───────────────────────────────────────────────────────────────── */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

.card__media {
  --ar: var(--ar-product);
  position: relative;
}

.card__badge {
  position: absolute;
  top: var(--s2);
  left: var(--s2);
  z-index: 2;
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(4px);
}

/* ── Quick add ──────────────────────────────────────────────────────────── */
/* Phone: always visible, sitting at the foot of the image — hover does not
   exist on touch, so a hover-reveal would simply hide the action. */
.card__quick {
  position: absolute;
  left: var(--s2);
  right: var(--s2);
  bottom: var(--s2);
  z-index: 3;
}

.card__quick .btn {
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(6px);
  color: var(--text);
  border-color: transparent;
}

.card__quick .btn:hover {
  background: var(--ink);
  color: var(--text-on-ink);
}

@media (min-width: 768px) and (hover: hover) {
  /* Desktop: rise from the foot of the frame on hover, but stay reachable by
     keyboard.

     The button starts fully below the image's bottom edge rather than 8px
     under its resting place. .card__media is a .media, so it already clips —
     which means the button genuinely emerges from behind the photograph
     instead of cross-fading into position over it. The opacity is kept short
     and fast purely so it does not appear to slide out from under the price. */
  .card__quick {
    opacity: 0;
    transform: translateY(calc(100% + var(--s2)));
    transition:
      opacity var(--t-fast) var(--ease),
      transform var(--t-mid) var(--ease);
  }

  .card:hover .card__quick,
  .card:focus-within .card__quick {
    opacity: 1;
    transform: none;
  }
}

/* ── Body ───────────────────────────────────────────────────────────────── */
.card__body {
  display: grid;
  gap: 3px;
}

.card__name {
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  font-weight: 450;
  line-height: 1.3;
  letter-spacing: 0;
}

/* The stretched link: the whole card is clickable, without nesting the
   quick-add button inside an anchor. */
.card__link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

.card__link:focus-visible {
  outline: none;
}

.card__link:focus-visible::after {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
}

.card__sub {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  letter-spacing: 0.04em;
}

.card__price {
  margin-top: 2px;
  font-size: var(--fs-base);
}

/* ── Pattern label ──────────────────────────────────────────────────────── */
/**
 * One thread colour per pattern, so the shop's primary filter is readable at a
 * glance across a grid. The rule to the left is the thread itself: a 2px stub
 * rather than a dot, because this palette is woven and a dot is not.
 *
 * Each colour was contrast-checked as type on --bone: madder 6.55, cobalt 8.41,
 * jade 6.79, saffron-as-type 6.16. Saffron uses its darker type value here, not
 * the fill — see the note in tokens.css.
 */
.card__pattern {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  line-height: 1;
  color: var(--pattern-c, var(--text-muted));
}

.card__pattern::before {
  content: '';
  width: 14px;
  height: 2px;
  background: currentColor;
  flex: none;
}

.card__pattern--checked { --pattern-c: var(--cobalt); }
.card__pattern--printed { --pattern-c: var(--madder); }
.card__pattern--striped { --pattern-c: var(--jade); }
.card__pattern--solid   { --pattern-c: var(--saffron); }

/* ── Empty state ────────────────────────────────────────────────────────── */
.grid-empty {
  grid-column: 1 / -1;
  padding: var(--s9) 0;
  text-align: center;
  display: grid;
  gap: var(--s4);
  justify-items: center;
}

.grid-empty__title {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
}

.grid-empty p { color: var(--text-muted); font-size: var(--fs-sm); }


/* ── forms.css ── */
/* =============================================================================
   Forms, fields, size grid.
   ========================================================================== */

.field { display: grid; gap: var(--s2); }

.field__label {
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.field__label .req { color: var(--clay); }

.input,
.textarea,
.select {
  width: 100%;
  min-height: var(--tap);
  padding: var(--s3) var(--s4);
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sharp);
  font-size: var(--fs-md);   /* 16px — stops iOS zooming the page on focus */
  color: var(--text);
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.textarea { min-height: 8rem; padding-top: var(--s3); resize: vertical; }

.input::placeholder,
.textarea::placeholder { color: var(--text-muted); }

.input:hover,
.textarea:hover { border-color: var(--text-muted); }

.input:focus,
.textarea:focus,
.select:focus {
  outline: none;
  border-color: var(--ink);
  background: #fff;
}

.input:focus-visible,
.textarea:focus-visible,
.select:focus-visible { outline: 2px solid var(--clay); outline-offset: 1px; }

.input[aria-invalid="true"],
.textarea[aria-invalid="true"] { border-color: var(--danger); }

.field__error {
  font-size: var(--fs-xs);
  color: var(--danger);
}

.field__hint {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.select {
  appearance: none;
  padding-right: var(--s7);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 20px) calc(50% + 2px),
    calc(100% - 15px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}

.fieldrow {
  display: grid;
  gap: var(--s4);
}

@media (min-width: 560px) {
  .fieldrow--2 { grid-template-columns: 1fr 1fr; }
  .fieldrow--3 { grid-template-columns: repeat(3, 1fr); }
}

/* ── Radio cards (payment method) ───────────────────────────────────────── */
.radiocard {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s4);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sharp);
  background: var(--paper);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}

.radiocard:hover { border-color: var(--text-muted); }

.radiocard:has(input:checked) {
  border-color: var(--ink);
  background: #fff;
  box-shadow: inset 0 0 0 1px var(--ink);
}

.radiocard:has(input:focus-visible) { outline: 2px solid var(--clay); outline-offset: 2px; }

.radiocard input { margin-top: 3px; accent-color: var(--ink); width: 18px; height: 18px; flex: none; }

/* .radiocard--disabled was here. Its only use was the "Pay online — coming
   soon" card at checkout, which advertised a payment method the shop cannot
   take. Removed with it — a greyed-out option is still an option on the page. */

.radiocard__title { font-size: var(--fs-base); font-weight: 450; }

.radiocard__note {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 2px;
}

/* ── Size grid ──────────────────────────────────────────────────────────── */
.sizegrid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.sizeopt {
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 var(--s3);
  display: inline-grid;
  place-items: center;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sharp);
  background: var(--paper);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  transition:
    border-color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),
    color var(--t-fast) var(--ease);
}

.sizeopt:hover:not([disabled]) { border-color: var(--ink); }

.sizeopt.is-selected,
.sizeopt[aria-checked="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--text-on-ink);
}

.sizeopt[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
  text-decoration: line-through;
}

/**
 * Sold out — the PDP variant.
 *
 * The disabled attribute is on the INPUT, not on the label, so the selector
 * above cannot reach it. Styled with a real muted colour rather than opacity so
 * the text stays legible: "sold out" is information the customer needs to read,
 * not a control to be dimmed into illegibility.
 *
 * Line-through does the work; colour only reinforces it. Anyone who cannot
 * distinguish the hue still sees the strike, and the label carries a visually
 * hidden "— sold out" for screen readers.
 */
.sizeopt--out {
  color: var(--text-muted);
  background: var(--bone-2);
  border-color: var(--line);
  border-style: dashed;
  text-decoration: line-through;
  cursor: not-allowed;
}

.sizeopt--out:hover { border-color: var(--line); }

/* ── Size sheet ─────────────────────────────────────────────────────────── */
.sizesheet__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s4) var(--s5) var(--s3);
}

.sizesheet__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  margin-top: var(--s1);
}

.sizesheet__body {
  padding: 0 var(--s5) var(--s5);
  display: grid;
  gap: var(--s4);
}

.sizesheet__err { font-size: var(--fs-sm); color: var(--danger); }

.sizesheet__guide {
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  min-height: 34px;
  display: grid;
  place-items: center;
}

.sizesheet__guide:hover { color: var(--text); }

/* ── Checkbox row ───────────────────────────────────────────────────────── */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  font-size: var(--fs-sm);
  color: var(--text-soft);
  cursor: pointer;
  min-height: var(--tap);
}

.check input {
  margin-top: 2px;
  width: 18px;
  height: 18px;
  accent-color: var(--ink);
  flex: none;
}


/* ── hero.css ── */
/* =============================================================================
   Hero — scroll-driven expansion.

   A tall track (.hero__scroll) supplies scroll distance; a sticky stage
   (.hero__pin) holds still inside it. As the track scrolls past, JS writes a
   single custom property --p (0 → 1) and everything below is derived from it:

     --p 0   denim detail full-bleed, atelier photo as a small centred card
     --p 1   atelier photo full-bleed, denim faded out, headline split apart

   No scroll hijacking — see modules/hero.js for why.

   --p defaults to 1 so a no-JS visitor gets the finished state rather than a
   permanently tiny card. html.js resets it to 0 before paint.
   ========================================================================== */

/**
 * MOBILE is deliberately veil-only: the frame starts at full bleed, so the
 * "reveal" is the bone wash lifting off the photograph rather than a box
 * growing. A card that grows on a phone leaves the headline, lede and CTAs
 * spilling outside it onto flat ink at mid-scroll — legible, but visually
 * broken. Desktop keeps the full card-to-fullscreen expansion.
 */
.hero--expand {
  --p: 1;
  --frame-w: 100vw;
  --frame-h: calc(100svh - var(--header-h));
  position: relative;
  background: var(--ink);
}

html.js .hero--expand { --p: 0; }

/* ── Track + sticky stage ───────────────────────────────────────────────── */
/**
 * Height beyond one viewport IS the scroll budget for the reveal, and it is the
 * only speed control — the taller the track, the more scrolling each step of
 * --p costs, so the slower the reveal reads. It is symmetric: scrolling back up
 * runs the whole thing in reverse at the same pace.
 *
 * Mobile gets proportionally less than desktop because only the veil animates
 * here, and because scrolling is more effort on a phone.
 */
.hero__scroll {
  height: 250svh;   /* 150svh of travel */
}

.hero__pin {
  position: sticky;
  top: var(--header-h);
  height: calc(100svh - var(--header-h));
  overflow: hidden;
  display: grid;
  place-items: center;
  isolation: isolate;
}

/* ── Layer 1 · the pattern bands ────────────────────────────────────────── */
/**
 * Was a photograph; is now four woven fields, one per pattern in the
 * catalogue. Two consequences worth knowing:
 *
 *   • There is no LCP image on the homepage any more. The largest paint is the
 *     headline, which needs no network round trip at all.
 *   • The bands are drawn from the same .swatch primitive the product cards
 *     use, so the hero and the grid are woven by the same code.
 *
 * The opacity ramp is unchanged — it is what the reveal is built on.
 */
.hero__bg {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: calc(1 - var(--p));
  background: var(--bone);
}

/* Same specificity caveat as .hero__frame img — see the note there. */
.hero__bg picture,
.hero__bg img[width][height] {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.heroband {
  display: flex;
  flex-direction: column;
}

/* Band widths are set inline from the live pattern counts, so the field is a
   bar chart of the catalogue as well as a picture of it. */
.heroband__band {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
}

/* Thread name, set into the band's own edge like selvedge printing. Hidden on
   a phone, where four bands leave no room for four labels. */
.heroband__label {
  position: absolute;
  right: var(--pad);
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-on-ink);
  opacity: 0.34;
  display: none;
}

@media (min-width: 768px) {
  .heroband { flex-direction: row; }
  .heroband__label {
    display: block;
    right: auto;
    left: 50%;
    top: auto;
    bottom: var(--s6);
    transform: translateX(-50%);
    writing-mode: vertical-rl;
  }
}

/* ── Layer 2 · the atelier, expanding ───────────────────────────────────── */
/**
 * Absolutely positioned and sized in VIEWPORT units, not percentages.
 *
 * As a grid item, `height: 100%` resolved against an auto-sized grid row —
 * which depends on the item's own height. That circularity made the frame
 * SHRINK as --p rose (468px → 259px) instead of filling the stage. Viewport
 * units have no such dependency. Overshoot past the viewport is clipped by
 * .hero__pin's overflow:hidden, which also guarantees a true full bleed.
 */
.hero__frame {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  margin: 0;
  overflow: hidden;
  width: calc(var(--frame-w) + (100vw - var(--frame-w)) * var(--p));
  height: calc(var(--frame-h) + (100svh - var(--header-h) - var(--frame-h)) * var(--p));
  transform: translate(-50%, -50%);
  border-radius: calc(var(--r-card) * (1 - var(--p)));
  box-shadow: 0 30px 80px -30px rgb(20 18 16 / calc(0.45 * (1 - var(--p))));
  /**
   * Fades IN rather than sitting there at rest.
   *
   * When the hero began light, a pale card behind the type read as a designed
   * panel. Against a dark photograph it reads as a rectangle with a visible
   * edge cutting through the eyebrow and the headline — an artefact, not a
   * device. Fading it up over the first 40% of the scroll means the hero at
   * rest is simply the photograph, and the workroom materialises as it opens.
   */
  opacity: min(1, calc(var(--p) * 2.4));
}

/**
 * The [width][height] qualifier is load-bearing, not decoration.
 *
 * base.css carries a global CLS guard — `img[width][height] { height: auto }` —
 * whose specificity (0,2,1) beats a plain `.hero__frame img` (0,1,1). Without
 * matching those attributes here, the photograph kept its intrinsic ratio and
 * sat 390×520 inside a 390×784 frame, leaving a dead black band beneath it.
 */
.hero__frame picture,
.hero__frame img[width][height] {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/**
 * Two veils, in sequence — this is the actual "reveal".
 *
 * The card begins almost entirely washed out in bone, so it reads as a quiet
 * panel behind ink type rather than a dark rectangle the headline collides
 * with. Scrolling lifts the bone veil and the atelier emerges; a dark shade
 * then fades in underneath so the headline can flip to chalk with contrast
 * intact at both ends.
 */
.hero__frame-veil,
.hero__frame-shade {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/**
 * ── ONE PALETTE, NOT TWO ────────────────────────────────────────────────────
 *
 * The veil used to wash the frame in bone so the card read as a pale panel
 * behind INK type, and a dark shade faded in later so the type could flip to
 * chalk. That worked when layer 1 was a light backdrop.
 *
 * It stopped working the moment the denim photograph came back: layer 1 is now
 * dark for most of its area, so ink type sat on dark cloth and the second line
 * of the headline was effectively unreadable. A bone card behind chalk type
 * would fail the same way from the other side.
 *
 * So the hero is dark at BOTH ends now and the threshold flip is gone. The
 * veil no longer lightens — it deepens the frame while it is small, so the
 * card separates from the backdrop without ever going pale.
 */
/* Deepens the frame while it is still a card, clearing as it reaches full
   bleed — so the workroom arrives dark and resolves, rather than flashing. */
.hero__frame-veil {
  background: rgb(15 12 10 / 0.35);
  opacity: calc(1 - var(--p));
}

/* Same shape as layer 1's scrim, for the same reason: the workroom photograph
   has a lit bench running through its middle, and the type crosses it. */
.hero__frame-shade {
  background:
    linear-gradient(to bottom,
      rgb(15 12 10 / 0.62) 0%,
      rgb(15 12 10 / 0.72) 26%,
      rgb(15 12 10 / 0.72) 80%,
      rgb(15 12 10 / 0.66) 100%);
}

/**
 * Layer 1 carries its own scrim, and its shape is dictated by measurement
 * rather than taste.
 *
 * The photograph runs from a bright plaster wall in the upper left to
 * near-black denim in the lower right. Over that wall the chalk headline
 * measured 3.35:1 and the soft lede 1.68:1 — nowhere near AA. The first
 * version of this gradient was also weakest at 45% height, which is precisely
 * where the type sits: it thinned out exactly where it was needed.
 *
 * So it is STRONGEST through the middle band and eases only at the very top
 * and bottom, where nothing but photograph lives. At 0.80 over the brightest
 * wall tone the type clears AA with room:
 *   chalk 9.10:1 · soft 4.76:1 · saffron 4.57:1 · bone button 10.06:1
 */
.hero__bg::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgb(15 12 10 / 0.56) 0%,
      rgb(15 12 10 / 0.80) 26%,
      rgb(15 12 10 / 0.80) 80%,
      rgb(15 12 10 / 0.60) 100%);
}

/* ── Layer 3 · type ─────────────────────────────────────────────────────── */
.hero__lead {
  position: relative;
  z-index: 3;
  grid-area: 1 / 1;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--s4);
  text-align: center;
  width: 100%;
  max-width: var(--maxw);
  padding: var(--s6) var(--pad);
  pointer-events: none;
  /* Chalk throughout — both layers are dark photographs now. */
  color: var(--text-on-ink);
}

.hero__lead > * { pointer-events: auto; }

.hero__eyebrow {
  color: var(--text-on-ink-soft);
}

.hero__title {
  display: grid;
  gap: 0.04em;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 9vw, 6.5rem);
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: var(--ls-tight);
  color: inherit;
}

/**
 * Emphasis by WEIGHT AND COLOUR, never italic.
 *
 * Bricolage Grotesque ships no italic and we only self-host the upright, so
 * `font-style: italic` would hand the browser a synthesised oblique — a
 * mechanical slant that looks like a rendering fault at 6.5rem. A grotesque
 * emphasises by getting heavier, which is also the one place madder appears
 * at display scale.
 */
/* Saffron, not madder. Madder is the accent everywhere on the bone ground, but
   at 2.65:1 against the dark hero it is the one place it cannot go — saffron
   carries the same warmth at 7.55:1. */
.hero__title em {
  font-style: normal;
  font-weight: 800;
  color: var(--saffron-fill);
}

/* The two lines separate as the image opens — the original's signature move. */
.hero__title-a { transform: translateX(calc(var(--p) * -10vw)); }
.hero__title-b { transform: translateX(calc(var(--p) * 10vw)); }

.hero__lede {
  max-width: 46ch;
  font-size: var(--fs-lg);
  line-height: 1.5;
  color: var(--text-soft);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s3);
  margin-top: var(--s2);
}

/**
 * Buttons sit on dark at every scroll position now, so they no longer invert
 * mid-reveal. The primary is bone with an ink label (17.39:1); the secondary is
 * a chalk outline that fills to bone on hover.
 *
 * A near-black button on a near-black scrim would have a legible label and an
 * invisible edge — which is a button you cannot see until you hit it.
 */
.hero--expand .btn--ink {
  --btn-bg: var(--bone);
  --btn-fg: var(--ink);
  --btn-bd: var(--bone);
}

.hero--expand .btn--ink:hover {
  --btn-bg: var(--madder);
  --btn-fg: var(--paper);
  --btn-bd: var(--madder);
}

.hero--expand .btn--outline {
  --btn-fg: var(--text-on-ink);
  --btn-bd: currentColor;
}

.hero--expand .btn--outline:hover {
  --btn-bg: var(--bone);
  --btn-fg: var(--ink);
  --btn-bd: var(--bone);
}

/* ── Category jump links ────────────────────────────────────────────────── */
.hero__jump {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s2) var(--s5);
  margin-top: var(--s3);
  padding-top: var(--s4);
  border-top: 1px solid var(--line-on-ink);
  width: 100%;
  max-width: 34rem;
}

.hero__jump a {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  font-size: var(--fs-sm);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: inherit;
  opacity: 0.82;
  transition: opacity var(--t-fast) var(--ease);
}

.hero__jump a:hover { opacity: 1; }

/* ── Band index — the tappable half of the hero ─────────────────────────── */
/**
 * Each link carries a woven chip in the thread colours of the band above it.
 * That pairing is the whole point: the field says what we make, the index says
 * how much of it there is, and tapping either lands on the same filter.
 */
.bandindex { max-width: 44rem; }

.bandindex__thread {
  width: 26px;
  height: 18px;
  flex: none;
  border-radius: 1px;
  /* Chalk hairline: the chips are pale swatches on a dark scrim, so the edge
     has to lighten to separate them, not darken. */
  outline: 1px solid rgb(242 236 226 / 0.3);
  outline-offset: -1px;
  transition: transform var(--t-fast) var(--ease);
}

.bandindex a:hover .bandindex__thread { transform: scaleY(1.28); }

.hero__jump-n {
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

/**
 * ── The threshold flip is gone ──────────────────────────────────────────────
 *
 * There used to be an ink→chalk palette swap here, toggled by .is-expanded at
 * --p 0.5. It existed because the hero began light and ended dark, and
 * interpolating the colours across that range put the primary CTA through a
 * stage of grey text on a grey fill — invisible at exactly the midpoint. A
 * single threshold with two designed states was the fix.
 *
 * Both ends are dark photographs now, so there is nothing to flip between and
 * the whole mechanism has been removed rather than left in as a no-op. JS still
 * sets .is-expanded; nothing reads it for colour any more, and .hero__cue still
 * fades on --p directly.
 *
 * If a light hero backdrop ever returns, this is the block to restore — do not
 * try to interpolate the colours instead.
 */

/* ── Scroll cue — fades out once the expansion is under way ─────────────── */
.hero__cue {
  position: absolute;
  z-index: 3;
  bottom: var(--s4);
  left: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-on-ink-soft);
  opacity: calc(1 - var(--p) * 2.4);
  pointer-events: none;
}

/* ── Staged entrance ────────────────────────────────────────────────────── */
/**
 * TRANSFORM ONLY — no opacity. An element at opacity:0 is disqualified as an
 * LCP candidate until it becomes visible, so fading hero content in makes LCP
 * wait for JavaScript. Measured earlier: it cost 1.4s.
 */
.hero__eyebrow,
.hero__lede,
.hero__actions,
.hero__jump {
  transform: translateY(14px);
  transition: transform var(--t-slow) var(--ease);
}

.hero.is-ready .hero__eyebrow,
.hero.is-ready .hero__lede,
.hero.is-ready .hero__actions,
.hero.is-ready .hero__jump {
  transform: none;
}

.hero.is-ready .hero__eyebrow { transition-delay: 40ms; }
.hero.is-ready .hero__lede    { transition-delay: 140ms; }
.hero.is-ready .hero__actions { transition-delay: 220ms; }
.hero.is-ready .hero__jump    { transition-delay: 300ms; }

.hero.is-instant .hero__eyebrow,
.hero.is-instant .hero__lede,
.hero.is-instant .hero__actions,
.hero.is-instant .hero__jump {
  transition: none;
  transition-delay: 0ms;
}

html:not(.js) .hero__eyebrow,
html:not(.js) .hero__lede,
html:not(.js) .hero__actions,
html:not(.js) .hero__jump {
  transform: none;
}

/* ── Breakpoints ────────────────────────────────────────────────────────── */
@media (min-width: 768px) {
  /* Desktop reinstates the card-to-fullscreen expansion. */
  .hero--expand {
    --frame-w: 46vw;
    --frame-h: 52vh;
  }

  .hero__scroll { height: 380svh; }   /* 280svh of travel */
  .hero__title-a { transform: translateX(calc(var(--p) * -7vw)); }
  .hero__title-b { transform: translateX(calc(var(--p) * 7vw)); }
}

@media (min-width: 1200px) {
  .hero--expand { --frame-w: 38vw; }
}

/* ── Reduced motion — no pin, no expansion, finished state only ─────────── */
@media (prefers-reduced-motion: reduce) {
  html.js .hero--expand { --p: 1; }

  .hero__scroll { height: auto; }

  /**
   * `position: relative`, NOT `static`.
   *
   * Static removed the pin as a containing block, so .hero__frame — which is
   * absolutely positioned and sized in svh — resolved against .hero--expand
   * instead and escaped the pin's `overflow: hidden`. A frame 100svh tall
   * centred on a 760px block overflowed below the hero and painted the
   * photograph across the sections underneath it. Relative restores both the
   * containing block and the clipping.
   */
  .hero__pin {
    position: relative;
    height: min(760px, calc(100svh - var(--header-h)));
  }

  /* Belt and braces: with no expansion to animate, the frame is simply the
     stage. Sizing it in percentages removes the svh dependency entirely. */
  .hero__frame {
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    transform: none;
    opacity: 1;
  }

  .hero__title-a,
  .hero__title-b { transform: none; }

  .hero__cue { display: none; }
}


/* ── motion.css ── */
/* =============================================================================
   Motion — the entrance grammar.

   The site had exactly one entrance gesture (Rise: 18px lift + fade, in
   base.css) firing ~40 times between the marquee and the footer. That is what
   made everything after the hero read as one long decay: not too little
   animation, but the same animation too often.

   Four gestures, assigned by the role of the content:

     Rise    — 18px lift + fade .............. body copy, buttons, secondary text
     Unmask  — type rises from its own baseline  every .section-title / .display
     Wipe    — frame opens, image counter-moves  photographic plates
     Draw    — hairline scales from zero ....... structural rules

   Rules that keep it disciplined:
     • One gesture per element, never two.
     • Unmask precedes Rise within a block — headline first, then copy.
     • Draw completes last in a block. The rule closing a section is punctuation.

   All four ride the SAME IntersectionObserver already in modules/reveal.js: a
   gesture element carries `data-reveal` for the observer plus its own gesture
   attribute, and the rules below simply re-point what `.is-revealed` means for
   that element. No second observer, no new machinery.
   ========================================================================== */

/* ── Neutralise Rise where a specialised gesture takes over ─────────────────
   Unmask and Wipe do not move the element itself — they move its children — so
   the element must sit still and fully opaque while its gesture plays. */
.js [data-reveal][data-unmask]:not(.is-revealed),
.js [data-reveal][data-wipe]:not(.is-revealed) {
  opacity: 1;
  transform: none;
}

/* Draw is the exception: here the element IS the gesture. */
.js [data-reveal][data-draw]:not(.is-revealed) {
  opacity: 1;
  transform: scaleX(0);
}

/* ── Unmask ─────────────────────────────────────────────────────────────────
   Type slides up from behind its own baseline inside an overflow-clipped line
   box. Deliberately no fade: the letterform emerges rather than materialises,
   which is what makes it read as print rather than as a web animation.

   The line wrappers are built by modules/motion.js from the existing <br>s. If
   that file never runs the heading is simply a heading, so this degrades to
   nothing rather than to a broken layout. */
.unmask-line {
  display: block;
  overflow: hidden;
  /* The clip would otherwise shave descenders (the 'p' in "pillars", the comma
     after it). Pad the box, then pull the same amount back off the flow so the
     visual line spacing is unchanged. */
  padding-block-end: 0.14em;
  margin-block-end: -0.14em;
}

.unmask-line > span {
  display: block;
  transition: transform var(--t-slow) var(--ease);
  /* Composes the block's own reveal delay with this line's position in the
     stack, so a two-line title staggers inside an already-delayed block. */
  transition-delay: calc(var(--reveal-delay, 0ms) + var(--unmask-delay, 0ms));
}

.js [data-unmask]:not(.is-revealed) .unmask-line > span {
  /* 105%, not 100%: at exactly 100% a stray antialiased row of the tallest
     ascender stays visible along the clip edge on fractional device pixels. */
  transform: translateY(105%);
}

/* Two-beat accent — a clip box nested inside a line's clip box. Both start
   offset, so the accent is effectively translated twice and arrives after the
   words around it have already settled. */
.unmask-accent {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-block-end: 0.14em;
  margin-block-end: -0.14em;
}

.unmask-accent__inner {
  display: inline-block;
  /* Marginally slower than the line it sits in, so it lands rather than snaps. */
  transition: transform calc(var(--t-slow) * 1.15) var(--ease);
  transition-delay: calc(
    var(--reveal-delay, 0ms) + var(--unmask-delay, 0ms) + 140ms
  );
}

.js [data-unmask]:not(.is-revealed) .unmask-accent__inner {
  transform: translateY(105%);
}

/* ── Wipe ───────────────────────────────────────────────────────────────────
   The frame opens downward from its top edge while the image inside counter-
   moves up to meet it, so the photograph feels uncovered rather than inserted.

   ─── PROPERTY OWNERSHIP ───────────────────────────────────────────────────
   Three effects animate the same <img>. Each owns exactly ONE of the three
   independent transform properties, so they compose rather than overwrite:

     transform  → this Wipe counter-move
     scale      → the hover zoom (base.css .media--zoom)
     translate  → scroll parallax (below)

   Getting this wrong is why the naive version of this effect makes hover
   zoom stop working on any card that has revealed. */
.js [data-wipe]:not(.is-revealed) .media,
.js [data-wipe].media:not(.is-revealed) {
  clip-path: inset(0 0 100% 0);
}

.js [data-wipe]:not(.is-revealed) .media > img,
.js [data-wipe].media:not(.is-revealed) > img {
  transform: translateY(-6%) scale(1.06);
}

[data-wipe] .media,
[data-wipe].media {
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--t-slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

/* Per-property delays in the shorthand. `scale` is listed WITHOUT a delay on
   purpose: it belongs to the hover zoom, and inheriting a stagger delay of up
   to 480ms would make hovering a card in the fourth column feel broken. */
[data-wipe] .media > img,
[data-wipe].media > img {
  transition:
    transform var(--t-slow) var(--ease) var(--reveal-delay, 0ms),
    scale var(--t-slow) var(--ease);
}

/* Product cards settle the last fraction of a percent as their frame opens.
   Applied to .card, not to the [data-wipe] wrapper, so this stays one gesture
   per element. */
.js [data-wipe]:not(.is-revealed) .card { scale: 0.985; }

[data-wipe] .card {
  transition: scale var(--t-slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

/* The sale badge arrives last, after its card has already settled.
   button.css states the house position on this: "Sale is stated quietly — a
   hairline outline, not a shouting red block." A badge that rides in with the
   card announces itself; one that appears once the card is at rest does not. */
.js [data-wipe]:not(.is-revealed) .card__badge { opacity: 0; }

[data-wipe] .card__badge {
  transition: opacity var(--t-mid) var(--ease);
  transition-delay: calc(var(--reveal-delay, 0ms) + 420ms);
}

/* ── Draw ───────────────────────────────────────────────────────────────────
   A hairline scaling from nothing to full width. Structural, not decorative —
   this is the gesture that says "a section has closed". */
[data-draw] {
  transform: scaleX(1);
  transform-origin: left center;
  transition: transform var(--t-slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-draw='right']  { transform-origin: right center; }
[data-draw='center'] { transform-origin: center center; }

/* ── Parallax ───────────────────────────────────────────────────────────────
   Owns the `translate` property (see PROPERTY OWNERSHIP above). Driven by the
   shared `--sp` from modules/motion.js — no listener of its own.

   The image is deliberately taller than its frame so the drift never exposes
   an edge. Slack is ALWAYS larger than the maximum travel — check this before
   changing any number here, because the failure mode is a bar of background
   colour sliding in at the top or bottom of the frame:

     .media--parallax-sm  108% tall,  -4% top → 4% slack,  ±3.2% travel
     .media--parallax     112% tall,  -6% top → 6% slack,  ±4.5% travel
     .media--parallax-lg  124% tall, -12% top → 12% slack, ±9.9% travel

   The three sizes are also a crop budget. Oversizing the image means showing
   less of it, so product photography — where the crop is functional and a
   cropped collar is a bug — gets the smallest variant, and atmospheric
   lifestyle and banner imagery gets the largest.

   `--parallax` is the TOTAL range, so the image sits at ±half of it. */
.media--parallax-sm > img,
.media--parallax > img,
.media--parallax-lg > img {
  position: absolute;
  left: 0;
  translate: 0 calc((var(--sp, 0.5) - 0.5) * var(--parallax, 8%));
}

/* --parallax is declared on the CONTAINER, not the image, so a section can
   override the rate per card (see the lookbook's varied drift in
   sections.css) without having to restate the sizing. */
.media--parallax-sm { --parallax: 6%; }
.media--parallax    { --parallax: 8%; }
.media--parallax-lg { --parallax: 16%; }

.media--parallax-sm > img { height: 108%; top: -4%; }
.media--parallax > img    { height: 112%; top: -6%; }
.media--parallax-lg > img { height: 124%; top: -12%; }

/* Scoped to elements the observer says are actually on screen, so the
   compositor is not holding a layer for every image on the page. */
.is-scrolling .media--parallax-sm > img,
.is-scrolling .media--parallax > img,
.is-scrolling .media--parallax-lg > img,
.media--parallax-sm.is-scrolling > img,
.media--parallax.is-scrolling > img,
.media--parallax-lg.is-scrolling > img { will-change: translate; }

/* ── Story veil — the band's arrival ────────────────────────────────────────
   The ink section keeps its own ink background at all times, so if any of this
   fails the band is simply dark. What animates is a bone-coloured veil ON TOP
   of it, retracting downward as the section enters — so the reader watches the
   page turn dark from the top edge rather than scrolling onto an already-dark
   rectangle.

   This is the same veil technique the hero already uses for its reveal
   (.hero__frame-veil), driven the same way, which is why it reads as part of
   the system rather than as a new trick. */
.story { position: relative; }

.story__veil {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: var(--bone);
  pointer-events: none;
  transform-origin: bottom center;
  /* Inverted default, as with [data-reveal]: with no JS the veil is already
     gone, and can never leave the section covered. */
  transform: scaleY(0);
}

/* Complete over the first ~17% of the section's pass — the band is fully dark
   well before its body copy reaches the reveal threshold. */
.js .story__veil {
  transform: scaleY(clamp(0, calc(1 - var(--sp, 1) * 6), 1));
}

/* ── Cross-page continuity ──────────────────────────────────────────────────
   The site is multi-page PHP, so every navigation was a hard white flash —
   which quietly undoes the polish of everything above it in this file. Native
   cross-document view transitions replace that with a short cross-fade.

   No dependency, no router, no JS: browsers without support simply navigate
   the way they do today. The header is given a transition name so it is
   treated as the same element on both sides of the navigation and holds
   still while the page beneath it changes — which is what stops the site
   feeling like a series of documents. */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--t-mid);
  animation-timing-function: var(--ease);
}

.header { view-transition-name: site-header; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ── Reduced motion ─────────────────────────────────────────────────────────
   tokens.css already collapses --t-fast/-mid/-slow to 1ms, which neutralises
   every transition above. These rules clear the START states too, so nothing
   can be caught clipped or offset if a transition never fires. motion.js
   additionally skips the Unmask DOM rewrite entirely. */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal][data-draw]:not(.is-revealed) { transform: none; }

  .js [data-unmask]:not(.is-revealed) .unmask-line > span,
  .js [data-unmask]:not(.is-revealed) .unmask-accent__inner { transform: none; }

  .js [data-wipe]:not(.is-revealed) .media,
  .js [data-wipe].media:not(.is-revealed) { clip-path: none; }

  .js [data-wipe]:not(.is-revealed) .media > img,
  .js [data-wipe].media:not(.is-revealed) > img { transform: none; }

  .js [data-wipe]:not(.is-revealed) .card { scale: none; }
  .js [data-wipe]:not(.is-revealed) .card__badge { opacity: 1; }

  /* Parallax pins to its rest offset; the oversizing stays so the crop does
     not shift between motion settings. */
  .media--parallax-sm > img,
  .media--parallax > img,
  .media--parallax-lg > img { translate: none; will-change: auto; }

  /* The band is simply dark on arrival. */
  .js .story__veil { transform: scaleY(0); }
}


/* ── sections.css ── */
/* =============================================================================
   Homepage / shared section blocks.
   ========================================================================== */

/* ── Section heads ──────────────────────────────────────────────────────── */
.sectionhead {
  display: grid;
  gap: var(--s5);
  margin-bottom: var(--s7);
}

.sectionhead .section-title { margin-top: var(--s3); }

.sectionhead__aside { display: grid; gap: var(--s5); }

.sectionhead__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
}

.sectionhead__count {
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.sectionhead__count strong {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 500;
  color: var(--text);
  margin-right: var(--s1);
  vertical-align: -2px;
}

.sectionhead--row {
  grid-template-columns: 1fr;
  align-items: end;
}

@media (min-width: 900px) {
  .sectionhead {
    grid-template-columns: 1.1fr 1fr;
    gap: var(--s8);
    align-items: end;
  }

  .sectionhead--simple,
  .sectionhead--row { grid-template-columns: 1fr auto; align-items: end; }
}

/* ── Trust strip ────────────────────────────────────────────────────────── */
.trust {
  position: relative;
  padding-block: var(--s7);   /* borders are drawn — see .trust__rule */
}

.trust__rule {
  position: absolute;
  inset-inline: 0;
  height: 1px;
  background: var(--line);
}

.trust__rule--top    { top: 0; }
.trust__rule--bottom { bottom: 0; }

.trust__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s6) var(--s4);
}

.trust__icon { color: var(--clay); margin-bottom: var(--s3); }

/**
 * The icons draw themselves.
 *
 * These are already single-stroke line art, so they are built for this and it
 * costs no new assets. Every shape carries pathLength="1" (see the component),
 * which is what lets one dasharray value drive all four icons at a matched
 * rate. Inverted default as always: drawn is the CSS default, JS only ever
 * adds the "not yet".
 */
.trust__icon > * {
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 460ms var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

.js [data-reveal]:not(.is-revealed) .trust__icon > * { stroke-dashoffset: 1; }

/* These are reassurances, not products — they should not invite clicking. */
.trust__item .trust__icon {
  transition: translate var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}

@media (hover: hover) {
  .trust__item:hover .trust__icon { translate: 0 -2px; color: var(--clay-soft); }
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal]:not(.is-revealed) .trust__icon > * { stroke-dashoffset: 0; }
}

.trust__title {
  font-size: var(--fs-base);
  font-weight: 500;
  margin-bottom: 4px;
}

.trust__copy {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  max-width: 30ch;
}

@media (min-width: 900px) {
  .trust__grid { grid-template-columns: repeat(4, 1fr); gap: var(--s6); }
}

/* Inline variant — a single row of ticks near a buy button. */
.trust-inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  font-size: var(--fs-xs);
  color: var(--text-soft);
}

.trust-inline li { display: flex; align-items: center; gap: var(--s2); }
.trust-inline svg { color: var(--pine); flex: none; }

/* The category triptych that lived here is gone — the section is now the
   expanding row in components/catrow.css. */

/* ── Story (ink) ────────────────────────────────────────────────────────── */
.story {
  display: grid;
  margin-block: var(--section-y);
}

.story__media { --ar: 4 / 3; }

.story__body {
  padding: var(--s8) var(--pad);
  display: grid;
  align-content: center;
  gap: var(--s4);
}

.story__copy { display: grid; gap: var(--s4); max-width: 52ch; color: var(--text-on-ink-soft); }

.story__body .btn { justify-self: start; }

.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s4);
  margin-block: var(--s4);
  padding-top: var(--s5);
  border-top: 1px solid var(--line-on-ink);
}

.stats dt {
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-on-ink-soft);
  order: 2;
}

.stats dd {
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 5vw, 2.75rem);
  line-height: 1;
  margin-bottom: var(--s2);
}

.stats > div { display: flex; flex-direction: column; position: relative; }

/**
 * Hairlines between the stat columns, drawn once the numerals have landed.
 * Inverted default, as everywhere else in this system: the drawn state is the
 * CSS default and JS only ever adds the "not yet", so with JS blocked the
 * dividers are simply present.
 */
.stats > div + div::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--s4) / -2);
  width: 1px;
  background: var(--line-on-ink);
  transform: scaleY(1);
  transform-origin: top center;
  transition: transform var(--t-slow) var(--ease);
  /* Draw finishes last in the block — it is the punctuation, not the point. */
  transition-delay: calc(var(--reveal-delay, 0ms) + 320ms);
}

.js .stats:not(.is-revealed) > div + div::before { transform: scaleY(0); }

@media (min-width: 900px) {
  .story {
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
  }

  .story__media { --ar: auto; height: 100%; }

  /* Excludes the parallax case. This rule and .media--parallax-lg > img in
     motion.css have identical specificity, and sections.css is bundled later —
     so without the :not() this would win, flatten the image back to exactly
     100%, and leave the drift with no slack to move into. The visible symptom
     is a bar of --bone-2 sliding in at the top or bottom of the banner. */
  .story__media:not(.media--parallax-lg) img { height: 100%; }
  .story__body { padding: var(--s10) clamp(var(--s6), 5vw, var(--s10)); }
}

/* ── Card rail ──────────────────────────────────────────────────────────── */
.rail--cards { padding-block: var(--s2); }

.rail__item { width: min(62vw, 260px); }

/**
 * Entry drift — the rail's contents settle in from a small rightward offset as
 * the section arrives. It is a hint, not a movement: it tells the eye there is
 * more to the right before the reader has touched anything.
 *
 * Both properties are declared in one shorthand because the [data-reveal] rule
 * in base.css would otherwise put a stagger delay of up to 480ms on the press
 * feedback, which makes a tap feel broken. Only `translate` carries the delay.
 */
.rail--cards .rail__item {
  transition:
    translate var(--t-slow) var(--ease) var(--reveal-delay, 0ms),
    transform var(--t-fast) var(--ease);
}

.js .rail--cards .rail__item:not(.is-revealed) { translate: 40px 0; }

/* Pressing a card gives it back a little — the rail should feel physical. */
.rail--cards .rail__item:active { transform: scale(0.99); }

@media (prefers-reduced-motion: reduce) {
  .js .rail--cards .rail__item:not(.is-revealed) { translate: none; }
}

@media (min-width: 768px) {
  .rail__item { width: min(34vw, 300px); }
}

/* ── Offer band ─────────────────────────────────────────────────────────── */
/**
 * The hardest stop on the page. Its job is to interrupt, so its motion is the
 * sharpest and shortest here — not the biggest. No image, no parallax, no
 * counter: this section earns its contrast by having LESS motion than its
 * neighbours.
 */
.band {
  position: relative;
  background: var(--bone-2);
  border-bottom: 1px solid var(--line);   /* top edge is drawn, see .band__rule */
  padding-block: var(--section-y);
  text-align: center;
}

/* A symmetrical draw for the one symmetrical block on the page — it opens
   from the centre to both edges, which is what makes the band read as a full
   stop rather than as another section starting. */
.band__rule {
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background: var(--line);
}

.band__inner { display: grid; justify-items: center; gap: var(--s4); }

.band__title {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: var(--ls-tight);
  font-size: clamp(1.5rem, 4vw, 2.4rem);
}

.band__copy { color: var(--text-soft); max-width: 52ch; }

/* ── The price as a figure ──────────────────────────────────────────────── */
/**
 * Every shirt in the catalogue is ₹1,150. As a sentence that is a flat fact;
 * as a figure it becomes the offer's argument.
 *
 * The old price is struck through with a MADDER rule rather than a text
 * decoration, so the strike is a thread crossing the number — the same gesture
 * as the weave, at the one place on the page where a number is the message.
 * text-decoration-color could do the colour but not the weight or the overhang.
 */
.pricefig {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  font-variant-numeric: tabular-nums lining-nums;
}

.pricefig__now {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(3.25rem, 15vw, 9rem);
  line-height: 0.86;
  letter-spacing: -0.045em;
  color: var(--text);
}

.pricefig__meta {
  display: grid;
  gap: var(--s1);
  justify-items: start;
  text-align: left;
}

.pricefig__was {
  position: relative;
  font-size: clamp(1.1rem, 3vw, 1.6rem);
  font-weight: 500;
  color: var(--text-muted);
  padding-inline: 0.12em;
}

/* The thread. Overhangs both ends so it reads as crossing the number rather
   than as an underline that has slipped. */
.pricefig__was::after {
  content: '';
  position: absolute;
  left: -0.14em;
  right: -0.14em;
  top: 52%;
  height: 3px;
  background: var(--madder);
  transform: rotate(-3deg);
  transform-origin: center;
}

.pricefig__off {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  /* Saffron carries a fill, never type on a pale ground — see tokens.css. */
  background: var(--saffron-fill);
  color: var(--text);
  padding: 0.25em 0.6em;
  border-radius: var(--r-sharp);
}

@media (prefers-reduced-motion: reduce) {
  .pricefig__was::after { transform: none; }
}

/* The lookbook rail that lived here is gone — the section is now the
   self-scrolling reel in components/lookreel.css. */

/* ── Page hero (interior pages) ─────────────────────────────────────────── */
.pagehero {
  padding-block: var(--s8) var(--s7);
  border-bottom: 1px solid var(--line);
}

.pagehero__title {
  font-family: var(--font-display);
  font-size: var(--fs-3xl);
  margin-block: var(--s3) var(--s4);
}

.pagehero__lede { max-width: 56ch; }

/* ── Breadcrumbs ────────────────────────────────────────────────────────── */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  padding-block: var(--s4);
}

.crumbs a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; }
.crumbs li { display: flex; align-items: center; gap: var(--s2); }
.crumbs li + li::before { content: '/'; opacity: 0.5; }
.crumbs [aria-current] { color: var(--text-soft); }


/* ── catrow.css ── */
/* =============================================================================
   Category row — three expanding cards.

   The editorial categories side by side. One is open; hovering, focusing or
   activating another opens it and closes the rest. On a phone the row turns
   into a column and expands by height instead of width, because a 33%-wide
   column on a 390px screen is a stripe, not a card.

   ─── THE DEFAULT STATE IS THE FINAL STATE ───────────────────────────────────
   Everything below the `.is-live` line is the accordion. Everything above it
   is what the browser gets with no JavaScript: three equal cards, every panel
   open, every link reachable. modules/catrow.js adds `.is-live` only when it
   is going to do something useful with it — never under reduced motion, where
   the plain row IS the required "all cards equal width, no animation".

   That is also why the headings are plain <h3> in the markup and catrow.js
   promotes them to buttons. A control that does nothing without script should
   not be in the HTML.

   ─── WHAT ANIMATES ──────────────────────────────────────────────────────────
     .catcard        flex-grow  → the expand itself
     .catcard__panel opacity    → the copy and CTA, delayed behind the width
                     translate  → an 8px settle under the fade
   Nothing here animates width, height, or any other layout-triggering
   property directly; flex-grow redistributes space the flex container has
   already reserved, so the row's own box never changes size.
   ========================================================================== */

.catrow {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  /* A fixed box, so lazily-loaded banners cannot shift anything when they
     arrive. This is the whole CLS story for the section. */
  height: clamp(30rem, 116vw, 42rem);
}

.catcard {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border-radius: var(--r-card);
  background: var(--bone-2);
}

/* ── The photograph ─────────────────────────────────────────────────────── */
.catcard__media,
.catcard__media picture,
.catcard__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.catcard__media img {
  object-fit: cover;
  /* The interesting part of a banner is its middle; as the card narrows to a
     collapsed strip, a top-left origin would leave nothing but sky. */
  object-position: 50% 45%;
}

/* Scrim. Two jobs at once: a light wash over the whole frame so a vertical
   name stays legible against a bright patch anywhere down the card, and a
   heavier foot so the copy and CTA have real contrast. A single gradient
   does both and costs no extra element. */
.catcard__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgb(20 18 16 / 0.84),
    rgb(20 18 16 / 0.42) 48%,
    rgb(20 18 16 / 0.18) 100%
  );
}

/* ── Type ───────────────────────────────────────────────────────────────── */
.catcard__body {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s5) var(--s4) var(--s4);
  color: var(--text-on-ink);
}

.catcard__name {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: var(--ls-tight);
  font-size: var(--fs-2xl);
  line-height: var(--lh-tight);
  color: var(--text-on-ink);
}

/* A selvedge stripe down the leading edge of each card — the same four threads
   that bind the sections, turned on their side. It is what tells you the cards
   are cloth and not tiles, and it survives the accordion collapse because it
   is pinned to the edge rather than laid out in flow. */
.catcard::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  z-index: 3;
  background: linear-gradient(180deg,
    var(--madder) 0 22%,
    var(--saffron-fill) 22% 34%,
    var(--bone-2) 34% 42%,
    var(--cobalt) 42% 74%,
    var(--jade) 74% 100%);
}

.catcard__panel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s3);
}

.catcard__copy {
  font-size: var(--fs-sm);
  color: var(--text-on-ink-soft);
  max-width: 34ch;
}

/* Above the toggle's stretched hit area, so the CTA is its own target rather
   than another way to expand a card that is already expanded. */
.catcard__cta {
  position: relative;
  z-index: 2;
  color: var(--text-on-ink);
}

.catcard__cta svg { transition: translate var(--t-mid) var(--ease); }
.catcard__cta:hover svg { translate: 4px 0; }

/* Madder is a dark red: correct on bone, unreadable on a dimmed photograph.
   Saffron is the accent that survives a dark ground — 7.27:1 against it. */
.catcard__cta:hover { color: var(--saffron-fill); }

@media (min-width: 768px) {
  .catrow {
    flex-direction: row;
    gap: var(--s3);
    height: clamp(26rem, 42vw, 34rem);
  }

  .catcard__body { padding: var(--s6) var(--s5) var(--s5); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   The accordion. Everything from here down needs .is-live, which catrow.js
   adds only when it has wired the behaviour up.
   ══════════════════════════════════════════════════════════════════════════ */

.catrow.is-live .catcard {
  /* flex-grow is the one number that moves. 1 → 4 on a phone gives the open
     card ~two thirds of the column and leaves the other two as tappable
     strips; 1 → 5 on desktop is more emphatic because there is more room to
     be emphatic with. */
  flex-grow: 1;
  transition:
    flex-grow var(--t-slow) var(--ease),
    /* Restated: this block replaces the [data-reveal] transition in base.css
       wholesale, and without these the cards would pop in rather than fade.
       Only the entrance carries the stagger delay — a hover that waited
       270ms to widen would read as broken. */
    opacity var(--t-slow) var(--ease) var(--reveal-delay, 0ms),
    transform var(--t-slow) var(--ease) var(--reveal-delay, 0ms);
}

.catrow.is-live .catcard.is-open { flex-grow: 4; }

/* The heading catrow.js built. Reset the button chrome and let it inherit the
   display face from .catcard__name above. */
.catrow.is-live .catcard__toggle {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* The whole card is the hit area, not just the words. Sits at z-index 1, under
   the CTA at 2, so an open card's link still wins the click. */
.catrow.is-live .catcard__toggle::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

.catrow.is-live .catcard__toggle:focus-visible {
  outline: none;
}

.catrow.is-live .catcard__toggle:focus-visible::after {
  outline: 2px solid var(--clay-soft);
  outline-offset: -5px;
  border-radius: var(--r-card);
}

/* Closed cards hold the name and nothing else.

   THE PANEL MUST NOT HOLD SPACE WHEN CLOSED. It is only ~14% of the row wide
   at that point, so the tagline wraps to five or six lines — left in flow it
   would shove the name most of the way up the card, by a different amount per
   card depending on how long that category's tagline is.

   max-height is what collapses it, rather than `display: none` (not
   animatable) or `position: absolute` (the name would snap to the bottom
   instead of travelling there). Sharing flex-grow's duration and easing means
   the name rises exactly in step with the card widening. The ceiling is
   generous: one line of tagline plus a link is nowhere near 12rem, and
   overflow is hidden if a future tagline runs long.

   `visibility` on top of the fade is what takes the CTA out of the tab order
   and the accessibility tree — which is what aria-expanded="false" on the
   toggle is promising. Its 0s transition is delayed so the panel stays
   hittable right up until the fade has finished. */
.catrow.is-live .catcard__panel {
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  visibility: hidden;
  translate: 0 8px;
  transition:
    max-height var(--t-slow) var(--ease),
    opacity var(--t-mid) var(--ease),
    translate var(--t-mid) var(--ease),
    visibility 0s linear var(--t-mid);
}

.catrow.is-live .catcard.is-open .catcard__panel {
  max-height: 12rem;
  opacity: 1;
  visibility: visible;
  translate: 0 0;
  /* The fade is held back until the card has most of its width, or the copy
     reflows mid-fade and reads as a stutter. max-height is NOT held back — it
     has to move with the width, not after it. */
  transition:
    max-height var(--t-slow) var(--ease),
    opacity var(--t-mid) var(--ease) calc(var(--t-slow) * 0.45),
    translate var(--t-mid) var(--ease) calc(var(--t-slow) * 0.45),
    visibility 0s;
}

@media (min-width: 768px) {
  .catrow.is-live .catcard.is-open { flex-grow: 5; }

  /* A closed card is ~14% of the row — too narrow for "Shirts" to sit across
     it. Turned on its side it fits, and the change of axis is itself the
     signal that the card is closed.

     Not transitioned: writing-mode cannot tween, and trying to fake it with a
     rotation means the glyphs travel in an arc. The flip lands under the width
     animation, which is where the eye already is. */
  .catrow.is-live .catcard:not(.is-open) .catcard__name {
    writing-mode: vertical-rl;
    /* Reads bottom-to-top, which is the convention for a vertical label in
       Latin script. The body is bottom-anchored, so the text grows upward
       from the same baseline the open card's name sits on. */
    rotate: 180deg;
  }
}

/* ── Reduced motion ─────────────────────────────────────────────────────────
   catrow.js never adds .is-live here, so the accordion block above is
   inert and the plain row at the top of this file is what renders: equal
   cards, every panel open, no transitions anywhere. These two rules only
   have to catch a live OS toggle in the frame before JS tears it down. */
@media (prefers-reduced-motion: reduce) {
  .catrow.is-live .catcard,
  .catrow.is-live .catcard__panel {
    flex-grow: 1;
    max-height: none;
    opacity: 1;
    visibility: visible;
    translate: none;
    transition: none;
  }

  .catrow.is-live .catcard__name {
    writing-mode: horizontal-tb;
    rotate: none;
    margin-inline: 0;
  }
}


/* ── lookreel.css ── */
/* =============================================================================
   Lookreel — the lookbook, scrolling itself.

   Eight lifestyle frames drifting left in a continuous loop. Slow enough to be
   a backdrop rather than a ticker: one full cycle takes just under a minute,
   which is roughly 55px a second.

   Named `lookreel`, not `marquee`, because base.css already owns `.marquee`,
   `.marquee__track` and `@keyframes marquee` for the text band in the header —
   and motion.js drives that one's playbackRate off scroll velocity. Two things
   called marquee on one page would be a bad afternoon.

   ─── HOW THE SEAM WORKS ─────────────────────────────────────────────────────
   The track holds the eight frames twice and animates to translateX(-50%). At
   100% of the cycle the second run sits exactly where the first one started, so
   the jump back to 0 changes nothing on screen.

   That identity only holds if the two halves are the same width, which is why
   the spacing is `margin-inline-end` on each item and NOT a flex `gap`:

     with gap     16 items + 15 gaps → half is 8 items + 7.5 gaps  ✗ half-gutter
                                                                     jump
     with margin  16 × (item + gap)  → half is 8 × (item + gap)    ✓ exact

   The trailing margin after the sixteenth item looks redundant and is not —
   it is the eighth gap of the second half, and removing it reintroduces the
   jump. Same reason the track carries no padding: `.lookreel` holds the
   gutter instead, because padding on the track would land in the middle of
   the -50%.

   ─── COST ───────────────────────────────────────────────────────────────────
   `transform` only, so the loop never touches layout. The animation is paused
   until lookreel.js says the section is on screen, so a below-the-fold reel
   costs nothing on a page the reader never scrolls to.
   ========================================================================== */

.lookreel {
  --reel-item: min(64vw, 280px);
  --reel-gap: var(--s4);
  /* One full pass of the eight frames. The brief asks for 40–60s; at the
     desktop item width this works out around 55px/sec. */
  --reel-cycle: 55s;

  /* Doubles as the manual-swipe affordance on touch AND as the whole
     reduced-motion story: with the animation off, this is just a scroller. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;

  padding-block: var(--s2);
  padding-inline-start: var(--pad);

  /* Content dissolves at the bleed edge instead of being guillotined by it —
     the same treatment .rail gets in base.css, and what stops a frame from
     appearing to pop into existence at the right-hand edge. */
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--pad),
    #000 calc(100% - var(--pad)),
    transparent 100%
  );
}

.lookreel::-webkit-scrollbar { display: none; }

.lookreel__track {
  display: flex;
  width: max-content;
  animation: lookreel var(--reel-cycle) linear infinite;
  /* Off until the section is actually on screen — see lookreel.js. */
  animation-play-state: paused;
}

.lookreel__item {
  flex: 0 0 auto;
  width: var(--reel-item);
  margin-inline-end: var(--reel-gap);   /* NOT flex gap — see the seam note */
}

.lookreel__item .media { border-radius: var(--r-card); }

@keyframes lookreel {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Running only while the reel is in view. `will-change` rides the same class,
   so the compositor is not holding a layer for a section three screens down —
   same reasoning as `.is-scrolling` in motion.css. */
.lookreel.is-live .lookreel__track {
  animation-play-state: running;
  will-change: transform;
}

/* Pause on hover, on keyboard focus, and while a finger is on it. All three
   restate the full `.is-live` selector so they out-specify the rule above
   rather than losing to it. */
.lookreel.is-live:hover .lookreel__track,
.lookreel.is-live:focus-within .lookreel__track,
.lookreel.is-live.is-held .lookreel__track {
  animation-play-state: paused;
}

@media (min-width: 768px) {
  .lookreel {
    --reel-item: min(28vw, 340px);
    --reel-gap: var(--s5);
  }
}

/* ── Reduced motion ─────────────────────────────────────────────────────────
   A static row that can still be scrolled by hand — `overflow-x: auto` above
   is already doing that, so this only has to stop the animation and drop the
   duplicate. Without hiding the second run the reader would scroll past the
   same eight photographs twice with no indication why. */
@media (prefers-reduced-motion: reduce) {
  .lookreel__track,
  .lookreel.is-live .lookreel__track {
    animation: none;
    will-change: auto;
  }

  .lookreel__item[aria-hidden='true'] { display: none; }

  /* The last real frame keeps its gutter but no longer needs a trailing one. */
  .lookreel__item:nth-child(8) { margin-inline-end: var(--pad); }
}


/* ── wheel.css ── */
/* =============================================================================
   Wheel — the offers rail, lifted onto a rotating 3D carousel.

   The six discounted products sit on the circumference of a circle standing
   upright in Z. Scrolling the section past the viewport turns the ring; when
   the page is still it keeps drifting, very slowly, so the section reads as
   alive rather than parked.

   ─── PROGRESSIVE ENHANCEMENT ────────────────────────────────────────────────
   Nothing in here fires until modules/wheel.js adds `.is-3d`. Until then — and
   forever, on phones, under reduced motion, or with JS blocked — `.wheel__ring`
   is still `.rail` and this is the horizontal scroller it has always been,
   hairline position indicator included. The 3D layer is strictly additive;
   deleting the JS file restores the rail exactly.

   ─── PROPERTY OWNERSHIP ─────────────────────────────────────────────────────
   Same discipline as motion.css. One animated property per concern, so they
   compose instead of overwriting one another:

     .wheel__ring   transform  → ring rotation (--rot, written by JS)
     .wheel__card   transform  → the card's fixed seat on the circle
                    translate  → the rail's entry drift (sections.css)
                    scale      → press feedback, see PRESS below
     .card          opacity    → depth dim (--o, written by JS)
     .card__media img translate → in-frame parallax (motion.css)

   JS writes only custom properties. Every declaration lives here.
   ========================================================================== */

/* Desktop only. A ~800px-wide ring does not fit on a phone, and the rail is
   the better mobile design anyway — thumb-driven, three cards visible at once,
   no vertical cost. The media query is belt-and-braces: wheel.js also refuses
   to add `.is-3d` below this breakpoint, and re-decides on resize. */
@media (min-width: 768px) {

  .wheel.is-3d {
    /* One card width drives the whole geometry. Six cards sit 60° apart, which
       means they would touch on the circle at radius 0.87×w; 1.15× opens that
       to ~60px of daylight between the front card and its neighbours at both
       ends of the clamp. Check both ends before changing it — the ring's total
       width is ~2.8× the card width, and it has to clear the viewport at 768. */
    --wheel-card-w: clamp(15rem, 19vw, 18.75rem);
    --wheel-media-h: calc(var(--wheel-card-w) * 4 / 3);
    /* Name (up to two lines), subtitle, price and the flex gap above them. A
       rare three-line name overflows the nominal box by a few px and that is
       fine — deliberately NOT clipped, because `overflow: hidden` here would
       also clip .card__link's focus ring and its stretched hit area. */
    --wheel-body-h: 7rem;
    --wheel-card-h: calc(var(--wheel-media-h) + var(--wheel-body-h));
    --wheel-radius: calc(var(--wheel-card-w) * 1.15);
    --wheel-step: 60deg;           /* JS overwrites with 360deg / card count */

    /* .railwrap already sets position: relative — restated so this block is
       readable on its own if the rail classes ever move. */
    position: relative;
    /* Perspective magnifies the front card to ~1.27× at this radius, so 1.42×
       the card height is what clears it top and bottom. */
    height: calc(var(--wheel-card-h) * 1.42);
    /* Shallower than the 2000px a demo would use: this brand does not do
       dramatic. At this depth the turn is legible without the front card
       ballooning over its neighbours. */
    perspective: 1600px;
  }

  /* ── Undoing the rail ─────────────────────────────────────────────────────
     Two of these are not cosmetic. `overflow` other than visible, and any
     `mask-image`, each force the browser to flatten a `preserve-3d` subtree —
     the ring would silently collapse to a flat stack of overlapping cards with
     no error anywhere. If the carousel ever renders 2D, look here first. */
  .wheel.is-3d .wheel__ring {
    display: block;
    overflow: visible;             /* REQUIRED — see above */
    mask-image: none;              /* REQUIRED — see above */
    padding: 0;
    scroll-snap-type: none;
    cursor: auto;

    position: absolute;
    inset: 0;
    transform: rotateY(var(--rot, 0deg));
    transform-style: preserve-3d;
  }

  /* Nothing left to scroll, so the position readout is a lie. */
  .wheel.is-3d .railbar { display: none; }

  /* Scoped to the class wheel.js toggles from its IntersectionObserver, so the
     compositor is not holding a layer for a ring three sections down the page.
     Same reasoning as `.is-scrolling` in motion.css. */
  .wheel.is-3d.is-live .wheel__ring { will-change: transform; }

  /* ── The seat ─────────────────────────────────────────────────────────── */
  .wheel.is-3d .wheel__card {
    display: grid;                 /* one child; lets .card fill the height */
    position: absolute;
    left: 50%;
    top: 50%;
    width: var(--wheel-card-w);    /* beats .rail__item's min(34vw, 300px) */
    height: var(--wheel-card-h);
    margin: calc(var(--wheel-card-h) / -2) 0 0 calc(var(--wheel-card-w) / -2);
    transform:
      rotateY(calc(var(--i) * var(--wheel-step)))
      translateZ(var(--wheel-radius));
    /* No z-index. Inside `preserve-3d` the browser depth-sorts by actual Z
       position, which is already the answer we want; a z-index here would be
       ignored at best and would flatten the subtree at worst. */

    /* ── REQUIRED, AND ITS ABSENCE IS NOT SUBTLE ──────────────────────────────
       Cards sit on a ring facing outwards, so any card more than 90° from the
       front has turned away and the browser paints its BACK — which is the card
       rendered horizontally mirrored. Left visible, that is reversed product
       names and prices stacked over the cards in front of them: the whole
       section reads as a rendering fault rather than a carousel.

       The depth dim does not save you. It takes rear cards to 0.3 opacity, not
       to nothing, so the mirrored text stays perfectly legible through the
       front of the ring.

       Hiding the back face means a card simply stops being drawn once it passes
       side-on. wheel.js's opacity ramp is matched to this — it reaches 0 at
       exactly 90° so a card fades out rather than blinking off. */
    backface-visibility: hidden;
  }

  /* ── PRESS ────────────────────────────────────────────────────────────────
     sections.css gives a pressed rail card `transform: scale(0.99)`. Here
     `transform` is the card's seat on the ring, so that rule would fling the
     card to the centre of the stage on mousedown. The gesture is kept, moved
     onto the independent `scale` property where it composes instead.

     The transition mirrors the one it replaces: only `translate` (the rail's
     entry drift) carries the stagger delay, or press feedback would arrive up
     to 480ms after the pointer and read as broken. */
  .wheel.is-3d .wheel__card {
    transition:
      translate var(--t-slow) var(--ease) var(--reveal-delay, 0ms),
      scale var(--t-fast) var(--ease);
  }

  .wheel.is-3d .wheel__card:active { scale: 0.99; }

  /* ── Depth dim ────────────────────────────────────────────────────────────
     Applied to the .card INSIDE the seat, not to the seat itself. The seat is
     the [data-reveal] element, and base.css fades it in from opacity 0 — if
     the dim lived on the same element it would out-specify that rule and the
     cards would never fade in at all. On separate elements the two multiply,
     which is the correct compositing anyway.

     Not transitioned: JS rewrites --o every frame, so a transition would only
     make the dim lag behind the rotation that caused it. */
  .wheel.is-3d .wheel__card > * { opacity: var(--o, 1); }

  /* Keyboard focus can land on a card that is currently facing away. Bring it
     to a legible state rather than letting the user tab into something they
     cannot see. */
  .wheel.is-3d .wheel__card:focus-within > * { opacity: 1; }

  /* ── The card itself ──────────────────────────────────────────────────── */
  /* The seat is a fixed-height box; the media keeps its 3:4 ratio and the body
     takes what is left, so the six price lines sit on one plane around the
     ring instead of wandering with the name's wrap. */
  .wheel.is-3d .card { height: 100%; }
  .wheel.is-3d .card__media { flex: none; }
  .wheel.is-3d .card__body { align-content: start; }
}

/* wheel.js never enables the ring under reduced motion, so this only has to
   catch a live OS toggle in the frame before JS tears the ring down. */
@media (prefers-reduced-motion: reduce) {
  .wheel.is-3d .wheel__ring { transform: none; }
}


/* ── cart.css ── */
/* =============================================================================
   Cart — drawer body, lines, free-shipping meter.
   ========================================================================== */

.cartdrawer__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 500;
}

.cartdrawer__n {
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.cartdrawer__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* ── Empty state ────────────────────────────────────────────────────────── */
.cart-empty {
  margin: auto;
  padding: var(--s8) var(--s5);
  text-align: center;
  display: grid;
  gap: var(--s3);
}

.cart-empty__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
}

.cart-empty__note {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin-bottom: var(--s3);
}

/* ── Free-shipping meter ────────────────────────────────────────────────── */
.shipmeter {
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
  background: var(--paper);
  flex: none;
}

.shipmeter__label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--fs-sm);
  margin-bottom: var(--s2);
}

.shipmeter__label strong {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.shipmeter.is-complete .shipmeter__label { color: var(--pine); }

.shipmeter__track {
  height: 3px;
  background: var(--line);
  border-radius: var(--r-pill);
  overflow: hidden;
}

.shipmeter__fill {
  display: block;
  height: 100%;
  background: var(--clay);
  border-radius: inherit;
  transition: width var(--t-slow) var(--ease);
}

.shipmeter.is-complete .shipmeter__fill { background: var(--pine); }

/* ── Lines ──────────────────────────────────────────────────────────────── */
.cartlines {
  flex: 1;
  padding: var(--s3) var(--s5);
  display: grid;
  gap: var(--s2);
  align-content: start;
}

.cartline {
  display: grid;
  grid-template-columns: 68px 1fr auto;
  gap: var(--s3);
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line);
}

.cartline:last-child { border-bottom: 0; }

.cartline__media {
  width: 68px;
  aspect-ratio: var(--ar-product);
  overflow: hidden;
  background: var(--bone-2);
}

.cartline__media img { width: 100%; height: 100%; object-fit: cover; }

.cartline__info { min-width: 0; display: grid; gap: var(--s1); align-content: start; }

.cartline__name {
  font-size: var(--fs-sm);
  line-height: 1.3;
}

.cartline__name:hover { color: var(--clay); }

.cartline__meta {
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.cartline__controls {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s1);
  flex-wrap: wrap;
}

.qty--sm .qty__btn { width: 32px; height: 32px; font-size: var(--fs-sm); }
.qty--sm .qty__value { min-width: 1.75rem; font-size: var(--fs-sm); }

.cartline__remove {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  min-height: 32px;
  transition: color var(--t-fast) var(--ease);
}

.cartline__remove:hover { color: var(--danger); }

.cartline__price {
  font-size: var(--fs-sm);
  white-space: nowrap;
  align-self: start;
}

/* ── Drawer foot ────────────────────────────────────────────────────────── */
.cartdrawer__foot {
  flex: none;
  position: sticky;
  bottom: 0;
  padding: var(--s4) var(--s5) calc(var(--s5) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
  background: var(--bone);
  display: grid;
  gap: var(--s2);
}

.cartdrawer__row {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  font-size: var(--fs-base);
}

.cartdrawer__row--save { color: var(--clay); font-size: var(--fs-sm); }

.cartdrawer__note {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-bottom: var(--s2);
}

.cartdrawer__view {
  text-align: center;
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  min-height: 34px;
  display: grid;
  place-items: center;
}

.cartdrawer__view:hover { color: var(--text); }

.cartdrawer__trust {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s1) var(--s3);
  font-size: 10px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.cartdrawer__trust li { display: flex; align-items: center; gap: 5px; }

.cartdrawer__trust li::before {
  content: '';
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--pine);
}


/* ── whatsapp.css ── */
/* =============================================================================
   Floating WhatsApp button.

   ── ON THE COLOUR ───────────────────────────────────────────────────────────
   This is ink, not WhatsApp green. Two reasons, and both are reversible in one
   line if the client disagrees — change --wa-bg below.

   1. #25d366 sits at 1.7:1 against --bone. WCAG 2.1 SC 1.4.11 asks for 3:1 on
      the boundary of a control, so a green disc on this page ground fails it
      outright, and no amount of shadow fixes a contrast ratio. Ink is 17.4:1.
   2. Every other permanent affordance on this site — buttons, the header, the
      buy bar — is ink on bone. A single saturated green disc would be the only
      thing on the page not speaking the Madras Weave palette.

   The glyph still carries the recognition; it is the most recognisable mark in
   the product. To go official green anyway:
       --wa-bg: #25d366;  --wa-fg: #fff;  --wa-bg-hover: #1eb85a;
   ...and be aware you are accepting the 1.4.11 failure knowingly.

   ── ON THE STACKING ORDER ───────────────────────────────────────────────────
   z-index 80 puts it UNDER every other fixed layer on the site:
       buybar 90 · overlay/scrim 200 · drawer 210 · toast 300
   so no clearance rule here is load-bearing for correctness — if one is ever
   wrong the button loses, which is the right way round. The clearances below
   exist so it never *reaches* an overlap in the first place.
   ========================================================================== */

.wafab {
  --wa-size: 56px;
  --wa-bg: var(--ink);
  --wa-bg-hover: var(--ink-2);
  --wa-fg: var(--text-on-ink);

  position: fixed;
  right: var(--s4);
  bottom: calc(var(--s4) + env(safe-area-inset-bottom, 0px));
  z-index: 80;

  display: grid;
  place-items: center;
  width: var(--wa-size);
  height: var(--wa-size);
  border-radius: var(--r-pill);

  background: var(--wa-bg);
  color: var(--wa-fg);
  box-shadow: var(--shadow-md);

  transition: background var(--t-fast) var(--ease);
}

.wafab:hover { background: var(--wa-bg-hover); }

/* The global ring is 2px --clay at 3px offset; on a dark disc it needs the
   offset to clear the shadow, and the radius has to follow the pill. */
.wafab:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
  border-radius: var(--r-pill);
}

.wafab__icon { display: block; }

/* ── Clearance: the mobile sticky buy bar ────────────────────────────────────
   .buybar is fixed to the bottom edge at z-index 90 on the product page. It is
   translated off-screen until the buy panel scrolls out of view, but the
   clearance is applied whenever the bar EXISTS rather than when it is showing —
   a button that jumps 68px up the screen mid-scroll is worse than one sitting a
   little high for a moment. This mirrors what overlay.css already does for the
   toast, and 68px is the same measured bar height it uses.
   ────────────────────────────────────────────────────────────────────────── */
body.has-buybar .wafab {
  bottom: calc(var(--s4) + 68px + env(safe-area-inset-bottom, 0px));
}

@media (min-width: 768px) {
  .wafab {
    right: var(--s5);
    bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px));
  }
}

/* pages/product.css hides .buybar outright from 1024px, so the lift has to stop
   at exactly the same breakpoint or the button floats above nothing. */
@media (min-width: 1024px) {
  body.has-buybar .wafab {
    bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px));
  }
}

/* ── Clearance: the toast ────────────────────────────────────────────────────
   The toast is bottom-centred at width min(100% - 2rem, 420px). Below 564px
   viewport its right edge reaches past the button's left edge and they collide;
   at 576px and up there is clear air between them. So the lift is scoped to
   36rem and no wider — the toast keeps its normal resting position on every
   screen where it already fits.

   :has() rather than a body class so that no page template has to remember to
   opt in, and so the rule simply stops applying if CONTACT_WHATSAPP is cleared
   and the button stops rendering. 64px = the 56px disc + an 8px gap.
   ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 36rem) {
  body:has(.wafab) .toast {
    bottom: calc(var(--s5) + 64px + env(safe-area-inset-bottom, 0px));
  }

  /* Product page: clear the buy bar AND the button stacked on top of it. */
  body:has(.wafab).has-buybar .toast {
    bottom: calc(var(--s5) + 68px + 64px + env(safe-area-inset-bottom, 0px));
  }
}

@media print {
  .wafab { display: none; }
}


/* ── shop.css ── */
/* =============================================================================
   Shop — filter bar + filter sheet.
   ========================================================================== */

.filterbar {
  position: sticky;
  top: var(--header-h);
  z-index: 20;
  background: color-mix(in srgb, var(--bone) 94%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--line);
  padding-block: var(--s3);
}

.filterbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
}

/* Category pills scroll horizontally on a phone rather than wrapping into
   three cramped rows. */
.filterbar__cats {
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
  padding-block: 2px;
  /* Fade the trailing edge so a clipped chip reads as "more to scroll" rather
     than as a broken layout against the filter button. */
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}

@media (min-width: 768px) {
  /* Everything fits from tablet up — no need for the hint. */
  .filterbar__cats { mask-image: none; -webkit-mask-image: none; }
}

.filterbar__cats::-webkit-scrollbar { display: none; }

.chip.is-on {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--text-on-ink);
}

/* Pattern row — its own line under the category pills rather than a third
   column inside .filterbar__inner, which would leave all three cramped on a
   phone. Same horizontal scroll behaviour as the category pills. */
.filterbar__patterns {
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
  padding-block: var(--s2) 2px;
}

/**
 * Each pattern chip carries its own thread colour on the left edge — the same
 * colour that pattern's label uses on every product card. A customer who has
 * learned that checks are cobalt in the grid finds the cobalt chip in the
 * filter bar without reading it.
 */
.filterbar__patterns .chip {
  border-left-width: 3px;
  border-left-color: var(--pattern-c, var(--line));
}

.filterbar__patterns .chip:nth-of-type(2) { --pattern-c: var(--cobalt); }
.filterbar__patterns .chip:nth-of-type(3) { --pattern-c: var(--madder); }
.filterbar__patterns .chip:nth-of-type(4) { --pattern-c: var(--jade); }
.filterbar__patterns .chip:nth-of-type(5) { --pattern-c: var(--saffron); }

.filterbar__patterns::-webkit-scrollbar { display: none; }

/* The count is a footnote to the label, not a competing element. currentColor
   keeps it legible when the chip flips to the ink background. */
.chip__count {
  margin-inline-start: 0.5em;
  opacity: 0.5;
  font-variant-numeric: tabular-nums;
}

.filterbar__tools {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex: none;
}

.filterbar__count {
  display: none;
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}

.filterbar__count strong {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.filterbar__sort { display: none; }

.select--sm {
  min-height: 38px;
  padding: 0 var(--s6) 0 var(--s3);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background-color: transparent;
  border-color: var(--line-strong);
}

/* Active filter tags */
.filterbar__active {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  padding-top: var(--s3);
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 30px;
  padding: 0 var(--s3);
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--text-on-ink);
  font-size: var(--fs-xs);
  transition: background var(--t-fast) var(--ease);
}

.tag:hover { background: var(--clay); }

.tag--clear {
  background: transparent;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.tag--clear:hover { background: transparent; color: var(--clay); }

@media (min-width: 768px) {
  .filterbar__count { display: block; }
  .filterbar__sort { display: block; }
  .filterbar__toggle { display: none; }
}

/* ── Filter sheet ───────────────────────────────────────────────────────── */
.filtersheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s3) var(--s5) var(--s2);
  flex: none;
}

.filtersheet__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
}

.filtersheet__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s3) var(--s5) 0;
  display: grid;
  gap: var(--s6);
}

.filtersheet__group { border: 0; padding: 0; }

.filtersheet__group legend { margin-bottom: var(--s3); }

/* The radio inputs are visually hidden; the label IS the control. */
.filtersheet .sizeopt { cursor: pointer; }

.filtersheet .sizeopt:has(input:checked) {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--text-on-ink);
}

.filtersheet .sizeopt:has(input:focus-visible) {
  outline: 2px solid var(--clay);
  outline-offset: 2px;
}

/* How many SKUs carry this pattern. Muted so it reads as a footnote to the
   label rather than competing with it — and it inherits currentColor, so it
   stays legible when the option is selected and flips to the ink background. */
.sizeopt__count {
  margin-inline-start: 0.4em;
  font-size: var(--fs-sm);
  opacity: 0.55;
  font-variant-numeric: tabular-nums;
}

.filtersheet__foot {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: var(--s3);
  padding: var(--s4) 0 calc(var(--s5) + env(safe-area-inset-bottom, 0px));
  background: var(--bone);
  border-top: 1px solid var(--line);
  margin-top: var(--s2);
}

.filtersheet__foot .btn--outline { flex: none; }


/* ── product.css ── */
/* =============================================================================
   Product detail page.
   ========================================================================== */

.pdp { padding-bottom: var(--s8); }

/* ── Gallery ────────────────────────────────────────────────────────────── */
.gallery__stage { position: relative; }

/* Mobile: a swipeable rail with dots. Desktop: a stacked column. */
.gallery__rail {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.gallery__rail::-webkit-scrollbar { display: none; }

.gallery__slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

.gallery__badge {
  position: absolute;
  top: var(--s3);
  left: var(--s3);
  z-index: 2;
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(4px);
}

.gallery__dots {
  position: absolute;
  bottom: var(--s3);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  background: color-mix(in srgb, var(--paper) 78%, transparent);
  backdrop-filter: blur(6px);
  border-radius: var(--r-pill);
}

.gallery__dot {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
}

.gallery__dot::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--line-strong);
  transition: background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}

.gallery__dot.is-on::before { background: var(--ink); transform: scale(1.25); }

/* ── Thumbnails ─────────────────────────────────────────────────────────── */
/**
 * MOBILE: a horizontal strip under the stage. It used to be display:none here,
 * which was defensible at three shots — the dots covered it — but stops being
 * so at five, where a customer needs to see that a fifth frame exists before
 * they will swipe four times to reach it.
 *
 * Sizing is driven by --shots (written on .gallery from the real count), so the
 * strip fits the frames it has rather than a hardcoded maximum. Each thumb
 * takes an equal share of the row, capped at 64px:
 *
 *   • 360px phone → (320 − 4×8) / 5 = 57.6px wide, 76.8px tall
 *   • 320px phone → 49.6px wide, 66px tall
 *
 * Both clear the 44px minimum tap target in each axis, which is the number
 * that decides whether five thumbnails are usable or merely present.
 *
 * `min()` against that share is what stops 2 or 3 thumbs stretching into
 * banners; past 5 the row simply scrolls.
 */
.gallery__thumbs {
  display: flex;
  gap: var(--s2);
  margin-top: var(--s2);
  overflow-x: auto;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
}

.gallery__thumbs::-webkit-scrollbar { display: none; }

.gallery__thumb {
  flex: 0 0 min(64px, calc((100% - (var(--shots, 5) - 1) * var(--s2)) / var(--shots, 5)));
  scroll-snap-align: start;
  display: block;
  border: 1px solid transparent;
  background: var(--bone-2);
  transition: border-color var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease);
  opacity: 0.62;
  /* The tap target is the thumbnail itself; at 56–64px wide and ~4:3 tall it
     already clears 44px in both axes. */
}

.gallery__thumb img {
  display: block;
  width: 100%;
  aspect-ratio: var(--ar-product);
  object-fit: cover;
}

.gallery__thumb:hover { opacity: 1; }
.gallery__thumb.is-on { border-color: var(--ink); opacity: 1; }

.gallery__thumb:focus-visible {
  outline: 2px solid var(--madder);
  outline-offset: 2px;
  opacity: 1;
}

@media (min-width: 1024px) {
  /* Stack the images; the rail and dots step aside. */
  .gallery { display: grid; grid-template-columns: 72px 1fr; gap: var(--s3); align-items: start; }

  .gallery__rail { display: block; overflow: visible; }
  .gallery__slide { margin-bottom: var(--s3); }
  .gallery__dots { display: none; }

  /* Desktop returns the strip to a sticky vertical column beside the stack.
     `auto-fill` rather than a fixed row count, so 4 and 5 both lay out. */
  .gallery__thumbs {
    display: grid;
    grid-auto-rows: max-content;
    gap: var(--s2);
    margin-top: 0;
    order: -1;
    overflow: visible;
    position: sticky;
    top: calc(var(--header-h) + var(--s5));
  }

  .gallery__thumb { flex: none; width: 100%; }
}

/* ── Buy panel ──────────────────────────────────────────────────────────── */
.buy {
  display: grid;
  gap: var(--s4);
  align-content: start;
  padding-top: var(--s5);
}

@media (min-width: 1024px) {
  .buy { padding-top: 0; }
}

.buy__cat {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}

.buy__name {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  line-height: 1.04;
  margin-top: calc(var(--s2) * -1);
}

.buy__sub {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin-top: calc(var(--s3) * -1);
}

.buy__rating {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--fs-sm);
  min-height: 34px;
}

.buy__rating:hover { color: var(--clay); }

.buy__price {
  align-items: baseline;
  gap: var(--s3);
  flex-wrap: wrap;
}

.buy__now { font-size: var(--fs-xl); }

.buy__tax {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: calc(var(--s3) * -1);
}

.buy__desc {
  color: var(--text-soft);
  max-width: 52ch;
  padding-block: var(--s2);
  border-top: 1px solid var(--line);
  padding-top: var(--s4);
}

/* ── Sizes ──────────────────────────────────────────────────────────────── */
.buy__sizes { display: grid; gap: var(--s3); }

.buy__sizehead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
}

.buy__guide {
  font-size: var(--fs-xs);
  color: var(--text-soft);
  text-decoration: underline;
  text-underline-offset: 3px;
  min-height: 34px;
}

.buy__guide:hover { color: var(--clay); }

/* Radio inputs are visually hidden; the label is the control. */
.buy .sizeopt { cursor: pointer; }

.buy .sizeopt:has(input:checked) {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--text-on-ink);
}

.buy .sizeopt:has(input:focus-visible) {
  outline: 2px solid var(--clay);
  outline-offset: 2px;
}

.buy__sizeerr {
  font-size: var(--fs-sm);
  color: var(--danger);
}

.buy__actions {
  display: flex;
  gap: var(--s3);
  align-items: stretch;
}

.buy__add { flex: 1; }

.buy__form { display: grid; gap: var(--s4); }

/* ── Delivery estimator ─────────────────────────────────────────────────── */
.delivery {
  display: grid;
  gap: var(--s3);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  background: var(--paper);
}

.delivery__row { display: flex; gap: var(--s3); align-items: flex-start; }
.delivery__row svg { flex: none; margin-top: 2px; color: var(--pine); }

.delivery__text { font-size: var(--fs-sm); color: var(--text-soft); }
.delivery__text strong { color: var(--text); font-weight: 500; }

.delivery__form { display: flex; gap: var(--s2); }

.delivery__input { flex: 1; min-width: 0; }

.input--sm { min-height: 38px; padding: 0 var(--s3); font-size: var(--fs-sm); }

.delivery__result { font-size: var(--fs-sm); }
.delivery__result:empty { display: none; }
.delivery__result.is-ok { color: var(--pine); }
.delivery__result.is-error { color: var(--danger); }

.delivery__note { font-size: var(--fs-xs); color: var(--text-muted); }
.delivery__note a { text-decoration: underline; text-underline-offset: 2px; }

/* ── Reviews ────────────────────────────────────────────────────────────── */
.reviews__disclaimer { margin-bottom: var(--s6); max-width: 68ch; }

.reviews__empty { color: var(--text-muted); max-width: 56ch; }
.reviews__empty a { text-decoration: underline; text-underline-offset: 3px; }

.reviews { display: grid; gap: var(--s7); }

.reviews__summary {
  display: grid;
  gap: var(--s2);
  justify-items: start;
  align-content: start;
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  background: var(--paper);
}

.reviews__score {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 8vw, 3.5rem);
  line-height: 1;
}

.reviews__score span { font-size: 0.4em; color: var(--text-muted); }

.reviews__count { font-size: var(--fs-sm); color: var(--text-muted); }

.histogram { display: grid; gap: var(--s2); width: 100%; margin-top: var(--s3); }

.histogram__row {
  display: grid;
  grid-template-columns: 2.5rem 1fr 1.5rem;
  align-items: center;
  gap: var(--s2);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.histogram__track {
  height: 4px;
  background: var(--bone-2);
  border-radius: var(--r-pill);
  overflow: hidden;
}

.histogram__fill { display: block; height: 100%; background: var(--clay); }

.histogram__n { text-align: right; }

.reviews__list { display: grid; gap: var(--s5); }

.review {
  padding-bottom: var(--s5);
  border-bottom: 1px solid var(--line);
}

.review:last-child { border-bottom: 0; padding-bottom: 0; }

.review__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s2);
}

.review__date { font-size: var(--fs-xs); color: var(--text-muted); }

.review__title { font-size: var(--fs-base); font-weight: 500; margin-bottom: var(--s1); }

.review__body { font-size: var(--fs-sm); color: var(--text-soft); max-width: 62ch; }

.review__meta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: var(--s2); }

@media (min-width: 900px) {
  .reviews { grid-template-columns: minmax(260px, 300px) 1fr; gap: var(--s8); align-items: start; }
  .reviews__summary { position: sticky; top: calc(var(--header-h) + var(--s5)); }
}

/* ── Sticky buy bar — mobile only ───────────────────────────────────────── */
.buybar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--bone) 96%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
  border-top: 1px solid var(--line);
  transform: translateY(110%);
  transition: transform var(--t-mid) var(--ease);
}

.buybar[hidden] { display: none; }
.buybar.is-on { transform: none; }

.buybar__info { flex: 1; min-width: 0; display: grid; gap: 1px; }

.buybar__name {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.buybar__price { font-size: var(--fs-base); font-weight: 500; }
.buybar__price s { font-size: var(--fs-xs); }

.buybar__btn { flex: none; min-width: 8.5rem; }

/* Never on desktop: the buy panel is sticky there already. */
@media (min-width: 1024px) {
  .buybar { display: none !important; }
}

/* ── Size guide sheet ───────────────────────────────────────────────────── */
.guidesheet__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--s5) calc(var(--s6) + env(safe-area-inset-bottom, 0px));
  display: grid;
  gap: var(--s4);
}

.guidesheet__note { font-size: var(--fs-sm); color: var(--text-soft); }

.guidesheet__help {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}

/* ── Sold out ───────────────────────────────────────────────────────────── */
/**
 * Replaces the buy form entirely when nothing is left in any size. A form that
 * can never succeed is worse than no form: it invites an attempt and then
 * refuses it.
 */
.buy__soldout {
  padding: var(--s5);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-card);
  background: var(--bone-2);
  margin-bottom: var(--s5);
}

.buy__soldout-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-lg);
  letter-spacing: var(--ls-tight);
  margin-bottom: var(--s2);
}

.buy__soldout-copy {
  font-size: var(--fs-sm);
  color: var(--text-soft);
  line-height: 1.6;
  margin-bottom: var(--s4);
}

/* Which sizes have gone, stated in words under the grid — the strike-through on
   a button is a glance, this is the sentence that confirms it. */
.buy__sizenote {
  margin-top: var(--s3);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}


/* ── checkout.css ── */
/* =============================================================================
   Cart page, checkout, order confirmation.
   ========================================================================== */

.pagehero--slim { padding-block: var(--s7) var(--s5); border-bottom: 0; }

/* ── Cart page ──────────────────────────────────────────────────────────── */
.cartpage { padding-bottom: var(--s8); }

.cartpage__lines { display: grid; gap: var(--s4); }

.cartlines--page { padding: 0; }

.cartlines--page .cartline {
  grid-template-columns: 88px 1fr auto;
  padding-block: var(--s4);
}

.cartlines--page .cartline__media { width: 88px; }
.cartlines--page .cartline__name { font-size: var(--fs-base); }

.cartpage__continue { justify-self: start; margin-top: var(--s3); }

/* ── Summary panel ──────────────────────────────────────────────────────── */
.summary {
  display: grid;
  gap: var(--s4);
  align-content: start;
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  background: var(--paper);
}

.summary__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 500;
}

.summary__items {
  display: grid;
  gap: var(--s3);
  max-height: 17rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.summary__item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--s3);
  align-items: center;
  font-size: var(--fs-sm);
}

.summary__media { position: relative; width: 48px; flex: none; }

.summary__media img {
  width: 48px;
  aspect-ratio: var(--ar-product);
  object-fit: cover;
  background: var(--bone-2);
}

.summary__qty {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  padding-inline: 4px;
  background: var(--ink);
  color: var(--text-on-ink);
  border-radius: var(--r-pill);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}

.summary__info { min-width: 0; display: grid; gap: 2px; }
.summary__name { line-height: 1.3; }
.summary__size { font-size: var(--fs-xs); color: var(--text-muted); }
.summary__price { white-space: nowrap; }

.summary__totals { padding-top: var(--s4); border-top: 1px solid var(--line); }

.summary__save dd, .summary__save dt { color: var(--clay); }

.summary__total {
  padding-top: var(--s3);
  margin-top: var(--s2);
  border-top: 1px solid var(--ink);
  font-size: var(--fs-lg);
  font-weight: 500;
}

.summary__total dt { color: var(--text) !important; }

.summary__note { font-size: var(--fs-xs); color: var(--text-muted); }

.summary__edit {
  font-size: var(--fs-xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  justify-self: start;
  min-height: 34px;
  display: inline-flex;
  align-items: center;
}

.summary__edit:hover { color: var(--clay); }

/* Order summary above the form on mobile, beside it on desktop. */
@media (max-width: 1023px) {
  .checkout .summary { order: -1; margin-bottom: var(--s6); }
}

/* ── Checkout ───────────────────────────────────────────────────────────── */
.checkout { padding-bottom: var(--s9); }

.checkout__alert { margin-bottom: var(--s5); }

.checkout__form { display: grid; gap: var(--s7); }

.step { display: grid; gap: var(--s4); }

.step__title {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 500;
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--line);
}

.step__n {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--text-on-ink);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  font-weight: 600;
  flex: none;
}

.step__body { display: grid; gap: var(--s4); }

.paymethods { display: grid; gap: var(--s3); }

.paytrust {
  display: grid;
  gap: var(--s2);
  font-size: var(--fs-xs);
  color: var(--text-soft);
  padding-block: var(--s2);
}

.paytrust li { display: flex; align-items: center; gap: var(--s2); }
.paytrust svg { color: var(--pine); flex: none; }

.checkout__terms {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  text-align: center;
}

.checkout__terms a { text-decoration: underline; text-underline-offset: 2px; }

/* ── Confirmation ───────────────────────────────────────────────────────── */
.confirm {
  max-width: 46rem;
  margin-inline: auto;
  padding-block: var(--s7) var(--s9);
  display: grid;
  gap: var(--s4);
  justify-items: start;
}

.confirm__mark {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--pine-soft);
  color: var(--pine);
}

.confirm__title {
  font-family: var(--font-display);
  font-size: var(--fs-3xl);
}

.confirm__ref {
  font-size: var(--fs-lg);
  font-variant-numeric: tabular-nums;
}

.confirm__ref strong { font-weight: 600; }

.confirm__mailwarn { width: 100%; }

.confirm__grid {
  display: grid;
  gap: var(--s6);
  width: 100%;
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
  margin-top: var(--s3);
}

.confirm__block { display: grid; gap: var(--s3); align-content: start; }

.confirm__h {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}

.confirm__address {
  font-style: normal;
  font-size: var(--fs-sm);
  line-height: 1.7;
  color: var(--text-soft);
}

.confirm__pay { font-size: var(--fs-sm); color: var(--text-soft); }

.confirm__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  padding-top: var(--s4);
}

@media (min-width: 768px) {
  .confirm__grid { grid-template-columns: 1.2fr 1fr; gap: var(--s8); }
}

/* ── Payment waiting page ───────────────────────────────────────────────── */
.paywait { padding-block: var(--s10); display: grid; place-items: center; }
.paywait__fallback { margin-top: var(--s5); font-size: var(--fs-sm); color: var(--text-muted); }
.paywait__fallback a { text-decoration: underline; text-underline-offset: 3px; }

/* ── Payment, when there is only one method ─────────────────────────────── */
/**
 * A radio group with one option asks a question with a single answer, so with
 * Cash on Delivery alone the method is stated rather than chosen. The radios
 * return automatically the moment Razorpay keys are configured — see the note
 * in checkout.php.
 *
 * Styled as a statement, not a control: no border-as-target, no hover, nothing
 * that invites a click.
 */
.paystate {
  padding: var(--s4);
  background: var(--bone-2);
  border-radius: var(--r-card);
  border-left: 3px solid var(--ink);
}

.paystate__title {
  font-weight: 600;
  font-size: var(--fs-base);
  margin-bottom: 2px;
}

.paystate__note {
  font-size: var(--fs-sm);
  color: var(--text-soft);
  line-height: 1.55;
}


/* ── content.css ── */
/* =============================================================================
   Content pages (about, contact, policies, search, 404) + admin.
   ========================================================================== */

/* ── Policy / FAQ ───────────────────────────────────────────────────────── */
.policy { display: grid; gap: var(--s7); padding-bottom: var(--s9); }

.policy__section { display: grid; gap: var(--s3); }

.policy__section h2 {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
}

.policy__section p {
  color: var(--text-soft);
  max-width: var(--maxw-prose);
  white-space: pre-line;   /* the data uses \n for line breaks */
}

.policy__toc {
  display: grid;
  gap: var(--s2);
  align-content: start;
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  background: var(--paper);
  font-size: var(--fs-sm);
}

.policy__toc a { color: var(--text-soft); display: block; padding-block: var(--s1); }
.policy__toc a:hover { color: var(--clay); }

@media (min-width: 1024px) {
  .policy--withtoc { grid-template-columns: minmax(220px, 260px) 1fr; gap: var(--s8); }
  .policy__toc { position: sticky; top: calc(var(--header-h) + var(--s5)); }
}

/* ── About ──────────────────────────────────────────────────────────────── */
.about__intro { max-width: 60ch; }

.locations {
  display: grid;
  gap: var(--s4);
  grid-template-columns: 1fr;
}

.location {
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  background: var(--paper);
  display: grid;
  gap: var(--s1);
}

.location__city { font-family: var(--font-display); font-size: var(--fs-xl); }
.location__role { font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }

.standards { display: grid; gap: var(--s3); max-width: 62ch; }

.standards li {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  font-size: var(--fs-base);
  color: var(--text-soft);
}

.standards svg { flex: none; margin-top: 4px; color: var(--clay); }

@media (min-width: 768px) {
  .locations { grid-template-columns: repeat(3, 1fr); }
}

/* ── Contact ────────────────────────────────────────────────────────────── */
.contact { display: grid; gap: var(--s7); padding-bottom: var(--s9); }

.contact__form { display: grid; gap: var(--s4); }

.contact__cards { display: grid; gap: var(--s3); align-content: start; }

.contact__card {
  display: grid;
  gap: var(--s2);
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  background: var(--paper);
}

.contact__card h2 {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}

.contact__card a { font-size: var(--fs-lg); }
.contact__card a:hover { color: var(--clay); }
.contact__card p { font-size: var(--fs-sm); color: var(--text-soft); }

@media (min-width: 900px) {
  .contact { grid-template-columns: 1.2fr 1fr; gap: var(--s8); align-items: start; }
}

/* ── Search results page ────────────────────────────────────────────────── */
.searchpage__form {
  display: flex;
  gap: var(--s2);
  max-width: 34rem;
  margin-bottom: var(--s6);
}

.searchpage__form .input { flex: 1; }

/* ── 404 ────────────────────────────────────────────────────────────────── */
.notfound {
  display: grid;
  gap: var(--s5);
  justify-items: center;
  text-align: center;
  padding-block: var(--s10) var(--s11);
}

.notfound__code {
  font-family: var(--font-display);
  font-size: clamp(4rem, 18vw, 9rem);
  line-height: 0.9;
  color: var(--line-strong);
}

/* =============================================================================
   Admin — plain and dense on purpose. It is a tool, not a storefront.
   ========================================================================== */

.admin { min-height: 100svh; background: var(--bone); }

.admin__bar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--line);
  background: var(--paper);
  position: sticky;
  top: 0;
  z-index: 10;
}

.admin__tag {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}

.admin__logout {
  margin-left: auto;
  font-size: var(--fs-xs);
  text-decoration: underline;
  text-underline-offset: 3px;
  color: var(--text-muted);
}

.admin__logout:hover { color: var(--clay); }

.admin__login {
  max-width: 26rem;
  margin: var(--s10) auto;
  padding-inline: var(--s5);
  display: grid;
  gap: var(--s4);
}

.admin__h1 { font-family: var(--font-display); font-size: var(--fs-2xl); }

.admin__form { display: grid; gap: var(--s4); }

.admin__login code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85em;
  background: var(--bone-2);
  padding: 1px 4px;
}

.admin__body {
  padding: var(--s5);
  display: grid;
  gap: var(--s5);
  max-width: 1400px;
  margin-inline: auto;
}

.admin__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3); }

.admin__stat {
  padding: var(--s4);
  border: 1px solid var(--line);
  background: var(--paper);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  display: grid;
  gap: var(--s1);
}

.admin__stat span {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
  font-variant-numeric: lining-nums tabular-nums;
}

.admin__filters { display: flex; flex-wrap: wrap; gap: var(--s2); }
.admin__filters .input { flex: 1 1 14rem; min-width: 0; }

.admin__empty { color: var(--text-muted); padding: var(--s8) 0; text-align: center; }

.admin__list { display: grid; gap: var(--s2); }

/* Order row */
.orow { border: 1px solid var(--line); background: var(--paper); }

.orow__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
  padding: var(--s3) var(--s4);
  cursor: pointer;
  font-size: var(--fs-sm);
  list-style: none;
}

.orow__head::-webkit-details-marker { display: none; }
.orow__head:hover { background: var(--bone-2); }
.orow[open] .orow__head { border-bottom: 1px solid var(--line); }

.orow__ref { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--fs-xs); }
.orow__date { color: var(--text-muted); font-size: var(--fs-xs); }
.orow__cust { flex: 1; min-width: 8rem; }
.orow__total { font-variant-numeric: tabular-nums; font-weight: 500; }

.pill {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--s2);
  border-radius: var(--r-pill);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid var(--line-strong);
  color: var(--text-soft);
}

.pill--cod       { border-color: var(--pine); color: var(--pine); }
.pill--razorpay  { border-color: var(--clay); color: var(--clay); }
.pill--new       { background: var(--ink); color: var(--text-on-ink); border-color: var(--ink); }
.pill--confirmed,
.pill--packed    { border-color: var(--clay); color: var(--clay); }
.pill--shipped,
.pill--delivered { border-color: var(--pine); color: var(--pine); background: var(--pine-soft); }
.pill--cancelled { border-color: var(--danger); color: var(--danger); }
.pill--warn      { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }

.orow__body { padding: var(--s4); display: grid; gap: var(--s4); font-size: var(--fs-sm); }

.orow__cols { display: grid; gap: var(--s5); }

.orow__h {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--s2);
}

.orow__items { display: grid; gap: var(--s2); }

.orow__items li {
  display: flex;
  gap: var(--s2);
  justify-content: space-between;
  flex-wrap: wrap;
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}

.orow__items .muted { font-size: var(--fs-xs); }

.orow__totals { margin-top: var(--s3); color: var(--text-soft); }

.orow__addr { font-style: normal; line-height: 1.7; color: var(--text-soft); }
.orow__addr a { text-decoration: underline; text-underline-offset: 2px; }

.orow__notes { margin-top: var(--s3); color: var(--text-soft); }

.orow__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}

.orow__form { display: flex; gap: var(--s2); }

.orow__trail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

@media (min-width: 800px) {
  .orow__cols { grid-template-columns: 1.4fr 1fr; }
}


/* ── uniforms.css ── */
/* =============================================================================
   School uniforms.

   Deliberately the plainest page on the site. The fashion lines get an
   editorial hero, parallax and reveal stagger; this gets a heading, a table
   and a phone number, because the person reading it is a parent working to a
   school's specification and a term deadline.

   Everything below reuses existing tokens. No new colour literals — see the
   note in assets/css/tokens.css.
   ========================================================================== */

.uniform-head {
  max-width: 60ch;
  padding-block: var(--s6) var(--s5);
}

.uniform-head__title {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  line-height: 1.05;
  margin-bottom: var(--s3);
}

.uniform-head__lede {
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--text-soft);
}

.uniform-h {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  margin-bottom: var(--s4);
}

.uniform-note {
  max-width: 62ch;
  margin-bottom: var(--s4);
  color: var(--text-soft);
}

/* ── Steps ─────────────────────────────────────────────────────────────────
   A numbered list because the order genuinely matters: confirm the school
   before measuring, measure before ordering. */
.uniform-steps {
  display: grid;
  gap: var(--s4);
  grid-template-columns: 1fr;
  list-style: none;
  padding: 0;
  margin: 0;
}

@media (min-width: 768px) {
  .uniform-steps { grid-template-columns: repeat(3, 1fr); gap: var(--s5); }
}

.uniform-steps li {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
}

.uniform-steps__n {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-soft);
}

.uniform-steps h3 {
  font-size: var(--fs-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: var(--s1);
}

.uniform-steps p {
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--text-soft);
}

/* ── Empty state ──────────────────────────────────────────────────────────
   Shown until the client supplies a uniform catalogue. It is a real answer,
   not a placeholder: it explains why nothing is listed and what to do next. */
.uniform-empty {
  max-width: 60ch;
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--bone-2);
}

.uniform-empty p {
  margin-bottom: var(--s3);
  line-height: 1.6;
  color: var(--text-soft);
}

.uniform-empty .btn { margin-top: var(--s2); }

/* ── Bulk / school enquiry ────────────────────────────────────────────────── */
.uniform-cta {
  display: grid;
  gap: var(--s4);
  padding: var(--s5);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

@media (min-width: 900px) {
  .uniform-cta {
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--s6);
  }
}

.uniform-cta p {
  max-width: 58ch;
  line-height: 1.6;
  color: var(--text-soft);
}

.uniform-cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
}


/* ── account.css ── */
/* =============================================================================
   Accounts — sign in, sign up, order history, saved details.

   Utility surfaces, not editorial ones. No hero, no parallax, no reveal
   stagger: someone reaching these pages is trying to complete a task, and the
   only job here is to get out of their way. Same reasoning as /uniforms/.

   Everything reads from the Madras Weave tokens. No new colour literals.
   ========================================================================== */

/* ── Auth card ──────────────────────────────────────────────────────────── */
.authwrap {
  display: grid;
  place-items: start center;
  padding-block: clamp(2.5rem, 7vw, 5rem) var(--s10);
}

.authcard {
  width: 100%;
  max-width: 26rem;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: clamp(1.5rem, 4vw, 2.25rem);
}

/* The selvedge again — four threads binding the top edge, the same device that
   opens the homepage sections. Ties a utility page to the brand without
   dressing it up. */
.authcard {
  border-top: 3px solid transparent;
  border-image: linear-gradient(90deg,
    var(--madder) 0 22%,
    var(--saffron-fill) 22% 34%,
    var(--bone-2) 34% 42%,
    var(--cobalt) 42% 74%,
    var(--jade) 74% 100%) 1;
  border-image-width: 3px 0 0 0;
}

.authcard__head { margin-bottom: var(--s5); }

.authcard__title {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: var(--ls-tight);
  font-size: var(--fs-2xl);
  margin: var(--s2) 0 var(--s3);
}

.authcard__lede {
  color: var(--text-soft);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

.authform { display: grid; gap: var(--s4); }
.authform .btn { margin-top: var(--s2); }

.authcard__alt {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
  color: var(--text-soft);
}

/* The reassurance that keeps this an offer rather than a gate. Deliberately
   quiet, but always present on the two pages most likely to read as a wall. */
.authcard__guest {
  margin-top: var(--s3);
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--text-muted);
}

/* ── Account nav ────────────────────────────────────────────────────────── */
.accountnav {
  display: flex;
  align-items: center;
  gap: var(--s5);
  flex-wrap: wrap;
  padding-block: var(--s4);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s6);
  font-size: var(--fs-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.accountnav__here {
  font-weight: 600;
  border-bottom: 2px solid var(--madder);
  padding-bottom: 2px;
}

.accountnav__out { margin-left: auto; }

.linkbtn {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.linkbtn:hover { color: var(--madder); }

/* ── Order list ─────────────────────────────────────────────────────────── */
.orderlist {
  display: grid;
  gap: var(--s4);
  padding: 0;
  margin: 0;
  list-style: none;
}

.ordercard {
  position: relative;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s5);
}

.ordercard__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-bottom: var(--s3);
}

.ordercard__ref {
  font-weight: 600;
  font-size: var(--fs-sm);
  letter-spacing: 0.04em;
}

.ordercard__meta {
  font-size: var(--fs-sm);
  color: var(--text-soft);
  margin-bottom: var(--s2);
}

.ordercard__items {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  line-height: 1.5;
}

/* Stretched link: the whole card is the target, but the ref stays selectable. */
.ordercard__link {
  position: absolute;
  inset: 0;
  font-size: 0;
  border-radius: var(--r-card);
}

.ordercard__link:focus-visible {
  outline: 2px solid var(--madder);
  outline-offset: 2px;
}

/* ── Status pill ────────────────────────────────────────────────────────── */
/**
 * Status is encoded in colour AND in the word, never colour alone — a customer
 * who cannot distinguish the hues still reads "Delivered".
 */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px var(--s3);
  border-radius: var(--r-pill);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border: 1px solid currentColor;
}

.pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.pill--new,
.pill--confirmed { color: var(--cobalt); }
.pill--packed,
.pill--shipped   { color: var(--saffron); }
.pill--delivered { color: var(--jade); }
.pill--cancelled { color: var(--text-muted); }

/* ── Order detail ───────────────────────────────────────────────────────── */
.orderdetail { display: grid; gap: var(--s7); padding-bottom: var(--s10); }

@media (min-width: 900px) {
  .orderdetail { grid-template-columns: 1.6fr 1fr; gap: var(--s9); }
}

.account__h {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--s4);
}

.account__meta {
  font-size: var(--fs-sm);
  color: var(--text-soft);
  line-height: 1.6;
  margin-bottom: var(--s3);
}

.orderlines { list-style: none; padding: 0; margin: 0 0 var(--s5); }

.orderline {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-sm);
}

.orderline__meta {
  display: block;
  color: var(--text-muted);
  font-size: var(--fs-xs);
  margin-top: 2px;
}

.orderline__price { white-space: nowrap; font-weight: 500; }

.ordertotals { display: grid; gap: var(--s2); font-size: var(--fs-sm); }
.ordertotals > div { display: flex; justify-content: space-between; gap: var(--s4); }
.ordertotals dt { color: var(--text-soft); }
.ordertotals dd { margin: 0; }

.ordertotals__total {
  margin-top: var(--s2);
  padding-top: var(--s3);
  border-top: 1px solid var(--line-strong);
  font-weight: 600;
}

.orderaddress {
  font-style: normal;
  font-size: var(--fs-sm);
  line-height: 1.7;
  color: var(--text-soft);
  margin-bottom: var(--s5);
}

/* ── Details panels ─────────────────────────────────────────────────────── */
.accountpanels { display: grid; gap: var(--s6); padding-bottom: var(--s10); max-width: 44rem; }

.panel {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: clamp(1.25rem, 3vw, 1.75rem);
}

/* Deletion is irreversible, so the panel that does it does not look like the
   two above it. */
.panel--danger { border-color: color-mix(in srgb, var(--danger) 34%, transparent); }
.panel--danger .account__h { color: var(--danger); }

.btn--danger {
  --btn-bg: var(--danger);
  --btn-fg: var(--paper);
  --btn-bd: var(--danger);
}

.accountverify {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  margin-bottom: var(--s5);
}

.accountverify form { margin-left: auto; }

/* ── Checkout account offers ────────────────────────────────────────────── */
.signin-offer,
.signedin {
  font-size: var(--fs-sm);
  color: var(--text-soft);
  padding: var(--s3) var(--s4);
  background: var(--bone-2);
  border-radius: var(--r-sharp);
  margin-bottom: var(--s4);
}

.signedin__sep { margin-inline: var(--s2); opacity: 0.5; }

.saveme {
  margin-top: var(--s5);
  padding: var(--s4);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-card);
  background: var(--bone-2);
}

.saveme--post {
  margin-block: var(--s6);
  border-style: solid;
  background: var(--paper);
}

.saveme__check {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  cursor: pointer;
}

.saveme__check input { margin-top: 3px; width: 18px; height: 18px; flex: none; accent-color: var(--ink); }

.saveme__hint {
  display: block;
  margin-top: 4px;
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--text-muted);
  font-weight: 400;
}

/**
 * The password field is in the DOM whether or not the box is ticked, so this
 * works with JavaScript disabled. Script only collapses it for tidiness — see
 * the [hidden] fallback below, which never applies without JS.
 */
.saveme__pass { margin-top: var(--s4); }
.saveme__pass[hidden] { display: none; }

.saveme__form { margin-top: var(--s4); max-width: 24rem; }
.saveme-done { margin-bottom: var(--s5); }

/* ── Header account link ────────────────────────────────────────────────── */
.header__account {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding-inline: var(--s2);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: inherit;
}

.header__account:hover { color: var(--madder); }

/* The label is a nicety; the icon carries the meaning on a phone. */
.header__account-label { display: none; }

@media (min-width: 1024px) {
  .header__account-label { display: inline; }
}


/* ── admin-products.css ── */
/* =============================================================================
   Admin — product management.

   A tool, not a page. Dense, plain, no motion, no reveal stagger. The client is
   here to change a price and get out, and every flourish is something between
   them and that. Same reasoning as the orders table in content.css.

   All colours come from the Madras Weave tokens; nothing new is introduced.
   ========================================================================== */

/* ── Tabs ───────────────────────────────────────────────────────────────── */
.admin__tabs {
  display: flex;
  gap: var(--s1);
  margin-left: var(--s5);
}

.admin__tab {
  padding: var(--s2) var(--s3);
  border-radius: var(--r-sharp);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
  white-space: nowrap;
}

.admin__tab:hover { color: var(--ink); background: var(--bone-2); }

.admin__tab.is-here {
  background: var(--ink);
  color: var(--text-on-ink);
}

.admin__back {
  font-size: var(--fs-sm);
  margin-bottom: var(--s4);
  color: var(--text-soft);
}

/* ── Product list ───────────────────────────────────────────────────────── */
.prodlist {
  display: grid;
  gap: var(--s2);
}

.prow {
  display: grid;
  grid-template-columns: 60px 1fr;
  gap: var(--s4);
  align-items: start;
  padding: var(--s3);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

@media (min-width: 900px) {
  .prow {
    grid-template-columns: 60px minmax(0, 1fr) auto auto;
    align-items: center;
  }
}

/**
 * Status is carried by a left edge as well as by the pill, so a long list can
 * be scanned for drafts without reading a single word.
 *
 * --saffron, not --saffron-fill: the fill tone is 2.45:1 against white, under
 * the 3:1 floor for a non-text indicator. The pill states "Draft" in words at
 * 6.54:1, so the edge is arguably exempt as redundant — but a darker saffron
 * costs nothing and removes the argument.
 */
.prow--draft    { border-left: 3px solid var(--saffron); }
.prow--archived { border-left: 3px solid var(--line-strong); background: var(--bone-2); }
.prow--archived .prow__thumb { opacity: 0.55; }

.prow__thumb {
  display: block;
  width: 60px;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--r-sharp);
  background: var(--bone-2);
}

.prow__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.prow__main { min-width: 0; }

.prow__name {
  font-size: var(--fs-base);
  font-weight: 600;
  line-height: 1.3;
  margin-bottom: 2px;
}

.prow__meta {
  font-size: var(--fs-xs);
  color: var(--text-soft);
  line-height: 1.5;
}

.prow__meta--dim {
  color: var(--text-muted);
  font-family: var(--font-mono, ui-monospace, monospace);
  word-break: break-all;
}

.prow__flags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

@media (min-width: 900px) {
  .prow__flags { flex-direction: column; align-items: flex-end; }
}

.prow__photos { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* The most useful warning on the page: a live product with no photographs. */
.prow__photos--none { color: var(--danger); font-weight: 600; }

.prow__out {
  color: var(--ink);
  background: var(--bone-2);
  border: 1px solid var(--line-strong);
  padding: 2px 6px;
  border-radius: var(--r-pill);
}

.prow__out--part { color: var(--text-soft); font-weight: 400; }

.prow__ops {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  grid-column: 1 / -1;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}

@media (min-width: 900px) {
  .prow__ops {
    grid-column: auto;
    padding-top: 0;
    border-top: 0;
    justify-content: flex-end;
  }
}

.pill--live     { color: var(--jade); }
.pill--draft    { color: var(--saffron); }
.pill--archived { color: var(--text-muted); }

/* ── Form ───────────────────────────────────────────────────────────────── */
.pform__grid { display: grid; gap: var(--s5); }

@media (min-width: 1024px) {
  .pform__grid { grid-template-columns: minmax(0, 1fr) 22rem; align-items: start; }
}

.pform__main,
.pform__side { display: grid; gap: var(--s5); align-content: start; }

.pcard {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s5);
  display: grid;
  gap: var(--s4);
  align-content: start;
}

.pcard--wide { margin-top: var(--s5); }

.pcard__h {
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}

.pform__row { display: grid; gap: var(--s4); }

@media (min-width: 640px) {
  .pform__row { grid-template-columns: 1fr 1fr; }
}

/* Rupee sign inside the field, so nobody types one into the number. */
.pform__money {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sharp);
  overflow: hidden;
  background: var(--paper);
}

.pform__money span {
  display: grid;
  place-items: center;
  padding-inline: var(--s3);
  background: var(--bone-2);
  border-right: 1px solid var(--line-strong);
  color: var(--text-soft);
  font-size: var(--fs-sm);
}

.pform__money .input { border: 0; border-radius: 0; flex: 1; min-width: 0; }
.pform__money:focus-within { border-color: var(--ink); }

.field__opt {
  font-weight: 400;
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
}

.pform__save {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: var(--s3);
  flex-wrap: wrap;
  padding: var(--s4) 0;
  margin-top: var(--s5);
  background: linear-gradient(to top, var(--bone) 60%, transparent);
}

.pradio,
.pcheck {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  font-size: var(--fs-sm);
  cursor: pointer;
  line-height: 1.4;
}

.pradio input,
.pcheck input { margin-top: 2px; width: 17px; height: 17px; flex: none; accent-color: var(--ink); }

/* ── Sizes and availability ─────────────────────────────────────────────── */
/**
 * One control, two jobs. The upper checkbox says the product is MADE in that
 * size; the lower says it cannot be bought right now. Nesting them makes the
 * relationship obvious without a paragraph of explanation — you cannot mark a
 * size sold out that you are not offering.
 */
.psizes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.psize {
  border: 1px solid var(--line);
  border-radius: var(--r-sharp);
  padding: var(--s2) var(--s3);
  background: var(--bone-2);
  display: grid;
  gap: 4px;
  min-width: 5.5rem;
}

.psize.is-on { background: var(--paper); border-color: var(--line-strong); }

.psize__has {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-weight: 600;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.psize__out {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: pointer;
}

.psize__has input,
.psize__out input { accent-color: var(--ink); }
.psize__has input { width: 16px; height: 16px; }
.psize__out input { width: 13px; height: 13px; }

/* ── Slug ───────────────────────────────────────────────────────────────── */
.pslug {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fs-xs);
  color: var(--text-soft);
  word-break: break-all;
}

.pdetails summary {
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--cobalt);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.pdetails__body { display: grid; gap: var(--s3); padding-top: var(--s3); }

.pslug__old {
  list-style: none;
  padding: 0;
  margin: 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11px;
  color: var(--text-muted);
  display: grid;
  gap: 2px;
}

/* ── Photo slots ────────────────────────────────────────────────────────── */
.pshots {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--s4);
}

.pshot { display: grid; gap: var(--s2); align-content: start; }

/* Slot 1 is the card shot. Marked, because it is the only one whose position
   carries meaning and the difference is invisible otherwise. */
.pshot--main .pshot__frame { border-color: var(--ink); border-width: 2px; }
.pshot--main .pshot__label { color: var(--ink); font-weight: 600; }

.pshot__frame {
  aspect-ratio: 3 / 4;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sharp);
  overflow: hidden;
  background: var(--bone-2);
  display: grid;
  place-items: center;
}

.pshot__frame img { width: 100%; height: 100%; object-fit: cover; display: block; }

.pshot__empty {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pshot__label {
  font-size: var(--fs-xs);
  color: var(--text-soft);
}

.pshot__up { display: grid; gap: var(--s2); }

/* The file input is visually hidden but still focusable through its label. */
.pshot__up label { cursor: pointer; text-align: center; }
.pshot__up input[type="file"]:focus-visible + label {
  outline: 2px solid var(--madder);
  outline-offset: 2px;
}

.pshot__rm {
  font-size: var(--fs-xs);
  color: var(--danger);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.pcopy {
  margin-top: var(--s5);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
  display: grid;
  gap: var(--s2);
}

.pcopy__row { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center; }
.pcopy__row .select { flex: 1; min-width: 12rem; }

/* ── Delete confirmation ────────────────────────────────────────────────── */
.pconfirm {
  max-width: 40rem;
  background: var(--paper);
  border: 1px solid color-mix(in srgb, var(--danger) 34%, transparent);
  border-radius: var(--r-card);
  padding: clamp(1.25rem, 3vw, 2rem);
}

.pconfirm__lede {
  font-size: var(--fs-base);
  color: var(--text-soft);
  margin-bottom: var(--s4);
}

.pconfirm__list {
  display: grid;
  gap: var(--s2);
  margin: 0 0 var(--s5);
  padding-left: var(--s5);
  font-size: var(--fs-sm);
  color: var(--text-soft);
  line-height: 1.6;
}

.pconfirm__form { display: grid; gap: var(--s4); justify-items: start; }
.pconfirm__form .field { width: 100%; max-width: 24rem; }

/* ── Outstanding photography ────────────────────────────────────────────── */
/**
 * The most actionable number on the page: a live product with no photograph is
 * hidden from the shop, and nothing else in the admin would say so.
 */
.admin__stat--warn span { color: var(--saffron); }

.admin__awaiting { margin-bottom: var(--s5); }

.awaitlist {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--s2);
  max-height: 18rem;
  overflow-y: auto;
}

.awaitlist li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  font-size: var(--fs-sm);
  padding-block: 2px;
  border-bottom: 1px solid var(--line);
}

.awaitlist__id {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ── New-order notifications ───────────────────────────────────────────────
   "Unseen" is a different axis from status. An order can be status:new and
   already read, or status:shipped and never opened. The two markers therefore
   look nothing alike: status is a pill in the row's own colour language,
   unseen is a madder rule down the left edge of the whole row.
   ────────────────────────────────────────────────────────────────────────── */

/* Tab badge. Sits inside .admin__tab, which is uppercase and letterspaced —
   the badge opts out of both so a two-digit number stays a number. */
.admin__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  margin-left: var(--s2);
  padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--madder);
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0;
  font-variant-numeric: lining-nums tabular-nums;
}

.admin__badge[hidden] { display: none; }

/* On the active (ink) tab the madder disc loses too much contrast against the
   dark ground, so it inverts to the tab's own foreground. */
.admin__tab.is-here .admin__badge {
  background: var(--text-on-ink);
  color: var(--ink);
}

/* Unread summary bar */
.adminnew {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--madder);
  border-left-width: 3px;
  background: var(--paper);
}

.adminnew__msg { font-size: var(--fs-sm); }
.adminnew__msg strong { font-variant-numeric: lining-nums tabular-nums; }

.adminnew__hint {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.adminnew__form { flex: none; }

.adminnew__live[hidden] { display: none; }

/* Unread row */
.orow--unread {
  border-left: 3px solid var(--madder);
  background: var(--paper);
}

.orow--unread .orow__ref,
.orow--unread .orow__cust { font-weight: 600; }

.pill--unseen {
  background: var(--madder);
  border-color: var(--madder);
  color: #fff;
  font-weight: 600;
}

