/* ==========================================================================
   Heaven Furniture Mart — page-level components
   ========================================================================== */

/* ==========================================================================
   HERO — a product on a stage, not a video band.

   What this replaced: the band used to be EXACTLY the aspect ratio of a
   background video (16/9, or 9/16 on portrait) so `object-fit: contain` could
   fit it edge-to-edge. That shape came from the file, not from the content, and
   on a short laptop window it pushed the CTA below the fold. The video is gone
   (see §8 in index.html and js/hero.js); the hero is now a GLB rendered on a
   transparent canvas over a cut-out still (js/hero3d.js).

   So the band is sized by its CONTENT instead: min-height in svh, which on
   mobile browsers is the *small* viewport — the height you get with the URL bar
   showing — so the composition never jumps when that bar retracts.
   ========================================================================== */
#hero {
  position: relative;
  overflow: hidden;
  background: var(--ink);
  min-height: min(92svh, 940px);
  display: grid;
  align-items: center;
  /* --dock-h ALONE is not enough. nav.css zeroes the body's top padding on
     data-nav="over-dark" pages so the bar can float over this section, which
     makes clearing it THIS element's job. The clamp is the breathing room that
     stops the eyebrow landing on the bar's hairline at 375px. */
  padding-block: calc(var(--dock-h, 62px) + clamp(1.25rem, 5vh, 3rem))
                 clamp(3rem, 9vh, 6rem);
}

/* The ghost year, sitting behind everything. --ghost-y is written by the
   parallax loop in js/home.js and is simply absent under reduced motion, where
   the fallback 0 leaves the word parked. */
.hero-ghost {
  position: absolute;
  left: 50%; top: 46%;
  transform: translate(-50%, -50%) translateY(var(--ghost-y, 0px));
  /* On ink the ghost has to be a LIGHTENED ink rather than --ghost (#E9E9E9),
     which would read as near-white type. #1E2029 is 1.11:1 on #111219 —
     texture, deliberately below the threshold where it competes with copy. */
  color: #1E2029;
  /* font-size comes from .ghost-type (--fs-ghost) — one ghost scale sitewide. */
  line-height: .8;
  white-space: nowrap;
  z-index: 0;
}

.hero-grid {
  position: relative;
  z-index: 1;
  display: grid;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
}
@media (min-width: 900px) {
  .hero-grid { grid-template-columns: minmax(0, 1fr) minmax(0, .92fr); }
}

.hero-body { position: relative; }
.hero-body h1 {
  font-size: var(--fs-hero);
  /* NOT 800. The re-skin's display voice is thin and wide, and this rule was
     the one place heavy enough to defeat the base h1 weight — leaving the
     site's largest headline in a weight nothing else uses. */
  font-weight: 200;
  line-height: .95;
  margin-bottom: 1rem;
  max-width: 20ch;
}
.hero-body .lead { max-width: 44ch; margin-bottom: 1.75rem; }
.hero-cta { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }

/* ---- The stage -----------------------------------------------------------
   Two rows: the product, then its caption. The still and the model share row 1
   so the swap is a cross-fade with no reflow — the same trick as the piece
   page's .stage — while the caption sits in row 2, clear of the silhouette and
   of the drag that turns the model. */
.hero-stage {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  isolation: isolate;
}
.hero-stage > .hero-still,
.hero-stage > model-viewer {
  grid-area: 1 / 1;
  width: 100%;
  /* height:auto is not redundant. model-viewer's own :host rule sets
     `width:300px; height:150px` like a bare <canvas>, and an explicit height
     beats aspect-ratio — so without this the model renders in a 150px-tall
     letterbox with the piece a thumbnail in the middle of it. */
  height: auto;
  aspect-ratio: 1 / 1;
  max-height: min(64svh, 620px);
  margin-inline: auto;
}
.hero-still {
  object-fit: contain;
  /* A cut-out with no ground reads as pasted on. The shadow is baked into the
     webp by scripts/make_cutouts.py; this only deepens it against ink. */
  filter: drop-shadow(0 28px 34px rgba(0, 0, 0, .55));
  transition: opacity var(--dur-xl) var(--ease);
}
.hero-stage > model-viewer {
  background: transparent;   /* the default — and what makes it a free cut-out */
  opacity: 0;
  transition: opacity var(--dur-xl) var(--ease);
  --poster-color: transparent;
}
.hero-stage > model-viewer.is-in { opacity: 1; }
/* data-mode="3d" is set on `load`, so the still is never hidden before there
   is something to hide it behind. A broken GLB deletes the attribute again. */
.hero-stage[data-mode="3d"] .hero-still { opacity: 0; }

.hero-note {
  grid-row: 2;
  justify-self: center;
  margin: 1rem 0 0;
  display: flex; align-items: center; gap: .6rem;
  font-size: var(--fs-small);
}
.hero-note span {
  font-family: var(--sans);
  font-weight: 700; font-size: .6875rem;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--primary-lite);
}
.hero-note .rule-link { padding: 0; font-size: var(--fs-small); }

/* ==========================================================================
   META STRIP — three facts on hairlines, closing the dark opening.
   ========================================================================== */
