/* ==========================================================================
   Heaven Furniture Mart — base, type, buttons, shell
   Concept: hairlines, whitespace, weight contrast and one signal red.
   The page is PAPER by default; .section-dark is the exception.
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--on-light);
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, video, svg { max-width: 100%; height: auto; display: block; }

/* -deep, not --primary: a selection highlight sits under body copy, which
   needs 4.5:1, and #ED1C2E only reaches 4.37:1. */
::selection { background: var(--primary-deep); color: var(--primary-ink); }

/* ---- Type ---------------------------------------------------------------
   This INVERTS the previous system, which read: "real weight contrast, tight
   display tracking, uppercase reserved for eyebrows only" — headings at
   700-800 and letter-spacing -.02em.

   The new voice is the opposite on every axis: weight 200 instead of 800,
   POSITIVE tracking instead of negative, and uppercase promoted from
   exception to the rule for h1/h2/h3. Weight contrast still exists, it has
   just moved: it is now thin-display against bold-tiny-eyebrow, rather than
   bold-display against light-body. Deliberate reversal, not drift. ------- */

h1, h2, h3, h4 {
  font-family: var(--display);
  margin: 0 0 .5em;
  line-height: 1.02;
  text-transform: uppercase;
  text-wrap: balance;
}
/* Tracking scales inversely with size: the smaller the caps, the more air
   they need to stay countable as separate letters. */
h1 { font-size: var(--fs-h1); font-weight: 200; letter-spacing: .045em; }
h2 { font-size: var(--fs-h2); font-weight: 200; letter-spacing: .045em; }
h3 { font-size: var(--fs-h3); font-weight: 300; letter-spacing: .06em; line-height: 1.12; }
h4 { font-size: 1rem;         font-weight: 600; letter-spacing: .08em; line-height: 1.3; }

p { margin: 0 0 1.1em; max-width: var(--maxw-text); }
p:last-child { margin-bottom: 0; }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: var(--on-light-mute);
  max-width: 56ch;
}

/* The counterweight to the thin display type: small, heavy, wide, dark. */
.eyebrow {
  font-family: var(--sans);
  font-size: var(--fs-eyebrow);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--on-light);
  margin: 0 0 1rem;
  display: block;
}
/* -deep, not --brass/--primary: 13px is body-copy size and needs 4.5:1. */
.eyebrow--brass { color: var(--primary-deep); }
/* Eyebrow with the reference's short leading rule. */
.eyebrow--rule { display: flex; align-items: center; gap: .7rem; }
.eyebrow--rule::before {
  content: ""; flex: none; width: 26px; height: 2px; background: var(--primary);
}

a { color: inherit; }

/* Tabular figures for anything numeric — dimensions ARE content. */
.spec, .tabular, table { font-variant-numeric: tabular-nums; }

/* Giant low-contrast background type. Decorative by definition — 1.21:1 on
   paper — so it must never be the only carrier of a word that matters. */
.ghost-type {
  font-family: var(--display);
  font-weight: 200;
  font-size: var(--fs-ghost);
  line-height: .78;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--ghost);
  user-select: none;
  pointer-events: none;
}

/* ---- Layout ------------------------------------------------------------- */
.wrap { width: min(100% - 2.5rem, var(--maxw)); margin-inline: auto; }
.wrap--narrow { width: min(100% - 2.5rem, 860px); margin-inline: auto; }

section { padding-block: var(--section); }

/* ---- Surface bands -----------------------------------------------------
   .section-dark is the new exception (hero, meta strip, corridor, red band
   opt out separately). Everything inside it flips to the on-dark roles.

   .section-light predates the flip and appears in 15 places across 13 pages.
   It used to mean "the light one in a dark site"; it now means "the quieter
   paper band", so those pages keep working and gain a subtle alternation
   instead of a jarring inversion. -------------------------------------- */
.section-light { background: var(--paper-2); color: var(--on-light); }

