/* ==========================================================================
   BLACK ZONE ABAYA — main.css
   Design tokens are fixed by PLAN.md §14. Do not introduce new colours.

   Structure
     1. Fonts
     2. Tokens
     3. Reset & base
     4. Type
     5. The rail (signature element, §14)
     6. Layout primitives
     7. Header / nav / wordmark
     8. Buttons & links
     9. Product cards
    10. Home sections, editorial blocks, content pages
    11. Collection
    12. Product page
    13. Footer
    14. Motion
   ========================================================================== */

/* 1. Fonts ---------------------------------------------------------------- */

@font-face {
  font-family: 'Bodoni Moda';
  src: url('../fonts/bodoni-moda-latin.woff2') format('woff2');
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
  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;
}
@font-face {
  font-family: 'Bodoni Moda';
  src: url('../fonts/bodoni-moda-latin-ext.woff2') format('woff2');
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
  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: 'Karla';
  src: url('../fonts/karla-latin.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  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;
}
@font-face {
  font-family: 'Karla';
  src: url('../fonts/karla-latin-ext.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  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;
}

/* 2. Tokens --------------------------------------------------------------- */

:root {
  /* Colour — "Ink & Pearl", PLAN.md §14. Five values. No others. */
  --ink: #151311;
  --pearl: #F2EFEA;
  --bone: #DCD5CA;
  --brass: #9A7B4F;
  --smoke: #6E6862;

  /* Brass measures 3.44:1 on pearl — below the 4.5:1 body-text floor but above
     the 3:1 floor for rules, large display type and focus rings. On ink it is
     4.76:1 and safe for small text. Hence: brass is a hairline on pearl, and
     may only become type on an ink ground. See CLAUDE.md. */

  /* A sixth *value*, not a sixth colour: bone dissolved into pearl, so the page
     can step pearl → wash → ink instead of only pearl → ink. Two grounds gave
     every quiet section the same weight. The mix is the same device
     scripts/make-placeholders.py already uses for the editorial frames.
     Measured on wash: ink 14.75:1, brass 3.14:1 (hairline and icon strokes
     only, as on pearl), smoke 4.38:1 — SMOKE FAILS HERE, just under the 4.5
     floor. On wash, secondary text is ink; that is what --muted below is for. */
  --wash: #E9E5DD;
  --wash: color-mix(in srgb, var(--bone) 40%, var(--pearl));

  /* Secondary text, resolved per ground. Never hard-code --smoke in a component
     that can land on more than one ground. */
  --muted: var(--smoke);

  --font-display: 'Bodoni Moda', Didot, 'Bodoni MT', 'Playfair Display', Georgia, serif;
  --font-body: 'Karla', 'Helvetica Neue', Arial, sans-serif;

  /* Display scale. The hero is deliberately far ahead of h1 — it is the only
     place the didone is allowed to be the loudest thing on the page. Capped at
     6rem rather than larger because the headline now sits ON the photograph:
     it has to fit inside the scrim's plateau (see .hero__frame::after), and
     past ~7vw it wraps to a third line and walks out of the bottom of it.
     Still 1.75x the h1 cap, so it keeps the billing. */
  /* The 2.25rem floor is measured, not chosen. The headline sits at the top of
     the hero photograph and must land clear of the subjects' heads; on a narrow
     phone it always takes three lines, so the floor is what decides whether it
     clears them. At 2.6rem it overlapped at 320px and cleared by only 1.6pts at
     360px. */
  /* Cap raised 6rem -> 6.75rem with the real photography. The old cap was set
     by the top-band scrim plateau; from 860px the headline no longer lives in
     that band, it lives in the leading wall, where the limit is the width of
     the band and not the depth of the plateau. Below 860px nothing changed —
     the floor is still what clears the subjects' heads on a phone. */
  --fs-hero: clamp(2.25rem, 7vw, 6.75rem);
  --fs-h1: clamp(2.1rem, 5vw, 3.4rem);
  --fs-h2: clamp(1.6rem, 3.6vw, 2.5rem);
  --fs-h3: clamp(1.2rem, 2.5vw, 1.6rem);
  --fs-lead: clamp(1.05rem, 0.97rem + 0.55vw, 1.4rem);
  --fs-body: clamp(1rem, 0.97rem + 0.14vw, 1.0625rem);
  --fs-small: 0.875rem;
  --fs-util: 0.75rem;

  --track-util: 0.14em;
  --track-display: -0.02em;

  --sp-xs: 0.5rem;
  --sp-s: 1rem;
  --sp-m: 1.5rem;
  --sp-l: 2.5rem;
  --sp-xl: 4rem;
  --sp-section: clamp(3.5rem, 8vw, 7rem);

  --gutter: clamp(28px, 6vw, 88px);
  --rail-x: clamp(14px, 3.2vw, 42px);
  --maxw: 1420px;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* 3. Reset & base --------------------------------------------------------- */

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

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

body {
  margin: 0;
  position: relative;
  background: var(--pearl);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.65;
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, svg { display: block; max-width: 100%; height: auto; }

button { font: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
}
.on-ink :focus-visible { outline-color: var(--bone); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.skip-link {
  position: absolute;
  inset-block-start: -100%;
  inset-inline-start: var(--gutter);
  z-index: 100;
  padding: var(--sp-xs) var(--sp-s);
  background: var(--ink);
  color: var(--pearl);
  text-decoration: none;
}
.skip-link:focus { inset-block-start: var(--sp-xs); }

/* 4. Type ----------------------------------------------------------------- */

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: var(--track-display);
  line-height: 1.04;
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }

p { margin: 0 0 var(--sp-s); }
p:last-child { margin-block-end: 0; }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.5;
  max-inline-size: 34ch;
}

.util {
  font-family: var(--font-body);
  font-size: var(--fs-util);
  font-weight: 500;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
  color: var(--smoke);
  line-height: 1.4;
}
.on-ink .util { color: var(--brass); }

.measure { max-inline-size: 58ch; }

/* 5. The rail — signature element ---------------------------------------
   A brass hairline running the full height of the document in the outer
   margin. It echoes the vertical fall of the fabric. Section eyebrows carry
   their own opaque ground, so the rule breaks wherever a title crosses it. */

body::before {
  content: "";
  position: absolute;
  inset-block: 0;
  /* Anchored to the outer edge of the content column, not the viewport, so the
     eyebrows still reach it once the layout centres at wide widths. */
  inset-inline-start: calc(max((100% - var(--maxw)) / 2, 0px) + var(--rail-x));
  inline-size: 1px;
  background: var(--brass);
  opacity: 0.75;
  z-index: 2;
  pointer-events: none;
}

/* The eyebrow straddles the rail and interrupts it. */
.eyebrow {
  position: relative;
  z-index: 3;
  display: inline-block;
  margin-inline-start: calc(-1 * (var(--gutter) - var(--rail-x) + 14px));
  padding-inline: calc(var(--gutter) - var(--rail-x) + 14px) var(--sp-m);
  padding-block: 2px;
  background: var(--ground, var(--pearl));
  margin-block-end: var(--sp-m);
}
.on-ink { --ground: var(--ink); }

/* 6. Layout primitives ---------------------------------------------------- */

.wrap {
  max-inline-size: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--pad, var(--sp-section)); }
.section--tight { --pad: var(--sp-xl); }
.section--flush-top { padding-block-start: 0; }
/* For sections that end in a full-bleed block: the block's own edge is the
   section's edge, and padding under it just reads as a gap. */
.section--flush-bottom { padding-block-end: 0; }

.on-ink {
  background: var(--ink);
  color: var(--pearl);
  --muted: var(--bone);
}
.on-ink h1, .on-ink h2, .on-ink h3 { color: var(--pearl); }

/* The middle ground. Quiet sections sit on wash so the ink block stays the one
   hard cut in the page. Smoke does not clear 4.5:1 here — see --wash. */
.on-wash {
  background: var(--wash);
  --ground: var(--wash);
  --muted: var(--ink);
}
.on-wash .util { color: var(--ink); }

.rule {
  border: 0;
  border-block-start: 1px solid var(--bone);
  margin: 0;
}

/* 7. Header / wordmark ---------------------------------------------------- */

/* Kept deliberately thin — the header is a nameplate, not a chrome bar, and the
   hero should start close to the top of the window. The asymmetry is the point:
   a little more room beneath the nav than above it, so the mark sits optically
   centred in the band rather than measuring equal. */
.header {
  position: relative;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-m);
  /* Only bites once the nav wraps under the wordmark, below ~480px. The
     column gap still needs to be generous; the row gap does not, and leaving
     it at --sp-m was most of what made the phone header tall. */
  row-gap: var(--sp-xs);
  flex-wrap: wrap;
  padding-block: var(--sp-s) calc(var(--sp-s) + 4px);
}

/* Single source of the identity mark: js/wordmark.js.
   The name is set in Karla rather than Bodoni — the logo's own lettering is a
   squared techno sans, and a didone beside it reads as two different brands. */
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: clamp(10px, 1.4vw, 16px);
  text-decoration: none;
  color: inherit;
  line-height: 1;
}
.wordmark__mark {
  inline-size: auto;
  block-size: clamp(30px, 4.4vw, 40px);
}
.wordmark__text {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: clamp(0.8rem, 2.1vw, 0.95rem);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.wordmark__suffix {
  display: block;
  font-size: 0.72em;
  font-weight: 400;
  letter-spacing: 0.32em;
  color: var(--smoke);
  margin-block-start: 0.5em;
}
.on-ink .wordmark__suffix { color: var(--brass); }

.wordmark--lockup { display: block; }
.wordmark--lockup img {
  inline-size: auto;
  block-size: clamp(88px, 11vw, 126px);
}

.nav {
  display: flex;
  align-items: baseline;
  gap: clamp(var(--sp-m), 4vw, var(--sp-xl));
}
.nav a {
  font-size: var(--fs-util);
  font-weight: 500;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  padding-block: 14px;          /* ≥44px tap target with line-height */
  border-block-end: 1px solid transparent;
  transition: border-color 0.25s var(--ease);
}
.nav a:hover, .nav a[aria-current="page"] { border-block-end-color: var(--brass); }

/* 8. Buttons -------------------------------------------------------------- */
/* Brass is never a fill (§14). Primary is ink, secondary is a hairline. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  min-block-size: 48px;
  padding-inline: var(--sp-l);
  padding-block: 0.85rem;
  font-size: var(--fs-util);
  font-weight: 600;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--ink);
  border-radius: 0;
  background: var(--ink);
  color: var(--pearl);
  cursor: pointer;
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease);
}
.btn:hover { background: transparent; color: var(--ink); }

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--bone);
}
.btn--ghost:hover { background: var(--ink); color: var(--pearl); border-color: var(--ink); }

.on-ink .btn { background: var(--pearl); color: var(--ink); border-color: var(--pearl); }
.on-ink .btn:hover { background: transparent; color: var(--pearl); }

.btn--block { inline-size: 100%; }
.btn[aria-disabled="true"] { opacity: 0.45; pointer-events: none; }

.link-under {
  color: inherit;
  text-decoration: none;
  border-block-end: 1px solid var(--brass);
  padding-block-end: 2px;
  transition: border-color 0.25s var(--ease);
}
.link-under:hover { border-block-end-color: currentColor; }

/* Floating WhatsApp — every page, §10 */
.wa-float {
  position: fixed;
  inset-block-end: clamp(16px, 4vw, 32px);
  inset-inline-end: clamp(16px, 4vw, 32px);
  z-index: 20;
  inline-size: 52px;
  block-size: 52px;
  display: grid;
  place-items: center;
  background: var(--ink);
  color: var(--pearl);
  /* Brass hairline, not a fill: without it the button vanishes into the ink
     sections it floats over. */
  border: 1px solid var(--brass);
  text-decoration: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease), visibility 0.35s;
}
.wa-float.is-shown { opacity: 1; visibility: visible; transform: none; }
.wa-float:hover { background: var(--pearl); color: var(--ink); }
.wa-float svg { inline-size: 24px; block-size: 24px; fill: currentColor; }