.meta-strip {
  padding-block: clamp(2rem, 5vw, 3.25rem);
  border-top: 1px solid rgba(255, 255, 255, .1);
}
.meta-grid {
  display: grid;
  gap: 1px;                                 /* the gap IS the hairline */
  background: rgba(255, 255, 255, .1);
}
@media (min-width: 720px) {
  .meta-grid { grid-template-columns: repeat(3, 1fr); }
}
.meta-cell {
  background: var(--ink);
  padding: 1.25rem clamp(1rem, 3vw, 2rem);
  display: grid; gap: .35rem;
  align-content: start;
}
.meta-cell b {
  font-family: var(--display);
  font-weight: 200;
  font-size: clamp(2.75rem, 7vw, 4.5rem);
  line-height: .9;
  color: var(--on-dark);
  font-variant-numeric: tabular-nums;
}
.meta-cell span {
  font-size: var(--fs-small);
  color: var(--on-dark-mute);
  max-width: 26ch;
}

/* ==========================================================================
   PIECE CARDS — hairlines, not boxes.

   The reference's grid cell has no container: a cut-out floats on the page, and
   the only structure is a rule above the cell and a rule above its foot. This
   block used to paint a filled --ink-2 rectangle with a border, which on a
   paper page read as a dark slab and was the single most visible symptom of the
   half-migrated skin.

   What replaces the box, and why each part is load-bearing:
     • a 1px --hair rule on TOP of every cell, so a 3-column grid reads as a
       ruled table rather than as floating scraps. Top rather than bottom: the
       last row then has no dangling rule under it, and the grid's own gap does
       not need compensating.
     • the cut-out over --product-bg (css/experience.css), which is paper with a
       barely-there studio vignette.
     • hover moves the RULE to red and lifts the piece, rather than changing a
       fill. With no box there is no fill to change, and the rule is the only
       thing the cell shares with its neighbours — so colouring it is what
       reads as "this cell".

   Contrast: the body text roles all flip to the on-light set. --clay
   (#A4888B) was correct on #111219 at 5.83:1 but is only 3.24:1 on paper, and
   both the eyebrow (12px/700) and the foot (15px/600) are under WCAG's
   large-text cutoff, so they owe the full 4.5:1. --on-light-fade is 7.69:1 and
   --on-light-mute is 5.57:1.
   ========================================================================== */
.piece-card {
  position: relative;
  display: flex; flex-direction: column;
  padding-top: 1.15rem;
  background: transparent;
  border: 0;
  border-top: 1px solid var(--hair);
  text-decoration: none;
  color: var(--on-light);
  transition: border-color var(--dur) var(--ease);
}
.piece-card:hover, .piece-card:focus-visible { border-top-color: var(--primary); }

.piece-figure {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  margin: 0;                    /* <figure> ships with 1em 40px in every UA */
}
/* `contain`, not `cover`: a cut-out is re-padded square with an 8% margin by
   scripts/make_cutouts.py, and cropping to fill would eat that margin and
   clip the silhouette's feet. The opaque fallback in js/card.js is a 4:3
   photograph, so it re-crops via [data-fallback] below. */
.piece-figure img {
  width: 100%; height: 100%;
  object-fit: contain;
  padding: 0;
  transition: transform .5s var(--ease);
}
.piece-figure[data-fallback="photo"] img { object-fit: cover; }
.piece-card:hover .piece-figure img { transform: translateY(-6px) scale(1.03); }

.piece-body { padding: 1.1rem .1rem 1.35rem; display: grid; gap: .35rem; }
/* Two lines, then ellipsis. Piece names run from "Emerald Bed" to "Ivory
   Marble Dining Set · Eight", and an unclamped 3-line title in one cell of a
   3-column grid drags every sibling's foot out of alignment. */
.piece-body h3 {
  margin: 0;
  font-size: clamp(1.15rem, 1.6vw, 1.35rem);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
/* Eyebrow with the reference's short leading rule, drawn rather than marked up
   so js/card.js stays free of presentational elements. */
.piece-cat {
  display: flex; align-items: center; gap: .55rem;
  font-size: .75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .12em; color: var(--on-light-fade);
}
.piece-cat::before {
  content: ""; flex: none; width: 18px; height: 2px; background: var(--primary);
}
.piece-tag { color: var(--on-light-mute); font-size: var(--fs-small); margin: 0; }

/* The cell foot: the reference's price row, carrying lead time instead. The
   label stays quiet and the value takes the ink, so the number is what the eye
   lands on when scanning a column of cells. */
.piece-foot {
  margin: .55rem 0 0; padding-top: .7rem;
  border-top: 1px solid var(--hair);
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  font-size: var(--fs-small);
}
.piece-foot-k {
  font-size: .75rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .1em; color: var(--on-light-mute);
}
.piece-foot-v { font-weight: 600; color: var(--on-light); font-variant-numeric: tabular-nums; }

/* AR marker on cards — the signal colour, doing its one job.

   White on the signal, not ink. This pairing is a leftover from the retired
   lime AR law: against #C6F24E, dark ink was the only legible choice. Folding
   AR into red flipped the ground from bright to dark but left the text behind,
   so the badge shipped ink-on-#D41929 — measured 3.53:1 at 11px/800, which owes
   4.5:1 and fails. --primary-ink on the same ground is 5.29:1. Same correction
   applies to .ar-inline below; they are the only two places the badge is drawn. */
.ar-flag {
  position: absolute; top: 10px; right: 10px;
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--ar-signal); color: var(--primary-ink);
  font-size: .6875rem; font-weight: 800; letter-spacing: .02em;
  padding: 5px 9px;
}
.ar-flag svg { width: 13px; height: 13px; }