.section-dark { background: var(--ink); color: var(--on-dark); }
.section-dark .lead { color: var(--on-dark-mute); }
.section-dark .eyebrow { color: var(--on-dark); }
/* THE COACH OVERLAY NEEDED NAMING SEPARATELY. It is a dark surface but it is not
   a .section-dark, so it never matched the selector below and kept
   --primary-deep — measured 3.53:1 on its own ground, which FAILS AA for small
   text. --primary-deep is the token for small red text on PAPER; --primary-lite is
   the one for small red text on dark, exactly as documented in README.md. Same
   class of miss as .site-foot needing to be named in js/shell.js's dark-band list. */
.coach .eyebrow--brass { color: var(--primary-lite); }        /* 5.07:1, was 3.53:1 */
.section-dark .eyebrow--brass { color: var(--primary-lite); }  /* 5.04:1 on ink.
                                                             --primary is 4.33:1
                                                             here, and 13px/700 is
                                                             NOT WCAG large text
                                                             (that needs 18.66px
                                                             bold), so this owes
                                                             the full 4.5:1 */
.section-dark .hairline { background: rgba(255, 255, 255, .14); }
.section-dark h1, .section-dark h2, .section-dark h3 { color: var(--on-dark); }

.stack > * + * { margin-top: var(--gap); }

.grid { display: grid; gap: var(--gap); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); }

/* Skip link — accessibility. The fill is now dark red, so the label is
   white; the old skin's lime took dark text. */
.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--ar-signal); color: var(--primary-ink);
  padding: 12px 20px; font-weight: 700; z-index: 100;
}
.skip:focus { left: 0; }

/* Visible focus everywhere. #D41929 is 5.29:1 on paper and 3.53:1 on --ink,
   so one outline colour clears the 3:1 non-text bar on both surfaces. */
:focus-visible {
  outline: 3px solid var(--ar-signal);
  outline-offset: 3px;
}

/* ==========================================================================
   BUTTONS
   ========================================================================== */

/* ---- Primary: the "AntiMetal" pill --------------------------------------
   A dark pill with a signal-red accent chip pinned to its left edge. On hover
   the chip sweeps across the full width and the pill lifts. Ported by hand from
   a React/Tailwind component — this codebase has no build step, so the port is
   CSS custom properties rather than utility classes.

   THIS REPLACES THE "TENON" (face sliding into its own offset shadow block).
   That mechanic is on the project's motion-preservation checklist; retiring it
   was an explicit decision, recorded here so it is not read as drift. The AR
   viewfinder brackets went with it — they lived on the same pseudo-elements the
   accent chip now uses, and the dot wave is the continuous animation in their
   place.

   WHY THE ACCENT IS A BACKGROUND LAYER AND NOT A PSEUDO-ELEMENT:
   the note further down this file records a real bug — many .btn labels are raw
   text nodes, and an absolutely-positioned pseudo-element paints straight over
   them. The reference component dodges this by wrapping its label in a <span>;
   our markup cannot, across 71 instances. Animating background-size sweeps the
   panel with the label always on top, needs no markup change, and cannot
   regress that bug. `overflow: hidden` clips the layer to the pill's radius so
   the chip's left corners follow the curve instead of poking through it.

   The two pseudo-elements are then free for the dot field, which is confined to
   the chip and never reaches the label. */
.btn {
  --btn-body: var(--ink);
  --btn-fg:   var(--on-dark);
  --btn-dot:  rgba(255, 255, 255, .92);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;   /* label sits to the right of the chip */
  gap: .55rem;
  min-height: var(--btn-h);
  /* Left padding clears the chip so the label never sits on the accent. */
  padding: 13px 24px 13px calc(var(--btn-chip-w) + 1.15rem);
  border: 0;
  border-radius: var(--btn-r);
  overflow: hidden;
  font-family: var(--sans);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .01em;
  text-align: right;
  text-decoration: none;
  text-transform: none;        /* sentence case */
  cursor: pointer;
  color: var(--btn-fg);
  background-color: var(--btn-body);
  background-image: linear-gradient(180deg, var(--btn-accent) 0%, var(--btn-accent-deep) 100%);
  background-repeat: no-repeat;
  background-position: 0 50%;
  background-size: var(--btn-chip-w) 100%;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08), 0 4px 12px rgba(17, 18, 25, .18);
  --mag-x: 0px;   /* magnetic lean toward the cursor (js/magnetic.js) */
  --mag-y: 0px;
  transform: translate(var(--mag-x), var(--mag-y));
  transition: background-size var(--dur) var(--ease-panel),
              transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              color var(--dur) var(--ease);
}

