/* Karat & Kin storefront — the adopted modern-heritage system at storefront
   density (design/DESIGN_SYSTEM.md; specimen specimen-storefront.html,
   ADOPTED 2026-08-07). Flat porcelain surfaces, hairline borders, square
   geometry, one midnight accent (DECISIONS 2026-10-13; the token keeps its
   historical name --verdigris). Body floor 16px (1rem), targets 44–48px.
   Token values are the same canon as workbench.css and check_contrast.py —
   workbench/tests/test_design_contrast.py keeps all three in lockstep. */

@font-face {
  font-family: "Fraunces";
  src: url("../fonts/fraunces-var-latin.069cb90e11a3.woff2") format("woff2");
  font-weight: 300 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Hanken Grotesk";
  src: url("../fonts/hanken-grotesk-var-latin.c501f578bf3f.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
/* The storefront display serif (operator decision + research,
   2026-08-19 — DESIGN_SYSTEM.md §2 holds the full rule). */
@font-face {
  font-family: "Lora";
  src: url("../fonts/lora-var-latin.9577af3ccda5.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --porcelain: #fcfcfd;
  --ivory: #f2f4f7;
  --card: #ffffff;
  --ink: #1a1c1e;
  --stone: #595f66;
  --hairline: #e1e4e9;
  --hairline-strong: #868c93;
  /* Midnight replaced verdigris on 2026-10-13 (DECISIONS): the accent
     token keeps its old NAME so operator themes and every rule that reads
     it carry over. There is no second accent: on dark grounds the
     "accent on ink" token is plain porcelain white. */
  --verdigris: #1e2f4f;
  --verdigris-soft: rgba(30, 47, 79, 0.09);
  --mint: #fcfcfd;
  --champagne: #e8ebf0;
  --ok: #1e2f4f;
  --ok-tint: #e8ebf0;
  --warn: #96560f;
  --warn-tint: #f7ede1;
  --err: #b13a4a;
  --err-tint: #f9e9eb;
  --on-ink: #fcfcfd;
  --on-ink-muted: #b9bcbf;
  --on-ink-faint: #8b9095;
  /* The dark chrome bands (announce bar, footer, proof band) are midnight,
     not ink (2026-10-13). Not themeable: the on-ink tiers are cleared
     against it in check_contrast.py. */
  --chrome: #1e2f4f;
  /* Lora (operator decision after research, 2026-08-19): the
     storefront's display face for EVERY heading via this token — never
     hardcode a family on a selector (DESIGN_SYSTEM.md §2). A warm
     moderate-contrast serif: heirloom voice with none of the hairline
     modulation the operator rejected. Fraunces' @font-face stays
     declared for the operator theme menu; the Workbench keeps Fraunces
     in its own stylesheet. */
  --display: "Lora", "Iowan Old Style", Georgia, serif;
  --ui: "Hanken Grotesk", "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;
  /* The media lift (life pass, 2026-08-08): the ONE sanctioned shadow —
     framed imagery only (product cards, PDP gallery, tiles, thumbs), so
     photographs sit ON the page instead of flat IN it. Chrome, cards,
     buttons, and text surfaces stay shadowless. */
  --lift: 0 1px 2px rgba(26, 28, 30, 0.05), 0 6px 20px rgba(26, 28, 30, 0.07);
  --lift-hover: 0 2px 4px rgba(26, 28, 30, 0.06), 0 12px 30px rgba(26, 28, 30, 0.10);
}

* { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--porcelain);
  color: var(--ink);
  font: 400 1rem/1.6 var(--ui); /* storefront body floor: 16px */
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; transition: color 150ms ease-out; }
a:hover { color: var(--verdigris); }
button { font: inherit; color: inherit; cursor: pointer; }
:focus-visible { outline: 2px solid var(--verdigris); outline-offset: 2px; }
img, svg { max-width: 100%; }
/* Image-save deterrence (2026-08-08, operator: paid original imagery):
   media can't be dragged to the desktop. Deterrence only — rendering IS
   distribution; provenance lives in the JSON-LD ImageObject copyright.
   Text stays selectable everywhere: never extend this beyond media. */
img, video { -webkit-user-drag: none; user-drag: none; }
/* Operator exemption (2026-08-09): a superuser's body carries
   data-operator (base.html) — dragging works again for them, matching
   the JS right-click exemption. Public pages never have the flag. */
body[data-operator] img, body[data-operator] video { -webkit-user-drag: auto; user-drag: auto; }
h1, h2, h3, h4, h5 { margin: 0; }

.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--ink); color: var(--porcelain); padding: 12px 20px;
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
}
.skip-link:focus { left: 0; z-index: 10; color: var(--porcelain); }

.eyebrow {
  display: block;
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--verdigris);
}

/* ---- chrome: announce bar + masthead ---- */
.announce {
  text-align: center; padding: 6px 16px;
  background: var(--chrome); color: var(--porcelain);
  font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase;
}
/* The header hairline is full-bleed on .mast-band; on the centered .mast
   column it would stop mid-air on viewports wider than 1240px (it only
   ever LOOKED right on pages whose first band drew its own full-width
   border-top at the same pixel — operator report, 2026-08-08). */
/* position: relative — the masthead search panel (below) anchors to the
   whole band, so it opens under BOTH rows of the two-tier masthead. */
.mast-band { position: relative; border-bottom: 1px solid var(--hairline); }
/* Row one: the wordmark centred between an empty rail and the icons. It
   gave up the nav to row two (.mast-navband) on 2026-08-14 — the menu had
   an 813px column here, where six items already wrapped, and a mega
   menu's triggers want the page. Three explicit columns rather than a
   flex row, so the wordmark is centred on the PAGE and not on whatever is
   left over after the icon cluster. */
/* 5/3 padding (was 22/16 — operator, 2026-08-18, twice: "the white
   space, make it smaller … look into IceCarats"): IceCarats runs a
   ~28px promo strip over a ~55px logo row; the 40px icon targets (24px
   is the WCAG 2.5.8 floor) set this row's working height and the
   paddings only clear the hairline. */
.mast {
  max-width: 1240px; margin: 0 auto; padding: 5px 32px 3px;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px;
}
.mast > .wordmark { grid-column: 2; }
.mast > .em-chip { grid-column: 1; justify-self: start; }
.wordmark {
  font: 500 1.1875rem/1 var(--display); letter-spacing: 0.22em; text-transform: uppercase;
  white-space: nowrap;
}
.wordmark span { color: var(--verdigris); }
/* The badge lockup (2026-10-13): an outlined SVG, so the wordmark's
   letterforms no longer depend on a web font. Height sets the size; the
   44px-tall link keeps the target. */
.wordmark.logo { display: inline-flex; align-items: center; min-height: 44px; line-height: 0; }
.wordmark.logo img { display: block; height: 40px; width: auto; }
/* Width-led in the footer: at 320px its grid cell is ~124px, narrower
   than the 30px-tall lockup, and a fixed height would squash it. */
.foot .wordmark.logo img { width: 149px; max-width: 100%; height: auto; }
.mast-nav { display: flex; gap: 26px; justify-content: center; flex-wrap: wrap; }
.mast-nav a {
  position: relative;
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 12px 2px; /* ≥44px total target with the line box */
}
/* The active page's underline, grown from the left on hover (life pass,
   2026-08-08) — same position the old inset box-shadow occupied. */
.mast-nav a::after {
  content: ""; position: absolute; left: 2px; right: 2px; bottom: 0; height: 2px;
  background: var(--verdigris);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 240ms ease-out;
}
.mast-nav a:hover::after, .mast-nav a[aria-current="page"]::after { transform: scaleX(1); }
.mast-nav a[aria-current="page"] { color: var(--verdigris); }
.mast-icons { grid-column: 3; justify-self: end; display: flex; gap: 6px; }
.icon-btn {
  width: 40px; height: 40px; display: grid; place-items: center;
  background: none; border: 0; border-radius: 50%;
  transition: background-color 150ms ease-out;
}
.icon-btn:hover { background: var(--ivory); }

/* The compact nav lives at the tail of this file, beside the mega menu it
   is the phone expression of. Only its resting state is here, because the
   masthead's icon row has to size it: closed, it is the third 44px icon
   button and nothing more. */
.drawer { display: none; position: relative; }
.drawer > summary { list-style: none; cursor: pointer; }
.drawer > summary::-webkit-details-marker { display: none; }

/* ---- masthead search (2026-08-17) ----
   A <details> whose summary is the 44px search icon and whose panel is a
   full-width band under the masthead — the mega menu's flat recipe (card
   ground between hairlines, never a floating shadow), anchored to
   .mast-band so it clears both masthead rows. Complete without JS; the
   script adds focus-on-open, dismissal, and autosuggest. z-index above
   the mega panel's 40: an open search outranks a hover menu. */
.mast-search { position: static; }
.mast-search > summary { list-style: none; cursor: pointer; }
.mast-search > summary::-webkit-details-marker { display: none; }
.mast-search[open] > summary { background: var(--ivory); }
.mast-search-panel {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 45;
  background: var(--card); border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}
.mast-search-inner {
  max-width: 760px; margin: 0 auto; padding: 22px 32px 24px;
  display: grid; gap: 14px;
}
/* The shared .search-bar caps at 560px for the search PAGE's column; in
   the panel the field takes the full 760px measure. */
.mast-search-inner .search-bar { max-width: none; }
.suggest { display: grid; gap: 10px; }
.suggest-label {
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--stone);
}
.suggest-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.suggest-row {
  display: flex; align-items: center; gap: 12px;
  min-height: 44px; padding: 4px 8px;
  transition: background-color 150ms ease-out;
}
.suggest-row:hover { background: var(--ivory); }
.suggest-thumb {
  flex: none; width: 44px; height: 44px; object-fit: contain;
  background: var(--card); border: 1px solid var(--hairline);
}
.suggest-name { color: var(--ink); font-size: 0.9375rem; }
.suggest-meta {
  margin-left: auto; flex: none; color: var(--stone);
  font-size: 0.875rem; font-variant-numeric: tabular-nums;
}
.suggest-empty { color: var(--stone); font-size: 0.9375rem; }
.suggest-all {
  display: inline-flex; align-items: center; min-height: 44px;
  border-top: 1px solid var(--hairline); padding-top: 10px;
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--verdigris);
}
.suggest-all:hover { text-decoration: underline; }
@media (max-width: 560px) {
  .mast-search-inner { padding: 16px 20px 18px; }
}

/* ---- buttons ---- */
.btn-solid {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 12px 30px;
  background: var(--ink); color: var(--porcelain); border: 1px solid var(--ink);
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  transition: background 180ms ease-out, border-color 180ms ease-out;
}
.btn-solid:hover:not(:disabled) { background: var(--verdigris); border-color: var(--verdigris); color: var(--porcelain); }
.btn-quiet {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 12px 30px;
  background: transparent; color: var(--ink); border: 1px solid var(--hairline-strong);
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  transition: border-color 180ms ease-out;
}
.btn-quiet:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }
.btn-solid:disabled, .btn-quiet:disabled { opacity: 0.45; cursor: not-allowed; }
/* A control shipped ahead of its plumbing (2026-08-11): the PDP's Add to
   bag is disabled because checkout doesn't exist yet, not because this
   piece can't be had — and the operator wants to see it "looking how it
   would if it did work". So it keeps the CTA at full strength while
   staying genuinely disabled: not-allowed on hover, no hover recolor, no
   focus stop, "dimmed" to a screen reader, and the note beneath it says
   plainly that checkout is coming. The washed-out treatment is reserved
   for controls that are dead on their own merits — .btn-unavailable. */
.btn-solid.btn-preview:disabled { opacity: 1; }
/* Can't be ordered, and says so by its shape rather than by fading the
   CTA (operator, 2026-08-11: "the add to cart being changed to
   unavailable looks too similar"). Same footprint as .btn-solid so the
   column's rhythm doesn't move; nothing else in common. The dashed rim is
   the same language .opt.forced speaks. */
.btn-unavailable {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 12px 30px;
  background: var(--ivory); color: var(--stone);
  border: 1px dashed var(--hairline-strong);
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  cursor: not-allowed;
}
/* Press feedback: a 1px settle — positional state, kept under reduced
   motion because nothing animates, the button simply sits down. */
.btn-solid:active:not(:disabled), .btn-quiet:active:not(:disabled), .icon-btn:active { transform: translateY(1px); }

/* ---- photography placeholders (real images replace these) ----
   Gradients are permitted ONLY here and in the category-tile scrim
   (DESIGN_SYSTEM §3 Allocation). */
.photo {
  background:
    radial-gradient(420px 300px at 70% 30%, rgba(236, 229, 216, 0.9), transparent 70%),
    linear-gradient(160deg, #eee9e0, #ddd5c8);
  display: grid; place-items: center;
  /* stone, not the specimen's #746b58 — that was 4.36:1 on the lightest
     gradient stop (check_contrast.py "storefront photo-placeholder label") */
  color: var(--stone);
  font-size: 0.75rem; letter-spacing: 0.18em; text-transform: uppercase; text-align: center;
}

/* A real photograph occupying a photo slot (hero, category tile, journal
   band — operator images, 2026-08-08): same footprint as the placeholder
   block, cover-cropped to the slot's fixed ratio, never distorting. */
.photo-img { display: block; width: 100%; object-fit: cover; }
/* height: auto, or the tile img's height="880" CLS attribute becomes the
   BOX height: the attribute maps to a presentational height hint, width:
   100% overrides only the width half, and a non-auto height switches the
   aspect-ratio off — the arch rendered 275×880 (operator, 2026-08-16). */
.cat .photo-img { aspect-ratio: 1; height: auto; }
.photo-editorial-img { aspect-ratio: 16 / 10; }

/* ---- the arch (life pass, 2026-08-08; scope narrowed same day) ----
   ONE arch moment per page: the category tiles (rules live in the tile
   section). The hero and journal-band images went back to square when
   arching all three read as overdone — operator. Everything else
   (product grids, PDP gallery, article figures) stays square too. */

/* ---- hero ---- */
/* Full-bleed hero (2026-08-12, supersedes the specimen's 50/50 split):
   one campaign photograph spans the page edge to edge; the copy overlays
   its left side above an ink scrim. The scrim holds ≥0.78 ink across the
   copy zone (0–45%) — worst case over a white photo composites to
   #4C4E4F, gated in check_contrast.py — then dissolves so the piece and
   the model stay clear on the right. Copy z-index sits ABOVE the scrim
   (scrim between photo and copy, never over the text). */
/* Banners (2026-10-13): every slide is a whole hero — photo, scrim, copy,
   shop chip — stacked in the section's one grid cell, so the band is as
   tall as its tallest message and no slide ever reflows the page. */
.hero { position: relative; display: grid; min-height: clamp(440px, 46vw, 700px); overflow: hidden; }
.hero-slide { grid-area: 1 / 1; position: relative; min-height: inherit; }
.hero-slide > .photo { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Hero crop (2026-08-18, operator's second "head is cut" report): a
   1920px screen shows only ~66% of a 16:9 frame's height (700px band ÷
   1063px displayed height), so a centered crop beheads a full-framed
   model no matter how the photo is composed. Fix: hero slides ship as
   2.72:1 BANNERS — the 16:9 render extended LEFT with its own blurred
   edge (the pad sits under the scrim and headline) — so at the 700px
   cap the band shows the banner's FULL height (1905 / 2.72 ≈ 700) and
   heads and jewelry can never crop on desktop. 80% x keeps the
   right-third subject centered in narrow (phone) windows; the 6%
   y-bias means anything a nonstandard band does discard is sky, never
   a head. */
.hero-slide > .photo-img { object-fit: cover; object-position: 80% 6%; }
.hero-scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(90deg,
    rgba(26, 28, 30, 0.82) 0%, rgba(26, 28, 30, 0.78) 45%,
    rgba(26, 28, 30, 0.38) 68%, rgba(26, 28, 30, 0) 88%);
}
.hero-copy {
  position: relative; z-index: 2; display: grid; align-content: center;
  justify-items: start; gap: 22px; min-height: inherit;
  max-width: 1240px; margin: 0 auto; padding: 64px 32px;
}
.hero-copy > * { max-width: min(520px, 44%); }
.hero .eyebrow { color: var(--mint); }
/* 480/520, not the display-light 340/400 the ink-on-light bands use
   (operator, 2026-08-18: the hero copy read "blurry, not sharp"):
   white hairline strokes over a photo get grayscale-antialiased and
   look soft at every zoom — reversed-out text needs the extra stroke
   weight that dark-on-light does not. */
.hero-copy .hero-title { margin: 0; font: 700 clamp(2.25rem, 4.2vw, 3.375rem)/1.12 var(--display); letter-spacing: -0.015em; color: var(--on-ink); text-wrap: balance; }
.hero-copy p { margin: 0; color: var(--on-ink); font-size: 1.0625rem; font-weight: 520; max-width: min(44ch, 44%); }
.hero-cta { display: flex; gap: 14px; flex-wrap: wrap; max-width: none; }
.hero .btn-solid { background: var(--porcelain); border-color: var(--porcelain); color: var(--ink); }
.hero .btn-solid:hover:not(:disabled) { background: var(--champagne); border-color: var(--champagne); color: var(--ink); }
.hero .btn-quiet { border-color: rgba(253, 252, 250, 0.7); color: var(--on-ink); }
.hero .btn-quiet:hover:not(:disabled) { border-color: var(--porcelain); color: var(--porcelain); }
.hero :focus-visible { outline-color: var(--mint); }

/* ---- bands ---- */
.band { border-top: 1px solid var(--hairline); }
.band-inner { max-width: 1240px; margin: 0 auto; padding: 56px 32px; }
.band-head { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; margin-bottom: 30px; }
.band-head h2 { font: 400 1.75rem/1.2 var(--display); }
.band-head a {
  font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--verdigris); padding: 12px 0;
}
/* Quiet uppercase links answer hover with an underline grown from the
   left (life pass, 2026-08-08) — feedback without a color change. */