/* Inline "AR" token used in body copy to point at the card badge. */
.ar-inline {
  display: inline-block; background: var(--ar-signal); color: var(--primary-ink);
  font-family: var(--sans); font-size: .75em; font-weight: 800;
  letter-spacing: .03em; padding: 1px 6px; vertical-align: .08em;
}

/* Count line under a category heading. */
.cat-count {
  margin-top: .75rem; font-size: var(--fs-small);
  font-weight: 600; color: var(--on-light-fade);
}

/* ==========================================================================
   PIECE PAGE — AR-first product layout
   ========================================================================== */
.piece-hero { display: grid; gap: clamp(1.5rem, 4vw, 3rem); padding-top: 2rem; }
@media (min-width: 900px) {
  .piece-hero { grid-template-columns: 1.15fr .85fr; align-items: start; }
}
.stage-wrap { position: relative; }
/* The stage is the piece page's hero: one square frame holding either the
   poster or the loaded model. Paper with a hairline now — the ground itself
   comes from --product-bg in css/experience.css, so the stage matches every
   grid cell on the site without repeating the gradient. --edge-dk is the
   hairline-on-light token; --edge is white rgba and is invisible here. */
.stage {
  position: relative;
  aspect-ratio: 1 / 1;
  border: var(--edge-dk);
  overflow: hidden;
}
.stage model-viewer,
.stage > img {
  width: 100%; height: 100%;
  --poster-color: transparent;
}
.stage > img { object-fit: cover; padding: 0; }
.stage-poster {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  cursor: pointer; border: 0; background: transparent; padding: 0;
  width: 100%; height: 100%;
}
.stage-poster img { width: 100%; height: 100%; object-fit: cover; padding: 0; }
/* js/piece-fill.js sets data-cut once it has swapped in the alpha cut-out. A
   cut-out must not be cropped to fill — its 8% margin is what stops the piece
   touching the frame — so it switches to `contain`. The fallback photograph
   keeps `cover` above. */
.stage[data-cut] .stage-poster img { object-fit: contain; }
/* "View in 3D" sits on top of a photograph of unknown lightness, so it cannot
   rely on the page's ground for contrast — it carries its own ink chip.
   White-on-ink is 17.4:1 whatever the photo does underneath. */
.stage-poster .play-3d {
  position: absolute; bottom: 14px; left: 14px;
  background: var(--ink); color: var(--on-dark);
  font-size: var(--fs-small); font-weight: 700;
  padding: 11px 18px;
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: 44px;
}
/* A poster with no model behind it must not look tappable. */
.stage-poster--static { cursor: default; }
.stage-poster--static:disabled { opacity: 1; }

.stage-note {
  margin-top: .6rem; font-size: .8125rem; color: var(--on-light-mute);
  min-height: 1.2em;
}
.ar-row { margin-top: 1rem; display: grid; gap: .75rem; }
/* `display:grid` outranks the hidden attribute, which silently kept the AR
   button on screen for pieces that have no model. Restore hidden's meaning. */
.ar-row[hidden] { display: none; }

/* Dimensions table — tabular, weighted, hairline ruled. */
.spec-table { width: 100%; border-collapse: collapse; margin: 0; }
.spec-table caption {
  text-align: left; font-size: var(--fs-eyebrow); font-weight: 700;
  text-transform: uppercase; letter-spacing: .12em; color: var(--on-light-fade);
  padding-bottom: .6rem;
}
.spec-table th, .spec-table td {
  text-align: left; padding: .7rem 0; border-bottom: 1px solid var(--hair);
  font-size: var(--fs-body);
}
/* The label is the quiet half and the measurement is the answer, so the weight
   and the ink both go to the value. --on-light-mute is 5.57:1 at 17px. */
.spec-table th { font-weight: 500; color: var(--on-light-mute); }
.spec-table td {
  font-weight: 600; color: var(--on-light);
  font-variant-numeric: tabular-nums; text-align: right;
}

/* Honest warning where AR scale is unverified. This is the one notice on the
   page a buyer must not skim past, so it keeps a filled ground — the quietest
   paper step, with the signal red carrying the left edge. */
.ar-caveat {
  margin-top: .9rem; padding: .8rem 1rem;
  background: var(--paper-2); border: 1px solid var(--hair);
  border-left: 3px solid var(--primary);
  font-size: .8125rem; color: var(--on-light-mute);
}

