/* ==========================================================================
   Layout — page skeleton, sections, and the collage geometry
   Reusable UI pieces live in components.css.
   ========================================================================== */

/* ── Primitives ─────────────────────────────────────────────────────────── */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wrap--narrow { max-width: var(--wrap-narrow); }
.wrap--hq     { max-width: var(--wrap-hq); }

.section { padding-block: clamp(var(--s-8), 11vw, var(--s-10)); }
.section--tight { padding-block: clamp(var(--s-7), 7vw, var(--s-8)); }

/* Scroll is locked for the whole loading sequence and while a drawer or modal
   is open. js/main.js owns this class; nothing else should touch overflow. */
body.is-locked {
  overflow: hidden;
  /* Preserve the scrollbar's width so locking does not shift the layout. */
  padding-right: var(--scrollbar-w, 0px);
}

/* ── Header ─────────────────────────────────────────────────────────────────
   Three-column grid so the wordmark stays optically centred regardless of how
   wide the left and right clusters get.

   Two states, and the whole bar inverts between them: solid ink over the ink
   hero, solid ivory once .is-stuck is set by the IntersectionObserver in
   js/nav.js. The divider line inverts with it — a black rule would be
   invisible on the black hero, so over the hero it is a light hairline and
   below it is ink. Something always marks where the header ends. The logo
   flips with the bar too; see .header__logo img. */

.header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--z-header);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s-4);
  min-height: var(--header-h);
  padding-inline: var(--gutter);
  color: var(--ivory);
  background: var(--ink);
  border-bottom: 1px solid color-mix(in srgb, var(--ivory) 26%, transparent);
  transition:
    background-color var(--t-base) var(--ease),
    border-color var(--t-base) var(--ease),
    color var(--t-base) var(--ease);
}

.header.is-stuck {
  color: var(--ink);
  background: var(--ivory);
  border-bottom-color: var(--ink);
}

.header__left,
.header__right {
  display: flex;
  align-items: center;
  gap: clamp(var(--s-2), 1.4vw, var(--s-4));
}

.header__right { justify-self: end; }

.header__center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15em;
  text-align: center;
}

/* The logo is portrait (artwork occupies 344x445 of a 500x500 canvas), so it
   is sized by HEIGHT and the width follows. The canvas carries transparent
   margin on all four sides — cropping the PNG to its bounding box would let it
   render ~15% larger in the same space. */
.header__logo {
  display: block;
  line-height: 0;
}

.header__logo img {
  height: clamp(46px, 5.5vw, 68px);
  width: auto;

  /* Toned for the bar it sits on — light over the ink hero, dark once the bar
     goes ivory — WITHOUT flattening the artwork.

     The mark is a fine greyscale engraving: its shape is in the alpha channel
     but all of its modelling is in RGB luminance (median 132, SD 58). An
     earlier version of this rule opened with brightness(0), which zeroes every
     RGB value and leaves only alpha — so invert() then produced one uniform
     tone and the whole panther became a flat white slab. Measured, that filter
     gave identical contrast at the 50th and 95th percentile of the mark: proof
     there was no internal detail left at all. Never reintroduce brightness(0)
     here.

     invert() is not optional for the dark state. The lines that define the
     engraving are its darkest ~10%, and unfiltered those sit at about 1.2:1
     against ink — they simply disappear. Inverting turns them into the
     brightest part of the mark, which is how an engraving is meant to read on
     a dark ground.

     The rest is tone mapping, picked by measuring the composite against each
     background rather than by eye:
       over ink    invert + a 15% lift  → median 5.0:1, detail SD 67
       over ivory  62% brightness, 1.2 contrast → median 6.9:1, detail SD 51
     (Source detail SD is 58, so both states keep the engraving intact.)

     Both states MUST list invert, brightness and contrast in that same order,
     even where a value is the identity. A filter transition only interpolates
     between matching function lists — mismatched ones jump discretely at the
     halfway point, which reads as a lag rather than a fade. */
  filter: invert(1) brightness(1.15) contrast(1);
  transition: filter var(--t-base) var(--ease);
}

.header.is-stuck .header__logo img {
  filter: invert(0) brightness(0.62) contrast(1.2);
}