/* 9. Product cards -------------------------------------------------------- */
/* Full-bleed 4:5. No card, no border, no shadow, no radius (§14). */

.card { display: block; text-decoration: none; color: inherit; }

.card__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--bone);
  overflow: hidden;
}
.card__frame img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: transform 0.7s var(--ease), opacity 0.4s var(--ease);
}
.card:hover .card__frame img { transform: scale(1.028); }

.card__flag {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  padding: 6px 12px;
  background: var(--ink);
  color: var(--pearl);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
}

/* Editorial caption, set beneath the image. Not centred, not in a box. */
.card__meta { padding-block-start: 12px; }
.card__name {
  font-family: var(--font-body);
  font-size: var(--fs-util);
  font-weight: 600;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
  margin: 0 0 3px;
  line-height: 1.4;
}
.card__price {
  font-family: var(--font-display);
  font-size: 1.05rem;
  letter-spacing: 0.02em;
}
.card__price span { font-family: var(--font-body); font-size: 0.7rem; letter-spacing: var(--track-util); color: var(--smoke); }
.card__price { margin: 0; line-height: 1.3; }
.card__fabric { font-size: var(--fs-small); color: var(--smoke); margin: 4px 0 0; line-height: 1.4; }
/* Sold out fades toward the page ground, not toward the bone loading mount —
   otherwise the tile reads as a broken image rather than a retired piece. */