/* Fabric / finish swatches — a row of pressable pills, each with a colour dot. */
.swatches { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; padding: 0; list-style: none; }
.swatch {
  display: inline-flex; align-items: center; gap: .5rem;
  min-height: 48px; padding: 8px 14px 8px 10px;
  background: transparent; border: var(--edge-dk); color: var(--on-light);
  font-size: var(--fs-small); font-weight: 600; cursor: pointer;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
/* Same three states as .filter-bar .chip, for the same reasons: hover takes the
   red boundary, and pressed inverts to an ink pill so the current selection is
   marked by shape and weight rather than by colour alone. The old rule used
   --clay on hover (3.24:1 on paper) and a --bone fill edged in --bone, i.e. a
   white pill outlined in white on white. */
.swatch:hover { border-color: var(--primary); }
.swatch[aria-pressed="true"] {
  background: var(--on-light); border-color: var(--on-light); color: var(--paper);
}

/* The 20px colour dot. It shares the .chip class with the filter-bar chips
   (js/piece.js builds it as <span class="chip">), and .chip is a 48px-tall
   text control — so every one of its box declarations has to be undone here or
   the dot inherits them. min-height is the one that actually bites: it is
   applied AFTER height, so `height: 20px` alone loses and the dot renders 48px
   tall. padding is the second: with the global border-box, .chip's
   `padding: 11px 18px` is 36px of horizontal padding inside a 20px box, which
   floors the used width near 38px. Both are reset explicitly.

   The border is intentionally left dark-ish rather than --hair: this element's
   background is an arbitrary fabric colour set inline by js/piece.js:83, so a
   near-white edge disappears against pale swatches like Ivory (#EFE7D6). Black
   at 25% keeps a visible edge on every tint in the TINTS map. */
.swatch .chip {
  width: 20px; height: 20px; min-height: 0; flex: none;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, .25);
}

.matlist { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.matlist li {
  padding-left: 1.1rem; position: relative;
  font-size: var(--fs-small); color: var(--on-light-mute);
}
.matlist li::before {
  content: ""; position: absolute; left: 0; top: .62em;
  width: 7px; height: 7px; background: var(--primary);
}

/* ==========================================================================
   QR HANDOFF (desktop → phone)
   ========================================================================== */
.qr-card {
  display: grid; gap: 1rem; align-items: center;
  grid-template-columns: auto 1fr;
  background: var(--paper-2); border: var(--edge-dk); padding: 1.1rem;
}
/* The code keeps a white pad on any surface: QR decoders need the quiet zone to
   be the lightest thing in frame, so this padding is functional, not styling. */
.qr-card canvas, .qr-card img {
  width: 116px; height: 116px; background: #fff; padding: 7px;
}
.qr-card p { margin: 0; font-size: var(--fs-small); color: var(--on-light-mute); }
.qr-card strong { color: var(--on-light); display: block; font-size: var(--fs-body); }
@media (max-width: 767px) { .qr-card { display: none; } }

/* ==========================================================================
   AR COACHING OVERLAY
   ========================================================================== */
/* The one component that stays dark on purpose: it is a full-screen modal shown
   immediately before the camera opens, and dimming the room is what makes the
   handoff into AR feel continuous. Only the tint changed — rgba(23,16,15,.97)
   was oxblood-era warm and read brown against the new blue-black chrome. This
   is --ink's own value at the same opacity, so the modal, the hero and the
   floating pill are one family. */
.coach {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(17, 18, 25, .97);
  color: var(--on-dark);
  display: grid; place-items: center; padding: 1.5rem;
}
.coach[hidden] { display: none; }
/* A dark island in a light page has to say so: base.css only flips heading
   colour under .section-dark, and this is fixed-position outside any section. */
.coach h2, .coach h3, .coach h4 { color: var(--on-dark); }
.coach-inner { max-width: 420px; text-align: center; }
.coach-steps { display: grid; gap: 1.1rem; margin: 1.75rem 0; text-align: left; }
.coach-step {
  display: grid; grid-template-columns: 40px 1fr; gap: .9rem; align-items: start;
}
/* Third and last instance of the lime-era pairing (see .ar-flag above): the
   numeral inherits --fs-body at weight 800, which is 17px — under WCAG's
   18.66px-bold large-text cutoff — so it owes 4.5:1 and ink-on-#D41929 gives
   3.53:1. White is 5.29:1. */
.coach-num {
  width: 40px; height: 40px; display: grid; place-items: center;
  background: var(--ar-signal); color: var(--primary-ink); font-weight: 800;
}
.coach-step p { margin: 0; font-size: var(--fs-small); color: var(--on-dark-mute); }
.coach-step h4 { margin: 0 0 .15rem; }

/* ==========================================================================
   ROOM BOARD
   ========================================================================== */
.room-list { display: grid; gap: 1rem; list-style: none; margin: 0; padding: 0; }
.room-item {
  display: grid; grid-template-columns: 92px 1fr auto; gap: 1rem; align-items: center;
  background: transparent; border: var(--edge-dk); padding: .85rem;
}
/* Thumbnails here are the opaque 512 masters (room/index.html builds them), so
   `cover` is right — there is no alpha margin to protect. The ground still comes
   from --product-bg, so a cut-out source would also sit correctly if this list
   is ever switched over. */
.room-item img {
  width: 92px; height: 76px; object-fit: cover;
  padding: 0;
}
.room-item h3 { margin: 0 0 .2rem; font-size: 1.1rem; }
.room-item p { margin: 0; font-size: .8125rem; color: var(--on-light-mute); }
/* A destructive control, so its boundary owes 3:1 on its own under WCAG 1.4.11
   — --hair (1.44:1) is fine for a divider but not for the only thing outlining
   a remove button. --hair-strong is 3.23:1. */
.room-remove {
  min-width: 48px; min-height: 48px;
  background: transparent; border: 1px solid var(--hair-strong);
  color: var(--on-light-mute);
  cursor: pointer; font-size: 1.2rem;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.room-remove:hover { border-color: var(--primary); color: var(--primary-deep); }
.room-empty {
  border: 1px dashed var(--hair-strong); padding: 2.5rem 1.5rem; text-align: center;
  color: var(--on-light-mute);
}

/* ==========================================================================
   FORMS
   ========================================================================== */
/* Inputs are the clearest case for --hair-strong. WCAG 1.4.11 asks 3:1 of a
   control's boundary, and on a text field the boundary IS the affordance —
   there is no fill and no inside label saying "you can type here". #8F8F8F is
   3.23:1 on white; --hair (#D7D7D7) is 1.44:1 and leaves the field invisible
   until it is focused.

   The old `.section-light .field input` override is gone: it existed to patch a
   dark default back to white inside light bands, and now that paper IS the
   default there is nothing to patch. One rule, every surface. */
.field { display: grid; gap: .4rem; margin-bottom: 1.1rem; }
.field label { font-size: var(--fs-small); font-weight: 600; }
.field input, .field select, .field textarea {
  min-height: 52px; padding: 13px 15px;
  background: var(--paper); border: 1px solid var(--hair-strong); border-radius: 0;
  color: var(--on-light); font-family: var(--sans); font-size: var(--fs-body);
  transition: border-color var(--dur) var(--ease);
}
.field textarea { min-height: 130px; resize: vertical; }
/* :focus-visible already draws the 3px ring (base.css); recolouring the border
   too is what identifies WHICH field has it when several sit in a column. */
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--primary); outline: none;
}
.field-hint { font-size: .8125rem; color: var(--on-light-mute); }