/* Pages without a hero (FAQ, account, HQ) start stuck and need top padding. */
.page-offset { padding-block-start: var(--header-h); }

/* ── Loader ─────────────────────────────────────────────────────────────── */

.loader {
  position: fixed;
  inset: 0;
  z-index: var(--z-loader);
  display: grid;
  place-items: center;
  /* Ink, not the hero's velvet. js/loader.js sets this to transparent in the
     same frame it hands the collage back, so the cloth underneath is revealed
     at exactly that moment — the panel does not need to reproduce it, and a
     copy would be a second <img> of the page's largest asset. */
  background: var(--ink);
  transition: opacity var(--loader-fade-out) var(--ease);
}

.loader.is-done {
  opacity: 0;
  pointer-events: none;
}

/* ── Fabric ─────────────────────────────────────────────────────────────────
   The velvet ground, full-bleed behind the loader stage and the hero collage.
   Three stacked layers, cheapest first:

     .fabric__plate  the cloth itself, the only thing that moves
     .fabric__sheen  a soft highlight that follows the pointer
     .fabric__scrim  darkening, so the white polaroids stay readable

   The scrim is what makes "velvet behind a collage" work at all. Undimmed
   cloth and white frames compete: the fold highlights run right through the
   polaroid borders and the pile reads as subject rather than ground. Its
   strength was set by sampling the composited result, not by eye — see the
   note on the measured contrast in the README.                               */

.fabric {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* Fades up from the hero's ink once the loading sequence finishes. */
  transition: opacity var(--velvet-in) var(--ease);
  /* .hero__stage-inner and .loader__stage are position:relative with no
     z-index, so all three are positioned siblings at the same level and DOM
     order decides — the fabric is first, so it paints underneath. Stated
     explicitly because giving this a positive z-index, or reordering the
     partial, would bury the collage behind the cloth with no other symptom. */
  z-index: 0;
}

/* <picture> is an inline wrapper with nothing to contribute to layout — the
   plate inside it is absolutely positioned against .fabric. display:contents
   removes the wrapper box entirely rather than leaving an empty line box.
   Source selection is unaffected; that is the picture element's job, not its
   box's. */
.fabric picture { display: contents; }

/* Overscanned on the block axis so the drift in js/parallax.js never exposes
   an edge. Same trick .collection__media uses for its own drift. */
.fabric__plate {
  position: absolute;
  inset-block: -9%;
  inset-inline: 0;
  width: 100%;
  height: 118%;
  object-fit: cover;
  object-position: 50% 50%;
}

/* Pointer-tracked sheen. --mx/--my are written by js/parallax.js and default
   to the centre, so with no pointer (and with reduced motion, where the JS
   never runs) this is a still highlight rather than a missing one. */
/* Holds the cloth back while the polaroids drop, so it can bloom in behind them
   when the loader leaves. js/loader.js adds this class and js/loader.js removes
   it — it is never written in the markup, and every exit from the sequence
   (finished, skipped, reduced motion, or thrown) clears it. That asymmetry is
   the point: a page whose JavaScript never runs at all has no hold applied and
   shows the velvet immediately, which is the resting state this codebase
   requires. Never move this into the HTML. */
body.velvet-hold .fabric { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .fabric { transition: none; }
}

/* Dims the whole cloth to ink as the fly-through ends. js/parallax.js writes
   --velvet-dim; CSS only consumes it, the same split --hero-fade uses.

   As ::after it is generated after .fabric__scrim, so it paints over every
   layer including the blended sheen — the cloth resolves to flat ink rather
   than to a tinted version of itself. That is what makes the hand-off into the
   ivory collection below read as one movement instead of a cut from velvet. */
.fabric::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  opacity: var(--velvet-dim, 0);
  pointer-events: none;
}

.fabric__sheen {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    58% 46% at var(--mx, 50%) var(--my, 42%),
    rgb(255 226 212 / 0.16) 0%,
    rgb(255 226 212 / 0.06) 42%,
    rgb(255 226 212 / 0) 72%
  );
  mix-blend-mode: screen;
}