.band-head a, .article-back a, .journal-read {
  position: relative;
}
.band-head a::after, .article-back a::after, .journal-read::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 8px; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: left center;
  transition: transform 240ms ease-out;
}
.journal-read::after { bottom: 4px; }
.band-head a:hover::after, .article-back a:hover::after,
.journal-row > a:hover .journal-read::after { transform: scaleX(1); }

/* ---- category tiles ---- */
/* One row, always: column auto-flow gives every tile an equal track no
   matter how many the operator activates (6 as of 2026-08-12), so the
   band never breaks into a ragged second line on desktop. Phones keep
   their stacked rules below. */
.grid-cats { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 22px; }
/* The tile is the arch frame: its own radius + overflow clip keep the
   hover zoom inside the arch. The imgs carry the radius too, so edit
   mode (overflow: visible, chips must not be clipped by the curve)
   still renders arched tiles. */
.cat { position: relative; display: grid; overflow: hidden; border-radius: 50% 50% 0 0; box-shadow: var(--lift); }
.em-on .cat { overflow: visible; }
.cat .photo, .cat .photo-img { border-radius: 50% 50% 0 0; }
.cat .photo { aspect-ratio: 1; }
.cat .photo-img { transition: transform 650ms cubic-bezier(0.22, 0.61, 0.36, 1); }
a.cat:hover .photo-img, a.cat:focus-visible .photo-img { transform: scale(1.05); }
.cat span {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 14px;
  /* 0.70 ink, not the specimen's 0.55: over a pure-white photo 0.55 leaves
     the white label at 3.85:1 — 0.70 holds 6.3:1 in that worst case
     (check_contrast.py "category-tile label over scrim"). */
  background: linear-gradient(transparent, rgba(26, 28, 30, 0.7)); /* label scrim */
  color: #fff; font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
}

/* ---- product grid ---- */
.grid-products { display: grid; grid-template-columns: repeat(4, 1fr); gap: 26px; }
.product { display: grid; gap: 4px; align-content: start; }
.product .photo { aspect-ratio: 1; background: linear-gradient(165deg, #f2f4f7, #e3e7ed); }
.product h3 { margin: 12px 0 0; font: 500 1rem/1.4 var(--ui); }
.product .meta { color: var(--stone); font-size: 0.875rem; }
.product .price { font-size: 0.9375rem; font-weight: 600; margin-top: 2px; }
.product:hover h3 { color: var(--verdigris); }
/* Out of stock, on the card, everywhere a card appears (operator,
   2026-08-09).
   A tag on the ivory ground in stone text — the sanctioned "muted pill
   text" pairing, so no new contrast pairing enters the gate — sitting on the
   frame's own bottom edge so it reads over the photo without covering the
   piece. Words, not colour: the label IS the signal. It sits inside the
   frame's overflow:hidden, so the hover photo slides behind it. */
.p-flag {
  position: absolute; left: 0; bottom: 0; z-index: 1;
  padding: 5px 10px; background: var(--ivory); color: var(--stone);
  border-top: 1px solid var(--hairline); border-right: 1px solid var(--hairline);
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
}
/* Card hover: the frame's border firms up and the media-lift shadow
   deepens — a still answer, no movement (the ≤4px lift was retired
   2026-08-09: with the photo swapping behind it, the card moving too read
   as too much). The frame (not the img) carries the border, ground, and
   shadow so the second photo can live inside it. */
.p-frame {
  position: relative; display: block; overflow: hidden;
  background: var(--card); border: 1px solid var(--hairline); box-shadow: var(--lift);
  transition: border-color 250ms ease-out, box-shadow 250ms ease-out;
}
.product:hover .p-frame, .product:focus-visible .p-frame {
  border-color: var(--hairline-strong); box-shadow: var(--lift-hover);
}
/* Second-photo reveal (2026-08-09): the card's next image — usually the
   on-model shot — glides in from the right, and it is the card's ONLY
   hover movement. The two properties run on DIFFERENT clocks on purpose:
   opacity is a swap, so it lands on the 240ms feedback beat and the first
   photo is gone quickly; the travel is atmosphere, so it keeps gliding to
   560ms on the house soft ease. That ease decelerates hard — the layer is
   82% of the way home when the fade completes, so on a 275px card the seam
   between the two photos only ever finishes as a ~5px settle, never a hard
   edge sliding any real distance. The layer is opaque (its own card ground) so it covers the
   first photo cleanly, and the frame's overflow clips the incoming slide
   out of the neighbouring card. Cards whose product has one photo render
   no second layer at all. */
.p-img-alt {
  position: absolute; inset: 0; height: 100%;
  opacity: 0; transform: translateX(10%);
  transition: opacity 240ms ease-out,
              transform 560ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.product:hover .p-img-alt, .product:focus-visible .p-img-alt {
  opacity: 1; transform: translateX(0);
}

/* ---- trust row ---- */
.trust { background: var(--ivory); border-top: 1px solid var(--hairline); }
.trust-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 28px; padding-top: 44px; padding-bottom: 44px; }
.trust-item { display: grid; justify-items: center; text-align: center; gap: 10px; }
.trust-item svg { width: 26px; height: 26px; color: var(--verdigris); }
.trust-item strong { font-size: 0.9375rem; font-weight: 600; }
.trust-item span { color: var(--stone); font-size: 0.875rem; line-height: 1.5; }

/* ---- editorial / journal band ---- */
.editorial { background: var(--champagne); }
.editorial-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 48px; align-items: center; }
.editorial h2 { margin: 0 0 14px; font: 400 clamp(1.75rem, 3.4vw, 2.625rem)/1.15 var(--display); }
/* Champagne-band ink variants: the fallbacks are the approved specimen's
   values (the default identity); an active theme overrides the custom
   properties with tiers BLENDED from its own ink/champagne
   (content/theming.py DERIVED_MIXES), so bold and dark themes retheme
   this band instead of breaking on fixed colors (2026-08-08). */
.editorial p { margin: 0 0 24px; color: var(--editorial-text, #4e5052); font-size: 1rem; max-width: 52ch; }
.eyebrow-editorial { color: var(--editorial-eyebrow, #606265); margin-bottom: 14px; }
.btn-editorial { border-color: var(--editorial-border, #797b7f); color: var(--editorial-btn, #2f3133); }
.photo-editorial { aspect-ratio: 16 / 10; background: linear-gradient(150deg, #dde1e8, #cbd1db); color: var(--stone); }

/* ---- journal index (/blog/) ---- */
.journal { max-width: 880px; }
.journal-head { display: grid; gap: 14px; margin-bottom: 44px; }
.journal-head h1 { font: 400 clamp(2rem, 4vw, 3rem)/1.1 var(--display); letter-spacing: -0.01em; }
.journal-head p { margin: 0; color: var(--stone); font-size: 1.0625rem; max-width: 54ch; }
.journal-row { border-top: 1px solid var(--hairline); }
.journal-row > a { display: flex; gap: 28px; align-items: center; justify-content: space-between; padding: 28px 0; }
.journal-text { display: grid; gap: 8px; justify-items: start; }
.journal-meta { margin: 0; color: var(--stone); font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.journal-row h2 { font: 400 1.5rem/1.25 var(--display); }
.journal-row a:hover h2 { color: var(--verdigris); }
.journal-summary { margin: 0; color: var(--stone); max-width: 62ch; }
.journal-read { font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--verdigris); padding: 8px 0; }
.journal-thumb { width: 220px; aspect-ratio: 16 / 10; object-fit: cover; flex: none; box-shadow: var(--lift); }
.journal-empty { color: var(--stone); }
.journal-pager { display: flex; gap: 14px; margin-top: 36px; }

/* ---- article page (/blog/<slug>/) ----
   Editorial breakout (operator, 2026-08-08): the column is 960px so the
   hero and body figures render generously, while running text and the
   header keep a ~720px reading measure, centered — images break out,
   sentences stay comfortable. */
.article-inner { max-width: 960px; }
.crumbs {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px;
  margin-bottom: 36px; color: var(--stone); font-size: 0.8125rem;
}
.crumbs a { padding: 6px 0; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.crumbs a:hover { color: var(--verdigris); }
.article-head { display: grid; gap: 16px; margin-bottom: 32px; justify-items: start; }
.article-head h1 { font: 400 clamp(2rem, 4.2vw, 3.125rem)/1.12 var(--display); letter-spacing: -0.01em; }
.article-meta { margin: 0; color: var(--stone); font-size: 0.9375rem; }
.article-hero { margin: 0 0 36px; }
/* Natural ratio — never crop or distort the photograph. */
.article-hero img { width: 100%; height: auto; display: block; }

/* Article body: styles the guide's clean semantic HTML (h2/h3/p/ul/
   blockquote/table/figure) — article content carries no classes. */
.article-body { font-size: 1.0625rem; line-height: 1.75; }
.article-body p { margin: 0 0 1.5em; }
.article-body h2 {
  font: 400 clamp(1.5rem, 2.6vw, 1.9375rem)/1.25 var(--display);
  margin: 2.1em 0 0.7em; padding-top: 1.1em; border-top: 1px solid var(--hairline);
}
.article-body h3 { font: 600 1.1875rem/1.35 var(--ui); margin: 1.8em 0 0.5em; }
.article-body a { color: var(--verdigris); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.article-body a:hover { text-decoration-thickness: 2px; }
.article-body ul, .article-body ol { padding-left: 1.3em; margin: 0 0 1.5em; }
.article-body li { margin-bottom: 0.6em; }
.article-body blockquote {
  /* Pull quote at display register (Fraunces stays out of body sizes). */
  margin: 2em 0; padding: 4px 0 4px 24px;
  border-left: 3px solid var(--verdigris);
  font: 400 1.4375rem/1.5 var(--display);
}
.article-body table { width: 100%; border-collapse: collapse; margin: 2em 0; font-size: 0.9375rem; line-height: 1.5; }
.article-body th, .article-body td { text-align: left; padding: 10px 14px 10px 0; border-bottom: 1px solid var(--hairline); vertical-align: top; }
.article-body thead th, .article-body thead td { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--stone); }
.article-body tbody th { font-weight: 600; }
/* Per-section reading time, injected after each h2 at render (toc.py).
   margin-block only — the reading-measure rules below own the inline axis. */
.article-body .section-read {
  margin-block: 0.35em 1.2em;
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--stone);
}
.article-body figure { margin: 2.4em 0; }
.article-body img { width: 100%; height: auto; display: block; }
.article-body figcaption { color: var(--stone); font-size: 0.875rem; margin-top: 8px; }
.tag-list { list-style: none; display: flex; flex-wrap: wrap; gap: 10px; margin: 40px 0 0; padding: 0; }
.tag-pill {
  border: 1px solid var(--hairline); background: var(--ivory); color: var(--stone);
  border-radius: 999px; padding: 6px 16px;
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
}
.article-back { margin: 36px 0 0; }
.article-back a { display: inline-block; font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--verdigris); padding: 12px 0; }
/* "Further reading" (operator, 2026-08-08): static tag-related links in the
   journal-index row language — internal discovery for readers and crawlers,
   deliberately not a carousel. Sits on the reading measure. */
.article-related { max-width: 720px; margin: 48px auto 0; }
.related-title { margin: 0 0 4px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--stone); }
.related-list { list-style: none; margin: 0; padding: 0; }
.related-list li { border-top: 1px solid var(--hairline); }
.related-list a { display: grid; gap: 6px 20px; padding: 18px 0; align-items: center; }
/* With a hero: quieter cousin of the journal-index row — a 130px 16:10
   cover thumbnail (the stored derivative) rides the right edge. */
.related-list a.has-thumb { grid-template-columns: 1fr 130px; }
.related-list a.has-thumb .related-meta,
.related-list a.has-thumb .related-name { grid-column: 1; }
.related-thumb {
  grid-column: 2; grid-row: 1 / span 2;
  width: 130px; aspect-ratio: 16 / 10; object-fit: cover; display: block;
  box-shadow: var(--lift);
}
.related-meta { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--stone); }
.related-name { font: 400 1.375rem/1.3 var(--display); color: var(--ink); }
.related-list a:hover .related-name { color: var(--verdigris); }
/* Prev/next article pager (operator, 2026-08-08): hairline-topped like the
   journal index rows; real anchors so readers page through and crawlers
   walk the chain. Centered on the same 720px measure as the text column. */
.article-pager {
  display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
  max-width: 720px; margin: 44px auto 0;
  border-top: 1px solid var(--hairline); padding-top: 28px;
}
.article-pager .pager-item { display: grid; gap: 8px; align-content: start; padding: 6px 0; }
.article-pager .pager-prev { grid-column: 1; }
.article-pager .pager-next { grid-column: 2; text-align: right; }
.article-pager .pager-label { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--stone); }
.article-pager .pager-title {
  font: 400 1.25rem/1.3 var(--display); color: var(--ink);
  /* Long titles clamp to two lines — the label above carries the direction. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.article-pager .pager-item:hover .pager-title { color: var(--verdigris); }
@media (max-width: 640px) {
  .article-pager { grid-template-columns: 1fr; }
  .article-pager .pager-next { grid-column: 1; text-align: left; }
}
/* ---- "On this page" rail (scrollspy TOC) ----
   Lives in the right gutter beside the article column. The nav is an
   absolute column spanning .article-layout (= the article body's height);
   the sticky .toc-inner rides along while reading and PARKS when its
   column ends — the rail never follows the reader past the article into
   related/footer (fix 2026-08-13; the 50% here is .article-layout's own
   width, whose center is the viewport center, so the gutter position is
   unchanged). Wide screens only — below 1480px the gutter can't hold it
   and the plain document order serves. Links are real anchors (work
   without JS); storefront.js sets aria-current on the active one. */
.article-layout { position: relative; }
.article-toc { display: none; position: absolute; top: 0; bottom: 0; left: calc(50% + 512px); width: 212px; }
.toc-inner { position: sticky; top: 190px; }
@media (min-width: 1480px) { .article-toc { display: block; } }
.toc-title { margin: 0 0 12px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--stone); }
.article-toc ul { list-style: none; margin: 0; padding: 0; border-left: 1px solid var(--hairline); display: grid; }
.article-toc a {
  display: block; padding: 7px 0 7px 15px; margin-left: -1px;
  border-left: 2px solid transparent;
  color: var(--stone); font-size: 0.8125rem; line-height: 1.45;
}
.article-toc a:hover { color: var(--ink); }
.article-toc a[aria-current="true"] { color: var(--verdigris); border-left-color: var(--verdigris); font-weight: 600; }

/* ---- back-to-top ----
   Hidden until storefront.js reveals it after ~a viewport of scrolling;
   visibility:hidden also keeps it out of the no-JS tab order. */
.back-top {
  position: fixed; right: 28px; bottom: 28px; z-index: 40;
  width: 48px; height: 48px; display: grid; place-items: center;
  background: var(--card); border: 1px solid var(--hairline-strong);
  color: var(--ink); cursor: pointer; padding: 0;
  opacity: 0; visibility: hidden; transform: translateY(6px);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
}
.back-top.show { opacity: 1; visibility: visible; transform: none; }
.back-top:hover { border-color: var(--ink); }
.back-top svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; }
@media (prefers-reduced-motion: reduce) { .back-top { transition: none; transform: none; } }

/* ---- operator edit-mode entry (superuser sessions only) ----
   Lives HERE, not in edit-mode.css: the control renders on every page a
   superuser browses, while edit-mode.css loads only inside edit mode —
   styling it there left the resting link unstyled (a giant bare SVG,
   operator report 2026-08-08). Resting state is a quiet 36px icon chip;
   the labeled ink "Done editing" pill appears only in edit mode. */
.em-pill {
  position: fixed; left: 18px; bottom: 18px; z-index: 40;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 36px; height: 36px; padding: 0;
  background: var(--card); border: 1px solid var(--hairline-strong); color: var(--stone);
}
.em-pill svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.9; }
.em-pill:hover { border-color: var(--verdigris); color: var(--verdigris); }
.em-pill-on {
  width: auto; padding: 0 14px;
  background: var(--ink); border-color: var(--ink); color: var(--porcelain);
  font: 600 0.75rem/1 var(--ui); letter-spacing: 0.14em; text-transform: uppercase;
}
.em-pill-on:hover { background: var(--verdigris); border-color: var(--verdigris); color: var(--porcelain); }

/* ---- operator-only image regeneration (superuser sessions only —
   article-admin.js adds the chips and the body class) ---- */
.img-admin .article-hero, .img-admin .article-body figure { position: relative; }
.regen-chip {
  position: absolute; top: 12px; right: 12px;
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--card); border: 1px solid var(--hairline-strong); color: var(--ink);
  font: 600 0.75rem/1 var(--ui); letter-spacing: 0.08em; text-transform: uppercase;
  padding: 9px 13px; cursor: pointer;
}
.regen-chip:hover { border-color: var(--verdigris); color: var(--verdigris); }
.regen-chip svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 1.75; }
.regen-dialog { border: 1px solid var(--hairline-strong); padding: 0; width: min(560px, 92vw); }
.regen-dialog::backdrop { background: rgba(26, 28, 30, 0.45); }
.regen-form { display: grid; gap: 14px; padding: 22px; background: var(--card); margin: 0; }
.regen-title { margin: 0; font: 500 1.25rem/1.3 var(--display); }
.regen-preview { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border: 1px solid var(--hairline); display: block; }
.regen-label { font-size: 0.8125rem; font-weight: 600; display: grid; gap: 6px; }
.regen-promptline { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: -8px; }
.regen-label-text { font-size: 0.8125rem; font-weight: 600; }
.regen-btn.ghost { border-color: transparent; background: none; color: var(--verdigris); display: inline-flex; align-items: center; gap: 6px; padding: 8px 10px; }
.regen-btn.ghost:hover { text-decoration: underline; }
.regen-btn.ghost svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 1.75; }
.regen-select { font: 400 0.9375rem/1.4 var(--ui); color: var(--ink); padding: 9px 10px; border: 1px solid var(--hairline-strong); background: var(--card); }
.regen-textarea { font: 400 0.9375rem/1.5 var(--ui); color: var(--ink); padding: 10px 12px; border: 1px solid var(--hairline-strong); resize: vertical; }
/* Reference photos (2026-08-08): operator-attached files both Claude and
   Gemini look at. Thumb strip on hairline cards; square remove chips. */