/* ---- The dot field -----------------------------------------------------
   The reference draws five double-chevrons of ten dots each as inline SVG, with
   a per-dot staggered opacity wave. Fifty elements per button is not available
   here without editing markup in 73 places, so it is two radial-gradient dot
   layers offset from each other and pulsed out of phase.

   TUNED DOWN after looking at it rendered. The first pass used .9px dots on a
   5px grid, which at chip size merged into a dense crosshatch — it read as a
   patch of woven red fabric rather than a field of dots. Now .75px dots on an
   8px grid: roughly a third of the coverage, so individual dots are legible and
   the chip reads as a signal rather than a texture swatch. The two layers are
   offset by half a cell (4px) so together they form a diagonal lattice, which is
   what suggests the reference's chevrons.

   Both layers are pinned to the chip and pointer-events:none, so they never
   reach the label and never intercept a click — which is what keeps the
   "positioned pseudo-element eats the raw text label" bug from returning. */
.btn::before,
.btn::after {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: var(--btn-chip-w);
  pointer-events: none;
  background-image: radial-gradient(circle, var(--btn-dot) .75px, transparent 1.15px);
  background-size: 8px 8px;
  animation: btn-dots 1.4s var(--ease-io) infinite;
}
.btn::before { background-position: 5px 4px;  animation-delay: 0s; }
.btn::after  { background-position: 9px 8px;  animation-delay: .16s; }

@keyframes btn-dots {
  0%, 70%, 100% { opacity: .3; }
  35%           { opacity: 1; }
}

/* Hover: the chip sweeps to full width and the pill lifts. The transform MUST
   keep var(--mag-x)/var(--mag-y) in it — js/magnetic.js writes those on
   mousemove, and dropping them from any state silently kills the magnetic lean
   for the whole hover duration, which is when it matters. */
.btn:hover,
.btn:focus-visible {
  background-size: 100% 100%;
  transform: translate(var(--mag-x), calc(var(--mag-y) - var(--btn-lift)));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 10px 22px rgba(17, 18, 25, .26);
}
.btn:active {
  transform: translate(var(--mag-x), calc(var(--mag-y) - var(--btn-lift))) scale(.98);
}

/* Light-bodied variant: paper pill, ink label, same red accent. Used on the
   home grid and inside the red consult band, where a dark pill would read as a
   hole punched in the band. */
.btn--bone {
  --btn-body: var(--paper);
  --btn-fg:   var(--on-light);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9), 0 4px 12px rgba(17, 18, 25, .22);
}
/* On hover the red panel covers the whole pill, so an ink label would land on
   #D41929 at 3.53:1. The label flips to white (5.29:1) exactly when the ground
   beneath it does. */
.btn--bone:hover,
.btn--bone:focus-visible { color: var(--primary-ink); }

/* ---- Secondary: the ringed outline ------------------------------------
   Ported from the second reference: a transparent face with a 1px border and a
   3px inset ring in the same colour, so the edge reads as one thick rule, and a
   straight lift on hover. No accent panel — that is what separates it from the
   primary at a glance. */
.btn--ghost {
  --btn-body: transparent;
  --btn-fg: var(--on-light);
  background-color: transparent;
  background-image: none;
  border: 1px solid var(--on-light);
  border-radius: var(--btn-r-sm);
  box-shadow: inset 0 0 0 3px var(--on-light);
  color: var(--on-light);
  font-weight: 700;
  padding: 13px 26px;
  text-align: center;
  justify-content: center;
}
/* No chip, so no dots. */
.btn--ghost::before, .btn--ghost::after { content: none; }
.btn--ghost:hover,
.btn--ghost:focus-visible {
  background-color: var(--on-light);
  color: var(--paper);
  transform: translate(var(--mag-x), calc(var(--mag-y) - var(--btn-lift)));
  box-shadow: inset 0 0 0 3px var(--on-light);
}
.section-dark .btn--ghost {
  --btn-fg: var(--on-dark);
  border-color: var(--on-dark);
  box-shadow: inset 0 0 0 3px var(--on-dark);
  color: var(--on-dark);
}
.section-dark .btn--ghost:hover,
.section-dark .btn--ghost:focus-visible {
  background-color: var(--paper);
  color: var(--on-light);
  box-shadow: inset 0 0 0 3px var(--paper);
}