/* Tuned against measurement, not taste, and RE-tuned when the placeholder was
   replaced by photography — the two need very different amounts.

   The flat term is only 0.08 because this photograph is already dark: 60% of
   it sits below luminance 10 and its median is 7.3, so the cloth needs almost
   no help. The heavy 0.24 the synthetic plate wanted just muddied it, costing
   40% of the visible fold highlight for legibility that .hero__text::before
   was already providing locally. Measured over the composite, the wordmark
   sits at 11.7:1, the slogan 9.4:1 and the preorder line 5.1:1.

   The two dark ends of the gradient stay: they are what the header sits on at
   the top and the preorder line at the bottom. The near-transparent middle is
   the part meant to look like fabric.

   A brighter photograph would need the flat term back up. Re-measure; do not
   assume this number transfers. */
.fabric__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      to bottom,
      rgb(11 11 12 / 0.5) 0%,
      rgb(11 11 12 / 0.02) 34%,
      rgb(11 11 12 / 0.05) 62%,
      rgb(11 11 12 / 0.62) 100%
    ),
    rgb(11 11 12 / 0.08);
}

/* The stage carries the perspective. It deliberately does NOT carry
   transform-style: preserve-3d — the hero fade writes `opacity` here, and an
   element that is both preserve-3d and semi-transparent is forced back to
   flat, which would collapse the entire fly-through into a 2D slide. The
   perspective passes down to .frame through .collage's preserve-3d instead. */
.loader__stage,
.hero__stage-inner {
  /* Frame size is a fraction of THIS, not of the viewport. Sizing the frames
     in vw makes them shrink faster than the stage does, and the collage falls
     apart into a sparse row at middle widths. One variable keeps the
     composition proportionally identical at every size. */
  --stage-w: min(1180px, 94vw);
  position: relative;
  width: var(--stage-w);
  /* The third term is what keeps the two rows overlapping. Without it, a tall
     narrow window (a 768px-wide tablet in portrait) gives the stage its full
     720px of height while the frames stay small, and the rows drift 50px
     apart into a grid. Height follows width, so the composition holds. */
  height: min(78svh, 720px, calc(var(--stage-w) * 0.62));
  perspective: 1200px;
  perspective-origin: 50% 46%;
}

/* js/parallax.js writes --hero-fade; CSS only consumes it. */
.hero__stage-inner { opacity: var(--hero-fade, 1); }

/* Lives INSIDE .collage, so it shares the frames' preserve-3d context and the
   browser sorts it against them by real Z during the fly-through.

   Centring uses `translate`, NOT `transform` — js/parallax.js writes
   translate3d() to `transform` every frame, and centring there would be
   overwritten on the first tick. Same split the frames use.

   z-index sits above all 32 frames so the text wins while everything is
   coplanar at rest; once Z starts moving, depth order takes over. */
.hero__text {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 50%;
  translate: 0 -50%;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3em;
  text-align: center;
  pointer-events: none;
}

/* The collage is dense enough now that frames land directly behind the
   wordmark, and a light polaroid under ivory type is unreadable. This is a
   soft ink pool that travels with the text — it sits inside .hero__text's own
   stacking context, so it is above the collage but below the type.

   Lighter and wider than it was when the hero was flat black. At the old 0.94
   centre it reads as a distinct dark ellipse painted onto the cloth; the
   velvet is already dark enough to carry ivory type on its own, so this only
   has to cover the polaroids and then dissolve into the ground. */
.hero__text::before {
  content: "";
  position: absolute;
  inset: -280% -46%;
  z-index: -1;
  background: radial-gradient(
    ellipse at center,
    rgb(11 11 12 / 0.8) 0%,
    rgb(11 11 12 / 0.62) 34%,
    rgb(11 11 12 / 0.24) 56%,
    rgb(11 11 12 / 0) 76%
  );
  pointer-events: none;
}

/* Ivory on the ink hero. The header carries the logo now, so this no longer
   has to colour-match anything there. */