.ref-hint { font-weight: 400; color: var(--stone); letter-spacing: 0; text-transform: none; }
.ref-strip { list-style: none; display: flex; flex-wrap: wrap; gap: 12px; margin: -2px 0 0; padding: 2px 0 0; }
.ref-strip li { position: relative; }
.ref-strip img { width: 64px; height: 64px; object-fit: cover; border: 1px solid var(--hairline); display: block; }
.ref-remove {
  position: absolute; top: -8px; right: -8px; width: 24px; height: 24px;
  display: grid; place-items: center; padding: 0;
  background: var(--card); border: 1px solid var(--hairline-strong);
  color: var(--ink); font-size: 0.875rem; line-height: 1; cursor: pointer;
}
.ref-remove:hover { border-color: var(--err); color: var(--err); }
.regen-status { margin: 0; min-height: 1.3em; font-size: 0.875rem; color: var(--stone); }
.regen-status.err { color: var(--err); }
/* In-flight AI call (Claude prompt / Gemini render): stable verdigris dot
   with the design system's outward-only pulse (§7 motion). The dot + the
   status words carry the state on their own, so reduced motion (which
   stills the ring) and color-blindness lose nothing. */
.regen-status.working { position: relative; padding-left: 18px; color: var(--ink); }
.regen-status.working::before,
.regen-status.working::after { content: ""; position: absolute; left: 2px; top: 50%; width: 8px; height: 8px; margin-top: -4px; border-radius: 50%; }
.regen-status.working::before { background: var(--verdigris); }
.regen-status.working::after { border: 1px solid var(--verdigris); animation: regen-pulse 1.8s cubic-bezier(0.16, 1, 0.3, 1) infinite; }
@keyframes regen-pulse { 0% { transform: scale(1); opacity: 0.7; } 70%, 100% { transform: scale(2.3); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .regen-status.working::after { animation: none; opacity: 0; } }
.regen-actions { display: flex; justify-content: flex-end; gap: 10px; }
.regen-btn { font: 600 0.8125rem/1 var(--ui); padding: 11px 16px; border: 1px solid var(--hairline-strong); background: var(--card); color: var(--ink); cursor: pointer; }
.regen-btn:hover { border-color: var(--ink); }
.regen-btn.primary { background: var(--ink); border-color: var(--ink); color: var(--porcelain); }
.regen-btn.primary:hover { background: var(--verdigris); border-color: var(--verdigris); }
.regen-btn:disabled { opacity: 0.55; cursor: default; }

/* The reading measure — MUST come after the element rules above: their
   margin shorthands would otherwise reset margin-inline back to 0 and pin
   the text to the left while media runs full width. Body figures joined
   the measure 2026-08-08 (breakout retired — only the hero spans the
   column); images scale down at natural ratio, never cropped or squished. */
.article-inner > .crumbs,
.article-inner > .article-head,
.article-inner > .tag-list,
.article-inner > .article-back,
.article-body > p,
.article-body > h2,
.article-body > h3,
.article-body > ul,
.article-body > ol,
.article-body > blockquote,
.article-body > figure,
.article-body > table,
.article-body > .privacy-choices {
  max-width: 720px; margin-inline: auto;
}
/* The analytics toggle (/pages/privacy-choices/, 2026-10-10) is prose plus
   one form; it only needs to sit in the measure and clear the body below. */
.article-body > .privacy-choices { margin-block-end: 2.4em; }

/* ---- catalog index (/products/) ---- */
.catalog-head { max-width: 1240px; margin: 0 auto; padding: 44px 32px 0; display: grid; gap: 10px; justify-items: start; }
.catalog-head h1 { font: 400 clamp(2rem, 4vw, 3rem)/1.1 var(--display); letter-spacing: -0.01em; }
.catalog-count { margin: 0; color: var(--stone); font-size: 0.9375rem; }
.catalog-empty { margin: 0; color: var(--stone); }
/* The search page's query bar (2026-08-17): the bag's flat input
   vocabulary (white fill, hairline-strong rim, radius 0, 44px target)
   at a width that fits a spoken query. */
.search-bar { display: flex; gap: 8px; width: 100%; max-width: 560px; }
.search-input {
  flex: 1; min-width: 0; min-height: 44px; padding: 0 14px;
  font: 400 1rem/1.4 var(--ui); color: var(--ink);
  background: #fff; border: 1px solid var(--hairline-strong); border-radius: 0;
}
.search-btn { min-height: 44px; }
/* The 404 page (2026-08-23; direction "A · The broken link", same day):
   the catalog-head idiom — the chain, eyebrow, display h1, a stone lede —
   with the search bar as its one primary action, a crumbs-register row of
   quiet links, then a product band (closest matches / best sellers) and a
   help line. catalog-head has no bottom padding (a grid normally follows
   it); the band follows here, so a short one. */
.notfound { padding-bottom: 44px; gap: 18px; }
.notfound-lede { margin: 0; max-width: 56ch; color: var(--stone); font-size: 1.0625rem; line-height: 1.6; }
.notfound-links { display: flex; flex-wrap: wrap; gap: 4px 28px; margin-top: 6px; font-size: 0.8125rem; }
.notfound-links a { padding: 6px 0; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
.notfound-links a:hover { color: var(--verdigris); }
/* The chain: tube wire = porcelain halo + ink wire + porcelain core, the
   sprung link in the accent. Drawn at its natural width and CLIPPED on a
   narrow screen — a chain running off the edge is a chain; a shrunken one
   is a squiggle. (justify-self stretch + min-width 0: the grid item must
   not widen to the SVG's intrinsic width.) */
.notfound-art { position: relative; height: 62px; justify-self: stretch; width: 100%; min-width: 0; overflow: hidden; margin: 6px 0 4px; color: var(--ink); }
/* Absolutely positioned so the SVG's intrinsic width (425px) never
   reaches the grid track's min-content — at 320px it widened the whole
   column (seen in the iframe check, 2026-08-23). */
.notfound-art svg { position: absolute; top: 0; left: 0; display: block; height: 62px; width: auto; max-width: none; }
.notfound-art .notfound-core { stroke: var(--porcelain); }
.notfound-art .notfound-open { stroke: var(--verdigris); }
.notfound-meant { margin: -6px 0 0; max-width: 56ch; color: var(--stone); font-size: 0.9375rem; }
.notfound-band .band-inner { padding-top: 40px; padding-bottom: 64px; }
.notfound-help { margin: 34px 0 0; max-width: 64ch; color: var(--stone); font-size: 0.9375rem; }
.notfound-help a { color: var(--verdigris); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
/* Listing sort menu (2026-08-17, supersedes the same-day select+Apply):
   a <details> of links in the house disclosure idiom — one click applies
   a sort, no confirm step (the e-commerce norm; a link activation is
   user-initiated navigation, so WCAG 3.2.2 never applies). Flat popover:
   card ground, hairline-strong rim, square corners, NO shadow — the
   border is what separates it from the page. Current choice = the
   pager's aria-current idiom (verdigris + 600: weight carries the
   meaning, color seconds it). */
.sort-row { display: flex; justify-content: flex-end; margin: 0 0 20px; }
.sort-menu { position: relative; }
.sort-menu summary {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 16px;
  font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink);
  background: var(--card); border: 1px solid var(--hairline-strong);
  cursor: pointer; list-style: none;
}
.sort-menu summary::-webkit-details-marker { display: none; }
.sort-menu summary:hover { border-color: var(--ink); }
.sort-menu summary svg {
  width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2;
}
.sort-menu[open] summary svg { transform: rotate(180deg); }
.sort-current { color: var(--verdigris); }
.sort-options {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 30;
  min-width: 220px; margin: 0; padding: 4px 0; list-style: none;
  background: var(--card); border: 1px solid var(--hairline-strong);
}
.sort-options a {
  display: block; padding: 11px 16px;
  font-size: 0.9375rem; color: var(--ink); text-decoration: none;
}
.sort-options a:hover { background: var(--ivory); color: var(--verdigris); }
.sort-options a[aria-current] { color: var(--verdigris); font-weight: 600; }
/* Numbered pager (2026-08-11): quiet stone numbers on a hairline-topped
   band, the current page in the masthead's aria-current idiom (verdigris,
   600, 2px underline). Cells are 44px touch targets; the elided middle is
   a plain ellipsis cell; everything wraps and re-centers at phone widths. */
.catalog-pager {
  margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 4px 14px;
}
.pg-list { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; list-style: none; margin: 0; padding: 0; }
.pg-num {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; padding: 0 8px;
  color: var(--stone); font-size: 0.9375rem; font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.pg-num:hover { color: var(--ink); }
.pg-num[aria-current="page"] {
  color: var(--verdigris); font-weight: 600;
  text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 8px;
}
.pg-gap {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 44px; color: var(--stone);
}
.pg-step {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px;
  color: var(--ink); font-size: 0.8125rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.pg-step:hover { color: var(--verdigris); }
/* Index cards carry h2 names (the page h1 is the catalog head; levels must
   not skip) — same look as the homepage's h3 cards. */
.product h2 { margin: 12px 0 0; font: 500 1rem/1.4 var(--ui); }
.product:hover h2 { color: var(--verdigris); }
/* Real cutout imagery: contained, never cropped — vendor cutouts sit on a
   white ground, so the white card behind them reads as one surface.
   SQUARE box (2026-08-09): product photos are square (vendor cutouts and
   every AI render at 1:1), so a 4:5 portrait box letterboxed them with
   dead bands — the box now matches the image and fills edge to edge.
   The hairline border and lift shadow moved to the wrapping .p-frame the
   same day (second-photo reveal) — the framed look is unchanged, but the
   frame must own them so it can clip the incoming photo. */
.p-img { width: 100%; aspect-ratio: 1; object-fit: contain; background: var(--card); display: block; }

/* ---- product page (the "jeweler's counter" PDP, adopted 2026-08-08) ---- */
.pdp-inner { padding-top: 24px; }
.pdp { display: grid; grid-template-columns: 92px minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(20px, 3vw, 40px); align-items: start; }
.pdp-single { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }
.thumbs { display: grid; gap: 10px; align-content: start; }
.thumb { position: relative; display: block; border: 1px solid var(--hairline); background: var(--card); box-shadow: var(--lift); }
.thumb img { display: block; width: 100%; aspect-ratio: 1; object-fit: contain; }
.thumb-video { display: block; width: 100%; aspect-ratio: 1; background: var(--ivory); }
.thumb[aria-current="true"] { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
/* Collapsed rail (JS): hidden thumbs must actually vanish (the attribute
   alone loses to display:block), and the "+N" opener reads as one more
   quiet tile, not a button. */
.thumb[hidden] { display: none; }
.thumb-more { width: 100%; aspect-ratio: 1; display: grid; place-items: center; background: var(--ivory); color: var(--stone); font: 600 0.875rem/1 var(--ui); cursor: pointer; padding: 0; }
.thumb-more:hover { color: var(--ink); border-color: var(--ink); }
/* All-photographs overlay (2026-08-09): the "+N" tile opens this dialog
   instead of unfolding the rail inline — a nine-image rail expanded to
   roughly twice the stage's height and hung orphan thumbs against blank
   space (operator bug report; Baymard's truncation pattern routes the
   overflow into an overlay). House dialog idiom (regen-dialog): hairline
   card over the sanctioned backdrop dim, square geometry, no new colors.
   Tiles reuse .thumb, so aria-current and the lift read identically to
   the rail. html:has() holds the page still while it's open; a browser
   without :has merely keeps background scroll. */
.gal-dialog { border: 1px solid var(--hairline-strong); padding: 0; width: min(1080px, 94vw); max-height: 88vh; overflow-y: auto; background: var(--card); }
.gal-dialog::backdrop { background: rgba(26, 28, 30, 0.45); }
html:has(.gal-dialog[open]) { overflow: hidden; }
.gal-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 14px 22px; background: var(--card); border-bottom: 1px solid var(--hairline); }
.gal-title { margin: 0; font: 500 1.25rem/1.3 var(--display); }
.gal-count { color: var(--stone); font: 400 0.9375rem/1 var(--ui); margin-left: 4px; }
.gal-close { width: 44px; height: 44px; display: grid; place-items: center; background: var(--card); border: 1px solid var(--hairline-strong); color: var(--ink); cursor: pointer; padding: 0; }
.gal-close:hover { border-color: var(--verdigris); color: var(--verdigris); }
.gal-close svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.75; }
.gal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(200px, 40vw), 1fr)); gap: 12px; padding: 22px; }
.play-badge { position: absolute; inset: 0; display: grid; place-items: center; color: var(--porcelain); }
.play-badge span { width: 30px; height: 30px; display: grid; place-items: center; background: rgba(26, 28, 30, 0.72); border-radius: 50%; }
.g-stage { position: relative; }
/* The stage video's own pause/play (2026-08-10). The operator asked for the
   browser's control bar gone and the video looping; the bar is the WCAG
   2.2.2 pause mechanism, so it may only be dropped once this replaces it —
   storefront.js unhides this in the same breath as it removes `controls`,
   and a no-JS session keeps the native bar and never sees this. Bottom-left,
   inside the frame, over the lens (which is inert anyway). The disc is the
   thumb play-badge's exact recipe at target size, so the two read as one
   family; ink at 72% on any photograph clears non-text contrast several
   times over. */