/* ==========================================================================
   MISC PAGE BITS
   ========================================================================== */
.stat-row {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
}
.stat { border-left: 3px solid var(--primary); padding-left: 1rem; }
/* Weight 200 with positive tracking, matching the display law in base.css. The
   old 800 / -.03em was the retired system's voice and made these numerals the
   heaviest type on the pages that carry them. */
.stat b {
  display: block; font-family: var(--display); font-size: 2.4rem; font-weight: 200;
  line-height: 1; letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
}
.stat span { font-size: var(--fs-small); color: var(--on-light-mute); }

.step-list { counter-reset: s; list-style: none; margin: 0; padding: 0; display: grid; gap: 1.75rem; }
.step-list li { counter-increment: s; display: grid; grid-template-columns: 54px 1fr; gap: 1.1rem; }
/* Red is safe on this numeral where it would not be on body copy: 1.6rem at
   weight 200 is 25.6px, over WCAG's 24px large-text bar, so 4.37:1 clears the
   3:1 it owes. The ordering is also carried by document order, not by hue. */
.step-list li::before {
  content: counter(s, decimal-leading-zero);
  font-family: var(--display); font-size: 1.6rem; font-weight: 200;
  letter-spacing: .02em;
  color: var(--primary); line-height: 1;
}
.step-list h3 { margin: 0 0 .3rem; }
.step-list p { margin: 0; color: var(--on-light-mute); font-size: var(--fs-small); }

.filter-bar { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 2rem; }

/* The filter bar is the only TEXT .chip on the site and it lives on paper
   (collections/ is the sole page with a .filter-bar), so all three states are
   scored against white. Every colour in this block used to be a dark-surface
   token, and all three states were broken in a different way:

     rest    --on-dark-mute #A4888B — 5.83:1 on #111219, which is what it was
             picked for, but only 3.24:1 on white. A 15px/600 control owes 4.5:1.
     hover   --bone, which the re-skin aliased to --paper. Pointing at a chip
             painted white text on white and the control vanished.
     pressed --bone fill with a --bone border, i.e. a white pill edged in white
             on white — which is why the active filter read as unstyled body
             text with no affordance at all.

   All three states measured in-browser after the fix: rest #686868 at 5.57:1,
   hover #191919 with an #ED1C2E border at 17.58:1 / 4.37:1, pressed an inverted
   ink pill at 17.58:1. Hover matches the hair -> primary transition the
   rule-links use; the pressed pill is what the reference does with an active
   filter, and it marks the current state by shape and weight, not colour alone.
   --edge-dk is the hairline-on-light token; --edge is white rgba and was
   invisible here.

   The rest hairline is #D7D7D7 on white = 1.44:1, under the 3:1 that 1.4.11
   asks of a control boundary. Left as-is deliberately: 1.4.11 exempts a
   boundary that is not the only thing identifying the control, and the label
   itself carries 5.57:1. Raising it would mean changing --hair sitewide, and
   faint hairlines instead of card borders ARE the reference's design language.

   Safe for .swatch .chip (the 20px colour dot on piece pages): that one carries
   an inline style.background from js/piece.js:83, which outranks any stylesheet
   rule, it overrides this border at :316, and it holds no text so colour never
   applies to it. */