.card--out .card__frame { background: var(--pearl); }
.card--out .card__frame img { opacity: 0.5; }

.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  gap: clamp(var(--sp-m), 2.4vw, 2.5rem) clamp(0.75rem, 1.6vw, 1.5rem);
}

/* 10. Home ---------------------------------------------------------------- */

/* Hero — the garment is the hero (§12).
   Headline, lead and CTA are one block (.hero__body), never three separately
   placed things, so they cannot drift off each other's axis. The block has two
   arrangements and the breakpoint picks one:

   From 860px it sits ON the photograph, in the leading wall, over a scrim.
   Below 860px it sits on pearl beneath the photograph, and needs no scrim.

   Which one is right is a property of the photograph, not a preference. The
   rule from CLAUDE.md is unchanged — display type sits on guaranteed ground and
   never on a face — but the frame crops to a portrait below 860px, the clear
   wall crops out with it, and the subjects' heads land at 8-18% of the frame,
   which is exactly the band a top-anchored headline occupies. There is no
   clear band left at that width, so the type comes off the photograph. */
.hero { margin-block-end: var(--sp-section); }

/* One grid cell with everything stacked in it. Grid rather than absolute
   positioning so the frame can never clip the type: if the block is ever taller
   than the crop, the row grows instead of overflowing. */
.hero__frame {
  display: grid;
  grid-template-areas: "stack";
  /* Own stacking context, so the body's z-index stays a local matter and does
     not compete with the rail (body::before, z-index 2) — the rail is meant to
     run over the hero like everywhere else. */
  isolation: isolate;
  background: var(--bone);
}
.hero__frame > *,
.hero__frame::after { grid-area: stack; }