.btn--sm {
  min-height: 48px;
  padding: 11px 20px 11px calc(var(--btn-chip-w) + .9rem);
  font-size: var(--fs-small);
}
.btn--sm.btn--ghost { padding: 11px 20px; }

/* Reduced motion: the state change survives, the movement does not. The accent
   panel still sweeps to full width — that is the affordance telling you the
   control is live, not a flourish — but it arrives instantly, the pill does not
   translate or scale, the magnetic lean is off, and the dot field holds still at
   full opacity rather than pulsing. */
@media (prefers-reduced-motion: reduce) {
  .btn { --mag-x: 0px !important; --mag-y: 0px !important; }
  .btn::before, .btn::after { animation: none; opacity: 1; }
  .btn:hover, .btn:focus-visible,
  .btn:active { transform: none; }
  .btn--bone:hover, .btn--bone:focus-visible { color: var(--primary-ink); }
}

/* ---- Secondary: the "Rule" link ---------------------------------------
   No box. A 2px baseline rule inset from both ends that draws out to full
   width on hover, and goes red as it lands. --hair would be 1.44:1 here —
   correct for a divider, far too faint for the only affordance on a link. */
.rule-link {
  position: relative;
  display: inline-block;
  padding: 6px 0 10px;
  font-weight: 600;
  text-decoration: none;
  color: inherit;
  min-height: 44px;
}
.rule-link::after {
  content: "";
  position: absolute;
  left: 12%; right: 12%; bottom: 4px;
  height: 2px;
  background: var(--on-light);
  transition: left var(--dur) var(--ease), right var(--dur) var(--ease),
              background var(--dur) var(--ease);
}
.rule-link:hover::after,
.rule-link:focus-visible::after { left: 0; right: 0; background: var(--primary); }
.section-dark .rule-link::after { background: rgba(255, 255, 255, .55); }
.section-dark .rule-link:hover::after { background: var(--primary); }

/* ==========================================================================
   THE AR BUTTON — the same AntiMetal pill as .btn, turned up.

   It is still the loudest control on the site, but the volume now comes from
   size, weight and the accent sweep rather than from the four viewfinder
   brackets, which are RETIRED along with the Tenon. They lived on the exact
   pseudo-elements the accent chip and dot field now need, and stacking a
   breathing bracket outline on top of a sweeping accent panel reads as two
   competing animations rather than one confident affordance.

   Both retired behaviours ("Tenon offset-shadow button", "AR viewfinder
   breathe") are on the project's motion-preservation checklist. Replacing them
   was an explicit decision, not drift.

   .btn-ar is NOT a modifier of .btn — the two classes are never combined in
   markup, so this block repeats the pill rather than inheriting it. What it adds
   is a wider chip, a heavier label, and its own hover ramp.
   ========================================================================== */