.chip {
  min-height: 48px; padding: 11px 18px;
  background: transparent; border: var(--edge-dk); color: var(--on-light-mute);
  font-family: var(--sans); font-size: var(--fs-small); font-weight: 600;
  cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center;
}
.chip:hover { border-color: var(--primary); color: var(--on-light); }
.chip[aria-pressed="true"], .chip[aria-current="page"] {
  background: var(--on-light); border-color: var(--on-light); color: var(--paper);
}

.media-split { display: grid; gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center; }
@media (min-width: 860px) { .media-split { grid-template-columns: 1fr 1fr; } }
/* Ground comes from --product-bg (css/experience.css); this rule only draws the
   edge. --edge-dk is the hairline-on-light token — --edge is white rgba and was
   invisible against paper, which is why these images looked unframed. */
.media-split img { border: var(--edge-dk); }
.media-split--flip img { order: 2; }

/* Small inline label — a piece of metadata, not a control. Hairline on paper. */
.pill {
  display: inline-flex; align-items: center; gap: .4rem;
  background: var(--paper-2); border: var(--edge-dk);
  padding: 7px 12px; font-size: .8125rem; font-weight: 600;
  color: var(--on-light);
}

/* Callout paragraph. Keeps a filled ground because its job is to interrupt the
   reading flow; the quietest paper step plus the signal red on the leading edge
   is enough to do that without becoming a card. */
.notice {
  background: var(--paper-2); border: 1px solid var(--hair);
  border-left: 3px solid var(--primary);
  padding: 1.1rem 1.25rem; font-size: var(--fs-small);
  color: var(--on-light);
}

/* Plain information card, used across the supporting pages (ar, materials,
   showroom, contact). Now hairline-ruled on paper for every surface — the old
   version was a dark --ink-2 fill with a `.section-light` override bolted on to
   turn it white again. With paper as the page default that override was doing
   the real work everywhere and the base rule was the exception, so the two have
   been collapsed into one honest declaration. */
.card-plain {
  border: var(--edge-dk); background: transparent;
  padding: 1.5rem 1.4rem;
  color: var(--on-light);
}
.card-plain h3 { margin: 0 0 .55rem; }
.card-plain p { margin: 0; font-size: var(--fs-small); color: var(--on-light-mute); }
.card-plain p + p { margin-top: .6rem; }
/* On the quieter paper band the card needs to stay distinguishable from its
   ground, so it takes pure white where the band is --paper-2. */
.section-light .card-plain { background: var(--paper); }

/* Stacked action group; guarded so `hidden` always wins over display. */
.stack-actions { display: grid; gap: .85rem; }
.stack-actions[hidden] { display: none; }

/* ==========================================================================
   §5 AR CAROUSEL — cut-outs breaking out of hairline rectangles.

   The frame is drawn BEHIND the piece and shorter than it, so the silhouette
   crosses the rule at the top. That overlap is the whole effect: it is what
   makes the product look placed on the page rather than photographed in a box.
   ========================================================================== */
.arc { padding-block: var(--section); overflow: hidden; }
.arc-head {
  display: grid;
  gap: 1.5rem;
  align-items: end;
  margin-bottom: clamp(2rem, 5vw, 3.5rem);
}
@media (min-width: 800px) {
  .arc-head { grid-template-columns: minmax(0, 1fr) auto; }
}
.arc-head h2 { margin: .4rem 0 .75rem; }
.arc-head .lead { max-width: 52ch; margin: 0; }