.hero picture { display: block; }
.hero__img {
  display: block;
  inline-size: 100%;
  aspect-ratio: 4 / 5;
  /* 52vh here until the headline moved to the top of the frame. That cap was
     for the CTA's sake, but on a short phone the CTA sits below the fold at
     any cap — and the cap was actively harmful once the type went up top,
     because a short frame makes a fixed-size headline occupy a huge fraction
     of it and drives the type down onto the subjects' faces. 68vh keeps the
     crop tall enough that the type clears them. */
  max-block-size: 68vh;
  object-fit: cover;
  object-position: 50% 18%;
  background: var(--bone);
}

/* The scrim exists only where type is over the photograph, so it is declared
   only in the 860px arrangement below. Below that width the picture is clean. */
.hero__frame::after { content: ""; pointer-events: none; }

.hero__title {
  font-size: var(--fs-hero);
  /* 600, not the 400 the other display type carries. A didone gains most of its
     weight in the stems and keeps the hairlines thin, so this reads heavier
     without going blunt — and it is the one line on the site that has to hold
     its own against a photograph. */
  font-weight: 600;
  line-height: 0.93;
  margin-block: 0 var(--sp-m);
  /* Held to a measure rather than run to the gutters. Narrow on purpose: the
     measure is what makes the type big — held to a few short lines it fills its
     column instead of running across the page. */
  max-inline-size: 16ch;
  text-wrap: balance;
}
.hero__title em {
  font-style: italic;
  display: block;
}

/* On pearl, beneath the picture. Start-aligned, so the headline, the lead and
   the button hang off one edge and read as a single column. */
.hero__body {
  padding: var(--sp-l) var(--gutter) 0;
  text-align: start;
}
.hero__lead {
  font-size: var(--fs-lead);
  line-height: 1.45;
  max-inline-size: 40ch;
  margin-block-end: var(--sp-l);
}

/* From 860px the block moves onto the photograph, and the scrim turns with it.
   All four subjects stand in the trailing half of this frame and the leading
   half is empty wall, top to bottom — so the guaranteed ground is a column, not
   a band, and the type takes the column. Confining it there means vertical
   clearance stops mattering at all, which is what makes this arrangement more
   robust than the top-anchored one and not merely better-looking: a replacement
   photograph has to keep one wall clear, not one wall clear at one height. */
@media (min-width: 860px) {
  /* Both children share the one cell, so the block floats over the picture.
     `stretch`, not `center`: the row is as tall as whichever child is taller,
     and the frame then fills it. Centring instead let the type escape the band
     between roughly 860px and 1300px — where the crop is at its shortest but
     the headline is already at full size — putting the first line under the
     header and the button out on the pearl below. */
  .hero {
    display: grid;
    grid-template-areas: "stack";
    align-items: stretch;
  }
  .hero > * { grid-area: stack; }

  /* The wide crop is a band, not a portrait — 12piece is 2.36:1 natively, so
     40:17 takes only a sliver off the ends. The aspect ratio sets the band's
     natural height; `block-size: 100%` then lets it grow past that to cover the
     row when the type needs more, cropping rather than letterboxing. */
  .hero__frame { min-block-size: 0; }
  .hero picture { block-size: 100%; }
  .hero__img {
    aspect-ratio: 40 / 17;
    block-size: 100%;
    max-block-size: none;
    object-position: 50% 50%;
  }

  /* Same plateau-then-release shape as a top scrim, turned to the inline axis:
     ~66% ink flat across the type column, then released to nothing.
     The 42% stop is measured, not chosen. The longest line ("shoulder") reaches
     41.0% of the frame at 9ch, so the plateau has to outrun that — which is
     also why the measure below is 9ch and not 10: at 10ch the line runs to
     52.1%, past the plateau and onto the first subject, and both failures
     arrive together.
     Contrast, composited over the real crop: pearl is 6.8:1 on the wall in this
     frame, and 5.2:1 over pure white — the brightest ground any replacement
     photograph could present, so a floor and not an estimate. Bar is 4.5:1.
     Not a new colour: ink at reduced alpha, the same color-mix device as
     --wash. */
  .hero__frame::after {
    background: linear-gradient(var(--hero-scrim-dir, to right),
      color-mix(in srgb, var(--ink) 68%, transparent),
      color-mix(in srgb, var(--ink) 66%, transparent) 42%,
      color-mix(in srgb, var(--ink) 24%, transparent) 58%,
      transparent 72%);
  }

  /* Hung off the content column's leading edge — the same line the rail and
     every section eyebrow are hung from, so the hero starts where the rest of
     the page starts. */
  .hero__body {
    z-index: 1;
    align-self: center;
    padding-block: var(--sp-l);
    padding-inline: max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter))) var(--gutter);
    color: var(--pearl);
  }
  /* 9ch, not the 16ch the pearl arrangement uses: on the photograph the measure
     is set by the width of the clear wall, not by reading comfort. See the
     scrim above — the two numbers were measured together. */
  .hero__title { max-inline-size: 9ch; }
  .hero__lead { max-inline-size: 32ch; }
  /* Pearl on ink is the button's own contrast; on the scrim it would be an ink
     block on a dark ground. Inverted, it is the brightest thing in the frame. */
  .hero__body .btn {
    background: var(--pearl);
    color: var(--ink);
  }
  .hero__body .btn:hover { background: var(--bone); }
}