.btn-ar {
  --btn-body: var(--ink);
  --btn-fg:   var(--on-dark);
  --btn-dot:  rgba(255, 255, 255, .95);
  --btn-chip-w: 44px;              /* wider than .btn: more dots, more signal */
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: .65rem;
  min-height: 56px;
  padding: 15px 26px 15px calc(var(--btn-chip-w) + 1.15rem);
  border: 0;
  border-radius: var(--btn-r);
  overflow: hidden;
  font-family: var(--sans);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: .01em;
  text-align: right;
  text-decoration: none;
  cursor: pointer;
  color: var(--btn-fg);
  background-color: var(--btn-body);
  background-image: linear-gradient(180deg, var(--btn-accent) 0%, var(--btn-accent-deep) 100%);
  background-repeat: no-repeat;
  background-position: 0 50%;
  background-size: var(--btn-chip-w) 100%;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 4px 14px rgba(17, 18, 25, .22);
  --mag-x: 0px;
  --mag-y: 0px;
  transform: translate(var(--mag-x), var(--mag-y));
  transition: background-size var(--dur) var(--ease-panel),
              transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
/* The dot field. .btn-ar keeps its <span class="br-b"> from the old bracket
   markup — 37 instances across 18 piece pages plus the hero — so instead of
   editing all of them the span is repurposed as the second dot layer. The
   element's own ::before is the first. Nothing in the HTML changed.

   Same tuning as .btn above (.75px dots on an 8px grid, not .9px on 5px): the
   denser first pass merged into a crosshatch that read as woven fabric. The
   44px chip here gets one more column than .btn's 36px, which is the point of
   it being wider. */
.btn-ar::before,
.btn-ar > .br-b {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: var(--btn-chip-w);
  inset-inline-end: auto;
  pointer-events: none;
  background-image: radial-gradient(circle, var(--btn-dot) .8px, transparent 1.2px);
  background-size: 8px 8px;
  animation: btn-dots 1.4s var(--ease-io) infinite;
}
.btn-ar::before      { background-position: 6px 4px; animation-delay: 0s; }
.btn-ar > .br-b      { background-position: 10px 8px; animation-delay: .16s; }
/* The brackets these pseudo-elements used to draw are gone; kill them
   explicitly so a stale cached stylesheet cannot leave half of each outline
   floating over the new chip. */
.btn-ar > .br-b::before,
.btn-ar > .br-b::after,
.btn-ar::after { content: none; }

.btn-ar:hover,
.btn-ar:focus-visible {
  background-size: 100% 100%;
  transform: translate(var(--mag-x), calc(var(--mag-y) - var(--btn-lift)));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 12px 26px rgba(17, 18, 25, .3);
}
.btn-ar:active {
  transform: translate(var(--mag-x), calc(var(--mag-y) - var(--btn-lift))) scale(.98);
}
@media (prefers-reduced-motion: reduce) {
  .btn-ar { --mag-x: 0px !important; --mag-y: 0px !important; }
  .btn-ar::before, .btn-ar > .br-b { animation: none; opacity: 1; }
  .btn-ar:hover, .btn-ar:focus-visible, .btn-ar:active { transform: none; }
}

.btn-ar svg { width: 20px; height: 20px; flex: none; }

.btn-ar--block { width: 100%; }

/* ==========================================================================
   MOBILE BOTTOM DOCK
   The site's only header is .pill-head (css/nav.css, built by js/chrome.js);
   the .site-head / .menu / .menu-btn / .nav block that used to live here was
   removed in the re-skin after grep confirmed zero call sites in any of the
   33 HTML files or any module — it had been unreachable dead code.
   ========================================================================== */
.dock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-dock);
  display: none;
  /* PINNED, so the token and the rendered box cannot drift.
     --dock-bar-h is the MOBILE DOCK. It is not --dock-h, which despite the name
     is the desktop header's height (css/nav.css:22) — the two were easy to
     confuse and one of them was wrong.
     The body's reserved padding below was a hardcoded 64px while the dock
     actually rendered 80px: `.dock a` is min-height 60 + 16px padding + the 1px
     border, and two lines of label push past the minimum. Measured 80px at 375px
     wide, so the reservation was 16px short — and short by more again on a
     notched phone, because the dock adds a safe-area inset and the body padding
     did not. Both now read the same token. */
  min-height: var(--dock-bar-h);
  background: rgba(17, 18, 25, .96);
  border-top: var(--edge);
  padding-bottom: env(safe-area-inset-bottom);
}
/* Four slots now that the cart is here. `1fr` each rather than the old
   `1fr 1.3fr 1fr` that gave AR extra room: at 375px four equal columns are 90px,
   and weighting one of them pushes another under the 24px WCAG 2.5.8 target. */