.hero__wordmark {
  font-family: var(--font-script);
  font-size: var(--fs-wordmark-hero);
  font-weight: var(--wt-normal);

  /* A connecting script must NOT be tracked. The letters join stroke to
     stroke, so the site-wide --track-display of -0.02em pulls them into a
     smear, and any positive value snaps the joins apart. This is the one place
     in the site that deliberately opts out of the display tracking, and it
     holds for any script face you swap in here. */
  letter-spacing: 0;

  /* Script ascenders and descenders run well past the em box — the swash on
     the capital L and the descender on the p both clip at line-height 1. */
  line-height: 1.28;

  color: var(--ivory);
  /* Deliberately allowed to wrap. "La Panthera Parlor" in a wide script is
     roughly 60% longer than the old two-word name; forcing nowrap on a narrow
     phone would either overflow the stage or drive the clamp down to a size
     the fine strokes cannot survive. Two lines is the better failure. */
  text-wrap: balance;
}

/* Blush rather than rouge: rouge on ink is about 1.9:1 and the slogan would
   effectively disappear. */
.hero__slogan {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: var(--wt-light);
  /* Always smaller and lighter than the wordmark — 0.2× its size. */
  font-size: clamp(0.8125rem, 1.9vw, 1.125rem);
  letter-spacing: var(--track-tight);
  line-height: 1.2;
  color: var(--blush);
  white-space: nowrap;
}

/* ── Collage ────────────────────────────────────────────────────────────────
   A scattered pile of polaroids whose overall silhouette is an oval. Every
   value below is generated — see assets/generate-collage.mjs for how the
   positions are sampled and why.

   Per-frame custom properties, all percentages of the stage:
     --fx / --fy  centre of the frame
     --fs         size multiplier, ~0.83…1.17 — frames are NOT all one size,
                  which is most of what stops the pile reading as machine-made
     --rot        resting angle (−8deg … +8deg)
     --drop-rot   angle it falls at, same sign as --rot
     --order      drop order — outermost first, centre last, so the pile
                  closes inward around the wordmark
     --z          stacking order, deliberately shuffled rather than DOM order

   Parallax depth is a `data-depth` attribute in the HTML rather than a custom
   property, because js/parallax.js reads it every frame and a dataset lookup
   is far cheaper than getComputedStyle.

   Each frame is two elements: .frame owns the static position (never
   animated) and .polaroid owns every animated transform. Keeping them apart
   means the drop, the resting angle, and the scroll parallax can compose
   without fighting over one transform property.                              */

.collage {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  z-index: 1;
}

.frame {
  /* Base size; --fs varies each frame around it. The scatter's minimum centre
     gap is ~113px at a 1180px stage, so at this width every frame overlaps a
     neighbour and the pile has no holes. Change this and re-check the gap the
     generator reports. */
  --pw: calc(var(--stage-w) * 0.13);
  position: absolute;
  inset-block-start: var(--fy);
  inset-inline-start: var(--fx);
  width: calc(var(--pw) * var(--fs, 1));
  /* Shuffled, so the pile does not read as laid down in a sweep. Frames are
     coplanar at rest, so this decides overlap; once the fly-through starts,
     translateZ takes over and sorts them by real depth instead. */
  z-index: var(--z, auto);
  translate: -50% -50%;      /* static centring, kept off `transform` */
  transform-style: preserve-3d;
}

/* White stock on the ink ground. The hairline is what separates one print
   from the print it overlaps — at this density adjacent frames would
   otherwise fuse into a single white mass. */
.polaroid {
  display: block;
  padding: 5% 5% 0;
  background: var(--ivory-warm);
  border: 1px solid var(--silk-deep);
  transform: rotate(var(--rot));
  transform-origin: 50% 50%;
  backface-visibility: hidden;
}

/* Thick bottom border is the polaroid tell. Drawn as padding on the media
   wrapper rather than a border so the ratio box stays exact. */
.polaroid__media {
  position: relative;
  aspect-ratio: 4 / 5;
  margin-block-end: 18%;
  overflow: hidden;
  background: var(--silk);   /* the 404 fallback — a frame with a solid fill */
}

.polaroid__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* An image that failed to decode is hidden, leaving the --silk block behind
   it. The frame still animates identically. */
.polaroid__media img[data-failed] { visibility: hidden; }