/* The scrim is the one gradient on the site with a direction, and gradients
   have no logical keywords. One variable is the whole RTL cost. */
[dir="rtl"] .hero__frame::after { --hero-scrim-dir: to left; }

/* Category entry — a mosaic, not a row of equal tiles. The tall tile carries
   the biggest category; the wide one at the foot opens the whole collection. */
.cats { --cols: 1; gap: var(--sp-m); }
/* Stacked on a phone, the wide tile keeps its own landscape crop — forcing the
   rail artwork into a 4:5 frame would cut it down to two garments. */
.cats > :nth-child(4) .cat__frame { aspect-ratio: 16 / 9; }

@media (min-width: 720px) {
  .cats {
    --cols: 4;
    grid-auto-rows: 1fr;
    block-size: clamp(430px, 50vw, 690px);
    gap: clamp(0.75rem, 1.6vw, 1.5rem);
  }
  .cats > :nth-child(1) { grid-area: 1 / 1 / 3 / 3; }
  .cats > :nth-child(2) { grid-area: 1 / 3 / 2 / 4; }
  .cats > :nth-child(3) { grid-area: 1 / 4 / 2 / 5; }
  .cats > :nth-child(4) { grid-area: 2 / 3 / 3 / 5; }
  .cat { display: flex; flex-direction: column; min-block-size: 0; }
  .cat .cat__frame { flex: 1; aspect-ratio: auto; min-block-size: 0; }
  /* The two half-height tiles land at about 1.10:1 against a 0.81:1 photograph
     — the widest gap in the mosaic, so they throw away 27% of the height where
     the tall tile throws away 24% and the wide one none. At the shared 20% bias
     that still starts the crop 5% down, which is inside the hijab on both of
     these shots. They are the one pair that has to start at the top edge. */
  .cats > :nth-child(2) .cat__frame img,
  .cats > :nth-child(3) .cat__frame img { object-position: 50% 0%; }
}

.cat__frame { aspect-ratio: 4 / 5; overflow: hidden; background: var(--bone); }
/* Biased up the frame, not centred. These are full-length garment shots with
   the head in the top fifth, and the desktop mosaic gives each tile an aspect
   ratio the photograph was never cut to — so cover always has height to throw
   away. Discarding it evenly takes the crown of the head off. Everything the
   tile is selling is in the upper half anyway: shoulder line, sleeve, neck. */
.cat__frame img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: 50% 20%;
  transition: transform 0.7s var(--ease);
}
.cat:hover .cat__frame img { transform: scale(1.03); }
.cat__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-xs);
  flex-wrap: wrap;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--bone);
  margin-block-start: 12px;
}
.cat__name { font-family: var(--font-display); font-size: var(--fs-h3); }

/* Featured — garments hung at different heights on a rail. The drop is halved
   at four columns so the density gain is not spent back on vertical space. */
.featured { --cols: 2; }
@media (min-width: 640px) { .featured { --cols: 3; } }
@media (min-width: 900px) {
  .featured > :nth-child(3n + 2) { margin-block-start: var(--sp-l); }
}
@media (min-width: 1100px) {
  .featured { --cols: 4; }
  .featured > :nth-child(3n + 2) { margin-block-start: 0; }
  .featured > :nth-child(4n + 2),
  .featured > :nth-child(4n + 4) { margin-block-start: var(--sp-m); }
}

.section__head {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: var(--sp-m);
  flex-wrap: wrap;
  margin-block-end: var(--sp-l);
}

/* Fabric & craft — ink ground, the one place brass becomes type */
.craft { display: grid; gap: var(--sp-l); }
@media (min-width: 800px) { .craft { grid-template-columns: 1fr 1fr; gap: clamp(var(--sp-l), 6vw, 6rem); align-items: center; } }
.craft__img img { aspect-ratio: 1 / 1; object-fit: cover; inline-size: 100%; }
.craft__list { margin: var(--sp-l) 0 0; padding: 0; }
.craft__list > div { padding-block: var(--sp-m); border-block-start: 1px solid rgb(242 239 234 / 0.16); }
.craft__list dt { font-size: var(--fs-util); font-weight: 600; letter-spacing: var(--track-util); text-transform: uppercase; color: var(--brass); margin-block-end: 6px; }
.craft__list dd { margin: 0; color: var(--bone); }

/* Voices — real customer messages only. Placeholder until permission exists. */
/* Follow */
.follow { display: flex; flex-wrap: wrap; gap: var(--sp-m); align-items: center; }

/* --- Editorial blocks ---------------------------------------------------
   Composition borrowed from the Kaira reference and rebuilt on these tokens:
   the oversized ghost word, the centred section head, the four-up assurance
   row, the overlapping split banner and the social strip. No carousels: every
   one of Kaira's swipers is a static grid here (§14). */