.dock-inner { display: grid; grid-template-columns: repeat(4, 1fr); }
.dock a {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; min-height: 60px; padding: 8px 4px;
  font-size: .75rem; font-weight: 600; text-decoration: none;
  color: var(--on-dark-mute);
}
.dock a svg { width: 20px; height: 20px; }
.dock a.is-ar { background: var(--ar-signal); color: var(--primary-ink); font-weight: 800; }
@media (max-width: 767px) {
  .dock { display: block; }
  body { padding-bottom: calc(var(--dock-bar-h) + env(safe-area-inset-bottom)); }
}

/* ==========================================================================
   FOOTER — one of the four surfaces that stays dark.
   ========================================================================== */
.site-foot {
  background: var(--ink-2);
  color: var(--on-dark);
  border-top: 0;
  padding-block: clamp(2.5rem, 6vw, 4.5rem) 2rem;
}
.foot-grid {
  display: grid; gap: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}
.foot-grid h4 { color: var(--on-dark); margin-bottom: .8rem; }
.foot-grid ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.foot-grid a {
  color: var(--on-dark-mute); text-decoration: none; font-size: var(--fs-small);
  transition: color var(--dur) var(--ease);
}
.foot-grid a:hover { color: var(--on-dark); }
.foot-base {
  margin-top: 2.5rem; padding-top: 1.5rem; border-top: var(--edge);
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;
  color: var(--on-dark-mute); font-size: .8125rem;
}
/* The reference's pipe bar under the footer: one row of the collections on a
   half-step-lighter ground. Full-bleed, so it sits outside .wrap and eats the
   footer's bottom padding. Without :has you just get 2rem more --ink-2 below
   it — visually inert, not a break. */
.site-foot:has(.foot-rail) { padding-bottom: 0; }
.foot-rail {
  background: var(--ink-3);
  margin-top: 2.5rem;
  padding-block: .9rem;
}
.foot-rail ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .45rem 1.1rem;
}
.foot-rail a {
  color: var(--on-dark-mute); text-decoration: none;
  font-size: .75rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  transition: color var(--dur) var(--ease);
}
.foot-rail a:hover { color: var(--on-dark); }
.foot-rail li + li::before {
  content: "|"; margin-right: 1.1rem;
  color: rgba(255, 255, 255, .22);
}

/* ==========================================================================
   BREADCRUMBS — now on paper.
   ========================================================================== */
.crumbs {
  padding-top: 1.5rem;
  font-size: var(--fs-small);
  color: var(--on-light-mute);
}
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 0; }
.crumbs a { text-decoration: none; color: var(--on-light); font-weight: 600; }
.crumbs a:hover { color: var(--primary-deep); }
.crumbs li + li::before { content: "›"; margin-right: .4rem; color: var(--hair-strong); }

/* ==========================================================================
   SCROLL REVEAL — opt-in, and it fails OPEN.
   Content is visible by default; JS adds .reveal-on to enable the effect, so
   a dead script can never leave the page blank.
   ========================================================================== */
.reveal-on .reveal { opacity: 0; transform: translateY(18px); }
.reveal-on .reveal.in {
  opacity: 1; transform: none;
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}

/* ==========================================================================
   UTILITIES
   ========================================================================== */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.center { text-align: center; }
.center p, .center .lead { margin-inline: auto; }
.center .eyebrow--rule { justify-content: center; }
.mt-0 { margin-top: 0; }
.hairline { height: 1px; background: var(--hair); border: 0; margin: 0; }

/* Defensive: main carries the page ground itself, so a stale cached
   stylesheet or a partial body rule can't leave content on a bare canvas. */
main { background: var(--paper); }

/* The dock's own cart badge. Inline rather than overlapping, because a dock item
   is a stacked icon-over-label and there is no corner to hang a badge on. */
.dock-count {
  display: inline-grid; place-items: center;
  min-width: 16px; height: 16px; margin-left: 4px; padding: 0 4px;
  background: var(--primary-deep); color: var(--primary-ink);
  border-radius: 8px;
  font-size: .625rem; font-weight: 800; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.dock-count[hidden] { display: none; }