.arc-nav { display: flex; gap: .75rem; }
.arc-btn {
  width: 54px; height: 54px;
  border-radius: 50%;
  border: 0;
  background: var(--primary-deep);
  color: var(--primary-ink);
  display: grid; place-items: center;
  cursor: pointer;
  transition: background var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.arc-btn svg { width: 22px; height: 22px; }
.arc-btn:hover:not(:disabled) { background: var(--ar-signal-dp); transform: scale(1.06); }
/* Disabled is a real state here (the track has two ends), so it must be visible
   without relying on colour alone — the opacity change is paired with the
   cursor and with the button genuinely ceasing to respond. */
.arc-btn:disabled { opacity: .3; cursor: default; }

.arc-track {
  list-style: none;
  margin: 0;
  padding: 0 max(1.25rem, (100vw - var(--maxw)) / 2) 1rem;
  display: flex;
  gap: clamp(1.25rem, 3vw, 2.25rem);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  /* A focusable scroll container needs a visible ring of its own — keyboard
     users scroll this with the arrow keys. */
  outline-offset: 4px;
}
.arc-track::-webkit-scrollbar { display: none; }
.arc-slide {
  flex: 0 0 auto;
  width: min(78vw, 340px);
  scroll-snap-align: start;
  /* The track carries a gutter equal to the page margin so slide 1 lines up
     with the headline. Without a correction, "start" then snaps that slide to
     the SCROLLPORT edge — the track rests at scrollLeft: 100, clipping slide 1
     against the window and leaving the prev arrow live at rest.
     Outsetting each slide's snap area by the same gutter fixes the rest
     position. This is deliberately scroll-MARGIN on the slide rather than
     scroll-PADDING on the track: measured in this Chrome, any non-zero
     scroll-padding-inline on the track makes every programmatic
     scrollBy({behavior:'smooth'}) resolve to the position it started from, so
     the prev/next buttons silently stop working (a plain instant scroll still
     moves, which is what makes it so easy to miss). */
  scroll-margin-inline-start: max(1.25rem, (100vw - var(--maxw)) / 2);
  display: grid;   /* so the link below can stretch to the tallest slide */
}
/* Rows: frame · title · description(1fr) · meta. The 1fr absorbs the difference
   between a one-line and a three-line description, so the hairline and the AR
   chip land on the SAME baseline across every slide — the row of rules reads as
   one line across the carousel instead of a stagger. */
.arc-slide > a {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  text-decoration: none;
  color: var(--on-light);
}
.arc-frame {
  position: relative;
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  margin-bottom: 1.1rem;
}
/* The rectangle: inset from the top so the piece overshoots it. */
.arc-frame::before {
  content: "";
  position: absolute;
  inset: 18% 0 0;
  border: var(--edge-dk);
  background: var(--paper-2);
  transition: border-color var(--dur) var(--ease);
}
.arc-frame img {
  position: relative;
  width: 92%; height: 92%;
  object-fit: contain;
}
.arc-slide a:hover .arc-frame::before { border-color: var(--primary); }
.arc-slide a:hover .arc-frame img { transform: translateY(-8px) scale(1.02); }
.arc-frame img { transition: transform var(--dur-lg) var(--ease); }
.arc-slide h3 { margin: 0 0 .4rem; font-size: var(--fs-h3); }
.arc-tag {
  margin: 0 0 .8rem;
  font-size: var(--fs-small);
  color: var(--on-light-mute);
}
.arc-meta {
  margin: 0;
  padding-top: .7rem;
  border-top: var(--edge-dk);
  display: flex; align-items: center; gap: .6rem;
  font-size: var(--fs-small);
  color: var(--on-light-fade);
}
.arc-fallback { flex: 0 0 auto; padding-inline: 1.25rem; }

/* ==========================================================================
   §6 CATEGORY RAIL — one rule slides between six nodes.
   ========================================================================== */
.rail-sec { padding-block: clamp(2rem, 5vw, 3.5rem); border-block: var(--edge-dk); }
/* ONE row, always — it scrolls sideways rather than wrapping.
   Not a style preference: .rail-ind is absolutely positioned at bottom: 0 of
   this box, so the moment the six nodes wrap to a second line the rule lands
   under the LAST line no matter which node is hovered. Measured at 1440 with
   flex-wrap: wrap — hovering "All pieces" (row 1, bottom 209) drew the rule at
   270, sixty pixels adrift and floating beneath row 2; only the final row was
   ever right. Keeping the rail to a single line makes bottom: 0 correct by
   construction instead of needing a second custom property to chase the Y.
   It also matches the reference (one row of six) and makes the `+ scrollLeft`
   term in js/home.js's move() meaningful rather than dead code. */
.rail {
  position: relative;
  margin-top: 1.25rem;
  display: flex;
  flex-wrap: nowrap;
  gap: 0 clamp(1rem, 2.6vw, 2.25rem);
  overflow-x: auto;
  scrollbar-width: none;             /* the row is short; a bar would read as chrome */
  overscroll-behavior-x: contain;    /* no page-back gesture from the last node */
}
.rail::-webkit-scrollbar { display: none; }
.rail-node {
  position: relative;
  flex: 0 0 auto;                    /* never squeeze a label into two lines */
  white-space: nowrap;
  display: flex; align-items: baseline; gap: .5rem;
  padding: .75rem 0;
  text-decoration: none;
  color: var(--on-light);
  font-family: var(--display);
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: .05em;
  transition: color var(--dur) var(--ease);
}
.rail-node b { font-weight: 300; font-size: clamp(1.05rem, 1.55vw, 1.35rem); }
.rail-node i {
  font-style: normal;
  font-family: var(--sans);
  font-weight: 600;
  font-size: .75rem;
  letter-spacing: .04em;
  color: var(--on-light-fade);       /* 7.69:1 — small text, so not a hairline grey */
  font-variant-numeric: tabular-nums;
  transition: color var(--dur) var(--ease);
}
.rail-node:hover, .rail-node:hover i { color: var(--primary-deep); }
/* The sliding rule. Width and X arrive from js/home.js as custom properties, so
   the movement is one compositor transform rather than six transitions. Purely
   decorative: :hover already recolours the node, so a browser that never runs
   the JS loses nothing legible. */
.rail-ind {
  position: absolute;
  left: 0; bottom: 0;
  height: 2px;
  width: var(--ind-w, 0px);
  background: var(--primary);
  transform: translateX(var(--ind-x, 0px));
  opacity: 0;
  transition: transform var(--dur) var(--ease), width var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
  pointer-events: none;
}
.rail[data-ind="on"] .rail-ind { opacity: 1; }

/* ==========================================================================
   §8 WORKSHOP FILM — click-to-play, poster until asked.
   ========================================================================== */
.shop { position: relative; padding-block: var(--section); overflow: hidden; }
.shop-ghost {
  position: absolute;
  left: 50%; top: 6%;
  transform: translateX(-50%) translateY(var(--ghost-y, 0px));
  font-size: clamp(4rem, 15vw, 13rem);
  line-height: 1;
  white-space: nowrap;
  text-transform: uppercase;
  letter-spacing: .02em;
}
.shop-grid {
  position: relative;
  display: grid;
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: center;
}
@media (min-width: 900px) {
  .shop-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
}
.shop-body h2 { margin: .4rem 0 1rem; }
.shop-body p { color: var(--on-light-mute); }

.film {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--ink);
  border: var(--edge-dk);
  overflow: hidden;
  display: grid;
}
.film-poster, .film-video {
  grid-area: 1 / 1;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: opacity var(--dur-lg) var(--ease);
}
/* Until js/hero.js sets data-mode="play" the <video> has no src at all, so it
   would paint as a black rectangle over the poster. Hidden until it has
   something to show. */