.g-play {
  position: absolute; left: 12px; bottom: 12px; z-index: 2;
  width: 44px; height: 44px; display: grid; place-items: center;
  padding: 0; border: 0; border-radius: 50%;
  background: rgba(26, 28, 30, 0.72); color: var(--porcelain);
  transition: background-color 150ms ease-out;
}
.g-play:hover { background: var(--ink); color: var(--porcelain); }
.g-play svg { width: 18px; height: 18px; fill: currentColor; }
/* One glyph shows at a time: pause while it plays, play while it doesn't.
   Both ship in the markup so a toggle never waits on anything. */
.g-play .g-play-pause { display: none; }
.g-play[data-playing] .g-play-pause { display: block; }
.g-play[data-playing] .g-play-play { display: none; }
.stage-media { display: block; width: 100%; aspect-ratio: 1; object-fit: contain; background: var(--card); border: 1px solid var(--hairline); box-shadow: var(--lift); }
.g-stage .photo.stage-media { border: 0; }
/* Hover zoom (2026-08-08): storefront.js sets data-zoomable on the PDP
   grid for fine-pointer, motion-tolerant sessions, then data-zoomed on
   the stage while the cursor is over the image. The zoom is a LENS pane:
   an overlay that pans the 2048px rendition at 225% via
   background-position while the img underneath never moves — the lens is
   pointer-events: none and inset 1px (the img's hairline stays visible),
   so the hover target's geometry never changes. Never zoom by
   transforming the img itself: the scale slides the pixel under the
   cursor away and shifts the hit area, and mouseout/mouseover then chase
   each other in a collapse/expand loop (operator bug report,
   2026-08-08). Only opacity eases (feedback tempo) — the pan itself
   never eases, an eased pan lags the cursor. Keyboard, touch, no-JS, and
   reduced-motion sessions simply see the stage image, which the zoom
   only enhances (the old magnifier link to the raw file was removed at
   the operator's request — never send a shopper off the page). */
[data-zoomable] [data-stage-img] { cursor: zoom-in; }
.g-lens {
  position: absolute; inset: 1px; pointer-events: none; opacity: 0;
  background-color: var(--card); background-repeat: no-repeat; background-size: 225%;
  transition: opacity 150ms ease-out;
}
.g-stage[data-zoomed] .g-lens { opacity: 1; }

.buy { display: grid; align-content: start; gap: 18px; }
/* The background-refresh wrapper mirrors .buy's grid so wrapping the
   dynamic region in one node changes no spacing (storefront.js swaps its
   innerHTML when availability moves). */
.buy-live { display: grid; gap: 18px; }
.buy h1 { font: 400 clamp(1.625rem, 3vw, 2.125rem)/1.2 var(--display); }
.buy .price { margin: 0; font-size: 1.375rem; font-weight: 500; }
/* The price line holds its ground on a combination we don't make
   (2026-08-11) so the pills never slide up under the cursor that just
   clicked one. A dash is the spec-sheet convention for "no value here" and
   needs no words; the sentence below the button carries the meaning, and
   the glyph is aria-hidden so it is never read as one. */
.buy .price-none { color: var(--stone); }
.buy .sku { margin: 0; font-family: var(--mono); font-size: 0.8125rem; color: var(--stone); }
.axis { display: grid; gap: 8px; justify-items: start; }
.opt-label { font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--stone); }
.opt-label b { color: var(--ink); font-weight: 600; letter-spacing: normal; text-transform: none; font-size: 0.84375rem; }
.opts { display: flex; gap: 10px; flex-wrap: wrap; }
.opt {
  display: inline-flex; align-items: center; min-height: 44px; padding: 9px 18px;
  background: var(--card); border: 1px solid var(--hairline-strong); font-size: 0.9375rem;
}
.opt:hover { border-color: var(--ink); color: var(--ink); }
.opt.sel { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
/* Not made with the shopper's OTHER choices (2026-08-10). Deliberately not
   the sold-out treatment: this value IS made and buyable, just not in this
   combination, so it recedes (dashed rim, stone label) instead of being
   struck. Silent by design — the meaning arrives on the click, where the
   column says it in one sentence and offers the real pieces. The mark
   never carries meaning alone: each pill also names the fact in its
   accessible name, and clicking one is always safe. `forced` and `off` are
   mutually exclusive now (a combination we don't make can't be out of
   stock), but .off stays below so a strike would win if both ever landed. */
.opt.forced { border-style: dashed; color: var(--stone); }
.opt-swatch.forced .swatch { border-style: dashed; opacity: 0.7; }
/* Sold out: struck but visible (honest sparse availability) and still a
   link — the landing page says "currently unavailable". Stone, not
   hairline-strong: the label is text and must hold AA. */
.opt.off { color: var(--stone); text-decoration: line-through; background: var(--ivory); }
/* The one line a combination we don't make gets (2026-08-10), standing
   where the availability line stands for a real piece: "We don't make 10 KT
   Yellow in finger size 6.5." Ink, not warn and not stone — it is the
   column's primary fact in that state, and nothing has gone wrong. The
   prose that used to explain a forced switch is gone with the forced
   switch: nothing moves the shopper's choices any more, so there is
   nothing to apologise for. */
.nomake { margin: 0; font-size: 0.9375rem; line-height: 1.5; }
.buy .btn-solid, .buy .btn-unavailable { width: 100%; }
.bag-note { margin: -8px 0 0; color: var(--stone); font-size: 0.875rem; }
.avail { margin: 0; display: flex; align-items: center; gap: 8px; color: var(--verdigris); font-size: 0.875rem; font-weight: 600; }
.avail::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.avail.out { color: var(--stone); }
/* Truthful low stock (DECISIONS 2026-08-08): warn register, and the words
   carry the meaning ("Almost gone — only N left") — never color alone. */
.avail.low { color: var(--warn); }

/* The below-grid REPEATS the gallery grid's columns (and its gap) so its
   cells align with what sits above: the description under the thumb rail +
   stage, the Hallmarks card under the buy column. Explicit grid-column on
   .specs keeps it under the buy column even when there is no description. */
.pdp-below { display: grid; grid-template-columns: 92px minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(20px, 3vw, 40px); padding-top: 48px; align-items: start; }
.pdp-below .pdp-desc { grid-column: 1 / 3; }
.pdp-below .specs { grid-column: 3; }
.pdp-below-nothumb { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }
.pdp-below-nothumb .pdp-desc { grid-column: 1; }
.pdp-below-nothumb .specs { grid-column: 2; }
.pdp-desc h2 { font: 400 1.5rem/1.25 var(--display); margin: 0 0 14px; }
.pdp-desc p { margin: 0 0 12px; color: var(--stone); font-size: 0.96875rem; max-width: 62ch; }
/* The open spec sheet — a card head over quiet hairline rows. */
.specs { border: 1px solid var(--hairline); background: var(--card); }
.specs h2 {
  font: 600 0.8125rem/1.4 var(--ui); letter-spacing: 0.14em; text-transform: uppercase;
  padding: 14px 18px; border-bottom: 1px solid var(--hairline);
}
.spec-list { margin: 0; }
.spec-row { display: flex; justify-content: space-between; gap: 16px; padding: 11px 18px; border-bottom: 1px solid var(--hairline); font-size: 0.90625rem; }
.spec-row:last-child { border-bottom: 0; }
.spec-row dt { color: var(--stone); }
.spec-row dd { margin: 0; font-weight: 500; text-align: right; }
.spec-row dd.mono { font-family: var(--mono); font-size: 0.8125rem; font-weight: 400; }

.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- footer ---- */
.foot { background: var(--chrome); color: var(--on-ink-muted); }
.foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 36px; }
.foot .wordmark { color: var(--porcelain); font-size: 1.125rem; }
.foot p { font-size: 0.875rem; line-height: 1.6; max-width: 34ch; }
/* h3, not h5 — heading levels must not skip (h1 hero → h2 bands → h3);
   the label look is a style, not a semantic level. */
.foot h3 { margin: 0 0 14px; color: var(--porcelain); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; font-family: var(--ui); }
.foot ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; font-size: 0.90625rem; }
.foot a { padding: 2px 0; }
/* var(--on-ink), not #fff: a dark theme's footer chrome is LIGHT and
   its on-ink text dark — the hover must follow the theme's polarity. */