/* The ghost word. A section's own name set enormous in bone behind it, so the
   eyebrow and headline land on top of it. Decorative only — aria-hidden, never
   the accessible heading, and it must never be the only place a word appears. */
/* Deliberately no z-index on the siblings. The ghost is first in the DOM and
   sits at z-index 0; giving the siblings `position: relative` alone (z-index
   auto) is enough to paint them over it in tree order, and it leaves
   .eyebrow's own z-index 3 intact — without that the brass rail, at z-index 2,
   would paint straight over the eyebrow and the rail would stop breaking. */
.ghost-host {
  position: relative;
  /* A long word set at 15vw is wider than a 360px screen. `clip` on one axis
     only — `hidden` would force the other axis to auto and cut off the ghost's
     rise above the host, which is the whole effect. Safari below 16 drops the
     declaration and falls back on body's overflow-x. */
  overflow-x: clip;
  overflow-y: visible;
}
.ghost-host > *:not(.ghost) { position: relative; }

.ghost {
  position: absolute;
  inset-block-start: -0.2em;
  inset-inline-start: -0.06em;
  z-index: 0;
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(3rem, 15vw, 12rem);
  font-weight: 400;
  line-height: 0.8;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--bone);
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
}
/* Light grounds only. On ink the word has to fall to ~9% to stay a ghost, and
   at that strength it reads as a smudge, not a layer — so there is no .on-ink
   variant on purpose. On wash it needs pulling back, or bone-on-bone thickens. */
.on-wash .ghost { color: rgb(220 213 202 / 0.62); }

/* Centred head. Note it uses .util, not .eyebrow: the eyebrow's negative inline
   margin exists to reach the rail, and that geometry is meaningless centred. */
.section-head--center {
  text-align: center;
  margin-inline: auto;
  margin-block-end: var(--sp-xl);
  max-inline-size: 60ch;
}
.section-head--center .lead { margin-inline: auto; max-inline-size: 52ch; }
.section-head--center > * + * { margin-block-start: var(--sp-m); }

/* Four-up assurance row. The reference sells delivery promises; this sells the
   four things a stranger actually needs to believe before opening WhatsApp. */