/* Geometry below is generated — edit assets/generate-collage.mjs, not this.
   --fx/--fy centre, --rot resting angle, --drop-rot fall angle, --order drop
   order (outermost first, centre last). Parallax depth is a data-depth
   attribute in the HTML: js/parallax.js reads it every frame and a dataset
   lookup is far cheaper than getComputedStyle. */
/* COLLAGE:START */
.frame:nth-child( 1) { --fx:  50.00%; --fy:  50.00%; --fs: 0.959; --rot:   5.16deg; --drop-rot:   11.03deg; --order: 31; --z: 25; }
.frame:nth-child( 2) { --fx:  57.11%; --fy:  66.22%; --fs: 0.978; --rot:  -1.74deg; --drop-rot:  -12.64deg; --order: 28; --z: 18; }
.frame:nth-child( 3) { --fx:  59.03%; --fy:  34.19%; --fs: 0.832; --rot:  -3.35deg; --drop-rot:  -13.84deg; --order: 26; --z: 11; }
.frame:nth-child( 4) { --fx:  37.09%; --fy:  71.47%; --fs: 1.097; --rot:   3.83deg; --drop-rot:   11.33deg; --order: 23; --z: 30; }
.frame:nth-child( 5) { --fx:  23.73%; --fy:  58.89%; --fs: 1.070; --rot:  -4.82deg; --drop-rot:  -11.88deg; --order: 20; --z:  4; }
.frame:nth-child( 6) { --fx:  63.27%; --fy:  51.53%; --fs: 0.974; --rot:  -4.84deg; --drop-rot:  -12.87deg; --order: 30; --z: 22; }
.frame:nth-child( 7) { --fx:  55.57%; --fy:   7.29%; --fs: 1.082; --rot:  -6.37deg; --drop-rot:  -10.15deg; --order:  4; --z: 17; }
.frame:nth-child( 8) { --fx:  35.00%; --fy:  38.44%; --fs: 0.939; --rot:  -3.29deg; --drop-rot:  -10.26deg; --order: 25; --z: 29; }
.frame:nth-child( 9) { --fx:  37.06%; --fy:  54.48%; --fs: 1.089; --rot:  -2.67deg; --drop-rot:  -13.52deg; --order: 29; --z: 15; }
.frame:nth-child(10) { --fx:  48.89%; --fy:  93.35%; --fs: 1.171; --rot:  -3.64deg; --drop-rot:  -13.33deg; --order:  3; --z: 14; }
.frame:nth-child(11) { --fx:  58.61%; --fy:  88.01%; --fs: 0.901; --rot:   6.96deg; --drop-rot:   12.29deg; --order:  9; --z: 21; }
.frame:nth-child(12) { --fx:  12.94%; --fy:  68.55%; --fs: 1.130; --rot:  -1.69deg; --drop-rot:  -12.01deg; --order:  7; --z:  3; }
.frame:nth-child(13) { --fx:  34.89%; --fy:  87.09%; --fs: 0.942; --rot:  -1.40deg; --drop-rot:  -11.65deg; --order:  8; --z:  1; }
.frame:nth-child(14) { --fx:  64.91%; --fy:  19.76%; --fs: 0.903; --rot:  -7.19deg; --drop-rot:  -11.85deg; --order: 16; --z: 32; }
.frame:nth-child(15) { --fx:  47.54%; --fy:  75.89%; --fs: 1.004; --rot:   7.75deg; --drop-rot:   13.05deg; --order: 21; --z: 28; }
.frame:nth-child(16) { --fx:  49.42%; --fy:  32.40%; --fs: 1.045; --rot:   1.73deg; --drop-rot:   12.30deg; --order: 27; --z:  6; }
.frame:nth-child(17) { --fx:  72.85%; --fy:  48.56%; --fs: 0.965; --rot:   2.08deg; --drop-rot:   13.29deg; --order: 24; --z: 13; }
.frame:nth-child(18) { --fx:  39.20%; --fy:  19.89%; --fs: 0.984; --rot:  -1.40deg; --drop-rot:  -13.93deg; --order: 18; --z: 26; }
.frame:nth-child(19) { --fx:  80.62%; --fy:  33.79%; --fs: 0.861; --rot:   1.40deg; --drop-rot:   11.63deg; --order: 15; --z: 23; }
.frame:nth-child(20) { --fx:  23.79%; --fy:  78.45%; --fs: 1.054; --rot:  -1.93deg; --drop-rot:  -10.96deg; --order: 12; --z: 20; }
.frame:nth-child(21) { --fx:  67.40%; --fy:  68.55%; --fs: 1.166; --rot:  -3.50deg; --drop-rot:  -12.26deg; --order: 22; --z: 27; }
.frame:nth-child(22) { --fx:  74.58%; --fy:  79.95%; --fs: 1.056; --rot:  -1.40deg; --drop-rot:  -10.56deg; --order: 10; --z:  5; }
.frame:nth-child(23) { --fx:  86.95%; --fy:  73.42%; --fs: 1.050; --rot:  -7.18deg; --drop-rot:  -12.76deg; --order:  6; --z: 19; }
.frame:nth-child(24) { --fx:  80.65%; --fy:  17.49%; --fs: 0.870; --rot:   1.40deg; --drop-rot:   10.87deg; --order:  1; --z:  7; }
.frame:nth-child(25) { --fx:  28.95%; --fy:  20.12%; --fs: 1.136; --rot:   2.34deg; --drop-rot:   10.78deg; --order: 13; --z: 16; }
.frame:nth-child(26) { --fx:  85.24%; --fy:  52.65%; --fs: 1.064; --rot:   6.19deg; --drop-rot:   13.66deg; --order: 14; --z:  2; }
.frame:nth-child(27) { --fx:   5.77%; --fy:  52.88%; --fs: 1.118; --rot:  -2.94deg; --drop-rot:  -12.61deg; --order:  5; --z: 24; }
.frame:nth-child(28) { --fx:  12.05%; --fy:  38.71%; --fs: 0.928; --rot:  -2.46deg; --drop-rot:  -10.85deg; --order: 11; --z: 31; }
.frame:nth-child(29) { --fx:  22.05%; --fy:  32.55%; --fs: 1.037; --rot:   2.51deg; --drop-rot:   13.68deg; --order: 17; --z:  8; }
.frame:nth-child(30) { --fx:  78.91%; --fy:  65.37%; --fs: 1.070; --rot:  -2.40deg; --drop-rot:  -12.15deg; --order: 19; --z:  9; }
.frame:nth-child(31) { --fx:  92.34%; --fy:  33.48%; --fs: 0.983; --rot:   5.69deg; --drop-rot:   10.38deg; --order:  2; --z: 10; }
.frame:nth-child(32) { --fx:  13.62%; --fy:  21.16%; --fs: 0.858; --rot:   3.91deg; --drop-rot:   10.27deg; --order:  0; --z: 12; }
/* COLLAGE:END */