.foot a:hover { color: var(--on-ink, #fff); }
/* The Follow list (2026-09-26) sits under the tagline in the wordmark cell
   and reads as a row, not a fourth column: the same label heading with
   room above it, and the platform names laid out inline with a wrap. */
.foot-follow-heading { margin-top: 28px; }
.foot-follow { grid-auto-flow: column; justify-content: start; gap: 8px 22px; }
/* The legal row (2026-09-25): entity + address under the columns; address is not italic. */
.foot-legal { padding-top: 28px; margin-top: 36px; border-top: 1px solid var(--on-ink-faint); display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 36px; font-size: 0.8125rem; line-height: 1.6; }
.foot-legal p { margin: 0; font-size: inherit; max-width: none; }
.foot-legal address { font-style: normal; text-align: right; }
/* Below 960px (operator, 2026-10-14): the brand cell no longer runs down
   the left column beside Shop. Logo and tagline span the width; under them
   a 2 x 2 grid pairs the columns by length, Care | Shop (two links each)
   then Follow | House (four each), so every pair's headings sit level.
   display: contents lifts the brand cell's children into the grid. */
@media (max-width: 960px) {
  .foot-grid { grid-template-columns: 1fr 1fr; gap: 32px 24px; }
  .foot-brand { display: contents; }
  .foot-brand > .logo, .foot-brand > p, .foot-brand > .em-chip { grid-column: 1 / -1; }
  .foot-brand > .logo { justify-self: start; }
  .foot-brand > p { margin: -20px 0 0; }
  .foot-col-care { grid-area: 3 / 1; }
  .foot-col-shop { grid-area: 3 / 2; }
  .foot-follow-cell { grid-area: 4 / 1; }
  .foot-col-house { grid-area: 4 / 2; }
  .foot-follow-heading { margin-top: 0; }
  .foot-follow { grid-auto-flow: row; }
}
/* Once the legal row wraps, a right-aligned address floats loose under a
   left-aligned copyright: stack both flush left, address first. */
@media (max-width: 480px) {
  .foot-legal { flex-direction: column; gap: 14px; }
  .foot-legal address { text-align: left; order: -1; }
}

/* ---- motion: arrival (life pass, 2026-08-08) ----
   Two tempos (DESIGN_SYSTEM §7): interaction feedback stays 150–240ms
   ease-out; atmosphere — page-head entrances, image zooms, scroll
   reveals — runs 550–650ms on a soft ease. The page head rises once on
   load (pure CSS, no JS dependency); lower bands fade up as they enter
   the viewport (storefront.js adds .reveal only to elements below the
   fold, so nothing ever flashes hidden and no-JS pages show everything). */
@keyframes page-enter { from { opacity: 0; transform: translateY(14px); } }
.hero-copy, .journal-head, .catalog-head, .article-head, .pdp {
  animation: page-enter 600ms cubic-bezier(0.22, 0.61, 0.36, 1) backwards;
}
.hero .photo, .hero .photo-img {
  animation: page-enter 600ms cubic-bezier(0.22, 0.61, 0.36, 1) 120ms backwards;
}
.reveal { opacity: 0; transform: translateY(16px); }
.reveal-in {
  opacity: 1; transform: none;
  transition: opacity 550ms ease-out, transform 550ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ---- responsive ---- */
/* The masthead menu carries COLLECTION names now, and those are longer
   than the four placeholder words it used to hold: measured on the live
   site, five of them need 779px and the nav's column is 658px at 1100px
   wide. A masthead that wraps to two rows reads as a mistake, and the
   drawer beside it already lists the same tree with its children — so
   below 1200px the drawer is simply the menu (2026-08-14). */
@media (max-width: 1200px) {
  /* Below the mega menu's width the whole nav BAND goes, not just the
     links — an empty 14px band under the wordmark is a stray hairline of
     padding nobody asked for. The drawer carries the same tree. */
  .mast-navband { display: none; }
  .drawer { display: block; }
  /* Without a nav row to centre against, the wordmark goes back to the
     left rail: a centred wordmark needs an empty rail as wide as the
     icon cluster, and at 390px there is no such width to spare. */
  /* Without a nav row to centre against, row one goes back to the flex
     layout it had before the split: wordmark left, icons pushed right.
     The three-column grid would spend a gap on an empty middle rail, and
     at 320px there are 5px to spare, not 10 (measured over CDP). */
  .mast { display: flex; gap: 10px; padding-bottom: 8px; }
  .mast-icons { margin-left: auto; }
}

@media (max-width: 960px) {
  .editorial-grid { grid-template-columns: 1fr; }
  /* Full-bleed hero on small screens: the copy takes the full measure, so
     the scrim stays strong across the frame (0.62 right-edge worst case
     over white = #717173, 4.9:1 for the porcelain copy; the short mint
     eyebrow stays left-anchored in the ≥0.75 zone). */
  .hero { min-height: 500px; }
  .hero-copy { padding: 48px 20px; }
  .hero-copy > *, .hero-copy p { max-width: 100%; }
  .hero-scrim { background: linear-gradient(90deg, rgba(26, 28, 30, 0.82) 0%, rgba(26, 28, 30, 0.62) 100%); }
  /* PDP stacks: stage first, thumbs as a horizontal strip, then the buy
     column and the below-grid single-column. */
  .pdp, .pdp-single, .pdp-below, .pdp-below-nothumb { grid-template-columns: 1fr; }
  /* Single column below too — without this the desktop grid-column
     placements would mint implicit columns off the 1fr track. */
  .pdp-below .pdp-desc, .pdp-below .specs { grid-column: 1; }
  .pdp .g-stage { order: 1; }
  .pdp .thumbs { order: 2; grid-auto-flow: column; grid-auto-columns: 64px; justify-content: start; overflow-x: auto; }
  .pdp .buy { order: 3; }
  .journal-row > a { flex-direction: column-reverse; align-items: stretch; }
  .journal-thumb { width: 100%; }
  .grid-products, .trust-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-cats { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mast-nav { display: none; }
  .drawer { display: block; }
}
@media (max-width: 480px) {
  /* Product grids stay two-up on phones (operator, 2026-08-12) — the 960px
     rule's two columns carry through; only the gap tightens so ~150px cards
     keep air. Category tiles keep the two columns too (operator, 2026-08-18:
     one ~355px arch per row cost two screens of scroll before any jewelry —
     DECISIONS 2026-08-18). The trust row stays 2 × 2 too, set smaller
     (operator, 2026-10-14: four full rows of promises read as oversized). */
  .trust-grid { gap: 22px 14px; padding-top: 28px; padding-bottom: 28px; }
  .trust-item { gap: 6px; }
  .trust-item svg { width: 22px; height: 22px; }
  .trust-item strong { font-size: 0.875rem; line-height: 1.3; }
  .trust-item span { font-size: 0.8125rem; line-height: 1.4; }
  .grid-products, .grid-cats { gap: 18px 14px; }
  .band-inner { padding-left: 18px; padding-right: 18px; }
  /* Compact masthead: keep all three 44px icon targets on one 320px row —
     the wordmark shrinks, the targets never do. The phone lockup
     (brand/logo-phone.svg, letters 1.45x the badge's partner) fills the
     row's free space: 166px = 12px padding x 2 + 10px gap + 3 x 44px
     targets. At 28px tall the desktop lockup's letters were ~8px, smaller
     than body text (operator, 2026-10-14; DECISIONS 2026-10-14). */
  .mast { gap: 10px; padding-left: 12px; padding-right: 12px; }
  .wordmark { font-size: 1rem; }
  .mast .wordmark.logo img { height: auto; width: min(224px, calc(100vw - 166px)); }
  .mast-icons { gap: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .btn-solid, .btn-quiet { transition: none; }
  /* The life pass's decorative movement disappears entirely; state
     feedback (underlines, borders, colors) lands instantly instead. */
  .hero-copy, .hero .photo, .hero .photo-img,
  .journal-head, .catalog-head, .article-head, .pdp { animation: none; }
  .cat .photo-img, .p-frame, .p-img-alt { transition: none; }
  a.cat:hover .photo-img, a.cat:focus-visible .photo-img { transform: none; }
  /* The second photo still answers the hover — it just lands instantly,
     with no slide (state feedback, not movement). */
  .product:hover .p-img-alt, .product:focus-visible .p-img-alt { transform: none; }
  .reveal, .reveal-in { opacity: 1; transform: none; transition: none; }
  a, .icon-btn, .mast-nav a::after,
  .band-head a::after, .article-back a::after, .journal-read::after { transition: none; }
}

/* ---------- PDP metal swatches (DECISIONS 2026-08-08) ----------
   Metal options render as circular metallic discs with the value name kept
   visible beneath (Baymard: swatch + label, never color alone). The fills
   are the MIRROR-POLISH gradient (mockup look E, operator-chosen
   2026-08-08): the chrome "horizon" reflection — bright sky above and
   below, a sharp dark line across the middle. Material representation,
   the sanctioned gradient exception class — never chrome decoration.
   Selected = ink ring + bold label; sold out = slashed disc + the
   inherited line-through on the label. Interaction: LIFT (mockup option
   K) — the disc rises to meet the hand on hover/focus; stripped under
   prefers-reduced-motion. The .sw-* fills have an exact twin in
   workbench.css; change TOGETHER. */
.opt.opt-swatch {
  flex-direction: column; justify-content: center; gap: 6px;
  padding: 6px 10px; min-width: 58px;
  background: transparent; border-color: transparent;
}
.opt.opt-swatch:hover { border-color: transparent; }
.opt.opt-swatch.sel { border-color: transparent; box-shadow: none; }
.swatch {
  /* 26px (operator sizing review, 2026-08-08: 30px outweighed the label
     and the selected ring pushed ~38px effective — 26 keeps the polish
     gradient legible while the disc and its caption read as one unit). */
  position: relative; width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--hairline-strong); flex: none;
}
.opt-swatch .swatch { transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, filter 0.2s ease-out; }
.opt-swatch:hover .swatch, .opt-swatch:focus-visible .swatch {
  transform: translateY(-4px);
  box-shadow: 0 6px 14px rgba(26, 28, 30, 0.22), 0 2px 4px rgba(26, 28, 30, 0.14);
  filter: brightness(1.06);
}
.opt-swatch.sel .swatch { box-shadow: 0 0 0 2px var(--porcelain), 0 0 0 4px var(--ink); }
/* Selected + hovered: the state ring rides along with the lift shadow. */
.opt-swatch.sel:hover .swatch, .opt-swatch.sel:focus-visible .swatch {
  box-shadow: 0 0 0 2px var(--porcelain), 0 0 0 4px var(--ink),
    0 6px 14px rgba(26, 28, 30, 0.22);
}
.opt-swatch-label { font-size: 0.75rem; line-height: 1.2; color: var(--stone); text-align: center; }
.opt-swatch.sel .opt-swatch-label { color: var(--ink); font-weight: 600; }
.opt-swatch.off { background: transparent; }
.opt-swatch.off .swatch { opacity: 0.55; }
.opt-swatch.off .swatch::after {
  content: ""; position: absolute; top: 50%; left: -20%;
  width: 140%; height: 2px; background: var(--stone);
  transform: rotate(-45deg);
}
.sw-yellow-gold { background: linear-gradient(180deg, #FBF0C8 0%, #F6E7AB 26%, #C9A24B 46%, #8C6A24 52%, #C9A24B 60%, #F6E7AB 84%, #FBF0C8 100%); }
.sw-rose-gold { background: linear-gradient(180deg, #FAE4DA 0%, #F4D3C5 26%, #D69C87 46%, #96584A 52%, #D69C87 60%, #F4D3C5 84%, #FAE4DA 100%); }
.sw-white-gold { background: linear-gradient(180deg, #FBFCFD 0%, #EFF1F3 26%, #C8CCD1 46%, #848B93 52%, #C8CCD1 60%, #EFF1F3 84%, #FBFCFD 100%); }
.sw-platinum { background: linear-gradient(180deg, #F5F6F5 0%, #E4E6E5 26%, #B7BCBE 46%, #757C80 52%, #B7BCBE 60%, #E4E6E5 84%, #F5F6F5 100%); }
.sw-silver { background: linear-gradient(180deg, #FDFDFC 0%, #ECEEEE 26%, #C4C8C9 46%, #838A8C 52%, #C4C8C9 60%, #ECEEEE 84%, #FDFDFC 100%); }
/* Stainless is the one BASE metal in the vocabulary and sits beside three
   near-identical precious grays, so its fill is deliberately cooler and
   darker: ΔE 12–17 from silver/white gold and 8–12 from platinum at the
   26/46/52% stops, where those three sit only 1.5–6.4 apart. */
.sw-stainless { background: linear-gradient(180deg, #EEF2F4 0%, #D8DEE2 26%, #9FA9B0 46%, #566068 52%, #9FA9B0 60%, #D8DEE2 84%, #EEF2F4 100%); }
@media (prefers-reduced-motion: reduce) {
  .opt-swatch .swatch { transition: none; }
  .opt-swatch:hover .swatch, .opt-swatch:focus-visible .swatch { transform: none; }
}

/* ==== Homepage concept bands (2026-08-12) ==================================
   Sections beyond the adopted specimen: as-worn, education,
   gifting, how-it-works, social proof, shipping & returns. Verdigris /
   Fraunces only; image slots reuse the sanctioned .photo placeholder;
   overlay scrims reuse the category-tile scrim pairing; the dark band uses
   the documented on-ink tiers. */

/* As worn — each card is a link to the exact piece worn in it
   (2026-08-12); hover answers with the category-tile zoom idiom. One row
   on desktop like the category tiles (column auto-flow: every active
   card gets an equal track); phones stack below. */
.worn-grid { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 22px; }
.worn-card { position: relative; display: block; overflow: hidden; box-shadow: var(--lift); }
.worn-card .photo, .worn-card .photo-img { aspect-ratio: 1; width: 100%; }
.worn-card .photo-img { transition: transform 650ms cubic-bezier(0.22, 0.61, 0.36, 1); }
a.worn-card:hover .photo-img, a.worn-card:focus-visible .photo-img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .worn-card .photo-img { transition: none; }
  a.worn-card:hover .photo-img, a.worn-card:focus-visible .photo-img { transform: none; }
}
.worn-cap { position: absolute; inset: auto 0 0 0; padding: 22px 24px;
  background: linear-gradient(transparent, rgba(26, 28, 30, 0.72)); }
.worn-cap .eyebrow { color: var(--mint); margin-bottom: 6px; }
.worn-cap h3 { margin: 0; font: 400 1.2rem/1.25 var(--display); color: #fff; }
.worn-cap .price { margin-top: 4px; font-size: 0.9375rem; color: #fff; }

/* Education */
.edu-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }
.edu-card { display: grid; align-content: start; }
.edu-card .photo { aspect-ratio: 5 / 4; box-shadow: var(--lift); }
.edu-card h3 { margin: 16px 0 0; font: 400 1.2rem/1.3 var(--display); }
.edu-card p { margin: 8px 0 12px; color: var(--stone); font-size: 0.9375rem; line-height: 1.6; }
.edu-card a, .gift-card a { display: inline-block; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--verdigris); }

/* Gifting */
.gifting { background: var(--champagne); }
.gift-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }
.gift-card { background: var(--card); border: 1px solid var(--hairline); }
.gift-card .photo { aspect-ratio: 4 / 3; }
.gift-body { padding: 26px 26px 30px; }
.gift-card h3 { margin: 12px 0 8px; font: 400 1.35rem/1.2 var(--display); }
.gift-card p { margin: 0 0 18px; color: var(--stone); font-size: 0.9375rem; line-height: 1.55; }

/* How it works */
.craft { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; border-top: 1px solid var(--hairline); }
.craft .photo { min-height: 460px; height: 100%; }
.craft-copy { padding: 48px clamp(32px, 5vw, 72px); }
.craft-copy h2 { margin: 14px 0 0; font: 400 clamp(1.75rem, 3vw, 2.375rem)/1.15 var(--display); }
.craft-copy > p { margin: 18px 0 0; color: var(--stone); font-size: 1.0625rem; max-width: 46ch; }
.craft-steps { list-style: none; margin: 26px 0 0; padding: 0; display: grid; gap: 14px; }
.craft-steps li { display: flex; gap: 14px; color: var(--ink); font-size: 0.95rem; line-height: 1.5; }
.craft-steps b { font: 400 1.15rem/1 var(--display); color: var(--verdigris); flex: none; width: 1.6em; }

/* Social proof — dark band (documented on-ink tiers) */
.proof { background: var(--chrome); color: var(--on-ink); }
.proof-head { text-align: center; max-width: 46ch; margin: 0 auto 44px; }
.proof-head .eyebrow { color: var(--mint); margin-bottom: 12px; }
.proof-head h2 { margin: 0; font: 400 clamp(1.75rem, 3.2vw, 2.375rem)/1.2 var(--display); color: var(--on-ink); }
.proof-stats { display: flex; flex-wrap: wrap; justify-content: center; margin-bottom: 50px; }
/* The stats are the last thing in the proof band while no real customer
   quotes exist (2026-09-26) — no trailing gap where the grid used to be. */
.proof-stats:last-child { margin-bottom: 0; }
.proof-stat { padding: 0 40px; text-align: center; border-right: 1px solid rgba(253, 252, 250, 0.16); }
.proof-stat:last-child { border-right: 0; }
.proof-stat .num { font: 400 1.9rem/1 var(--display); color: var(--on-ink); }
.proof-stat .lbl { margin-top: 8px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--on-ink-muted); }
.quote-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; }
.quote-card blockquote { margin: 0 0 14px; font: 400 1.05rem/1.5 var(--display); color: var(--on-ink); }
.quote-card cite { font-style: normal; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--on-ink-muted); }

/* Shipping & returns */
.policy-eyebrow { margin-bottom: 10px; }
.policy-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); margin-top: 8px; }
.policy-item { background: var(--card); padding: 28px 26px; }
.policy-item h3 { margin: 0 0 10px; font: 400 1.2rem/1.3 var(--display); }
.policy-item p { margin: 0; color: var(--stone); font-size: 0.9375rem; line-height: 1.6; }

/* Interactive hero (2026-08-12): when the operator sets hero_image_link,
   a "Shop this piece" chip anchors the hero's bottom-right corner and
   links to the piece worn in the photograph. Solid 0.70-ink chip ground —
   the same worst-case pairing as the tile label scrim, already gated. */
.hero-shop {
  position: absolute; right: 0; bottom: 0; z-index: 2;
  padding: 15px 22px; background: rgba(26, 28, 30, 0.7);
  color: #fff; text-decoration: none; font-size: 0.8125rem; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  transition: background 240ms ease-out;
}
.hero-shop::after { content: " →"; }
.hero-shop:hover { background: var(--ink); color: #fff; }
@media (prefers-reduced-motion: reduce) { .hero-shop { transition: none; } }

/* Hero slideshow (2026-08-12; banners 2026-10-13): storefront.js
   behavior 6 rotates the live banners, each for its own display time.
   The WCAG 2.2.2 pause mechanism is hover/focus (operator decision
   2026-08-12 — no pause button): resting a pointer on the hero or
   tabbing into it suspends the rotation. The visible slide sits on top;
   hidden ones are inert and take no pointer. Each banner picks how it
   ENTERS (data-anim); every exit is the same quiet 900ms fade, so a
   departing slide never moves. The entering state's transitions run;
   the leaving state's transform waits out the fade (0s, 900ms delay) so
   it resets unseen. Reduced motion: every slide swaps instantly and
   nothing moves (below). With JS off only the first slide shows. */
.hero-slide { transition: opacity 900ms ease; }
.hero-slide[aria-hidden="false"] { z-index: 1; }
.hero-slide[aria-hidden="true"] { opacity: 0; pointer-events: none; }
.hero-slide[data-anim="none"] { transition: none; }
.hero-slide[data-anim="slide"][aria-hidden="false"] { transform: none; transition: opacity 900ms ease, transform 900ms cubic-bezier(0.22, 0.61, 0.36, 1); }
.hero-slide[data-anim="slide"][aria-hidden="true"] { transform: translateX(8%); transition: opacity 900ms ease, transform 0s 900ms; }
/* Slow zoom: the photograph drifts in for the banner's whole display
   time (storefront.js sets --hero-hold from data-duration). */
.hero-slide[data-anim="zoom"] > .photo-img { transform: scale(1); transition: transform 0s 900ms; }
.hero-slide[data-anim="zoom"].is-current > .photo-img { transform: scale(1.06); transition: transform var(--hero-hold, 7s) linear; }
@media (prefers-reduced-motion: reduce) {
  .hero-slide,
  .hero-slide[data-anim="slide"][aria-hidden="false"],
  .hero-slide[data-anim="slide"][aria-hidden="true"] { transition: none; transform: none; }
  .hero-slide[data-anim="zoom"] > .photo-img,
  .hero-slide[data-anim="zoom"].is-current > .photo-img { transition: none; transform: none; }
}

/* Phones and tablets (operator, 2026-10-14, "A — Swipe rows" of
   design/mockup-home-mobile.html): nothing on the homepage runs one big card
   per row any more. As worn and the three guides become one sideways row
   each, bleeding to the screen edge; a card is under the full width so the
   next one peeks in, which is the cue to swipe. The cards are links, so a
   keyboard Tab scrolls the row too. The gifts become compact rows, photo
   left. */
@media (max-width: 900px) {
  .gift-grid, .quote-grid, .policy-grid { grid-template-columns: 1fr; }
  .worn-grid, .edu-grid { --row-gutter: 32px; display: flex; overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory; scroll-padding: 0 var(--row-gutter); scrollbar-width: none;
    margin: 0 calc(-1 * var(--row-gutter)); padding: 0 var(--row-gutter) 6px; }
  .worn-grid::-webkit-scrollbar, .edu-grid::-webkit-scrollbar { display: none; }
  .worn-grid { gap: 12px; }
  .edu-grid { gap: 16px; }
  .worn-card { flex: 0 0 78%; scroll-snap-align: start; }
  .edu-card { flex: 0 0 72%; scroll-snap-align: start; }
  .worn-card .photo, .worn-card .photo-img { aspect-ratio: 4 / 5; }
  .worn-cap { padding: 16px 18px; }
  .worn-cap h3 { font-size: 1.05rem; }
  .gift-grid { gap: 12px; }
  .gift-card { display: grid; grid-template-columns: 38% minmax(0, 1fr); }
  .gift-card .photo, .gift-card .photo-img { aspect-ratio: auto; height: 100%; object-fit: cover; }
  .gift-body { padding: 14px 16px 16px; }
  .gift-card h3 { margin: 6px 0 4px; font-size: 1.1rem; }
  .gift-card p { margin: 0 0 8px; font-size: 0.875rem; line-height: 1.45; }
  .craft { grid-template-columns: 1fr; }
  .craft .photo { min-height: 300px; }
  .proof-stat { padding: 0 20px; }
}
@media (max-width: 480px) {
  .worn-grid, .edu-grid { --row-gutter: 18px; }
}

/* Image-led homepage bands run wide (2026-08-12): photography grids may
   fill the viewport (capped at 1720px so ultra-wide monitors don't balloon
   the tiles), while prose bands keep the 1240px column — long text lines
   read worse (WCAG 1.4.8's line-measure guidance), imagery has no such
   limit. Same 32px side padding, so nothing touches the viewport edge. */
.band-inner.band-inner-wide { max-width: 1720px; }

/* ---------- collections (2026-08-14) ----------
   Collection pages reuse the catalog head, the product grid, the pager,
   the arch category tiles, and the article body's typography — the only
   pieces that did not already exist are a reading measure for the
   long-form copy under the grid, the hero image, and the header's
   dropdown panel. */
.catalog-intro { margin: 0; max-width: 62ch; color: var(--stone); font-size: 1.0625rem; line-height: 1.6; }
.collection-hero { display: block; width: 100%; aspect-ratio: 16 / 7; object-fit: cover; box-shadow: var(--lift); }

/* ── Sub-collection chips (2026-08-14) ──────────────────────────────────
   The way DOWN the tree, on a collection page and on /collections/. It
   replaced the 4-up arch tile here: eight children of Pendants & Charms
   stood ~640px tall in square arches, so the shopper met two rows of
   navigation before a single piece of jewellery. Three-up horizontal
   chips put the same eight in ~260px with nothing hidden.

   Thumbnails stay — Baymard 2025 measured 55% of sites missing clear
   subcategory imagery, and a text-only list of "Hoop / Drop & Dangle /
   Ball & Button" gives a shopper who does not know the vocabulary
   nothing to read. The count is the second piece of scent: it says
   whether a branch is worth the click before the click.

   What it is NOT is a carousel. Baymard's carousel work puts ten
   requirements on getting one right (most sites meet few of them, and
   e-commerce use fell from 52% to 32% of homepages); NN/g finds users
   scroll straight past them and read a partly-visible row as a complete
   one. Navigation cannot afford either. A static grid was Baymard's own
   named alternative — it performs as well and there is nothing to get
   wrong. */
.subcats { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px 20px; }
.subcat {
  display: grid; grid-template-columns: 56px 1fr; align-items: center; gap: 14px;
  padding: 8px 14px 8px 8px; min-height: 44px;
  background: var(--card); border: 1px solid var(--hairline);
}
.subcat:hover { border-color: var(--verdigris); }
/* The arch survives at chip scale: the brand's frame, 56px, clipping its
   own image. The full-size label-over-scrim arch tile stays the home
   page's — one arch moment per page (DESIGN_SYSTEM §5). */
.subcat-img {
  width: 56px; height: 56px; overflow: hidden; border-radius: 50% 50% 0 0;
  background: var(--ivory); display: grid; place-items: center;
}
.subcat-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No image yet: the collection's initial in the display face, the same
   answer the model roster's .roster-noportrait gives. A neutral
   placeholder beats an ambiguous photograph — Baymard: a thumbnail that
   misrepresents its branch is worse than none. */
.subcat-mono { font: 400 1.25rem/1 var(--display); color: var(--stone); }
.subcat-text { display: grid; gap: 1px; min-width: 0; }
.subcat-name { font-size: 0.9375rem; font-weight: 500; color: var(--ink); }
.subcat:hover .subcat-name { color: var(--verdigris); }
.subcat-count { font-size: 0.8125rem; color: var(--stone); font-variant-numeric: tabular-nums; }
/* Past twelve chips the tail folds into a native disclosure — every link
   stays in the HTML (the crawl path is never behind a control), it is
   only the visual weight that is capped. Baymard: twenty tiles at one
   weight is where choosing stops being possible. */
.subcats-more { margin-top: 12px; }
.subcats-more > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center;
  padding: 12px 2px; font-size: 0.71875rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--verdigris);
}
.subcats-more > summary::-webkit-details-marker { display: none; }
/* Open, the control moves to the END of the band and flips to "Show
   fewer": a summary always paints above its details content, which left
   "Show all n" stranded between chip 12 and chip 13 once opened
   (operator report, 2026-08-16). Column flex + order relocates it, and
   the summary's two labels swap so the relocated control reads as the
   collapse it now performs. A browser that ignores the reorder simply
   keeps the summary above the tail — the pre-fix layout, never worse. */
.subcats-more[open] { display: flex; flex-direction: column; }
.subcats-more[open] > summary { order: 1; }
.subcats-more > summary .subcats-more-hide { display: none; }
.subcats-more[open] > summary .subcats-more-show { display: none; }
.subcats-more[open] > summary .subcats-more-hide { display: inline; }
/* On /collections/ each chip also carries its own children as a line of
   quiet links: the index is the one page that must reach every branch,
   for a shopper and for a crawler alike. */
.subcats-rich { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px 26px; }
.subcats-rich > li { display: grid; align-content: start; gap: 8px; }
.subcat-kids { display: flex; flex-wrap: wrap; gap: 4px 12px; padding: 0 4px; }
.subcat-kids a { font-size: 0.8125rem; color: var(--stone); }
.subcat-kids a:hover { color: var(--verdigris); }
/* These live HERE, not up in the file's responsive section: the component
   is declared below that section, so a rule of equal specificity written
   up there loses to this block's base columns and the grid silently stays
   three-up at 390px. Verified over CDP — it cost 21px of horizontal
   overflow before it was caught. */
@media (max-width: 960px) {
  .subcats, .subcats-rich { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  /* One chip per row at phone width: 56px of arch plus "Ball & Button
     Earrings" does not survive being halved. */
  .subcats, .subcats-rich { grid-template-columns: 1fr; }
  /* And the fold arrives at six here, not twelve: the twelve that stand
     four rows tall on a desktop stand ~1.6 phone screens as one column
     (operator, 2026-08-14). Hidden only while the disclosure is shut —
     opening it un-matches :not([open]) and the tail returns — and only
     when a fold exists at all (the + sibling), so a short list never
     strands chips 7+. The links stay in the HTML either way; a browser
     without :has() simply keeps the twelve-chip fold. */
  .subcats:has(+ .subcats-more:not([open])) > li:nth-child(n+7) { display: none; }
}
/* The editorial copy sits BELOW the grid and keeps the reading measure the
   journal uses — the same element rules style it, so a collection's copy
   and an article's copy are typographically the same thing. */
.collection-copy { max-width: 760px; margin: 0 auto; }
.collection-related { display: flex; flex-wrap: wrap; gap: 12px; margin: 0; align-items: baseline; }
.collection-related a {
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; padding: 12px 2px; color: var(--verdigris);
}
.collection-related span { color: var(--stone); }

/* ── The mega menu (2026-08-14) ─────────────────────────────────────────
   Row two of the masthead. The nav band is the panel's positioning
   context, so a panel spans the page width instead of hanging off one
   word — and it sits FLUSH under the band, leaving no dead gap for the
   pointer to fall through on its way down (NN/g's "diagonal problem").
   CSS-only and keyboard-first: every trigger is a real link to its
   collection, and :focus-within holds the panel open while a child has
   focus. Card ground with a hairline, never a shadowed floating
   surface (the flat system, DECISIONS 2026-08-07). */
.mast-navband { position: relative; padding: 0 32px 8px; }
.mast-navband .mast-nav { max-width: 1240px; margin: 0 auto; }
.mast-drop { position: static; display: inline-flex; }
.mast-mega {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 40;
  background: var(--card); border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  visibility: hidden; opacity: 0;
  /* Both delays are the point, not decoration. Baymard 2025 measured 61%
     of e-commerce sites opening instantly: the panel then flickers open
     and shut as the pointer crosses sibling triggers on its way
     somewhere else. ~300ms each way is NN/g's and Baymard's range. */
  transition: opacity 160ms ease-out, visibility 160ms;
  transition-delay: 300ms;
}
.mast-drop:hover .mast-mega { visibility: visible; opacity: 1; transition-delay: 300ms; }
/* Keyboard opens it AT ONCE — a delayed answer to Tab reads as broken. */
.mast-drop:focus-within .mast-mega {
  visibility: visible; opacity: 1; transition-delay: 0s;
}
.mega-inner {
  max-width: 1240px; margin: 0 auto; padding: 30px 32px 26px;
  display: grid; gap: 22px;
}
/* Multi-column rather than a grid: only some children have children of
   their own, and on a GRID one tall column sets the row height and every
   short one beside it leaves a hole. Column flow packs them, and
   down-then-across is a mega menu's ordinary reading order anyway. */
.mega-cols { columns: 4; column-gap: 32px; }
.mega-col { break-inside: avoid; display: grid; align-content: start; gap: 2px; padding-bottom: 18px; }
/* The "all jewelry" panel is one flat list of branches — no sub-lists to
   pack, so a plain grid keeps its rows in line across the columns. */
.mega-cols-flat { columns: auto; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 32px; }
.mast-nav .mega-head {
  padding: 6px 0; font-size: 0.875rem; font-weight: 600; letter-spacing: 0.01em;
  text-transform: none; color: var(--ink);
}
.mast-nav .mega-head span { color: var(--stone); font-size: 0.75rem; font-weight: 400; font-variant-numeric: tabular-nums; }
.mast-nav .mega-link {
  padding: 5px 0; font-size: 0.8125rem; font-weight: 400; letter-spacing: 0.01em;
  text-transform: none; color: var(--stone);
}
.mast-nav .mega-head:hover, .mast-nav .mega-link:hover { color: var(--verdigris); }
.mast-nav .mega-all {
  border-top: 1px solid var(--hairline); padding: 14px 0 0;
  display: flex; gap: 12px; align-items: baseline;
  font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
}
.mast-nav .mega-all span { color: var(--stone); letter-spacing: 0.04em; font-weight: 400; text-transform: none; font-size: 0.8125rem; }
.mast-nav .mega-head::after, .mast-nav .mega-link::after, .mast-nav .mega-all::after { content: none; }
/* The trigger for the branch this page sits in stays marked while the
   shopper is inside it — Baymard 2025: 95% of sites never say where you
   are. aria-current="true" (the branch) reads quieter than the
   "page" underline the exact page already owns. */
.mast-nav > .mast-drop > a[aria-current="true"]::after { transform: scaleX(1); }
.mast-nav > .mast-drop > a[aria-current="true"] { color: var(--verdigris); }
@media (prefers-reduced-motion: reduce) {
  .mast-mega { transition: none; }
}

/* ── The compact nav: the mega menu folded for a phone (2026-08-14) ─────
   Below 1200px the panel has nowhere to open, so this sheet IS the menu.
   It replaced a flat list of every link, which measured 2,450px tall at
   390x844 — 2.90 screens — in a 220px column, 39 of whose 45 rows were
   children nobody had asked to see. Folding the branches puts six
   destinations on one screen and leaves each branch one tap away.

   Accordion rather than drill-down, on NN/g's mobile-subnavigation
   guidance: a branch of 6-8 children folds into the global menu, and
   anything deeper is the collection page's own sub-collection chips —
   the "section menu" the same guidance asks for past six. Drill-down was
   rejected on its documented costs (one level visible at a time, the back
   button's meaning broken, no way across to a sibling branch).

   Every level is <details>/<summary>: the disclosure semantics, the
   expanded state and Enter/Space arrive with the element, so the menu is
   complete with JavaScript off. storefront.js adds light dismiss only. */

/* Closed, the toggle is just the masthead's third icon button. */
.mnav-tog-label, .mnav-i-close { display: none; }

@media (max-width: 1200px) {
  /* Open, the <details> ITSELF becomes the layer: the dim fills it (the
     recipe .gal-dialog::backdrop uses) and the two children are placed
     against it as one 400px column at the right edge. Making the details
     the layer is what puts the toggle inside the sheet as its header — a
     <summary> must be its parent's first child, so this is the only
     arrangement where the close control cannot be scrolled off the top of
     the page with the masthead it normally sits in.

     The two children are placed ABSOLUTELY rather than laid out as grid
     rows: Chrome renders a <details>' content inside an internal slot, so
     the sheet is not itself a grid item and a `1fr` row stretched that
     slot while the sheet inside it kept its content height — a white
     panel that stopped under the last link with the dim showing beneath.
     Against the fixed details, `top: 60px; bottom: 0` is exact. */
  .drawer[open] {
    position: fixed; inset: 0; z-index: 60;
    background: rgba(26, 28, 30, 0.45);
  }
  .drawer[open] > .mnav-tog {
    position: absolute; top: 0; right: 0; z-index: 1;
    width: min(100%, 400px); height: 60px; border-radius: 0;
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 14px 0 20px;
    background: var(--card); border-bottom: 1px solid var(--hairline);
  }
  .drawer[open] > .mnav-tog:hover { background: var(--card); }
  .drawer[open] .mnav-tog-label {
    display: block; color: var(--stone);
    font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  }
  .drawer[open] .mnav-i-open { display: none; }
  .drawer[open] .mnav-i-close { display: block; }
  /* The sheet scrolls, the page behind it does not. */
  .drawer[open] .mnav-sheet {
    position: absolute; top: 60px; right: 0; bottom: 0;
    width: min(100%, 400px);
    background: var(--card); overflow-y: auto; overscroll-behavior: contain;
  }
}
/* Same lock the all-photographs overlay uses: without it the page keeps
   scrolling under a sheet that covers it. */
html:has(.drawer[open]) { overflow: hidden; }

.mnav-list { display: grid; align-content: start; }
.mnav-sec, .mnav-flat { border-bottom: 1px solid var(--hairline); }
/* A branch heading and a flat link are the same rank and read alike — the
   masthead's uppercase nav register. */
.mnav-sum, .mnav-flat {
  min-height: 56px; padding: 8px 14px 8px 18px;
  border-left: 2px solid transparent;
  display: flex; align-items: center; gap: 10px;
  font-size: 0.84375rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
}
.mnav-sum { list-style: none; cursor: pointer; }
.mnav-sum::-webkit-details-marker { display: none; }
.mnav-label { flex: 1 1 auto; }
.mnav-count {
  color: var(--stone); font-size: 0.75rem; font-weight: 400; letter-spacing: 0.04em;
  text-transform: none; font-variant-numeric: tabular-nums;
}
.mnav-chev { flex: none; color: var(--stone); transition: transform 180ms ease-out; }
.mnav-sec[open] > .mnav-sum .mnav-chev { transform: rotate(180deg); }
/* The panel is inset, not tinted: a second surface colour would collide
   with the ivory every row uses for hover. */
.mnav-panel { display: grid; border-top: 1px solid var(--hairline); padding-bottom: 6px; }
/* A <summary> is a toggle, never a link, so the branch's own page leads
   its panel — Baymard 2025 found a third of sites stranding the shopper
   who wants the whole branch. Same line the desktop panel closes on. */
.mnav-all {
  min-height: 48px; padding: 12px 20px 12px 30px;
  border-left: 2px solid transparent;
  display: flex; align-items: center; gap: 12px;
  font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
}
.mnav-all span {
  color: var(--stone); font-size: 0.8125rem; font-weight: 400; letter-spacing: 0.04em;
  text-transform: none;
}
/* Children drop out of the uppercase register entirely — a second level
   that shouts as loudly as the first is why the flat list read as long. */
.mnav-child {
  min-height: 48px; padding: 11px 20px 11px 30px;
  border-left: 2px solid transparent;
  display: flex; align-items: center; gap: 12px;
  font-size: 0.9375rem; font-weight: 400; letter-spacing: 0.01em;
  color: var(--stone);
}
.mnav-child span {
  margin-left: auto; color: var(--stone); font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}
.mnav-sum:hover, .mnav-flat:hover, .mnav-all:hover, .mnav-child:hover { background: var(--ivory); }
.mnav-flat:hover, .mnav-all:hover, .mnav-child:hover { color: var(--verdigris); }
/* Where the shopper is standing, in the article rail's active idiom
   (verdigris text behind a 2px verdigris rule). Baymard 2025: 95% of
   sites never mark it, and a phone is where that costs most — a shopper
   usually arrives on a product page from search. */
.mnav-sum[aria-current="true"], .mnav-flat[aria-current="true"], .mnav-child[aria-current="true"] {
  color: var(--verdigris); border-left-color: var(--verdigris); font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .mnav-chev { transition: none; }
}

/* ---- Shopping bag (2026-08-16, 11-cart-design.md slice 1) ----
   The masthead badge, the PDP's added-note, and the /cart/ page. All
   tokens and component language are documented ones: hairline cards,
   quiet/solid buttons, stone meta text, mono identifiers. The component's
   own media query lives HERE, beside it, never in the file's earlier
   responsive section (a later equal-specificity base rule wins — the
   2026-08-14 subcat lesson). */
.bag-link { position: relative; }
.bag-count {
  position: absolute; top: 2px; right: 0;
  min-width: 18px; height: 18px; padding: 0 5px;
  display: grid; place-items: center;
  background: var(--ink); color: var(--porcelain);
  border-radius: 999px;
  font-size: 0.6875rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.bag-count:empty { display: none; }

.bag-form { display: grid; }
.bag-added { font-size: 0.875rem; color: var(--stone); margin: 0; }

.bag-inner { max-width: 880px; }
.bag-lines { list-style: none; margin: 0; padding: 0; }
.bag-line {
  display: grid; grid-template-columns: 92px minmax(0, 1fr) auto auto;
  gap: 20px; align-items: start;
  padding: 24px 0; border-top: 1px solid var(--hairline);
}
.bag-line:last-child { border-bottom: 1px solid var(--hairline); }
.bag-thumb {
  width: 92px; height: 92px; background: #fff;
  border: 1px solid var(--hairline); box-shadow: var(--lift);
}
.bag-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.bag-thumb .photo {
  width: 100%; height: 100%; display: grid; place-items: center;
  background: var(--ivory); color: var(--stone); font-size: 0.75rem;
}
.bag-facts { display: grid; gap: 4px; }
.bag-title { font-weight: 500; color: var(--ink); text-decoration: none; }
a.bag-title:hover { color: var(--verdigris); }
.bag-options { color: var(--stone); font-size: 0.875rem; }
.bag-sku {
  font-family: var(--mono);
  font-size: 0.78125rem; color: var(--stone);
}
.bag-note-line { color: var(--stone); font-size: 0.875rem; }
.bag-controls { display: grid; gap: 8px; justify-items: start; }
.bag-qty { display: flex; align-items: end; gap: 8px; flex-wrap: wrap; }
.bag-qty-label {
  display: block; width: 100%;
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--stone);
}
.bag-qty-input {
  width: 76px; min-height: 44px; padding: 0 10px;
  font-size: 1rem; font-variant-numeric: tabular-nums;
  background: #fff; border: 1px solid var(--hairline-strong); border-radius: 0;
  color: var(--ink);
}
.bag-btn { min-height: 44px; }
.bag-remove {
  min-height: 44px; padding: 0 4px;
  background: none; border: 0; color: var(--err);
  font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; cursor: pointer;
}
.bag-remove:hover { text-decoration: underline; }
.bag-price { display: grid; gap: 2px; justify-items: end; text-align: right; }
.bag-line-total { font-weight: 600; }
.bag-unit { color: var(--stone); font-size: 0.8125rem; }

.bag-summary {
  margin: 32px 0 0 auto; max-width: 380px;
  background: #fff; border: 1px solid var(--hairline); padding: 24px;
  display: grid; gap: 12px;
}
.bag-summary-row { display: flex; justify-content: space-between; font-weight: 600; }
/* The tax row (2026-08-23) carries a statement, not an amount — Stripe
   Tax prices it on the payment page once the address is known — so its
   value reads in the stone register rather than as a bold figure. */
.bag-tax { gap: 16px; }
.bag-tax span:last-child { color: var(--stone); font-weight: 400; font-size: 0.875rem; text-align: right; }
.bag-ship { margin: 0; color: var(--stone); font-size: 0.875rem; }
/* The policy link inside the stone trust line: the article body's link
   recipe, so a link in running text is never color-only (WCAG 1.4.1). */
.bag-ship a { color: var(--verdigris); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.bag-checkout { width: 100%; }
/* The receipt's action row (2026-08-23): "Print receipt" above "Continue
   browsing", both quiet — neither outranks the other, and the order is
   already done, so the group has no primary. The print button is a pure
   enhancement: storefront.js drops its hidden attribute and wires
   window.print(); the [hidden] rule exists because .btn-quiet's
   display:inline-flex would otherwise beat the UA's [hidden] { display:none }. */
.bag-actions { display: grid; gap: 12px; }
.bag-print[hidden] { display: none; }
.bag-note { margin: 0; color: var(--stone); font-size: 0.875rem; text-align: center; }

.bag-empty { text-align: center; padding: 48px 0; display: grid; gap: 16px; justify-items: center; }
.bag-empty p { margin: 0; color: var(--stone); }

/* Stock-change status block (2026-08-16): the flat white card the summary
   already uses — a statement of fact in the house voice, not an alert. */
.bag-status {
  margin: 0 0 24px; padding: 12px 16px;
  background: #fff; border: 1px solid var(--hairline);
  display: grid; gap: 4px;
}
.bag-status-line { margin: 0; color: var(--ink); font-size: 0.875rem; }

@media (max-width: 700px) {
  .bag-line { grid-template-columns: 72px minmax(0, 1fr); }
  .bag-thumb { width: 72px; height: 72px; }
  .bag-controls { grid-column: 2; }
  .bag-price { grid-column: 2; justify-items: start; text-align: left; }
  .bag-summary { max-width: none; margin-left: 0; }
}

/* ---- Bag promotions (2026-08-16, 11-cart-design.md slice 3) ----
   Discount rows in the summary, entered-code chips, and the collapsed
   code disclosure (Baymard: the field hides behind a link because
   eligible promotions already applied themselves). Tokens only. */
.bag-discount { font-weight: 400; color: var(--verdigris); }
.bag-total { border-top: 1px solid var(--hairline); padding-top: 10px; }
.bag-code-chip {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0;
}
.bag-code-name {
  font-family: var(--mono);
  font-size: 0.8125rem; color: var(--ink);
  background: var(--ivory); border: 1px solid var(--hairline);
  border-radius: 999px; padding: 3px 12px;
}
.bag-code-x {
  min-height: 24px; padding: 0 4px; background: none; border: 0;
  color: var(--stone); font-size: 0.75rem; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase; cursor: pointer;
}
.bag-code-x:hover { color: var(--err); text-decoration: underline; }
.bag-code summary {
  cursor: pointer; color: var(--verdigris); font-size: 0.875rem;
  font-weight: 600;
}
.bag-code[open] summary { margin-bottom: 8px; }
.bag-code-row { display: flex; gap: 8px; align-items: stretch; }
.bag-code-input { width: 100%; max-width: 200px; text-transform: uppercase; }
.bag-code-error { margin: 6px 0 0; color: var(--err); font-size: 0.875rem; }

/* ---- print (2026-08-23): the receipt on paper ----
   One print sheet for the storefront; today the order page is the one
   that asks for it (its "Print receipt" button, and the browser's own
   print command on any page). The site chrome goes — announcement bar,
   masthead icons and menu, footer, back-to-top, operator chips, the
   action row — so what prints is the wordmark, the page head, the order
   lines and the summary, on white, in ink. The arrival motion is
   neutralised too: a band storefront.js has marked .reveal (below the
   fold, opacity 0 until scrolled to) would otherwise print as a blank.
   This block sits at the END of the file on purpose — it overrides at
   equal specificity, so source order is what makes it win (LESSONS
   2026-08-14). */
@media print {
  .skip-link, .announce, .mast-icons, .mast-navband, .em-chip, .em-pill,
  .foot, .back-top, .bag-actions { display: none; }
  body { background: #fff; color: #000; }
  .hero-copy, .journal-head, .catalog-head, .article-head, .pdp,
  .hero .photo, .hero .photo-img { animation: none; }
  .reveal, .reveal-in { opacity: 1; transform: none; transition: none; }
  .mast { grid-template-columns: 1fr; justify-items: start; }
  .mast > .wordmark { grid-column: 1; }
  .catalog-head { padding-top: 24px; }
  .band { border-top: 0; }
  .band-inner { padding-top: 24px; padding-bottom: 24px; }
  .bag-inner { max-width: none; }
  .bag-status { padding: 0; border: 0; }
  .bag-summary { max-width: none; margin: 8px 0 0; padding: 0; border: 0; }
  .bag-line { break-inside: avoid; }
  .bag-ship a { color: inherit; text-decoration: none; }
}

/* Listing filters (2026-09-02, storefront/facets.py + _filters.html): the
   sort menu's idiom, one disclosure per facet, options are LINKS with a
   check box and a count that apply on one click. Same flat popover: card
   ground, hairline-strong rim, square corners, no shadow. The applied
   filters sit under the row as removable pills; the sort menu closes the
   row on collection pages. Existing tokens only. */
.filter-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin: 0 0 16px; }
.filter-row .sort-menu { margin-left: auto; }
.filter-menus { display: flex; flex-wrap: wrap; gap: 8px 10px; }
.filter-drawer { display: none; }
.filter-menu { position: relative; }
.filter-menu > summary, .filter-drawer > summary, .filter-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 14px 8px 16px;
  font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink); text-decoration: none;
  background: var(--card); border: 1px solid var(--hairline-strong);
  cursor: pointer; list-style: none;
}
.filter-menu > summary::-webkit-details-marker, .filter-drawer > summary::-webkit-details-marker { display: none; }
.filter-menu > summary:hover, .filter-drawer > summary:hover, .filter-toggle:hover { border-color: var(--ink); }
.filter-menu > summary svg, .filter-drawer > summary svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2; }
.filter-menu[open] > summary svg, .filter-drawer[open] > summary svg { transform: rotate(180deg); }
.filter-menu[open] > summary { border-color: var(--ink); }
.filter-count { color: var(--verdigris); font-variant-numeric: tabular-nums; }
.filter-count:empty { display: none; }
.filter-options {
  position: absolute; left: 0; top: calc(100% + 4px); z-index: 30;
  min-width: 250px; margin: 0; padding: 4px 0;
  background: var(--card); border: 1px solid var(--hairline-strong);
}
.filter-opt {
  display: flex; align-items: center; gap: 12px; padding: 10px 16px;
  font-size: 0.9375rem; color: var(--ink); text-decoration: none; white-space: nowrap;
}
.filter-opt:hover { background: var(--ivory); color: var(--verdigris); }
.filter-opt .box, .filter-toggle .box {
  flex: none; width: 18px; height: 18px; border: 1px solid var(--hairline-strong); background: var(--card);
  display: inline-grid; place-items: center;
}
.filter-opt .box svg, .filter-toggle .box svg { width: 12px; height: 12px; stroke: var(--porcelain); fill: none; stroke-width: 2.5; opacity: 0; }
.filter-opt[aria-current] { font-weight: 600; color: var(--verdigris); }
.filter-opt[aria-current] .box, .filter-toggle[aria-current] .box { background: var(--verdigris); border-color: var(--verdigris); }
.filter-opt[aria-current] .box svg, .filter-toggle[aria-current] .box svg { opacity: 1; }
.filter-opt .n { margin-left: auto; padding-left: 16px; color: var(--stone); font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
/* A value the other filters leave nothing under stays listed (nothing
   silently disappears) and reads as unavailable: stone text, no fade, so
   the label keeps its AA contrast; the zero count says the rest. */
.filter-opt.none { color: var(--stone); }
/* A full facet's other values (MAX_VALUES_PER_FACET, 2026-10-13) are
   text, not links: the same stone register as an unavailable value, no
   hover, no check box mark. The note above them says why. */
.filter-opt.capped { color: var(--stone); cursor: default; }
.filter-opt.capped:hover { background: none; color: var(--stone); }
.filter-cap-note { margin: 0; padding: 8px 16px 6px; color: var(--stone); font-size: 0.8125rem; white-space: normal; max-width: 250px; }
.filter-toggle[aria-current] { color: var(--verdigris); border-color: var(--verdigris); }
.filter-applied { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 22px; }
.filter-pill {
  display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 4px 10px 4px 12px;
  border-radius: 999px; background: var(--ivory); color: var(--ink); font-size: 0.8125rem; font-weight: 600; text-decoration: none;
}
.filter-pill svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2; }
.filter-pill:hover { background: var(--verdigris-soft); color: var(--verdigris); }
.filter-clear { margin-left: 4px; padding: 6px 0; font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--verdigris); text-decoration: none; }
.filter-clear:hover { text-decoration: underline; text-underline-offset: 3px; }
.filter-result { margin: 0 0 22px; color: var(--stone); font-size: 0.9375rem; font-variant-numeric: tabular-nums; }
.filter-empty { padding: 48px 0; text-align: center; }
.filter-empty a { color: var(--verdigris); font-weight: 600; }
/* Price: a histogram of where the pieces sit, two thumbs on a log track,
   the numbers as real fields, and an explicit Apply. A slider is not a
   link, so it never navigates on its own (WCAG 3.2.2); the slider is a
   JavaScript upgrade of the fields (hidden until storefront.js reveals it). */
.price-panel {
  position: absolute; left: 0; top: calc(100% + 4px); z-index: 30;
  width: 340px; padding: 18px 18px 16px; display: grid; gap: 14px;
  background: var(--card); border: 1px solid var(--hairline-strong);
}
.price-hist { display: flex; align-items: flex-end; gap: 2px; height: 56px; }
.price-hist span { flex: 1; min-height: 2px; background: var(--hairline); }
.price-hist span.in { background: var(--verdigris); opacity: 0.35; }
.price-slider { position: relative; height: 24px; margin: -6px 10px 0; }
.price-slider[hidden] { display: none; }
.price-slider .track { position: absolute; left: 0; right: 0; top: 11px; height: 2px; background: var(--hairline-strong); }
.price-slider .fill { position: absolute; top: 11px; height: 2px; background: var(--verdigris); }
.price-slider input[type="range"] {
  position: absolute; left: -10px; width: calc(100% + 20px); top: 0; height: 24px; margin: 0;
  appearance: none; -webkit-appearance: none; background: transparent; pointer-events: none;
}
.price-slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; pointer-events: auto; width: 22px; height: 22px; border-radius: 50%;
  background: var(--card); border: 1.5px solid var(--ink); cursor: grab;
}
.price-slider input[type="range"]::-moz-range-thumb {
  pointer-events: auto; width: 20px; height: 20px; border-radius: 50%;
  background: var(--card); border: 1.5px solid var(--ink); cursor: grab;
}
.price-slider input[type="range"]:focus-visible { outline: none; }
.price-slider input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--verdigris); outline-offset: 2px; }
.price-slider input[type="range"]:focus-visible::-moz-range-thumb { outline: 2px solid var(--verdigris); outline-offset: 2px; }
.price-fields { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 10px; }
.price-fields label { display: grid; gap: 4px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--stone); }
.price-fields .dash { padding-bottom: 14px; color: var(--stone); }
.price-in { display: flex; align-items: center; min-height: 44px; padding: 0 10px; border: 1px solid var(--hairline-strong); background: var(--card); }
.price-in > span { color: var(--stone); font-size: 1rem; }
.price-in input { width: 100%; min-width: 0; padding: 8px 4px; border: 0; outline: 0; background: transparent; font: 400 1rem var(--ui); color: var(--ink); -moz-appearance: textfield; }
.price-in input::-webkit-outer-spin-button, .price-in input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.price-in:focus-within { outline: 2px solid var(--verdigris); outline-offset: 2px; }
.price-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.price-actions .btn-quiet { min-height: 44px; padding: 8px 18px; }
.price-range-note { color: var(--stone); font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
/* Phone: the facets fold into one Filter disclosure and stack as
   sections; the popovers join the flow. Every link is in the HTML either
   way: the fold is visual weight, never a gate on the crawl path. */
@media (max-width: 760px) {
  .filter-drawer { display: block; }
  .filter-menus { display: none; flex-basis: 100%; flex-direction: column; gap: 0; border: 1px solid var(--hairline-strong); background: var(--card); }
  .filter-drawer[open] ~ .filter-menus { display: flex; }
  .filter-menu > summary, .filter-toggle { width: 100%; border: 0; border-top: 1px solid var(--hairline); justify-content: space-between; }
  .filter-menus > :first-child > summary, .filter-menus > .filter-toggle:first-child { border-top: 0; }
  .filter-menu > summary svg { margin-left: auto; }
  .filter-menu[open] > summary { border-color: var(--hairline); }
  .filter-options, .price-panel { position: static; width: auto; min-width: 0; border: 0; border-top: 1px solid var(--hairline); }
  .filter-options { padding: 4px 0 8px; }
  .filter-opt { white-space: normal; padding: 12px 16px; }
}
/* Phone: "Filter · n" and the sort control share the band's 326px line
   only once the sort drops its "Sort by:" word — the current value in
   verdigris still names it (the summary keeps the words for assistive
   tech via a visually-hidden span). */
@media (max-width: 560px) {
  .filter-row .sort-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}

/* In-place listing swaps (2026-09-02): while the next grid band loads,
   the current one dims — the in-page feedback rule for anything async —
   and comes back at full strength when the band is replaced. */
#products[aria-busy="true"] .grid-products,
#products[aria-busy="true"] .filter-row,
#products[aria-busy="true"] .filter-applied { opacity: 0.55; transition: opacity 150ms ease-out; }
#products:focus { outline: none; }
@media (prefers-reduced-motion: reduce) {
  #products[aria-busy="true"] .grid-products,
  #products[aria-busy="true"] .filter-row,
  #products[aria-busy="true"] .filter-applied { transition: none; }
}

/* ---- The on-site pay page (2026-10-10, mockup B) ----
   Our fields left, the bag summary card right (stacked first on phones
   so the total is read before the form). Square inputs with the bag's
   hairline-strong border, verdigris focus, the eyebrow-label recipe.
   Stripe's card field mirrors these in checkout.js (literal hex). */
.band-inner.co-inner { padding-top: 32px; }
.co-grid { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 40px; align-items: start; }
.co-grid .co-summary { margin: 0; max-width: none; }
.co-steps { display: grid; gap: 28px; }
.co-step { border: 0; padding: 0; margin: 0; min-width: 0; }
.co-step legend { padding: 0; }
.co-step h2, .co-summary-title { font: 400 1.5rem/1.2 var(--display); margin: 0 0 14px; }
.co-summary-title { font-size: 1.25rem; margin: 0; }
.co-stack { display: grid; gap: 12px; }
.co-field { display: grid; gap: 6px; }
.co-field label {
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--stone);
}
.co-input {
  width: 100%; min-height: 48px; padding: 0 14px;
  font: 400 1rem var(--ui); color: var(--ink);
  background: #fff; border: 1px solid var(--hairline-strong); border-radius: 0;
}
.co-input:focus { outline: 2px solid var(--verdigris); outline-offset: 1px; }
.co-input[readonly] { background: var(--ivory); }
.co-3 { display: grid; grid-template-columns: 2fr 1.4fr 1fr; gap: 12px; }
.co-ship-opt {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 12px; align-items: center;
  min-height: 56px; padding: 8px 16px; cursor: pointer;
  background: #fff; border: 1px solid var(--hairline-strong);
}
.co-ship-opt:has(input:checked) { border-color: var(--verdigris); background: var(--verdigris-soft); }
.co-ship-opt input { width: 20px; height: 20px; margin: 0; accent-color: var(--verdigris); }
.co-ship-name { display: grid; gap: 2px; font-weight: 500; }
.co-ship-price { font-weight: 600; }
.co-payment { min-height: 48px; }
.co-fine { margin: 0; color: var(--stone); font-size: 0.875rem; font-weight: 400; }
.co-fine a { color: var(--verdigris); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.co-callout {
  max-width: 72ch; margin: 0 0 28px; padding: 12px 16px;
  background: #fff; border-left: 3px solid var(--verdigris);
  color: var(--ink); font-size: 0.9375rem;
}
.co-error:empty { display: none; }
.co-error {
  padding: 12px 16px; background: var(--err-tint); border-left: 3px solid var(--err);
  color: var(--ink); font-size: 0.9375rem;
}
.co-error:focus { outline: 2px solid var(--err); outline-offset: 2px; }
.co-pay { width: 100%; min-height: 52px; }
.co-pay[aria-busy="true"] { cursor: progress; }
.co-mini { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: 12px; align-items: center; font-size: 0.875rem; }
.co-mini img, .co-mini-blank {
  width: 56px; height: 56px; object-fit: contain; display: block;
  background: var(--ivory); border: 1px solid var(--hairline);
}
.co-mini-facts { display: grid; gap: 2px; }
.co-mini-title { font-weight: 500; }
.co-rule { border: 0; border-top: 1px solid var(--hairline); margin: 4px 0; width: 100%; }
.co-row { display: flex; justify-content: space-between; gap: 12px; color: var(--stone); font-size: 0.9375rem; }
.co-row[hidden] { display: none; }
@media (max-width: 900px) {
  .co-grid { grid-template-columns: 1fr; }
  .co-grid .co-summary { order: -1; }
}
@media (max-width: 560px) {
  .band-inner.co-inner { padding-left: 16px; padding-right: 16px; }
  .co-3 { grid-template-columns: 1fr; }
}

/* ---- the opening list (2026-10-13, core/opening.py; DECISIONS 2026-10-13) ----
   Pre-opening only: the bar's link, the one card (dialog / bag / page),
   the product page's note. The bar link is underlined — on the midnight
   band it is the only way to tell it is a link (WCAG 1.4.1). */
.announce a { color: var(--porcelain); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; font-weight: 600; }
.announce a:hover { text-decoration-thickness: 2px; }
.announce a:focus-visible { outline: 2px solid var(--porcelain); outline-offset: 2px; }
.announce .sep { opacity: 0.55; margin: 0 0.6em; }

.launch { background: var(--card); border: 1px solid var(--hairline); padding: 28px; display: grid; gap: 14px; text-align: left; position: relative; }
.launch h2 { margin: 0; font: 500 1.625rem/1.15 var(--display); color: var(--ink); }
.launch p { margin: 0; color: var(--stone); font-size: 0.9375rem; line-height: 1.55; }
.launch .big { font: 500 3rem/1 var(--display); color: var(--verdigris); letter-spacing: -0.01em; }
.launch .big small { display: block; margin-top: 6px; font: 600 0.75rem/1 var(--ui); letter-spacing: 0.18em; text-transform: uppercase; color: var(--stone); }
.launch-form { display: grid; gap: 8px; }
.launch-form label { font-size: 0.8125rem; font-weight: 600; color: var(--ink); }
.launch-row { display: flex; gap: 8px; }
.launch-row input { flex: 1; min-width: 0; min-height: 48px; padding: 10px 14px; border: 1px solid var(--hairline-strong); background: var(--card); font: 400 1rem var(--ui); color: var(--ink); }
.launch-row input:focus-visible { outline: 2px solid var(--verdigris); outline-offset: 2px; }
.launch-row .btn-solid { white-space: nowrap; }
.launch-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.launch .launch-error { color: var(--err); font-size: 0.875rem; }
.launch .launch-error:empty { display: none; }
.launch .fine { font-size: 0.8125rem; }
.launch .fine a { color: var(--verdigris); text-decoration: underline; text-underline-offset: 3px; }
.launch-done { display: none; }
.launch.is-done .launch-form, .launch.is-done .pre { display: none; }
.launch.is-done .launch-done { display: grid; gap: 10px; justify-items: start; }
.launch-done h2:focus { outline: none; }
/* In the bag's 380px summary the field and button stack. */
.bag-summary .launch { padding: 20px; border-color: var(--verdigris); }
.bag-summary .launch h2 { font-size: 1.25rem; }
.bag-summary .launch-row { flex-direction: column; }
.bag-opening span:last-child { color: var(--ink); }
.bag-opening small { font-weight: 400; color: var(--stone); font-size: 0.875rem; }

dialog.launch-dlg { border: 0; padding: 0; max-width: 520px; width: calc(100% - 32px); background: transparent; }
dialog.launch-dlg::backdrop { background: rgba(26, 28, 30, 0.45); }
dialog.launch-dlg .launch { padding: 36px; }
.launch-x { position: absolute; top: 8px; right: 8px; width: 44px; height: 44px; border: 0; background: none; font-size: 1.5rem; line-height: 1; color: var(--stone); cursor: pointer; }
.launch-x:hover { color: var(--ink); }
html:has(.launch-dlg[open]) { overflow: hidden; }

.open-note { margin: -6px 0 0; font-size: 0.875rem; color: var(--stone); }
.open-note a { color: var(--verdigris); text-decoration: underline; text-underline-offset: 3px; font-weight: 600; }

.opening-inner { max-width: 600px; display: grid; gap: 24px; }
.opening-head { display: grid; gap: 10px; }
.opening-head h1 { font: 400 clamp(2rem, 4vw, 3rem)/1.1 var(--display); letter-spacing: -0.01em; }
.opening-browse { color: var(--stone); }
.opening-browse a { color: var(--verdigris); text-decoration: underline; text-underline-offset: 3px; }

@media (max-width: 560px) {
  .launch-row { flex-direction: column; }
  dialog.launch-dlg .launch { padding: 28px 20px; }
  .launch .big { font-size: 2.5rem; }
}