.assure {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-l) clamp(var(--sp-m), 3vw, var(--sp-xl));
}
@media (min-width: 900px) { .assure { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.assure__icon {
  inline-size: 30px;
  block-size: 30px;
  margin-block-end: var(--sp-m);
  fill: none;
  stroke: var(--brass);
  stroke-width: 1.35;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.assure h3 {
  font-family: var(--font-body);
  font-size: var(--fs-util);
  font-weight: 600;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
  margin-block-end: var(--sp-xs);
}
.assure p { font-size: var(--fs-small); color: var(--muted); max-inline-size: 30ch; }

/* Split banner. Image and a panel that overlaps its trailing edge — the one
   place on the site where two blocks are allowed to overlap. */
.banner { display: grid; }
.banner__img { background: var(--bone); }
.banner__img img { inline-size: 100%; block-size: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
.banner__panel {
  background: var(--pearl);
  padding: clamp(var(--sp-l), 5vw, var(--sp-xl));
}
.banner__panel .lead { max-inline-size: 38ch; }
.banner__panel .btn { margin-block-start: var(--sp-l); }

@media (min-width: 880px) {
  .banner { grid-template-columns: 7fr 6fr; align-items: center; }
  .banner__img img { aspect-ratio: 5 / 4; }
  .banner__panel {
    position: relative;
    z-index: 1;
    margin-inline-start: calc(-1 * clamp(2rem, 6vw, 5.5rem));
  }
}

/* Social strip. Full-bleed, so it sits outside .wrap. Six square crops with the
   button straddling the bottom edge — ink on pearl reads over any tile. */
.social-strip { position: relative; }
.social-strip__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (min-width: 720px) { .social-strip__grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
.social-strip__tile { overflow: hidden; background: var(--bone); }
.social-strip__grid img { inline-size: 100%; aspect-ratio: 1; object-fit: cover; }
.social-strip__cta {
  position: absolute;
  inset-block-end: clamp(var(--sp-m), 4vw, var(--sp-l));
  inset-inline: 0;
  display: flex;
  justify-content: center;
  pointer-events: none;
}
.social-strip__cta .btn { pointer-events: auto; }

/* 10b. Content pages (about, contact) ------------------------------------- */

/* Prose column for the about page. Wider than .measure would allow at the lead,
   narrower than the full column, so a long read stays a single eye-sweep. */
.prose { max-inline-size: 62ch; }
.prose > * + * { margin-block-start: var(--sp-m); }
.prose h2 { margin-block-start: var(--sp-xl); }
.prose h2 + p { margin-block-start: var(--sp-m); }

/* Two-column read: a standing figure beside the text, image trailing. */
.split { display: grid; gap: var(--sp-l); }
@media (min-width: 860px) {
  .split { grid-template-columns: 6fr 5fr; gap: clamp(var(--sp-l), 6vw, 6rem); align-items: start; }
  .split__aside { position: sticky; inset-block-start: var(--sp-l); }
}
.split__aside img { inline-size: 100%; aspect-ratio: 4 / 5; object-fit: cover; background: var(--bone); }

/* Contact routes. Each one is a whole tile, not a link in a paragraph — the
   page has one job and every tile does it. */
.routes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: clamp(var(--sp-s), 2vw, var(--sp-m));
}
.route {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  padding: var(--sp-l);
  border: 1px solid var(--bone);
  background: var(--pearl);
  color: inherit;
  text-decoration: none;
  min-block-size: 100%;
  transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.route:hover { border-color: var(--ink); }
.route__name {
  font-family: var(--font-body);
  font-size: var(--fs-util);
  font-weight: 600;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
}
/* The email address is one long unbroken token — without this it pushes the
   tile wider than the column at 360px. */
.route__value { font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.15; overflow-wrap: anywhere; }
.route__note { font-size: var(--fs-small); color: var(--smoke); margin-block-start: auto; padding-block-start: var(--sp-s); }

/* Plain fact list — hours, address, how the sale actually happens. */
.facts { margin: 0; padding: 0; }
.facts > div { display: grid; gap: 2px; padding-block: var(--sp-m); border-block-start: 1px solid var(--bone); }
@media (min-width: 640px) { .facts > div { grid-template-columns: minmax(9rem, 12rem) 1fr; gap: var(--sp-m); } }
.facts dt { font-size: var(--fs-util); font-weight: 600; letter-spacing: var(--track-util); text-transform: uppercase; color: var(--muted); }
.facts dd { margin: 0; }

/* 11. Collection ---------------------------------------------------------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs) var(--sp-m);
  margin-block: var(--sp-l);
  padding-block-end: var(--sp-s);
  border-block-end: 1px solid var(--bone);
}
.filter {
  background: none;
  border: 0;
  border-block-end: 1px solid transparent;
  padding: 12px 0;
  min-block-size: 44px;
  font-size: var(--fs-util);
  font-weight: 500;
  letter-spacing: var(--track-util);
  text-transform: uppercase;
  color: var(--smoke);
  cursor: pointer;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.filter:hover { color: var(--ink); }
.filter[aria-pressed="true"] { color: var(--ink); font-weight: 600; border-block-end-color: var(--brass); }
.filter__count { font-size: 0.625rem; vertical-align: super; margin-inline-start: 3px; color: var(--smoke); }

.collection-grid { --cols: 2; }
@media (min-width: 640px) { .collection-grid { --cols: 3; } }
@media (min-width: 1100px) { .collection-grid { --cols: 4; } }

.grid-empty { padding-block: var(--sp-xl); color: var(--smoke); }

/* Loading -----------------------------------------------------------------
   Ported from Uiverse.io by PriyanshuGupta28. Retinted to ink, and the ten
   hand-written nth-child blocks collapsed into one rule driven by --i on the
   element — the same device the catalogue cards already use for their reveal
   stagger. Ten blades around a ring; a wave pushes each one outward in turn.

   It is used only where the page genuinely waits: the catalogue grids and the
   product page, while data/products.json is in flight. Everything else here is
   in the HTML already, and a spinner over content that is not loading is a
   lie. There is no full-page loading screen for the same reason — the site is
   static, and covering it would delay the one thing it has to do.

   No JS removes it: every container it sits in has its innerHTML replaced by
   the renderer, so the spinner is thrown away by the code that answers it.
   Under prefers-reduced-motion the global rule at the end of this file freezes
   the animation and it holds still as a mark. */
.loading {
  display: grid;
  place-items: center;
  min-block-size: 40vh;
}
.spinner {
  position: relative;
  inline-size: 9px;
  block-size: 9px;
}
.spinner i {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 50%;
  block-size: 150%;
  background: var(--ink);
  transform: rotate(calc(var(--i) * 36deg)) translate(0, 150%);
  animation: blade-pulse 1s calc(var(--i) * 0.1s) infinite ease;
}
.on-ink .spinner i { background: var(--pearl); }

@keyframes blade-pulse {
  0%, 40%, 60%, 100% { transform: rotate(calc(var(--i) * 36deg)) translate(0, 150%); }
  50% { transform: rotate(calc(var(--i) * 36deg)) translate(0, 225%); }
}

/* 12. Product page -------------------------------------------------------- */

.product { display: grid; gap: var(--sp-l); }
@media (min-width: 900px) {
  .product { grid-template-columns: 1.15fr 1fr; gap: clamp(var(--sp-l), 5vw, 5.5rem); align-items: start; }
  .product__info { position: sticky; inset-block-start: var(--sp-l); }
}

.gallery { display: grid; gap: var(--sp-xs); }
.gallery img { inline-size: 100%; aspect-ratio: 4 / 5; object-fit: cover; background: var(--bone); }

.product__title { font-size: var(--fs-h1); margin-block-end: var(--sp-s); }
.product__price {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3.5vw, 2rem);
  display: flex;
  align-items: baseline;
  gap: var(--sp-xs);
  margin-block-end: var(--sp-m);
}
.product__price span { font-family: var(--font-body); font-size: var(--fs-util); letter-spacing: var(--track-util); color: var(--smoke); }

.spec { margin: 0; padding: 0; }
.spec__row {
  display: grid;
  grid-template-columns: minmax(7rem, 9rem) 1fr;
  gap: var(--sp-s);
  padding-block: var(--sp-s);
  border-block-start: 1px solid var(--bone);
}
.spec__row dt { font-size: var(--fs-util); font-weight: 600; letter-spacing: var(--track-util); text-transform: uppercase; color: var(--smoke); }
.spec__row dd { margin: 0; }

/* Native radios, visually restyled: keyboard behaviour comes free. */
.sizes { margin: var(--sp-m) 0 var(--sp-xs); padding: 0; border: 0; }
.sizes legend { padding: 0; margin-block-end: var(--sp-xs); }
.sizes > label { margin-inline-end: var(--sp-xs); margin-block-start: var(--sp-xs); }
.size {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 54px;
  min-block-size: 46px;
  padding-inline: var(--sp-xs);
  border: 1px solid var(--bone);
  font-size: var(--fs-small);
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: border-color 0.2s var(--ease), background-color 0.2s var(--ease), color 0.2s var(--ease);
}
.size input {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  opacity: 0; margin: 0;
  pointer-events: none;
}
.size:hover { border-color: var(--smoke); }
.size:has(:focus-visible) { outline: 2px solid var(--brass); outline-offset: 3px; }
.size.is-picked { background: var(--ink); color: var(--pearl); border-color: var(--ink); }

.size-hint { font-size: var(--fs-small); color: var(--smoke); margin-block-end: var(--sp-l); min-block-size: 2.6em; }

/* Length chart — the size run is real information, so it is set as data. */
.lens { display: flex; flex-wrap: wrap; gap: 4px var(--sp-s); }
.len { font-size: var(--fs-small); white-space: nowrap; }
.len b { font-weight: 600; color: var(--smoke); margin-inline-end: 6px; }

.actions { display: grid; gap: var(--sp-xs); margin-block: var(--sp-l); }
.actions__note { font-size: var(--fs-small); color: var(--smoke); margin-block-start: var(--sp-xs); }

.related { --cols: 3; }
@media (max-width: 560px) { .related { --cols: 2; } }

.crumb { display: flex; gap: var(--sp-xs); align-items: center; margin-block-end: var(--sp-m); }
.crumb a { color: var(--smoke); text-decoration: none; }
.crumb a:hover { color: var(--ink); }

.state { padding-block: var(--sp-xl); }
.state h1 { font-size: var(--fs-h2); margin-block-end: var(--sp-s); }

/* 13. Footer -------------------------------------------------------------- */

.footer { margin-block-start: var(--sp-section); padding-block: var(--sp-xl) var(--sp-l); }
.footer__cols { display: grid; gap: var(--sp-l); }
@media (min-width: 640px) { .footer__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-l) var(--sp-xl); } }
/* The nav carries three links now, so the footer is where the rest of the site
   is reachable from. Four columns, brand column widest. */
@media (min-width: 980px) { .footer__cols { grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: var(--sp-xl); } }
.footer h2 { font-family: var(--font-body); font-size: var(--fs-util); font-weight: 600; letter-spacing: var(--track-util); text-transform: uppercase; color: var(--brass); margin-block-end: var(--sp-s); }
.footer ul { list-style: none; margin: 0; padding: 0; }
.footer li { margin-block-end: var(--sp-xs); }
.footer a { color: var(--bone); text-decoration: none; border-block-end: 1px solid transparent; }
.footer a:hover { border-block-end-color: var(--brass); }
.footer__legal {
  margin-block-start: var(--sp-xl);
  padding-block-start: var(--sp-m);
  border-block-start: 1px solid rgb(242 239 234 / 0.16);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs) var(--sp-l);
  color: var(--bone);
  font-size: var(--fs-small);
}

/* 14. Motion -------------------------------------------------------------- */
/* Fade-and-rise on scroll, ~60ms stagger. Nothing else (§14). */

.js .reveal {
  opacity: 0;
  transform: translateY(18px);
}
.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
  transition-delay: calc(var(--i, 0) * 60ms);
}

/* Hero headline fold (js/foldtext.js). The one page-load moment on the site:
   each word pivots down from a crease at its top edge, the way cloth falls
   open. Everything else stays fade-and-rise. */
.fold { perspective: 620px; }
.fold .fold-word {
  display: inline-block;
  transform-origin: 50% 0%;
  transform: rotateX(-92deg);
  opacity: 0;
}
.fold.is-folded .fold-word {
  transform: none;
  opacity: 1;
  transition: transform 0.78s var(--ease), opacity 0.5s var(--ease);
  transition-delay: calc(var(--i, 0) * 60ms);
}

@media (prefers-reduced-motion: reduce) {
  .fold .fold-word { transform: none; opacity: 1; transition: none; }
  .js .reveal, .js .reveal.is-in { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