/* ── Hero ───────────────────────────────────────────────────────────────────
   The stage is pinned for 120vh of scroll while the camera flies through the
   collage. Total height = 100svh viewport + 120vh of travel.                 */

.hero {
  position: relative;
  height: 220vh;
  background: var(--ink);
  z-index: var(--z-hero);
}

.hero__stage {
  position: sticky;
  inset-block-start: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* Bottom scrim, so the pre-launch line and the preorder link stay legible
   over whatever polaroid happens to land under them. Only in pre-launch mode
   — with no overlay there is nothing to protect. Sits above the collage
   (auto) and below .hero__overlay (4). */
body[data-mode="preorder"] .hero__stage::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: 34%;
  z-index: 3;
  background: linear-gradient(to top, rgb(11 11 12 / 0.88), rgb(11 11 12 / 0));
  pointer-events: none;
}

.hero__overlay {
  position: absolute;
  inset-block-end: clamp(var(--s-5), 4vh, var(--s-7));
  inset-inline: var(--gutter);
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--s-4);
  z-index: 4;
  color: var(--ivory);
  font-size: var(--fs-xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* The date line sits back; the preorder link is the only thing to act on. */
.hero__overlay p { color: var(--on-ink-muted); }

/* Pre-launch furniture. body[data-mode="live"] hides it with no markup edit. */
body:not([data-mode="preorder"]) .hero__overlay { display: none; }

/* ── Collection preview ─────────────────────────────────────────────────────
   Twelve-column grid where the card and the photo deliberately share columns
   5–7. The overlap is the composition; tidy adjacent halves are not. */

.collection {
  position: relative;
  background: var(--ivory);
  padding-block: clamp(var(--s-8), 10vw, var(--s-10));
  overflow: hidden;
}

.collection__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  align-items: center;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline-start: var(--gutter);
}