.film .film-video { opacity: 0; }
.film[data-mode="play"] .film-video { opacity: 1; }
.film[data-mode="play"] .film-poster { opacity: 0; }

.film-play {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: clamp(64px, 9vw, 88px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--primary-deep);
  color: var(--primary-ink);
  display: grid; place-items: center;
  transition: background var(--dur) var(--ease), transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
}
.film-play svg { width: 34%; height: 34%; }
.film-play:hover { background: var(--ar-signal-dp); transform: translate(-50%, -50%) scale(1.07); }
/* One icon, two glyphs, swapped by state — no second request, no layout shift. */
.film-play .ic-pause { display: none; }
.film[data-playing="true"] .film-play .ic-play { display: none; }
.film[data-playing="true"] .film-play .ic-pause { display: block; }
/* Once it is running the control gets out of the way, and comes back on hover
   or keyboard focus so it is never unreachable. */
.film[data-playing="true"] .film-play { opacity: 0; }
.film[data-playing="true"]:hover .film-play,
.film[data-playing="true"] .film-play:focus-visible { opacity: 1; }

/* ==========================================================================
   §9 CONSULT BAND

   --primary-deep, NOT --primary. The copy in this band is body size and the
   labels are smaller still: white on #ED1C2E is 4.37:1 (a fail); on #D41929 it
   is 5.29:1. Same call as --ovmenu-bg.
   ========================================================================== */
.consult {
  background: var(--primary-deep);
  color: var(--primary-ink);
  padding-block: var(--section);
}
.consult-grid {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}
@media (min-width: 900px) {
  .consult-grid { grid-template-columns: minmax(0, 1fr) minmax(0, .85fr); }
}
.consult h2 { margin: .5rem 0 1rem; }
.consult .eyebrow { color: var(--primary-ink); }
/* The colour here is not redundant with .consult's own `color`. base.css:57 sets
   .lead's colour DIRECTLY, and a rule that targets the element always beats a
   value the element merely inherits — specificity never enters into it. So the
   band's white was overridden by --on-light-mute and this paragraph rendered
   #686868 on #D41929: measured 1.05:1, which is not "low contrast", it is
   invisible. base.css:119 already patches the same hole for .section-dark; a
   coloured band that is neither dark nor paper falls through that gap.

   Plain white (5.29:1) rather than a softened white: rgba(255,255,255,.9) —
   the obvious way to keep the lead below the heading — composites to 4.49:1 on
   this red and fails AA by a hair. Size and measure carry the hierarchy
   instead. */
.consult .lead {
  max-width: 44ch;
  margin-bottom: 1.25rem;
  color: var(--primary-ink);
}
/* rule-link is authored for paper; on red both its rest and hover colours have
   to come from the band's own text colour. */
.consult .rule-link { color: var(--primary-ink); }
.consult .rule-link::after { background: rgba(255, 255, 255, .45); }
.consult .rule-link:hover::after { background: var(--primary-ink); }

.ask { display: grid; gap: 1rem; }
.ask-field { display: grid; gap: .4rem; }
.ask-field label {
  font-family: var(--sans);
  font-weight: 700; font-size: .6875rem;
  text-transform: uppercase; letter-spacing: .14em;
}
.ask-field select, .ask-field input {
  font: inherit;
  font-size: var(--fs-body);
  color: var(--on-light);
  background: var(--paper);
  /* 1.4.11 wants 3:1 on a control's boundary. White on this red is 5.29:1, so
     the fill itself is the boundary and no extra border colour is needed. */
  border: 1px solid var(--paper);
  border-radius: 0;
  padding: .8rem .9rem;
  width: 100%;
}
.ask-field input::placeholder { color: var(--on-light-mute); }
.ask-field select:focus-visible, .ask-field input:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}
.ask-note {
  margin: 0;
  font-size: var(--fs-small);
  /* Plain white, not rgba(255,255,255,.9). The alpha version looks like the
     right way to sit this note below the button without a second grey, and it
     is the one value that just misses: composited over #D41929 it computes
     4.49:1 against a 4.5:1 requirement at 15px. Softening white on this red has
     no headroom — the band is only 5.29:1 to begin with. */
  color: var(--primary-ink);
  max-width: 42ch;
}