.collection__media {
  grid-area: 1 / 5 / 2 / -1;
  position: relative;
  aspect-ratio: 3 / 4;
  max-height: 86svh;
  overflow: hidden;          /* the clipping container the photo drifts inside */
  /* Bleed off the right edge of the viewport. */
  margin-inline-end: calc(-1 * var(--gutter));
}

.collection__media img {
  width: 100%;
  height: 118%;              /* headroom for the 0.85× parallax drift */
  object-fit: cover;
  position: absolute;
  inset-block-start: -9%;
  will-change: transform;
}

.collection__card {
  grid-area: 1 / 2 / 2 / 8;
  position: relative;
  z-index: 2;
  background: var(--ink);
  color: var(--ivory);
  padding: clamp(var(--s-6), 5vw, var(--s-8));
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  align-items: flex-start;
}

.collection__inset {
  position: absolute;
  inset-block-end: calc(-1 * var(--s-6));
  inset-inline-end: calc(-1 * var(--s-6));
  width: clamp(88px, 11vw, 150px);
  aspect-ratio: 1;
  border: 6px solid var(--ivory);
  z-index: 3;
}

.collection__inset img { width: 100%; height: 100%; object-fit: cover; }

/* ── Featured products ──────────────────────────────────────────────────── */

.featured__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  margin-block-end: clamp(var(--s-6), 5vw, var(--s-7));
  padding-block-end: var(--s-4);
  border-bottom: 1px solid var(--silk);
}

.product-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(var(--s-5), 3vw, var(--s-7));
}

/* ── Footer ─────────────────────────────────────────────────────────────── */

.footer {
  background: var(--ink);
  color: var(--ivory);
  padding-block: clamp(var(--s-7), 8vw, var(--s-9)) var(--s-6);
}

.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(var(--s-6), 5vw, var(--s-8));
  padding-block-end: clamp(var(--s-7), 6vw, var(--s-8));
  border-bottom: 1px solid var(--ink-line);
}

.footer__col {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  align-items: flex-start;
}

.footer__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-4);
  padding-block-start: var(--s-5);
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
}

/* ── FAQ ────────────────────────────────────────────────────────────────── */

.faq-banner {
  position: relative;
  aspect-ratio: 21 / 9;
  max-height: 42svh;
  overflow: hidden;
  background: var(--silk);
}

.faq-banner img { width: 100%; height: 100%; object-fit: cover; }

.faq-group + .faq-group { margin-block-start: clamp(var(--s-7), 7vw, var(--s-8)); }

.faq-group__title {
  font-family: var(--font-display);
  font-size: var(--fs-display-m);
  letter-spacing: var(--track-display);
  padding-block-end: var(--s-3);
  border-bottom: 1px solid var(--silk);
  margin-block-end: var(--s-2);
  scroll-margin-top: calc(var(--header-h) + var(--s-5));
}

/* ── Auth / account pages ───────────────────────────────────────────────── */

.account-layout {
  display: grid;
  grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
  gap: clamp(var(--s-6), 5vw, var(--s-8));
  align-items: start;
}

.account-nav {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  position: sticky;
  inset-block-start: calc(var(--header-h) + var(--s-5));
}

/* ── Responsive ─────────────────────────────────────────────────────────── */

@media (max-width: 1024px) {
  .collection__grid { grid-template-columns: repeat(8, 1fr); }
  .collection__media { grid-area: 1 / 3 / 2 / -1; }
  .collection__card { grid-area: 1 / 1 / 2 / 6; margin-inline-start: var(--gutter); }
}

@media (max-width: 767px) {
  /* Collage: same 8/7/8/7 grid, but the frames take a bigger share of a much
     narrower stage so they overlap heavily and read as dense texture rather
     than 30 postage stamps. The shorter stage keeps the four rows overlapping
     at phone heights. */
  .loader__stage,
  .hero__stage-inner {
    --stage-w: 100vw;
    height: min(62svh, 540px, calc(var(--stage-w) * 1.06));
  }
  .frame { --pw: calc(var(--stage-w) * 0.22); }

  .header { grid-template-columns: auto 1fr auto; }
  .header__center { align-items: center; }

  /* Collection: photo full-width, card stacked below and overlapping its
     bottom edge by ~40px. */
  .collection { padding-block-start: 0; }
  .collection__grid {
    grid-template-columns: 1fr;
    padding-inline: 0;
  }
  .collection__media {
    grid-area: auto;
    margin-inline: 0;
    aspect-ratio: 4 / 5;
    max-height: none;
  }
  .collection__card {
    grid-area: auto;
    margin: -40px var(--gutter) 0;
  }
  .collection__inset { display: none; }

  /* Featured: horizontal scroll-snap carousel. */
  .product-row {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 74%;
    gap: var(--s-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--gutter);
    padding-inline: var(--gutter);
    margin-inline: calc(-1 * var(--gutter));
    scrollbar-width: none;
  }
  .product-row::-webkit-scrollbar { display: none; }
  .product-card { scroll-snap-align: start; }

  /* Footer: stacked, newsletter first. */
  .footer__grid {
    grid-template-columns: 1fr;
    gap: var(--s-7);
  }
  .footer__col--news { order: -1; }
  .footer__legal { flex-direction: column; align-items: flex-start; }

  .account-layout { grid-template-columns: 1fr; }
  .account-nav {
    position: static;
    flex-direction: row;
    overflow-x: auto;
    gap: var(--s-3);
    padding-block-end: var(--s-3);
    border-bottom: 1px solid var(--silk);
  }
}

@media (max-width: 420px) {
  .hero__overlay {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-2);
  }
}

/* ── Collection listing ─────────────────────────────────────────────────── */

.collection-head {
  max-width: 62ch;
  margin-block-end: clamp(var(--s-6), 5vw, var(--s-8));
}

.collection-head .lede { margin-block: var(--s-4) var(--s-3); }

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: clamp(var(--s-5), 3vw, var(--s-7)) clamp(var(--s-4), 2vw, var(--s-6));
}

/* ── Product detail ─────────────────────────────────────────────────────────
   Gallery scrolls, panel sticks. Below 900px they stack and the panel
   un-sticks — a sticky panel taller than the viewport traps the page. */

.pdp {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(var(--s-5), 4vw, var(--s-8));
  align-items: start;
  padding-block: clamp(var(--s-6), 5vw, var(--s-8));
}

.pdp__gallery {
  display: grid;
  gap: var(--s-4);
}

.pdp__image {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  background: var(--silk);
}

.pdp__panel {
  position: sticky;
  inset-block-start: calc(var(--header-h) + var(--s-5));
}

@media (max-width: 899px) {
  .pdp { grid-template-columns: 1fr; }
  .pdp__panel { position: static; }
  .pdp__gallery {
    grid-auto-flow: column;
    grid-auto-columns: 82%;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    margin-inline: calc(-1 * var(--gutter));
    padding-inline: var(--gutter);
  }
  .pdp__gallery::-webkit-scrollbar { display: none; }
  .pdp__image { scroll-snap-align: center; }
}

/* ── Checkout ───────────────────────────────────────────────────────────── */

.checkout {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: clamp(var(--s-6), 5vw, var(--s-8));
  align-items: start;
}

.checkout__summary {
  position: sticky;
  inset-block-start: calc(var(--header-h) + var(--s-5));
  background: var(--bg-raised);
  border: 1px solid var(--line);
  padding: clamp(var(--s-5), 3vw, var(--s-6));
}

.checkout__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
}

.checkout__grid .field--wide { grid-column: 1 / -1; }

@media (max-width: 899px) {
  .checkout { grid-template-columns: 1fr; }
  .checkout__summary { position: static; order: -1; }
}

@media (max-width: 520px) {
  .checkout__grid { grid-template-columns: 1fr; }
}
