/* ═══════════════════════════════════════════════════════════════
   AURAVOX · Shop CSS
   Supplements brand/tokens.css with shop-specific components.
   All token references use Tier 3 maps tokens only.
   ═══════════════════════════════════════════════════════════════ */

/* ── Layout ───────────────────────────────────────────────────── */

.shop-layout { min-height: 100vh; display: flex; flex-direction: column; }
.shop-main { flex: 1; }
.shop-container { max-width: 1200px; margin: 0 auto; padding: 0 var(--page-px); }

/* chrome.js injects .nav into this node. Without a reserved height the page
   starts at y=0 and jumps --nav-h once the bar arrives. */
#nav-root { min-height: var(--nav-h); }

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

/* Caption bones under a pending well. Two lines, never a third — comparison
   cards are locked to two (STYLE-STANDARDS §3.1). min-width: 0 so a long
   implicit min-content cannot blow a 1fr track. */
.skel-line {
  height: var(--lh-body);
  width: 60%;
  min-width: 0;
  margin-top: var(--space-1);
}
.skel-line.skel-w-40 { width: 40%; height: var(--type-caption); }

.product-card.is-pending,
.style-card.is-pending,
.look-card.is-pending {
  pointer-events: none;
  box-shadow: none;
  transform: none;
}
.product-card.is-pending:hover,
.style-card.is-pending:hover {
  box-shadow: none;
  transform: none;
}

/* ── Page header ──────────────────────────────────────────────── */

.page-header {
  padding: var(--space-6) 0 var(--space-5);
  margin-bottom: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.page-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}
/* With the trail lifted into its own bar, a header that held nothing else is left as an
   empty box still carrying its padding. */
.page-header:empty { display: none; }

/* Crumbs-only header (shot page) — same top inset as a listing, no title gap. */
.page-header-trail {
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-2);
}
.page-title {
  font-size: var(--type-h3);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  line-height: var(--lh-h3);
  margin-bottom: var(--space-2);
}
.page-subtitle {
  font-size: var(--type-body-sm);
  color: var(--text-muted);
  line-height: var(--lh-body);
}
.page-subtitle:empty { min-height: var(--lh-body); }


/* ── 925 Sterling Silver badge ────────────────────────────────── */
/* Looks like a hallmark — used on cards and page headers */

.badge-925 {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: var(--border-w-small) solid var(--border-info);
  border-radius: var(--radius-badge);
  padding: var(--space-1) var(--space-3);
  background: var(--surface-info);
  flex-shrink: 0;
}
.badge-925-num {
  font-size: var(--type-caption);
  font-weight: var(--weight-bold);
  color: var(--text-info);
  -webkit-text-fill-color: var(--text-info);
  letter-spacing: 0.05em;
  line-height: 1;
}
.badge-925-sep {
  width: 1px;
  height: var(--space-3);
  background: var(--border-info);
  opacity: 0.5;
  flex-shrink: 0;
}
.badge-925-label {
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-info);
  -webkit-text-fill-color: var(--text-info);
  line-height: 1;
}

/* Larger variant for page headers */
.badge-925-lg {
  padding: var(--space-1h) var(--space-4);
  border-radius: var(--radius-sm);
}
.badge-925-lg .badge-925-num { font-size: var(--type-body-sm); }
.badge-925-lg .badge-925-label { font-size: var(--type-label); }
.badge-925-lg .badge-925-sep { height: var(--space-4); }

/* Smaller variant for compact card rows */
.badge-925-sm {
  padding: 1px var(--space-1);
  gap: var(--space-1);
}
.badge-925-sm .badge-925-num { font-size: var(--type-label); }
.badge-925-sm .badge-925-label { font-size: var(--type-label); }
.badge-925-sm .badge-925-sep { height: var(--space-2); }

/* The "New" flag on category cards — the green twin of the 925 hallmark badge.
   Same pill geometry, and the same light-fill / mid-border / dark-text ramp the
   925 badge uses over card images (see .product-card-img-outer .badge-925), only
   shifted from blue to green. Fixed opaque hex, not the translucent
   --surface-success tint: this sits over the black chains macro, so the fill must
   cover rather than tint. Theme-independent for the same reason. */
.badge-new {
  display: inline-flex;
  align-items: center;
  border: var(--border-w-small) solid var(--badge-new-border);
  border-radius: var(--radius-badge);
  padding: var(--space-1) var(--space-3);
  background: var(--badge-new-fill);
  color: var(--badge-new-ink);
  -webkit-text-fill-color: var(--badge-new-ink);
  font-size: var(--type-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  line-height: 1;
  flex-shrink: 0;
}


/* ── Style page filter row ───────────────────────────────────── */
.style-filter-row {
  display: flex;
  align-items: flex-end;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}
.style-filter-row .field { width: 200px; }

/* ── Category cards (landing page) ───────────────────────────── */

.cat-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  padding: var(--space-8) 0 var(--space-16);
}
@media (min-width: 640px) {
  /* Four cards of one size, two up. Nothing is placed by hand any more: Chains used to
     span two rows with the two guides stacked beside it and Gallery full-width beneath,
     which is three different card shapes on one screen. The guides are now one card, so
     the grid is a plain 2x2 and DOM order IS reading order — Chains, Gallery, Chain
     Guide, Instagram. Rows size to their tallest card and both cards stretch to it. */
  .cat-grid { grid-template-columns: repeat(2, 1fr); }
}

.cat-card {
  position: relative;
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  overflow: hidden;
  text-decoration: none;
  display: flex;
  flex-direction: column;
  transition: box-shadow var(--transition-base), transform var(--transition-base);
}
.cat-card:not([aria-disabled="true"]):hover {
  box-shadow: var(--shadow-lg);
  transform: translateY(-3px);
}
.cat-card[aria-disabled="true"] { opacity: 0.55; pointer-events: none; }

.cat-card-badge {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  z-index: 1;
}
.cat-card-img {
  aspect-ratio: 4 / 3;
  background: var(--surface-raised);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
.cat-card-img img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--transition-layout);
}
.cat-card:not([aria-disabled="true"]):hover .cat-card-img img { transform: scale(1.07); }
.cat-card-img-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-subtle);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}
.cat-card-body {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
}
.cat-card-name {
  font-size: var(--type-h4);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  line-height: var(--lh-h4);
}
.cat-card-desc {
  font-size: var(--type-body-sm);
  color: var(--text-muted);
  line-height: var(--lh-body);
}
.cat-card-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-action);
}

/* ── Style cards (chains listing) ────────────────────────────── */

.style-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}
/* Phone breakpoints match .product-grid deliberately: style cards used to go two-up only at
   480px, so on a phone the style list was one enormous card per row while the width list on the
   next page was already two — the same shop behaving like two different ones.
   Desktop intentionally differs: .product-grid goes four-up at 1024px, but there are only six
   chain styles, and six is two tidy rows of three rather than a row of four and an orphan pair. */
/* 400px, not the sm tier (480). DELIBERATE EXCEPTION, measured — see the breakpoint policy in
   brand/tokens.css. The sm tier would hold this grid at ONE column from 400-479px, which is
   iPhone XR/11 (414), Pro Max (428/430) and most large Androids (412): the widths GROK.md's card
   protocol names first, and probably most of the shop's mobile traffic. Card width at 2-up on a
   400px viewport is ~180px, which the two-line caption already fits — the step is set by the
   narrowest card the caption survives, not by a device class, so it is content-driven the same
   way .finder-grid's are panel-driven. Verified against production before and after. */
@media (min-width: 400px) { .style-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 768px) { .style-grid { grid-template-columns: repeat(3, 1fr); } }

.style-card {
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  overflow: hidden;
  text-decoration: none;
  /* Without this the anchor falls back to the browser's default link blue — invisible
     only because every text child sets its own colour, but non-conforming and a hazard.
     Matches .product-card. */
  color: var(--text-body);
  display: flex;
  flex-direction: column;
  transition: box-shadow var(--transition-base), transform var(--transition-base);
}
.style-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}
.style-card-img {
  aspect-ratio: 1 / 1;
  background: var(--surface-raised);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.style-card-img img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transform: scale(var(--card-zoom, 1));
  transition: transform var(--transition-layout);
}
/* 10%, matching .product-card-img-primary — one hover feel across every card in the shop.
   Multiplied by --card-zoom rather than replacing it, so a frame that starts magnified still
   leans the same 10% further on hover. */
.style-card:hover .style-card-img img { transform: scale(calc(var(--card-zoom, 1) * 1.10)); }
@media (prefers-reduced-motion: reduce) {
  .style-card-img img { transition: none; }
}
.style-card-img-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-subtle);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
}
/* Same inset as .product-card-body (Find by fit, style listings).
   Two text lines, always: the name, then range + count on one spec
   row. Spec is caption (12px) — a step up from the 10px label that
   first locked the row, still under the 14px that wrapped on a
   two-up phone. The icon is its own flex item and never shrinks, so
   padding cannot clip it; a long name ellipsizes instead. */
.style-card-body {
  padding: var(--space-3) var(--space-2h);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}
.style-card-name {
  display: flex;
  align-items: baseline;
  min-width: 0;
  font-size: var(--type-h6);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  line-height: var(--lh-h6);
}
.style-card-name > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.style-card-name .chain-icon {
  flex: 0 0 auto;
}
.style-card-meta {
  font-size: var(--type-caption);
  color: var(--text-muted);
  line-height: var(--lh-caption);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.style-card-meta > span { white-space: nowrap; }

.style-card-meta-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-1h);
  flex-wrap: nowrap;
  min-width: 0;
}
.style-card-count {
  font-size: var(--type-caption);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  line-height: var(--lh-caption);
  flex: 0 0 auto;
  white-space: nowrap;
}
.style-card-price {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  margin-top: auto;
  padding-top: var(--space-3);
}
.style-card-cta {
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-action);
  margin-top: var(--space-2);
}

/* ── Chain style icons ────────────────────────────────────────
   Traced from the product renders (scripts/chain-icons/). Flat greyscale, not
   currentColor glyphs — they are pictures of a link pattern.

   LENGTH is the control, and it is identical for all six. Height falls out of each
   icon's own proportions, so a popcorn draws thicker than a rolo — which is true of
   the chains. Sizing by height instead left a ragged right edge down a column of
   cards, because the canvases run from 3.40:1 to 4.13:1.

   Both numbers are tied to the CAP HEIGHT of the text the icon sits beside, not to
   its font-size, so the icon sits IN the line rather than over it:

     --chain-icon-cap  0.70em — measured from Coconat's capital H, not assumed
     --chain-icon-len  cap x 3.40 — figaro is the squattest canvas at 3.40:1, so
                       this makes figaro exactly cap height and every other icon a
                       little under. Nothing can overshoot the capitals.

   em, not px, so an icon beside the 20px style-card title scales with it. */
:root {
  --chain-icon-cap: 0.7em;
  --chain-icon-len: 2.38em;
  --chain-icon-len-table: 62px;   /* comparison table — see .cmp-style below */
  /* Fraction of the cap a graded icon takes. chain-icon.js overrides it per card from
     the chain's width; 1 is both the default and the ceiling. */
  --chain-icon-grade: 1;
}
/* The icon rides on the title's own line as an inline box. Making the *titles* flex
   rows instead looks equivalent and is not: .pcard-mm holds "2<span>mm</span> ·
   50<span>cm</span>" on the finder card, and a flex row puts its gap between the
   number and its unit — "2 mm · 50 cm". Staying inline leaves every existing title
   untouched and needs no wrapper element. */
.chain-icon {
  display: inline-block;
  /* Bottom edge ON the text baseline. For a replaced element the baseline IS the
     bottom margin edge, so this seats the icon on the line exactly as a letter is —
     `middle` floated it off the line and made the row look loose. */
  vertical-align: baseline;
  margin-left: var(--space-2);
  width: var(--chain-icon-len);
  height: auto;
  /* Belt and braces: a canvas squatter than figaro's would otherwise poke above the
     capitals. Nothing in the set does today, and the unit test pins that. */
  max-height: var(--chain-icon-cap);
  /* If it ever lands as a direct flex child: the card bodies are flex columns, whose
     default align-items:stretch blows an <img> box out to the full card width. The
     SVG's own preserveAspectRatio then letterboxes the drawing inside that box, so
     it does not distort — it just silently centres under a left-aligned heading. */
  align-self: center;
  flex: 0 0 auto;
}

/* The product page's "About the … link" section leads with the drawing, so there it
   is a picture rather than a mark: its own line, well above cap height. */
.pdp-style-icon {
  display: block;
  width: min(224px, 49%);
  max-height: none;
  /* No margin: it sits between two definitions in .pdp-deflist, which is a flex column
     with its own gap. A margin here would space it twice. */
  margin: 0;
}

/* Sized by the chain's width instead of the shared length — style-listing cards only,
   where every card is the same link pattern. Height leads here so the grade lands
   exactly on a fraction of the cap; --chain-icon-grade tops out at 1, so the tallest
   icon on the page is precisely as tall as the capitals beside it. */
.chain-icon-graded {
  width: auto;
  height: calc(var(--chain-icon-cap) * var(--chain-icon-grade, 1));
}

/* The Style spec row puts the icon FIRST, so the gap goes on the other side. Height
   leads here rather than length, so the icon fills the cap exactly instead of stopping
   a little under it the way the shared length leaves it — the same ceiling as every
   other inline icon, just taken all the way up to it. */
.pdp-spec-val .chain-icon {
  margin-left: 0;
  margin-right: var(--space-2);
  width: auto;
  height: var(--chain-icon-cap);
}

/* The comparison table reads the icons as a set, down a column, at a size that has
   nothing to do with the row's type — so it sets its own length. */
.cmp-style .chain-icon {
  display: block;          /* its own line, under the style name */
  width: var(--chain-icon-len-table);
  max-height: none;
  margin: var(--space-1h) 0 0;
}

/* ── Product variant cards (style pages) ─────────────────────── */

.product-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}
/* 400px, not the sm tier (480). DELIBERATE EXCEPTION, measured — see the breakpoint policy in
   brand/tokens.css. The sm tier would hold this grid at ONE column from 400-479px, which is
   iPhone XR/11 (414), Pro Max (428/430) and most large Androids (412): the widths GROK.md's card
   protocol names first, and probably most of the shop's mobile traffic. Card width at 2-up on a
   400px viewport is ~180px, which the two-line caption already fits — the step is set by the
   narrowest card the caption survives, not by a device class, so it is content-driven the same
   way .finder-grid's are panel-driven. Verified against production before and after. */
@media (min-width: 400px)  { .product-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 768px)  { .product-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .product-grid { grid-template-columns: repeat(4, 1fr); } }

.product-card {
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  overflow: hidden;
  text-decoration: none;
  color: var(--text-body);
  -webkit-text-fill-color: var(--text-body);
  display: flex;
  flex-direction: column;
  transition: box-shadow var(--transition-base), transform var(--transition-base);
}
.product-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}
.product-card[data-hidden="true"] { display: none; }

.product-card-img {
  aspect-ratio: 1 / 1;
  background: var(--surface-raised);
  overflow: hidden;
}
.product-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Badge wrapper for recommended cards — just a positioned container for the absolute badge */
.product-card-img-outer {
  position: relative;
  overflow: hidden;
}
.product-card-img-outer .badge-925 {
  position: absolute;
  bottom: var(--space-3);       /* lower-left, same inset as before */
  left: var(--space-3);
  z-index: 1;
  /* The hallmark ramp — fixed, theme-independent (see --badge-925-* tokens):
     the badge sits over a photographic product image, so it can't tint. */
  --surface-info: var(--badge-925-fill);
  --border-info:  var(--badge-925-border);
  --text-info:    var(--badge-925-ink);
}
/* Force light-mode appearance in dark theme — product images have white backgrounds */
[data-theme="dark"] .product-card-img-outer .badge-925 {
  background: var(--badge-925-fill) !important;
  border-color: var(--badge-925-border) !important;
}
[data-theme="dark"] .product-card-img-outer .badge-925 .badge-925-num,
[data-theme="dark"] .product-card-img-outer .badge-925 .badge-925-label {
  color: var(--badge-925-ink) !important;
  -webkit-text-fill-color: var(--badge-925-ink) !important;
}
[data-theme="dark"] .product-card-img-outer .badge-925 .badge-925-sep {
  background: var(--badge-925-border) !important;
}

/* ── Result-card BADGES ──────────────────────────────────────────────────────
   Up to three (chain-traits.js MAX_BADGES), top-right, opposite the 925, over the
   macro image: a coloured FIT badge (.badge-rec, the recipient's colour) above the
   neutral ones (.badge-fact). Three is the cap because three collapsed marks are a
   column about 70px tall on a 190px image, and the photograph is what the card is for.
   An earlier version put the quiet badges in a row inside the card body instead; it
   read well and made every card taller, which is not this system's height to spend.
   The container is what's positioned; the badges flow inside it, right-aligned. */
.product-card-img-outer .pcard-tags {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1h);
  max-width: calc(100% - 2 * var(--space-3));
}

/* Shared stamp geometry — the hallmark pill (light fill / mid border / dark ink).
   Title Case, no extra tracking: the base font is Coconat (a display serif), so
   "Best match" is far wider than the short flags (NEW, 925); sentence case keeps
   a stamp inside a phone-width result card (~156px) instead of clipping. */
.badge-rec,
.badge-fact {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  border: var(--border-w-small) solid;
  border-radius: var(--radius-badge);
  padding: 1px var(--space-1h);
  font-size: var(--type-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-normal);
  line-height: 1.5;
  white-space: nowrap;
}

/* ── Showing the word, then the mark alone ───────────────────────────────────
   A badge paints with its word and collapses to its mark, so the shopper is taught the
   vocabulary and then keeps a scannable grid of small marks. badge-reveal.js decides
   WHEN and for which cards; this decides what it looks like. Scoped to the class rather
   than to .finder-grid, so the product page's recommendation row behaves identically —
   a shopper must not have to learn these badges twice.

   The label is CLIPPED, never hidden: max-width and opacity leave it in the
   accessibility tree, so the card's accessible name still reads "Anchor … Pendant"
   after the collapse. display:none or visibility:hidden would take it out, and the
   badge would become a picture with no name.

   The gap lives on the LABEL as a margin rather than on the flex container, because a
   container `gap` cannot be animated away — collapse the label alone and the badge
   keeps 4px of air where the word used to be, which reads as a rendering fault. */
/* OPENING IS SLOWER THAN CLOSING, deliberately, and the two timings live on the two
   destination states — a transition is read off the state being moved TO, so the base rule
   below governs every opening and the collapsed rule governs every closing.

   Closing is a dismissal and can be brisk. Opening is the moment the word arrives and has
   to be read, and at the closing speed it snapped into place faster than the eye settles.
   The curve changes with it: --ease-out is front-loaded, which is right for getting out of
   the way and too eager for arriving, so opening uses the gentler --ease-default. */
.badge-label {
  display: inline-block;
  margin-left: var(--space-1);
  max-width: 12rem;
  overflow: hidden;
  transition: max-width .60s var(--ease-default), margin-left .60s var(--ease-default),
              opacity .34s ease;
}
.badges-collapsed .badge-label {
  max-width: 0;
  margin-left: 0;
  opacity: 0;
  transition: max-width .42s var(--ease-out), margin-left .42s var(--ease-out),
              opacity .22s ease;
}
/* THE TWO WAYS THE WORD COMES BACK. Every card needs one of them, or a collapsed badge
   is a picture with no caption.

   Pointer — hovering the card. Guarded by (hover: hover) so a touch device cannot match
   it on a stray :hover left behind by a tap, which on iOS persists until you tap
   something else and would strand one card open. :focus-within covers the keyboard,
   since the card is a single <a>. */
@media (hover: hover) {
  .badges-collapsed .product-card:hover .badge-label,
  .badges-collapsed .product-card:focus-visible .badge-label,
  .badges-collapsed .product-card:focus-within .badge-label {
    max-width: 12rem;
    margin-left: var(--space-1);
    opacity: 1;
  }
}
/* Touch — the card being wholly on screen. badge-reveal.js sets .badges-open per card
   from an IntersectionObserver, so the card being looked at carries its words while the
   ones running off either edge are marks. */
.badges-collapsed .product-card.badges-open .badge-label {
  max-width: 12rem;
  margin-left: var(--space-1);
  opacity: 1;
}
/* The collapse is information, not decoration, so it still happens under reduced
   motion — it just stops sliding. */
@media (prefers-reduced-motion: reduce) {
  .badge-label { transition: none; }
}
/* Every tag is bold — the tier is carried by colour depth, not by weight, so the
   whole stack reads as one family of stamps.
   The gift mark — the configurator's own icon (ICON_GIFT_PATHS), drawn small.
   It strokes in currentColor, so it takes the tag's ink. */
.badge-ico { flex-shrink: 0; display: block; }

/* Fit tag — the recipient's own hue (see .chip-girl/.chip-woman/etc.), so the
   badge echoes the preset the shopper picked: maroon for girl/woman, blue for
   boy/man, the lighter step for the younger of each pair. Set one step DEEPER
   than the chips themselves so the tag stack outweighs the pale 925 stamp, and
   so each stays clear of the fact tag sharing its hue. Scale primitives, so
   they read the same in either theme. */
.badge-rec-girl  { background: var(--maroon-200); border-color: var(--maroon-300); color: var(--maroon-600); -webkit-text-fill-color: var(--maroon-600); }
.badge-rec-woman { background: var(--maroon-300); border-color: var(--maroon-400); color: var(--maroon-700); -webkit-text-fill-color: var(--maroon-700); }
.badge-rec-boy   { background: var(--blue-300);   border-color: var(--blue-400);   color: var(--blue-700); -webkit-text-fill-color: var(--blue-700); }
.badge-rec-man   { background: var(--blue-400);   border-color: var(--blue-500);   color: var(--blue-800); -webkit-text-fill-color: var(--blue-800); }

/* Neutral badges — TWO TONES, and the tone carries the TIER, not the kind:
     note  Classic, Lightest, Boldest, Densest, Longest   the rare ones
     spec  Pendant, Dense, Light, Layering/Everyday/…      plain facts

   This replaced four hues that collided with the rest of the shop: the old
   `substantial` blue sat one step off the 925 hallmark's own fill, so a "Dense"
   badge read as a second hallmark; the old `delicate` rose was the fit badges'
   hue, and maroon/blue carry this shop's gender convention, so light-vs-heavy
   arrived as feminine-vs-masculine. Blue is the hallmark's now, maroon is the fit
   badge's, and everything factual is beige — louder only by being rarer.

   The glyph says WHICH fact. The tone says how much to care. That division is what
   lets the marks stay readable once the labels have collapsed. */
.badge-fact-note { background: var(--tag-note-fill); border-color: var(--tag-note-border); color: var(--tag-note-ink); -webkit-text-fill-color: var(--tag-note-ink); }
.badge-fact-spec { background: var(--tag-spec-fill); border-color: var(--tag-spec-border); color: var(--tag-spec-ink); -webkit-text-fill-color: var(--tag-spec-ink); }

.product-card-img-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-subtle);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}
/* Tight on purpose. These cards carry three short things — a style, a
   dimension pair and a price — and 16px of padding left the widest pairing
   ("1.5mm · 50cm" beside a three-figure price) about 2px short of fitting on
   one line. It wrapped, and because the grid rows are equal height, one
   wrapped card made every card in that row taller.
   10px horizontal is the change that matters; the vertical 12px is what takes
   the height back out. Together they leave roughly 10px of slack on the
   tightest pairing in the catalogue, which is enough to survive a font
   fallback or a browser zoom step rather than being tuned to the pixel. */
.product-card-body {
  padding: var(--space-3) var(--space-2h);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}
/* Same two-line lock as .style-card-name: the icon never shrinks, so
   card padding cannot clip it; a long name ellipsizes instead. */
.product-card-name {
  display: flex;
  align-items: baseline;
  min-width: 0;
  font-size: var(--type-body-sm);
  font-weight: var(--weight-demi);
  color: var(--text-heading);
  -webkit-text-fill-color: var(--text-heading);
}
.product-card-name > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.product-card-name .chain-icon { flex: 0 0 auto; }
/* The struck price rides the NAME row, hard right, in the space the style name and its drawing
   leave empty. That is the whole point of putting it here: "was" ends up directly above "now"
   on the row below, sharing a right edge, and the card does not grow by a line to say so.
   `margin-left: auto` claims the gap; `flex: 0 0 auto` stops it being the thing that shrinks
   when the name is long — the name already ellipsizes, and a squeezed price would not.

   NO PADDING on this element. Padding is inside the box, and the strike is drawn across the
   whole box, so 8px of padding-left began the rule 9px before the first digit — a stroke
   leading into the number out of empty space. The box has to hug the glyphs. The floor on the
   gap therefore goes on the ICON instead, below, which is also how the rest of this row spaces
   itself: every item carries its own margin rather than the container carrying a gap. */
.product-card-name .pcard-was {
  margin-left: auto;
  flex: 0 0 auto;
}
/* The minimum the auto margin cannot guarantee: when the name is long enough to eat all the
   slack, `auto` resolves to 0 and the price would touch the drawing. Invisible when the icon
   is the last thing on the row, which is every card without a discount. */
.product-card-name .chain-icon { margin-right: var(--space-2); }
.product-card-spec {
  font-size: var(--type-label);
  color: var(--text-muted);
  -webkit-text-fill-color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  flex: 0 0 auto;
  white-space: nowrap;
}
.product-card-price {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  margin-top: auto;
  padding-top: var(--space-3);
}
/* Out-of-stock was previously a bare .product-card-spec row — same muted grey as "3 variants",
   so it read as neutral metadata rather than a status a shopper needs before clicking through.
   Matches the PDP's own .pdp-stock.out-stock treatment (--text-error) for one out-of-stock
   language across the site, not two. */
.product-card-oos {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-error);
  -webkit-text-fill-color: var(--text-error);
}

/* ── pcard: shared helpers for style-page and related cards ── */

.pcard-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-1h);
  flex-wrap: nowrap;
  min-width: 0;
}
.pcard-mm {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-regular);
  color: var(--text-heading);
  -webkit-text-fill-color: var(--text-heading);
  line-height: var(--leading-snug);
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Width-group cards put the graded icon on the label. Flex so the
   drawing never shrinks; the mm ellipsizes if the count needs the room. */
.pcard-mm-title {
  display: flex;
  align-items: baseline;
  overflow: visible;
}
.pcard-mm-title > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pcard-mm-title .chain-icon { flex: 0 0 auto; }
.pcard-unit {
  font-size: 0.7em;
  font-weight: var(--weight-regular);
}
.pcard-price-from {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: var(--space-1);
  flex-shrink: 0;
}
.pcard-from {
  font-size: var(--type-label);
  color: var(--text-muted);
  -webkit-text-fill-color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  line-height: var(--leading-tight);
}
.pcard-val {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  -webkit-text-fill-color: var(--text-heading);
  line-height: var(--leading-tight);
  flex: 0 0 auto;
  white-space: nowrap;
}
/* The struck figure on a card. Quieter than the price under it — regular weight and muted — but
   NOT smaller than the text beside it: at --type-caption the digits are 12px with a rule through
   them and stop being readable as a number at all, which makes the saving unverifiable and the
   card just look damaged. */
.pcard-was {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-regular);
  color: var(--text-muted);
  -webkit-text-fill-color: var(--text-muted);
  /* The rule takes the brightest rung at every discount, not the rung's own red. Graded, it
     made the same mark mean two things at once — how much off, and that something was off —
     and the weaker rungs read as a faded strike rather than a smaller saving. One red says
     "this price was replaced"; the percentage says by how much. The digits stay muted. */
  --strike-ink: var(--price-off-4);
}

/* The rung, resolved ONCE into --off-ink.
   The element carrying data-off is also the element the bubble sits inside, so the bubble reads
   the same custom property by inheritance and cannot end up a different red from the price it
   is annotating. Four rules total instead of four per component. The rung is data, not a class
   name, because the step that decides it lives in price-tag.js next to the ladder it reads —
   see priceOffStep() for why five points of discount per rung rather than one per tier row. */
[data-off="1"] { --off-ink: var(--price-off-1); }
[data-off="2"] { --off-ink: var(--price-off-2); }
[data-off="3"] { --off-ink: var(--price-off-3); }
[data-off="4"] { --off-ink: var(--price-off-4); }

/* .pcard-val-off carries no colour of its own: the charged price is black, the same as an
   undiscounted one, so a discounted card is not a differently-coloured card. The red on a
   card is the rule through the figure it replaced, and nothing else. The class stays as
   the hook the sr-only clause is positioned against. */
.pcard-length {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-demi);
  color: var(--text-heading);
}
.pcard-desc {
  font-size: var(--type-caption);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  line-height: var(--lh-body);
}

/* ── Tooltip ─────────────────────────────────────────────────── */
.tip-wrap { position: relative; display: inline-flex; align-items: center; cursor: pointer; }
.tip-btn {
  -webkit-appearance: none; appearance: none;
  -webkit-tap-highlight-color: transparent;
  outline: none; overflow: hidden; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; border-radius: 50%;
  border: 1px solid var(--text-muted);
  background: none; font-family: inherit; cursor: pointer;
  font-size: 0.55rem; font-weight: var(--weight-bold);
  color: var(--text-muted); -webkit-text-fill-color: var(--text-muted);
  line-height: 1; padding: 0; flex-shrink: 0;
}
.tip-wrap .tip-bubble {
  display: none; position: absolute; top: calc(100% + 8px);
  right: 0; left: auto; transform: none; width: min(220px, 80vw);
  background: var(--surface-raised); border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-sm); padding: var(--space-3);
  font-size: var(--type-label); font-weight: var(--weight-regular);
  color: var(--text-body); -webkit-text-fill-color: var(--text-body);
  /* One above --z-raised, and no further. .tip-wrap is `position: relative` with no z-index,
     so it is NOT a stacking context and this number competes at page level. It was a bare 200
     once — above --z-nav (100) — and a tooltip left open floated over the nav bar and over the
     mobile drawer. Same defect, and the same fix, as .look-filter-menu.is-pinned.
     Then it was exactly --z-raised, which is also what the sticky bars carry (.crumb-bar,
     .finder-tabs) — and an equal z-index is settled by DOCUMENT ORDER, so the bar declared
     after the header painted straight through the middle of the bubble on /gallery/. The +1 is
     the whole fix: it clears the chrome it is anchored above and nothing else. It must stay
     below --z-nav; anything at or over 100 brings the original bug back.
     `pointer-events: none` means elementFromPoint reports the element UNDER the bubble, so a
     hit test says the drawer wins here even when the bubble is painting on top of it. This one
     has to be checked with a screenshot. */
  line-height: var(--lh-body); z-index: calc(var(--z-raised) + 1); pointer-events: none;
  text-align: left; white-space: normal; box-shadow: var(--shadow-md);
}
@media (hover: hover) {
  .tip-wrap:hover .tip-bubble { display: block; }
}
.tip-wrap:focus-within .tip-bubble,
.tip-wrap.tip-open .tip-bubble { display: block; }

/* On mobile, page-header-row stacks vertically so the badge is always
   left-aligned. Switch bubble to open rightward (left:0) so it doesn't
   overshoot the left edge of the viewport. */
@media (max-width: 480px) {
  .page-header-row {
    flex-direction: column;
    align-items: flex-start;
  }
  .page-header-row .tip-wrap .tip-bubble {
    right: auto;
    left: 0;
  }
}

/* No results */
.no-results {
  grid-column: 1 / -1;
  text-align: center;
  padding: var(--space-16) var(--page-px);
  color: var(--text-muted);
}
.no-results p { font-size: var(--type-body-sm); margin-top: var(--space-3); }

/* Empty collection — a category with no products in the database yet (see catalog.js) */
.collection-empty {
  grid-column: 1 / -1;
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
  padding: var(--space-16) var(--page-px);
}
.collection-empty-title {
  font-size: var(--type-h4);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  margin-bottom: var(--space-4);
}
.collection-empty-body {
  font-size: var(--type-body-sm);
  color: var(--text-muted);
  line-height: var(--lh-body);
  margin-bottom: var(--space-8);
}

/* ── Shop footer ──────────────────────────────────────────────── */


/* ── Nav logo: always show horizontal wordmark ───────────────── */

.nav-logo-badge { display: none; }
.nav-logo-img   { display: block; height: 24px; width: auto; }
@media (min-width: 768px) { .nav-logo-img { height: 28px; } }

/* ── Product card image: show side shot on hover (desktop) ─────── */

/* One shot per card — the macro — magnified on hover rather than swapped for another photograph.
   These cards are a width comparison; the answer to "what does this link look like?" is to look
   closer, not somewhere else. Same 10% as .style-card-img, so every card in the shop behaves
   alike. The container already clips (overflow: hidden). */
/* --card-zoom is the per-image resting magnification (see the too-wide macros
   below). --card-zoom-extra is a per-SURFACE multiplier on top of it, so a grid
   can sit closer in without flattening a per-image correction: the Popcorn
   frames stay 1.5x tighter than their neighbours either way. Hover multiplies
   the product of both, so the 10% lean is unchanged everywhere. */
.product-card-img-primary {
  display: block;
  transform: scale(calc(var(--card-zoom, 1) * var(--card-zoom-extra, 1)));
  transition: transform var(--transition-layout);
}
@media (hover: hover) {
  .product-card:hover .product-card-img-primary {
    transform: scale(calc(var(--card-zoom, 1) * var(--card-zoom-extra, 1) * 1.10));
  }
}
/* The small cards — finder results and the PDP's recommendations — rest 30%
   closer than the full-width collection cards, so the link reads at their size. */
.finder-grid .product-card,
.pdp-related-grid .product-card {
  --card-zoom-extra: 1.3;
}
/* Someone who asked not to see motion gets the zoom applied without the travel. */
@media (prefers-reduced-motion: reduce) {
  .product-card-img-primary { transition: none; }
}

/* ── Macro frames that were shot too wide ──────────────────────────────────────
   Two of the 28 macros sit further back than the rest: the link occupies less of the frame, so
   they read as a thin chain rather than a close-up of a link, and the Popcorn pictures looked
   like a different kind of photograph from every other style.

   Every correction for those two frames lives in this one block, keyed on the filename AND on
   the folder. The folder is load-bearing: box shots are named per width exactly as macros are
   (migration 0017), so `[src$="/CHN-AG925-PPC-5.webp"]` matched the box shot too and magnified
   a photograph that was framed correctly. Match on `/macro/…` so only the frame this block is
   about is touched. Both
   card renderers — collection.js (style and width cards) and product.js (related cards) — emit
   the same classes, and product.html loads this stylesheet before its own, so one place covers
   every surface that shows the frame. If they are ever re-shot tighter, deleting this block is
   the whole change; nothing in JS or in the database knows about them.

   Cards: --card-zoom is the RESTING magnification and hover multiplies it, so these still lean
   the same 10% as everything else (1.5 → 1.65).

   Product page: the gallery gets the same 1.5 so the third image arrives framed like a macro.
   The lightbox deliberately does not — it is #pdp-lb-img, a different element with its own zoom
   toggle, so clicking through to full screen still gives the whole untouched frame. */
.product-card-img-primary[src$="/macro/CHN-AG925-PPC-3.webp"],
.product-card-img-primary[src$="/macro/CHN-AG925-PPC-5.webp"],
.style-card-img img[src$="/macro/CHN-AG925-PPC-3.webp"],
.style-card-img img[src$="/macro/CHN-AG925-PPC-5.webp"] {
  --card-zoom: 1.5;
}

/* Deliberately scoped to an image INSIDE a slide. The slide is what clips; a bare image has
   nothing to contain it, and a magnified one would spill across the neighbouring photograph and
   widen the strip. product.js is cached for four hours, so a browser can pair this stylesheet
   with a script that predates .pdp-gallery-slide — that visitor simply gets the frame unzoomed
   until their cache turns over, which is the correct way for this to fail. */
.pdp-gallery-slide > .pdp-gallery-img[src$="/macro/CHN-AG925-PPC-3.webp"],
.pdp-gallery-slide > .pdp-gallery-img[src$="/macro/CHN-AG925-PPC-5.webp"] {
  transform: scale(1.5);
}

/* ── Shared nav helpers ───────────────────────────────────────── */

/* Result count label */
.result-count {
  font-size: var(--type-label);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  align-self: center;
}

/* ── Legal pages ──────────────────────────────────────────────────────────── */
/* Long-form prose. Narrower measure than the shop grid — legal text is read a line
   at a time, and a 90-character line is where people start skipping. */
.legal-page {
  max-width: 46rem;
  margin: 0 auto;
  padding: var(--space-8) var(--space-5) var(--space-12);
}

/* This is fine print. The whole block sits one step down the type scale from the rest of the
   shop — an impressum is a reference document, not something to be read cover to cover. */
.legal-h1 {
  font-family: var(--font-coconat);
  font-size: var(--type-h5);          /* 24px */
  line-height: var(--lh-h5);
  color: var(--text-heading);
  letter-spacing: var(--tracking-tight);
  margin: var(--space-5) 0 var(--space-3);
}

.legal-lede {
  font-size: var(--type-body-sm);
  line-height: var(--leading-loose);
  color: var(--text-muted);
  margin-bottom: var(--space-6);
}

.legal-h2 {
  font-family: var(--font-coconat);
  font-size: var(--type-body);        /* 16px */
  line-height: var(--lh-body);
  font-weight: var(--weight-demi);
  color: var(--text-heading);
  letter-spacing: var(--tracking-tight);
  margin: 0;
}

/* ── Sections are collapsed disclosures ─────────────────────────────────────────────────
   Each section is a <details>. The rule that used to sit on .legal-h2 lives on the section
   itself now, so the dividers stay put whether a section is open or shut. */
.legal-section {
  border-top: 1px solid var(--border-default);
}
.legal-section:last-of-type { border-bottom: 1px solid var(--border-default); }

.legal-summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) 0;
  cursor: pointer;
  list-style: none;                   /* Firefox */
  user-select: none;
}
.legal-summary::-webkit-details-marker { display: none; }
.legal-summary:hover .legal-h2 { color: var(--text-action); }
.legal-summary:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* The chevron is ours, not the browser's, so it turns with the disclosure in both themes. */
.legal-summary::after {
  content: '';
  margin-left: auto;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1.5px solid var(--text-subtle);
  border-bottom: 1.5px solid var(--text-subtle);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--transition-base);
}
.legal-section[open] .legal-summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.legal-section-body { padding: 0 0 var(--space-5); }
/* The last block already carries its own bottom margin; don't pay for the gap twice. */
.legal-section-body > :last-child { margin-bottom: 0; }

.legal-p,
.legal-list {
  font-size: var(--type-body-sm);     /* 14px */
  line-height: var(--leading-loose);
  color: var(--text-body);
  margin-bottom: var(--space-4);
}
.legal-list { padding-left: var(--space-5); }
.legal-list li { margin-bottom: var(--space-2); }

.legal-page a { color: var(--text-heading); text-decoration: underline; text-underline-offset: 2px; }
.legal-page a:hover { color: var(--text-action); }
/* The trail is chrome, not prose, and the rule above outranks .breadcrumb-link's own
   `text-decoration: none` — so the legal trail was the one underlined trail in the shop.
   Overridden here rather than by excluding it above: `a:not(.breadcrumb-link)` also
   outranks `.legal-nav-link.active`, and repainted the active pill's beige text
   near-black. Same specificity as that pill, aimed only at the crumbs. */
.legal-page .breadcrumb-link { color: var(--text-muted); text-decoration: none; }
.legal-page .breadcrumb-link:hover { color: var(--text-action); }

.legal-page code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  background: var(--surface-raised);
  padding: 0.1em 0.35em;
  border-radius: var(--radius-sm);
}

/* Key/value blocks — the imprint is mostly these. */
.legal-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--space-5);
  font-size: var(--type-caption);     /* 12px — the scale's "caption / legal" step */
}
.legal-table th,
.legal-table td {
  text-align: left;
  vertical-align: top;
  padding: var(--space-3) var(--space-3) var(--space-3) 0;
  border-bottom: 1px solid var(--border-default);
  line-height: var(--leading-normal);
}
.legal-table th {
  width: 38%;
  font-weight: var(--weight-demi);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
}
.legal-table td { color: var(--text-body); }

/* The statutory model withdrawal form. Set apart so it is obviously a thing to copy. */
.legal-form {
  border: 1px solid var(--border-default);
  border-left: 3px solid var(--text-action);
  background: var(--surface-raised);
  padding: var(--space-5);
  margin: 0 0 var(--space-5);
}
.legal-form p {
  font-size: var(--type-caption);     /* 12px */
  line-height: var(--leading-loose);
  color: var(--text-body);
  margin: 0 0 var(--space-3);
}
.legal-form p:last-child { margin-bottom: 0; }

/* Anything still unset in company.js. Deliberately impossible to miss. */
.legal-todo {
  background: var(--surface-error);
  color: var(--text-error);
  padding: 0.1em 0.4em;
  border-radius: var(--radius-sm);
  font-style: italic;
  font-size: 0.92em;
}
/* Cross-links between the documents. */
.legal-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-5) 0 var(--space-2);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border-default);
}
.legal-nav-link {
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none !important;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
}
.legal-nav-link:hover { color: var(--text-heading); border-color: var(--border-strong); }
.legal-nav-link:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
/* The current document's pill. It was filled with --text-action, which is maroon in light mode
   but becomes maroon-300 (a pink) in dark mode — text-action is tuned for contrast AS TEXT on
   the page background, not as a fill behind text. A filled brand control uses the fixed
   action pair, the same maroon/beige every button in the shop uses, identical in both themes. */
.legal-nav-link.active {
  color: var(--text-on-action);
  background: var(--surface-action);
  border-color: var(--border-action);
}

/* The hub. */
.legal-cards { display: grid; gap: var(--space-3); margin-bottom: var(--space-8); }
.legal-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  text-decoration: none !important;
  transition: border-color var(--duration-base) var(--ease-out);
}
/* --border-strong, not --text-action: the latter goes pink on dark, and a card hover wants a
   neutral "the border firms up" cue, which is also how the shop's other cards read. */
.legal-card:hover { border-color: var(--border-strong); }
.legal-card:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
.legal-card-title {
  font-family: var(--font-coconat);
  font-size: var(--type-h6);
  font-weight: var(--weight-demi);
  color: var(--text-heading);
}
.legal-card-summary { font-size: var(--type-body-sm); color: var(--text-muted); }

.legal-updated {
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-subtle);
  margin-top: var(--space-8);
}


/* Pre-contract information at checkout — put in front of the customer before they pay. */
.checkout-legal {
  margin-top: var(--space-4);
  font-size: var(--type-body-sm);
  line-height: var(--leading-loose);
  color: var(--text-muted);
}
.checkout-legal a { color: var(--text-heading); text-decoration: underline; text-underline-offset: 2px; }
.checkout-legal a:hover { color: var(--text-action); }

/* ── Footer ───────────────────────────────────────────────────────────────────
   Beige on the brand maroon. The badge anchors it, four columns carry the links,
   and Legal is deliberately the shortest of them: EU distance selling requires the
   terms, the withdrawal notice and the privacy policy to be reachable from every
   page — that is four documents, not a wall of them. */

/* ==== FOOTER:BEGIN — byte-identical to brand/footer.css. Edit one, copy to the
   other; tests/unit/brand.test.mjs fails the build if they drift. ==== */
.shop-footer {
  background: var(--surface-action);      /* maroon-500 — the brand anchor */
  color: var(--text-on-action);           /* beige-500 */
  margin-top: var(--space-16);
}

/* ONE grid for the whole footer body, holding five blocks: the badge, Shop, Learn, Legal and
   Signup. The badge is a cell like any other — on a phone it takes the first slot of a 2×2 and
   the links fill the rest.

   That is why .ft-cols is `display: contents` below: the badge and the link columns sit in
   different parents in the markup (the columns are wrapped in a <nav> that carries the
   landmark), and grid only lays out its own direct children. `display: contents` drops the nav
   box out of the layout while keeping the element — and its landmark — in the tree, so all five
   blocks become children of this one grid without deleting the <nav>. */
.shop-footer-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-12) var(--page-px) var(--space-10);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-8) var(--space-6);
  align-items: start;
}
.ft-cols { display: contents; }

/* Badge, Shop / Learn, Legal — then Signup across the bottom. */
@media (min-width: 560px) {
  .shop-footer-inner { grid-template-columns: auto repeat(3, minmax(0, 1fr)); }
}
/* Everything on one line. The ratios reproduce the old two-part layout: the badge column and
   Signup each get 1.5fr against 1fr for the three link columns. */
@media (min-width: 900px) {
  .shop-footer-inner {
    grid-template-columns: 1.5fr 1fr 1fr 1fr 1.5fr;
    gap: var(--space-12) var(--space-6);
  }
}

/* ── Badge ── */
/* The badge carries the brand on its own. The tagline under it repeated what the bottom bar
   already says in the hallmark line. */
.ft-brand { display: flex; }
/* Sized off the block it sits beside, not picked by eye: the badge bottoms out level with the
   Shop column, which is the column it sits next to in EVERY layout — beside it in the desktop
   row, and beside it again in the phone 2×2.
   
   The HEIGHT is the thing being set, so the height is what is written. It used to be expressed
   as `width: 88px`, back-computed from a 152px column through the artwork's 479×828 ratio — and
   when a fifth link ("My Orders") was added to Shop the column grew to 184px while the badge
   stayed at 152, because nothing about `width: 88px` says what it was derived from. Setting the
   height directly means the next person changing it is changing the number that matters.
   
   tests/browser/footer.test.mjs derives the expected height from the Shop column rather than
   hardcoding it, so adding a sixth link fails loudly and says what to change here.
   
   One height, no breakpoint: it must not shrink on a phone. It used to drop to 72px below
   900px, from back when it sat above the links as a banner and had no band beside it — once it
   became a cell in the grid that rule did nothing but shrink it for no reason. */
/* The mark is a link now; the anchor must not add a line box of its own under the image. */
.ft-badge-link { display: block; }
.ft-badge {
  height: 152px;   /* Shop column: a title + four links — Instagram moved to Socials (2026-09-14) */
  width: auto;
  display: block;
}

/* Everything reads left-aligned — a list of links is scanned down its left edge, and centring
   leaves both sides ragged so the eye has to hunt for where each line starts. The badge follows
   the same edge.

   Each block is then indented off that edge, so the contents are not flush against the cell
   boundary. It applies to the badge and all four columns, signup included, which keeps every
   block on one shared left margin — indenting only some of them would just move the misalignment
   somewhere else. */
.ft-brand,
.ft-col { padding-left: var(--space-4); }

/* The eyebrow style from the brand system — 10px, uppercase, wide tracking. The old footer
   headings were body-sized, which made every column read as a paragraph. */
.ft-col-title {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-on-action-subtle);
  margin: 0 0 var(--space-4);
}
/* The heading IS the link now, so it inherits the heading's colour rather than the link colour
   the footer gives its list items — it is still a label first, and a row of bright headings
   would compete with the links beneath them. It brightens on hover, like they do. */
.ft-col-title a {
  color: inherit;
  text-decoration: none;
  transition: color var(--transition-fast);
}
.ft-col-title a:hover { color: var(--text-on-action); }

.ft-col-links { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
/* opacity, not colour: this is a hover fade on a bare link, and fading the whole element is
   exactly the intent. The static colours above are tokens instead — see the note in tokens.css. */
.ft-col-links a {
  font-size: var(--type-body-sm);
  color: var(--text-on-action);
  text-decoration: none;
  opacity: 0.85;
  transition: opacity var(--duration-base) var(--ease-out);
}
.ft-col-links a:hover { opacity: 1; text-decoration: underline; text-underline-offset: 3px; }

/* ── Signup ── */
/* It runs the full width of the grid at every size except the widest, where it finally earns a
   column of its own:
     < 560px   2 cols — badge, Shop / Learn, Legal — then Signup across both.
     560–899   4 cols — badge and the three link columns — then Signup across all four.
     ≥ 900px   5 cols — Signup takes the last one.
   `1 / -1` spans whatever the current column count is, so it does not have to be restated when
   the count changes. */
.ft-col-signup { grid-column: 1 / -1; }
@media (min-width: 900px) {
  .ft-col-signup { grid-column: auto; }
}

.ft-signup-note {
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-on-action-muted);
  margin: 0 0 var(--space-4);
}
.ft-signup { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.ft-signup-input {
  flex: 1 1 10rem;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-base);
  font-size: var(--type-body-sm);
  color: var(--text-on-action);
  background: var(--surface-on-action-subtle);
  border: var(--border-w-small) solid var(--border-on-action);
  border-radius: var(--radius-input);
}
.ft-signup-input::placeholder { color: var(--text-on-action-subtle); }
.ft-signup-input:focus {
  outline: none;
  border-color: var(--text-on-action);
  background: var(--surface-on-action-hover);
}
.ft-signup-btn {
  flex: 0 0 auto;
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-base);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--surface-action);                  /* maroon text on beige — inverted */
  background: var(--text-on-action);
  border: 1px solid var(--text-on-action);
  border-radius: var(--radius-btn);
  cursor: pointer;
  transition: opacity var(--duration-base) var(--ease-out);
}
.ft-signup-btn:hover:not(:disabled) { opacity: 0.86; }
.ft-signup-btn:disabled { opacity: 0.5; cursor: default; }

/* No mobile override for the input: Signup spans the full grid below 900px, so it has the whole
   footer width to work in and the button sits beside the field. (An earlier layout put Signup in
   a half-width cell, which needed one — the cell was narrower than the input's 10rem basis.) */

.ft-signup-msg {
  min-height: 1.2em;
  margin: var(--space-2) 0 0;
  font-size: var(--type-caption);
  line-height: var(--leading-snug);
  opacity: 0;
}
.ft-signup-msg.is-ok,
.ft-signup-msg.is-error { opacity: 1; }
.ft-signup-msg.is-ok    { color: var(--text-on-action); }
.ft-signup-msg.is-error { color: var(--text-error-on-action); }

/* ── Socials ── */
/* The foot of the Sign up column: an eyebrow title and the Instagram mark. The link is a 44px square
   so it can be tapped on a phone, pulled left by half the difference between that and the 22px glyph,
   so the GLYPH — not its hit area — sits on the column's left edge under the text above it. It fades
   like the footer's links. */
.ft-socials { margin-top: var(--space-6); }
.ft-socials .ft-col-title { margin-bottom: var(--space-2); }
.ft-social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-left: calc((22px - 44px) / 2);
  color: var(--text-on-action);
  opacity: 0.85;
  transition: opacity var(--duration-base) var(--ease-out);
}
.ft-social-link:hover,
.ft-social-link:focus-visible { opacity: 1; }
.ft-social-link svg { display: block; }

/* ── The star rule — the brand-mark divider (canonical component) ──────────────
   A hairline broken at its centre by the brand star. Two flex line-halves grow
   out from a centred star, rather than a border with a glyph laid on top —
   nothing has to be measured, and nothing has to be masked to hide the line
   behind the star. The star glyph comes from --brand-star, never retyped.
   The line colour, star ink, gap and star size are set by four locals so one
   structure serves every ground; the defaults suit a light surface. Modifiers
   repaint them: .rule-star--on-action for the maroon footer.
   Mark the element aria-hidden — a screen reader announces ✦ as "black four
   pointed star", which is noise in the middle of a divider.
   Used by: the shop footer, the configurator's "I need help" disclosure. The
   guide-page section dividers are the same mark by a different means (a glyph
   overlaid on a section's border-top — see #panel-length .browse-section). */
.rule-star {
  --rule-star-line: var(--border-default);
  --rule-star-ink:  var(--text-action);
  --rule-star-gap:  var(--space-3);
  --rule-star-size: var(--type-body-sm);
  display: flex;
  align-items: center;
  gap: var(--rule-star-gap);
}
.rule-star::before,
.rule-star::after {
  content: '';
  flex: 1;
  height: var(--border-w-small);
  background: var(--rule-star-line);
}
.rule-star-mark {
  line-height: 1;
  color: var(--rule-star-ink);
  font-size: var(--rule-star-size);
}
.rule-star-mark::before { content: var(--brand-star); }

/* On the maroon footer ground: subtle beige line and ink, a wider gap and a
   smaller star — the footer's long-standing look, now a modifier. */
.rule-star--on-action {
  --rule-star-line: var(--border-on-action-subtle);
  --rule-star-ink:  var(--text-on-action-subtle);
  --rule-star-gap:  var(--space-4);
  --rule-star-size: var(--type-caption);
}

/* The footer instance also spans the footer's content width. */
.ft-rule {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--page-px);
}

/* ── Bottom bar ── */
.shop-footer-bar {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-5) var(--page-px);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-on-action-subtle);
}
@media (min-width: 640px) {
  .shop-footer-bar { flex-direction: row; justify-content: space-between; }
}

/* ==== FOOTER:END ==== */

/* Visually hidden, still read by screen readers. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Coconat is a display serif, so its 700 weight reads as shouting when it lands mid-sentence.
   Emphasis inside long-form prose sits at demi. */
.legal-body strong,
.legal-form strong,
.checkout-legal strong,
.aff-info-body strong { font-weight: var(--weight-demi); }

/* ── 404 ──────────────────────────────────────────────────────────────────── */
/* Cloudflare Pages serves index.html for any unmatched path when a project has no 404.html —
   so until this file existed, every typo'd URL answered 200 with the homepage. A crawler reads
   that as duplicate content, and a customer following a stale link gets no signal at all that
   they arrived somewhere wrong. */
.notfound {
  max-width: 34rem;
  margin: 0 auto;
  padding: var(--space-20) var(--space-5) var(--space-24);
  text-align: center;
}

/* The number is the one place a bare numeral earns display size — it says "wrong page" before
   a word is read. Subtle, because it is a signpost, not the message. */
.notfound-code {
  font-family: var(--font-coconat);
  font-size: var(--type-h2);
  line-height: var(--lh-h2);
  color: var(--text-subtle);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-2);
}

.notfound-h1 {
  font-family: var(--font-coconat);
  font-size: var(--type-h4);
  line-height: var(--lh-h4);
  color: var(--text-heading);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-3);
}

.notfound-lede {
  font-size: var(--type-body-lg);
  line-height: var(--leading-loose);
  color: var(--text-muted);
  margin-bottom: var(--space-8);
}

.notfound-actions {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  flex-wrap: wrap;
}


/* ══════════════════════════════════════════════════════════════════════
   CHAIN CONFIGURATOR  ("Find by fit" on /chains/)
   The bust visual plus its length/gauge controls. Consumes only Tier-3
   tokens and the shared .option-group / .chip-option primitives above.
   Markup and behaviour: shop/chain-configurator.js
   ══════════════════════════════════════════════════════════════════════ */

/* Mobile first: the compact sizes below are the default and scale UP at 480px.
   The three gauge words cannot wrap, so their padding and the length column set
   the card's minimum width — at desktop sizing that floor is ~430px, which
   overflowed a 375px phone and pushed the length column off-screen. */
.chain-config {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4);
  max-width: 420px;             /* compact footprint */
}

/* Grid so each control sits in the right cell:
     stage   → col 1, row 1
     lengths → col 2, row 1  (only as tall as the display)
     gauge   → col 1, row 2  (only as wide as the display) */
.chain-config-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 56px;
  grid-template-rows: auto auto;
  column-gap: var(--space-3);
  row-gap: var(--space-3);
}
@media (min-width: 480px) {
  .chain-config { gap: var(--space-5); padding: var(--space-5); }
  .chain-config-top {
    grid-template-columns: minmax(0, 1fr) 68px;
    column-gap: var(--space-4);
    row-gap: var(--space-4);
  }
}
.chain-stage   { grid-column: 1; grid-row: 1; }
.chain-lengths { grid-column: 2; grid-row: 1; }
.chain-gauge   { grid-column: 1; grid-row: 2; }

/* ── Stage: the bust + chains asset ─────────────────────────────── */
.chain-stage {
  /* Colour the generated SVG from the maroon brand scale. Theme-aware so
     both the dark-red outline and the reds stay legible on either ground. */
  --bust-color:            var(--maroon-500);   /* dark red — the frame */
  --chain-color:           var(--maroon-300);   /* light red — ghost    */
  --chain-color-selected:  var(--maroon-400);   /* more red — selected  */
  --chain-ghost-opacity:   0.45;
  --gauge: 22;                                  /* JS overrides; medium */

  min-width: 0;
  /* Height tracks width. The art is drawn with preserveAspectRatio "slice", so
     it always scales to the frame's WIDTH — widening the frame without adding
     height just crops more of the bust and reads as an unwanted zoom. A fixed
     ratio keeps the same amount of bust visible at every size.
     …but that ratio is the height the frame WANTS, not a cap. The length
     buttons beside it carry their own minimum height and are the taller of the
     two on small and mid screens, which left the frame short of the row with a
     gap beneath it. So the ratio is expressed as a padding spacer (below)
     rather than `aspect-ratio`: percentage padding derives height from width
     one-way, so the box can still stretch to fill a taller row. `aspect-ratio`
     is two-way — pinning the height there dragged the WIDTH out with it and
     overflowed the card at 320px. A stretched frame simply reveals more of the
     bust's lower half, since the art is anchored at the top and crops from the
     bottom. */
  background: var(--surface-raised);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-md);
  /* No padding: the art bleeds to the card edge. Positioned so the
     absolutely-placed SVG cannot inflate the card's height — the card
     matches the length column, and the bust fills it. */
  position: relative;
  display: block;
  overflow: hidden;
}
/* 10/11 of the width — the frame's preferred height, and its floor. */
.chain-stage::before { content: ''; display: block; padding-top: 90.91%; }
[data-theme="dark"] .chain-stage {
  --bust-color:            var(--maroon-400);   /* lift to read on near-black */
  --chain-color:           var(--maroon-300);
  --chain-color-selected:  var(--maroon-200);   /* brightest red pops on dark */
  --chain-ghost-opacity:   0.32;
}
/* Picking a boy or a man swings the drawn chains onto the blue scale, matching
   those buttons. Only the chains move — the bust is the form the necklace hangs
   on, not part of the choice. Cleared again for a girl, a woman, or no pick. */
.chain-stage[data-tone="male"] {
  --chain-color:           var(--blue-300);
  --chain-color-selected:  var(--blue-600);
}
[data-theme="dark"] .chain-stage[data-tone="male"] {
  --chain-color:           var(--blue-400);
  --chain-color-selected:  var(--blue-200);     /* brightest blue pops on dark */
}
/* The neutral pole — the gauge guide's Medium middle. Neither maroon nor blue,
   so it carries no gendered read the way the two poles do. A muted warm grey
   from the beige scale, distinct from both. */
.chain-stage[data-tone="neutral"] {
  --chain-color:           var(--beige-800);   /* #9e968e ghost */
  --chain-color-selected:  var(--beige-900);   /* #7a6e68 selected */
}
[data-theme="dark"] .chain-stage[data-tone="neutral"] {
  --chain-color:           var(--beige-700);
  --chain-color-selected:  var(--beige-500);
}
/* Fills the frame. With preserveAspectRatio="xMidYMin slice" the art scales
   to the card's width and anchors at the TOP, so the bust runs off the
   bottom edge instead of letterboxing with a gap. */
.chain-stage svg {
  position: absolute;
  top: var(--space-3);          /* breathing room top and sides; */
  left: var(--space-3);         /* flush at the bottom           */
  width: calc(100% - 2 * var(--space-3));
  height: calc(100% - var(--space-3));
  display: block;
  transform-origin: center top; /* zoom grows down/outward, top stays put */
  transition: transform var(--transition-slow);
}
.chain-stage:hover svg { transform: scale(1.06); }

/* Ghost by default; selected draws solid and redder. Gauge is stroke-width. */
.chain-stage .chain {
  stroke: var(--chain-color);
  opacity: var(--chain-ghost-opacity);
  transition: opacity var(--transition-slow),
              stroke var(--transition-slow),
              stroke-width var(--transition-base);
}
.chain-stage .chain.selected { stroke: var(--chain-color-selected); opacity: 1; }
.chain-stage .chains { stroke-width: var(--gauge); }

/* ── Vertical length column (multi-select) ──────────────────────── */
/* As tall as the display: title floats at the top, buttons spread evenly
   from just below it to the bottom of the bust. */
.chain-lengths { width: 56px; display: flex; flex-direction: column; gap: var(--space-2); }
.chain-lengths .option-group-label { margin-bottom: 0; }
.chain-lengths-head { text-align: center; }
.chain-unit { display: block; font-size: var(--type-label); color: var(--text-subtle); letter-spacing: var(--tracking-wide); }
.chain-lengths .chip-group { flex: 1 1 auto; flex-direction: column; justify-content: space-between; gap: var(--space-1h); flex-wrap: nowrap; }
/* The buttons divide the frame's height between them, so they grow with it
   rather than staying phone-sized inside a large desktop card. min-height is
   the touch-target floor, not the size. */
.chain-lengths .chip-option { flex: 1 1 0; display: flex; }
.chain-lengths .chip-option-label {
  width: 100%;
  height: 100%;
  justify-content: center;
  min-height: 36px;
  padding: var(--space-1h) var(--space-1);
}

/* ── Gauge selector, under the display ──────────────────────────── */
.chain-gauge .chip-group { gap: var(--space-1h); flex-wrap: nowrap; }
.chain-gauge .chip-option { flex: 1 1 0; min-width: 0; }
.chain-gauge .chip-option-label {
  width: 100%;
  justify-content: center;
  min-height: 36px;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-1h);
}

@media (min-width: 480px) {
  .chain-lengths { width: 68px; gap: var(--space-2h); }
  .chain-lengths .chip-group { gap: var(--space-2); }
  .chain-lengths .chip-option-label { min-height: 40px; padding: var(--space-2); }
  .chain-gauge .chip-group { gap: var(--space-2); }
  .chain-gauge .chip-option-label { min-height: 40px; gap: var(--space-2); padding: var(--space-2) var(--space-4); }
  .chain-stage { min-height: 200px; }
}

/* The card widens on larger screens, so the controls grow with it — otherwise
   the bust inflates while the buttons stay at their phone size and the whole
   thing looks lopsided. */
/* Only the widths and the gauge row need stepping — the length buttons and the
   frame's height follow from the aspect ratio above. */
@media (min-width: 900px) {
  .chain-config-top { grid-template-columns: minmax(0, 1fr) 76px; column-gap: var(--space-5); }
  .chain-lengths { width: 76px; }
  .chain-gauge .chip-option-label { min-height: 44px; }
}
@media (min-width: 1200px) {
  .chain-config { padding: var(--space-6); gap: var(--space-6); }
  .chain-config-top { grid-template-columns: minmax(0, 1fr) 84px; }
  .chain-lengths { width: 84px; }
  .chain-gauge .chip-option-label { min-height: 48px; }
}
/* Col 2 / row 2 — beside the gauge buttons, under the length column,
   mirroring the LENGTH (cm) header above it. */
.chain-gauge-head { grid-column: 2; grid-row: 2; text-align: center; align-self: center; }

/* ── Recommendations: a disclosure below the configurator ────────── */
/* A hairline with the brand star at its centre — the canonical .rule-star
   component (see above) — then the "I need help" prompt below it. Putting the
   star ON the line instead of on its own row beneath the prompt takes a row of
   height off the card. The default (light-ground) rule-star look is used here.
   Open: the prompt has done its job — the panel's own close button is the way
   back out. The star-divider stays, introducing the panel. */
.chain-rec[data-open="true"] .chain-rec-toggle { display: none; }

/* .chain-config is a flex column on a --space-4 gap, which is the right distance between
   the controls but too much above a rule that is itself only a hairline. Pulled back to
   --space-2 so the prompt reads as one tight block — rule, star, words — rather than
   three things adrift in the bottom of the card. */
.chain-rec { margin-top: calc(var(--space-2) - var(--space-4)); }

/* The whole bar is the button, not just the words — it spans the card's full
   content width, lining up with the star rule directly above it, so the entire
   strip is one target rather than a small centred text link. */
.chain-rec-toggle {
  display: block;
  width: 100%;
  margin: var(--space-1) 0 0;
  padding: var(--space-1) var(--space-2);
  background: none;
  border: none;
  border-radius: var(--radius-base);
  cursor: pointer;
  text-align: center;
  font-family: var(--font-base);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color var(--transition-fast);
}
/* Text only. The filled hover put a second surface inside a card that is already a
   surface, which read as a button appearing under the cursor rather than a prompt
   answering it. Darkening the label off --text-muted says the same thing with nothing
   drawn. */
.chain-rec-toggle:hover { color: var(--text-heading); }
.chain-rec-toggle:focus-visible { outline: var(--border-w-medium) solid var(--border-focus); outline-offset: 2px; }

/* The panel replaces the prompt in place: clicking "I need help" swaps one line of text
   for another, and they have to be the SAME line or the swap reads as the card jolting.
   --space-1 is the toggle's own top margin, so the head row opens exactly where the
   button's box was; the extra pixel is the head centring its title against the close
   button beside it, which is taller than the line. Measured, not guessed — without it the
   two texts land 1px apart. */
.chain-rec-panel { padding-top: calc(var(--space-1) + 1px); }
/* Title stays optically centred in the panel; the close button rides on the
   same line at the right rather than floating above it. */
.chain-rec-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}
/* Empty on step one, so it must not hold a gap open. */
.chain-rec-head-icon { display: inline-flex; color: var(--text-muted); }
.chain-rec-head-icon:empty { display: none; }
/* The icon now sits beside "Change" — mixed-case text, whose visual mass is
   centred on its line box — so plain geometric centring (align-items on the
   lead cluster) is already correct. No optical nudge: the earlier -1.5px lift
   existed only to correct against the UPPERCASE title it used to sit next to. */
.chain-rec-head-icon .chip-icon {
  width: 15px; height: 15px;
}
.chain-rec-close {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}
/* Title and buttons read as one centred group. */
.chain-rec-head { text-align: center; margin-bottom: var(--space-3); }

/* ── Audience row: two steps in one place ───────────────────────── */
/* Both panes are stacked in the same grid area and cross-faded, so the card's
   height doesn't jump when the second one takes over. */
.chain-audience { display: grid; }
.chain-audience .chip-group {
  grid-area: 1 / 1;             /* stack the two panes */
  gap: var(--space-2);
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  transition: opacity var(--transition-base);
}
.chain-audience .chip-option { flex: 0 1 auto; min-width: 0; }
.chain-audience .chip-option-label {
  width: 100%;
  justify-content: center;
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
}
/* Step one is the entry point, so it gets more presence than the rest of the
   card. Only the chips grow — they sit in a centred, wrapping row, so nothing
   here can widen the card or the frame above it. */
.chain-audience [data-step-1] .chip-option-label {
  min-height: 52px;
  gap: var(--space-2h);
  padding: var(--space-3) var(--space-5);
  font-size: var(--type-caption);
}
/* Sized here rather than on the element, so the button and heading marks can
   differ — the heading sits against 10px caps and needs the smaller one. */
.chip-icon { flex-shrink: 0; width: 18px; height: 18px; }

/* Four options don't fit on one line on a narrow card — let them pair up. */
.chain-audience [data-step-2] .chip-option { flex: 1 1 40%; }
.chain-audience[data-step="1"] [data-step-2],
.chain-audience[data-step="2"] [data-step-1] { opacity: 0; pointer-events: none; visibility: hidden; }

/* Recipient chips carry their own selected colour, from the brand scales: a
   rose pair and a steel-blue pair, each with a lighter and a deeper step so the
   four stay distinguishable. Soft tints rather than saturated fills — these are
   a gentle hint, not the primary action on the card, and four solid blocks
   competed with the maroon length buttons above them. Text is the deep end of
   the same hue, which keeps every one of them well past AA. */
.chip-girl  input:checked + .chip-option-label { background: var(--maroon-100); border-color: var(--maroon-200); color: var(--maroon-500); }
.chip-woman input:checked + .chip-option-label { background: var(--maroon-200); border-color: var(--maroon-300); color: var(--maroon-600); }
.chip-boy   input:checked + .chip-option-label { background: var(--blue-100);   border-color: var(--blue-200);   color: var(--blue-600); }
.chip-man   input:checked + .chip-option-label { background: var(--blue-300);   border-color: var(--blue-400);   color: var(--blue-800); }

/* The icon and "Change" travel together as one cluster, pinned opposite the
   close button on the heading's line — the icon leads, the link follows. The
   cluster is what's positioned, so the title stays optically centred in the row
   regardless of how wide the pair gets. */
.chain-rec-head-lead {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1h);
}
.chain-audience-back {
  display: block;
  margin: 0;
  padding: 0;
  background: none;
  border: none;
  font-family: var(--font-base);
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wide);
  color: var(--text-action);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
/* `display: block` above would otherwise beat the UA stylesheet's [hidden] */
.chain-audience-back[hidden] { display: none; }
.chain-audience-back:hover { color: var(--text-action-hover); }
.chain-audience-back:focus-visible { outline: var(--border-w-medium) solid var(--border-focus); outline-offset: 2px; }

.gauge-bar-fine   { width: 2px; }
.gauge-bar-medium { width: 4px; }
.gauge-bar-bold   { width: 7px; }

@media (prefers-reduced-motion: reduce) {
  .chain-stage .chain,
  .chain-stage svg { transition: none; }
}


/* ── Sticky page chrome ─────────────────────────────────────────── */

/* The breadcrumb bar is shop-wide: every page with a trail pins it under the nav. The
   tab bar and the configurator below it belong to the chains page alone, and stack under
   it — which is why `top` is a running total. They stack under the site nav, each one parked
   below the last, so `top` is a running total.
   `--crumb-h` and `--tabs-h` are the heights of the two bars. chain-finder.js overrides
   them from the real boxes, because both wrap at narrow widths and a hard-coded height
   would either overlap the bar below or leave a gap under it. The values here are the
   unwrapped heights, so the page is still right in the frames before the first
   measurement lands — same arrangement as --chain-icon-grade above. */
:root {
  --crumb-h: 44px;
  --tabs-h: 56px;
  /* How far the configurator card keeps clear of whatever is above it. One value for
     every state — parked in its desktop column, pinned on mobile, or still in the flow
     — so the card never sits at a different distance from the chrome than it did a
     scroll-tick ago. chain-finder.js reads it back for the states it positions itself. */
  --config-gap: var(--space-4);
}
.crumb-bar,
.finder-tabs {
  position: sticky;
  z-index: var(--z-raised);
  background: var(--surface-page);
  /* Full-bleed to the container's gutters, otherwise content scrolls up through the
     padding either side of a bar that looks like it should be blocking it. The gutter is
     --page-px nearly everywhere, but the legal pages set their own measure, so it is a
     variable rather than the token. */
  --bleed: var(--page-px);
  margin-left: calc(var(--bleed) * -1);
  margin-right: calc(var(--bleed) * -1);
  padding-left: var(--bleed);
  padding-right: var(--bleed);
}
/* Locked, both bars give up padding — that space is worth more to the results than to
   the chrome, and it is the only space on the page that is spent whether or not anyone
   is looking at it.
   The margin is what makes the swap safe. A sticky element keeps its box in the flow,
   so shrinking it drags everything below up by the same amount on the single frame it
   locks — a visible lurch, and worse, it moves the document reference the mobile fold
   measures itself against. Margin is not part of the sticky box: handing the lost height
   back as margin means the bar paints shorter while the flow below it never moves. */
/* .legal-page is a narrow prose measure with a --space-5 gutter, not --page-px. */
.legal-page > .crumb-bar { --bleed: var(--space-5); }

.crumb-bar {
  --crumb-pad: var(--space-3);
  top: var(--nav-h);
  /* The bar is the first thing in the container, so without this it is already touching
     the nav at rest and locks on frame one — the roomier unlocked padding would never
     be seen. Margin, again, rather than padding: this is space the page gives up on the
     way down, not space the locked bar carries with it. */
  margin-top: var(--space-5);
  padding-top: var(--crumb-pad);
  padding-bottom: var(--crumb-pad);
  margin-bottom: calc((var(--space-3) - var(--crumb-pad)) * 2);
}
.crumb-bar.is-stuck { --crumb-pad: var(--space-1); }
/* .page-header opens with its own top padding, which sat under the crumbs. They are a
   bar of their own now and carry it. */
.crumb-bar + .page-header { padding-top: 0; }


/* ── Finder: tabs and results ───────────────────────────────────── */

/* Mobile: the two tabs split the bar, half the width each. From 600px they
   shrink back to their natural width and sit left-aligned. */
/* The bar ends AT its underline. Nothing is painted below it — no padding, no band of
   page colour — so the results run right up under the separator instead of stopping at
   a strip of background that reads as a gap the page forgot to fill.
   --tab-h shrinks when locked; --tab-rest-h is what it shrinks from, and the difference
   goes back into the bottom margin for the same reason it does on the crumb bar. */
.finder-tabs {
  --tab-rest-h: 44px;
  --tab-h: var(--tab-rest-h);
  --tab-pad-y: var(--space-3);
  display: flex;
  gap: 0;
  border-bottom: var(--border-w-small) solid var(--border-default);
  margin-bottom: calc(var(--space-6) + var(--tab-rest-h) - var(--tab-h));
  top: calc(var(--nav-h) + var(--crumb-h));
}
.finder-tabs.is-stuck {
  --tab-h: 36px;
  --tab-pad-y: var(--space-2);
}
/* The tab itself is the canonical .tab in tokens.css: type, colour, the 2px indicator, hover,
   selected and focus (U1). It was restated here property for property, which is how a third tab
   component starts. What a FINDER tab adds is its bar's geometry: half the bar each on a phone,
   centred labels, and a height the sticky bar shrinks when it locks. */
.finder-tab {
  flex: 1 1 0;                  /* mobile: two tabs, half the bar each */
  text-align: center;
  padding: var(--tab-pad-y) var(--space-4);
  min-height: var(--tab-h);
}
/* Smallest phones: "Browse styles" cannot shrink past its own nowrap width, so
   tighten the tracking and padding — otherwise it claims 54% and the split
   visibly favours one tab. */
@media (max-width: 359px) {
  .finder-tab { padding: var(--tab-pad-y) var(--space-2); letter-spacing: var(--tracking-wider); }
}
@media (min-width: 600px) {
  .finder-tabs {
    gap: var(--space-1);
    margin-bottom: calc(var(--space-8) + var(--tab-rest-h) - var(--tab-h));
  }
  .finder-tab { flex: 0 0 auto; padding: var(--tab-pad-y) var(--space-5); }
}

/* From 900px the configurator rides along in its own column while the results scroll
   past it. The grid is `align-items: start`, so the column is only as tall as the card
   and sticky has somewhere to travel. `max-height` is the short-laptop guard: a viewport
   that cannot fit the card scrolls it internally rather than burying its bottom edge
   below the fold with no way to reach it. The 4px padding/margin pair keeps --shadow-sm
   from being shaved off by that overflow. */
@media (min-width: 700px) {
  .finder-config-col {
    position: sticky;
    top: calc(var(--nav-h) + var(--crumb-h) + var(--tabs-h) + var(--config-gap) - var(--space-1));
    align-self: start;
    max-height: calc(100vh - var(--nav-h) - var(--crumb-h) - var(--tabs-h) - var(--space-10));
    overflow-y: auto;
    padding: var(--space-1);
    margin: calc(var(--space-1) * -1);
  }
}

/* Stacked on mobile — configurator first, since you set it before you have
   results to read. From 900px it moves to the RIGHT and the results take the
   left, and the card grows with the viewport instead of staying pinned at its
   mobile 420px. DOM order is unchanged; only the placement flips. */
.finder-layout { display: grid; grid-template-columns: 1fr; gap: var(--space-8); align-items: start; }
/* 700px is where a column of results and the configurator both fit side by side, and the
   card moves to the RIGHT. Below it the card is left-aligned above the results, which at
   these widths left it stranded against one edge with the results starting a screen
   further down. The card gets a narrower column here than at 900 — 380 leaves the results
   ~256px, which is wider than a card already gets in the two-up mobile grid. */
@media (min-width: 700px) {
  .finder-layout     { grid-template-columns: minmax(0, 1fr) 380px; gap: var(--space-8); }
  .finder-results    { grid-column: 1; grid-row: 1; }
  .finder-config-col { grid-column: 2; grid-row: 1; }
  .chain-config      { max-width: none; }   /* fill the column it was given */
}
@media (min-width: 900px) {
  .finder-layout { grid-template-columns: minmax(0, 1fr) 440px; gap: var(--space-10); }
}
@media (min-width: 1200px) {
  .finder-layout { grid-template-columns: minmax(0, 1fr) 500px; }
}

/* The count belongs with the controls that produce it, not above the results. */
.finder-config-col { display: flex; flex-direction: column; gap: var(--space-3); }
/* Right-justified at every width. Below 700px the card is capped at 420 and stacked above
   the results, and left-aligned it sat against one edge of a much wider column with nothing
   beside it; hard right is where it already lives from 700px up, so the card keeps the same
   side of the page whether or not the results are next to it. A no-op once the card fills
   its own column, where the cap is lifted.
   `width: 100%` is what makes the pair line up. An auto margin on a flex item also stops it
   stretching, so each one shrink-wraps its own content: the card looked right only because
   the configurator happens to fill 420, while the meta row collapsed to the width of its two
   labels and took "11 chains match" away from the card's left edge with it. An explicit width
   keeps both boxes the same size, so the count starts where the card starts and the guide
   link ends where the card ends. */
.finder-config-menu, .finder-meta { width: 100%; margin-left: auto; }

/* Mobile: one card. Closed = title + V. Open = same title row, mark
   becomes an X, the configurator fills the rest. Desktop always shows
   the configurator and hides this chrome. */
.finder-config-menu {
  max-width: 420px;
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.finder-config-toggle {
  /* Where the title and the mark sit, measured from the card's right edge — the one edge
     the shrink does not move. chain-finder.js sets both; the values here are only what
     holds until it does. --lens is the magnifier's diameter, read back by the same code
     to work out how wide the finished button has to be. */
  --title-right: 50%;
  --mark-right: 0px;
  --lens: 15px;
  /* The header's height with the title still in the flow. chain-finder.js measures it. */
  --summary-h: 44px;
  position: relative;
  display: grid;
  /* Same columns as .chain-config-top so the title sits over the bust
     and the mark sits over the length column. */
  grid-template-columns: minmax(0, 1fr) 56px;
  column-gap: var(--space-3);
  align-items: center;
  min-height: 44px;
  padding: var(--space-4);
  border: 0;
  background: none;
  color: var(--text-body);
  cursor: pointer;
  list-style: none;
}
.finder-config-toggle > span:first-child {
  min-width: 0;
  text-align: center;
  font-size: var(--type-body);
  font-weight: var(--weight-demi);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
}
@media (max-width: 359px) {
  .finder-config-toggle > span:first-child { font-size: var(--type-body-sm); }
}
.finder-config-toggle::-webkit-details-marker { display: none; }
/* Fixed 12×12 slot, always centred in the length column. Both marks
   meet at this box's centre so the V and the X do not jump. */
.finder-config-mark {
  position: relative;
  grid-column: 2;
  justify-self: center;
  width: 12px;
  height: 12px;
}
.finder-config-mark::before,
.finder-config-mark::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  height: 0;
  border-bottom: var(--border-w-medium) solid currentColor;
}
/* V: two 7px arms that meet at the box centre. */
.finder-config-mark::before {
  width: 7px;
  margin-top: -0.5px;
  margin-left: -7px;
  transform: rotate(40deg);
  transform-origin: right center;
}
.finder-config-mark::after {
  width: 7px;
  margin-top: -0.5px;
  margin-left: 0;
  transform: rotate(-40deg);
  transform-origin: left center;
}
/* X: two 10px bars through the same centre. */
.finder-config-menu[open] .finder-config-mark::before,
.finder-config-menu[open] .finder-config-mark::after {
  width: 10px;
  margin-left: -5px;
  transform-origin: center;
}
.finder-config-menu[open] .finder-config-mark::before { transform: rotate(45deg); }
.finder-config-menu[open] .finder-config-mark::after  { transform: rotate(-45deg); }
@media (min-width: 480px) {
  .finder-config-toggle {
    grid-template-columns: minmax(0, 1fr) 68px;
    column-gap: var(--space-4);
    padding: var(--space-5) var(--space-5) var(--space-4);
  }
}
/* Below 900px the card collapses into its own title bar as you scroll past it, and
   that bar then stays pinned under the tabs so it can be tapped open again.
   Why fixed and not sticky: a sticky box may only travel inside its own parent, so a
   sticky card would come unstuck the moment the dock scrolled by. The dock reserves
   the card's full height instead, and the card is lifted out of the flow — the results
   rise to meet the shrinking bar because the reserved block is scrolling away above
   them, not because anything below is being re-laid-out. chain-finder.js writes top,
   left and width; they follow the dock so the pinned card lands exactly where the
   in-flow one was. */
.finder-config-menu.is-pinned {
  position: fixed;
  z-index: var(--z-raised);
}
/* The panel's own height is driven frame by frame; its contents keep their size and
   are clipped by the card, which is what makes it read as a fold rather than a squash. */
.finder-config-menu.is-pinned > #finder-config { overflow: hidden; }

/* …except the gauge row, which is not willing to be clipped. It rides up over the bust
   instead, so the one control you are most likely to still be changing survives the
   whole fold, and only the artwork is spent paying for the space.
   `overflow: hidden` above is what makes this work: it gives the panel a scrollport, and
   a `bottom: 0` sticky item inside a box shorter than its own content is pulled up to the
   visible bottom edge. The travel limit comes free — a sticky item may not leave its
   containing block, and .chain-config-top starts at the top of the panel, so the row
   climbs the full height of the bust and then stops. Scoped to .is-pinned because without
   the clip the nearest scrollport is the viewport, and the row would sit at the bottom of
   the SCREEN while the card was sitting still.
   Last to go is the title in the summary bar, which is not in here at all. */
/* How far the band overruns the row it is painted behind. Named because chain-finder.js
   reads it back: the band's top edge is where the bust has to be cut, and the cut is what
   keeps the frame's lower corners round instead of shearing them off square. */
.finder-config-menu { --gauge-band: var(--space-2); }

.finder-config-menu.is-pinned .chain-gauge,
.finder-config-menu.is-pinned .chain-gauge-head {
  position: sticky;
  /* Parked a --gauge-band clear of the panel's bottom edge, which is what puts the band's
     lower half on screen rather than under the clip. */
  bottom: var(--gauge-band);
  z-index: 1;
}
/* The bust is opaque and passes underneath, so the row has to be opaque too — and with
   air above and below it, or the chips sit edge to edge against artwork.
   That air is painted, not padded. Padding would re-flow the card on the very frame the
   fold starts, and `natural` was measured before that: the whole fold would be measuring
   itself against a height that no longer existed. A pseudo-element overruns the box
   instead, so the band is taller while the layout is untouched. It sits at z-index -1,
   which is behind the row's own content but still inside the stacking context the sticky
   z-index opened — so it covers the bust and not the chips. */
.finder-config-menu.is-pinned .chain-gauge::after,
.finder-config-menu.is-pinned .chain-gauge-head::after {
  content: '';
  position: absolute;
  z-index: -1;
  top: calc(var(--gauge-band) * -1);
  bottom: calc(var(--gauge-band) * -1);
  left: 0;
  right: 0;
  background: var(--surface-default);
}
/* The two halves are separate grid items, so the column gap between them would show a
   stripe of bust. The left half's band runs across it by exactly that gap. */
.finder-config-menu.is-pinned .chain-gauge::after { right: calc(var(--space-3) * -1); }
@media (min-width: 480px) {
  .finder-config-menu.is-pinned .chain-gauge::after { right: calc(var(--space-4) * -1); }
}

/* ── Phase two: the bar narrows into a button ─────────────────────────────────
   Once folded, the card keeps shrinking sideways until only the mark is left.

   The card's RIGHT edge never moves through any of this, so anything measured from it
   holds still on screen while the left edge sweeps across. That is what the title is
   pinned to: it is lifted out of the grid and anchored at --title-right, a constant set
   from where it sat before the shrink began. The frame slides over stationary text and
   clips it, instead of dragging it along and shrinking it — the text is being covered,
   not squeezed, and it should look like it.

   The mark is lifted out too, onto --mark-right, and that one is driven per frame: it
   starts at the distance it already sat at, so nothing jumps on the frame the shrink
   begins, and finishes at half the button's width, which is what lands it centred.
   Both are measured from the right, and both use translate(50%) so the anchor is the
   element's own centre rather than its edge. */
/* Lifting the title out of the flow takes its line box with it, and the header collapses
   from 52px to its 44px floor — which, on a card pinned by its TOP, walks the title 4px
   up on the one frame the shrink starts. The header keeps the height it had instead. */
.finder-config-menu.is-glass .finder-config-toggle { min-height: var(--summary-h); }

.finder-config-menu.is-glass .finder-config-toggle > span:first-child,
.finder-config-menu.is-glass .finder-config-mark {
  position: absolute;
  top: 50%;
  transform: translate(50%, -50%);
}
.finder-config-menu.is-glass .finder-config-toggle > span:first-child { right: var(--title-right); }
.finder-config-menu.is-glass .finder-config-mark { right: var(--mark-right); }

/* The V and the X are two bars through a point, which cannot be bent into a lens, so at
   this size the mark is rebuilt rather than restyled: the element becomes the ring,
   ::before goes, and ::after is re-aimed as the handle. */
.finder-config-menu.is-glass .finder-config-mark {
  width: var(--lens);
  height: var(--lens);
  border: var(--border-w-medium) solid currentColor;
  border-radius: 50%;
}
.finder-config-menu.is-glass .finder-config-mark::before { display: none; }
.finder-config-menu.is-glass .finder-config-mark::after {
  content: '';
  position: absolute;
  top: auto;
  left: auto;
  right: -3px;
  bottom: -2px;
  width: 6px;
  height: 0;
  margin: 0;
  border-bottom: var(--border-w-medium) solid currentColor;
  border-radius: var(--radius-round);
  transform: rotate(45deg);
  transform-origin: center;
}

/* Nested card chrome would double the border; the menu is the card. */
.finder-config-menu .chain-config {
  border: 0;
  box-shadow: none;
  border-radius: 0;
  max-width: none;
  padding-top: 0;
  /* Top inset comes from the header, so the card gives up its own. */
}
/* The bottom inset depends on what is last in the card, because the two things that can
   be want opposite amounts.
   Closed, that is the "I need help" prompt, and it should sit evenly under its rule:
   there is 15px between the separator LINE and the text — the rule-star's box is 14px
   tall because of the star glyph, but the line runs through its centre, so half that box
   is part of the gap — and the card's own padding left only 9px below. 10px squares it.
   Open, the recipient chips are last, and they want the card's ordinary inset so the
   bottom matches the sides. That case is simply left alone: scoping the override to the
   closed state means the open one keeps the padding shorthand, which is already the side
   value at all three breakpoints (16 / 20 / 24) and stays right if any of them moves.
   Should :has() ever be unsupported the whole rule drops, and the card falls back to that
   same even inset — the old behaviour, not a broken one. */
.finder-config-menu .chain-config:not(:has(.chain-rec[data-open="true"])) {
  padding-bottom: var(--space-2h);
}
/* Desktop keeps the same card, titled the same way. It used to strip the menu back
   to nothing and re-draw the card chrome on .chain-config inside, which left the
   desktop configurator as the only unlabelled panel on the page — the title existed
   but was hidden with the toggle. Now the menu IS the card at every width; desktop
   only drops the chevron and the ability to close it (chain-finder.js swallows the
   summary's click above 900px, and holds `open` true across the breakpoint). */
@media (min-width: 700px) {
  .finder-config-menu { max-width: none; }
  .finder-config-toggle { cursor: default; }
  .finder-config-mark { display: none; }
  .finder-config-menu > #finder-config { display: block !important; }
}
/* One row under the card: match count on the left, the guide link on the right.
   Capped to the card's own width so the right-justified link lands on the card's
   right edge, not the wider column, in the single-column layout. */
/* One child now — the count. space-between and the gap were holding it apart from the
   guide hint that used to sit opposite; with nothing to be spaced from they were describing
   a layout that no longer exists. The cap stays: it is what keeps the count aligned with the
   configurator above it rather than running the width of the results. */
.finder-meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  max-width: 420px;             /* mirrors .chain-config's cap */
}
@media (min-width: 700px) { .finder-meta { max-width: none; } }
.finder-count {
  font-size: var(--type-caption);   /* one step up from --type-label */
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: left;
}

/* Right side of the meta row, opposite the count. Quiet line; the destinations
   are the links. */
/* Column counts step with the space the RESULTS actually get, not the viewport:
   at 900px the configurator claims 420px beside them, so the count drops back
   to 2 before climbing again. Keyed to the viewport alone, cards ended up at
   ~157px here and the mm · cm row wrapped.

   DELIBERATE EXCEPTION to the tokens.css breakpoint policy (sm/md/lg/xl only):
   these four values are keyed to available grid width next to a sibling panel,
   not to device-class tiers, so snapping them onto 480/768/1024/1200 would
   reintroduce the exact cramped-card regression this comment describes. Left
   as-is on purpose during the ux-ui-redesign pass. */
.finder-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); }
@media (min-width: 360px)  { .finder-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 560px)  { .finder-grid { grid-template-columns: repeat(3, 1fr); } }
/* From 700 the grid is sharing the row with the configurator and has about 256px to work
   in — one card wide. It gets its second column back at 900, where the row widens. */
@media (min-width: 700px)  { .finder-grid { grid-template-columns: 1fr; } }
@media (min-width: 900px)  { .finder-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1150px) { .finder-grid { grid-template-columns: repeat(3, 1fr); } }

.finder-empty {
  grid-column: 1 / -1;
  color: var(--text-muted);
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  padding: var(--space-8) 0;
}

/* ══════════════════════════════════════════════════════════════════
   BROWSE STYLES — the editorial half of /chains/
   The "Find by fit" tab narrows the catalogue. This one explains it:
   a lede, the style cards, a six-way comparison, and where each
   length actually sits. Copy comes from chain-copy.js.
   ══════════════════════════════════════════════════════════════════ */

.browse-intro { max-width: 68ch; margin-bottom: var(--space-8); }
.browse-lede {
  font-size: var(--type-body);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
/* Under the style cards, not the lede — see findHintHTML in collection.js. */
.browse-find-hint {
  max-width: 68ch;
  margin-top: var(--space-6);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
}
/* A button, not a link: it switches tabs in place rather than navigating, and
   pretending otherwise breaks middle-click and the status bar. */
.browse-tab-link {
  background: none; border: none; padding: 0;
  font: inherit; cursor: pointer;
  color: var(--text-action);
  text-decoration: underline; text-underline-offset: 2px;
}
.browse-tab-link:hover { color: var(--text-action-hover); }
.browse-tab-link:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px; border-radius: var(--radius-sm);
}

.browse-section {
  margin-top: var(--space-16);
  padding-top: var(--space-10);
  border-top: var(--border-w-medium) solid var(--border-default);
}
/* On the guide page (length + gauge tabs), the brand star sits at the centre of
   each section divider — the footer convention. Only sections that FOLLOW another
   section carry a divider (the first in each panel has border-top:none), so the
   star rides the same set. A page-coloured pad breaks the rule so it reads as
   line — star — line. */
#panel-length .browse-section,
#panel-gauge .browse-section { position: relative; }
#panel-length .browse-section + .browse-section::before,
#panel-gauge .browse-section + .browse-section::before {
  content: var(--brand-star);
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 0 var(--space-3);
  background: var(--surface-page);
  color: var(--text-action);
  font-size: var(--type-body-sm);
  line-height: 1;
}
.browse-h {
  font-size: var(--type-h5);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h5);
  color: var(--text-heading);
  margin-bottom: var(--space-2);
}
.browse-sub {
  max-width: 62ch;
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
  margin-bottom: var(--space-6);
}
.browse-note {
  max-width: 68ch;
  margin-top: var(--space-5);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
.browse-note strong { font-weight: var(--weight-demi); color: var(--text-heading); }
.browse-link { color: var(--text-action); text-decoration: underline; text-underline-offset: 2px; }
.browse-link:hover { color: var(--text-action-hover); }

/* ── Comparison tables ────────────────────────────────────────────
   Six columns do not fit a phone, so the table scrolls inside its own
   box rather than pushing the page sideways. The style column is
   sticky, because a row is unreadable once its label has scrolled
   off. */
.cmp-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-default);
}
.cmp-table {
  width: 100%;
  min-width: 720px;
  border-collapse: collapse;
  font-size: var(--type-body-sm);
}
.cmp-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-raised);
  text-align: left;
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-w-small) solid var(--border-strong);
  white-space: nowrap;
}
.cmp-table td,
.cmp-table tbody th {
  padding: var(--space-4);
  border-bottom: var(--border-w-small) solid var(--border-default);
  line-height: var(--leading-snug);
  color: var(--text-body);
  vertical-align: top;
  text-align: left;
}
.cmp-table tbody tr:last-child td,
.cmp-table tbody tr:last-child th { border-bottom: none; }
.cmp-table tbody tr:hover td,
.cmp-table tbody tr:hover th { background: var(--surface-raised); }

/* First column stays put on horizontal scroll, on BOTH tables — the label a row
   is read by (the style name, or the length) must not slide away under the
   finger on a phone. The header row already sticks to the top; the top-left
   cell is pinned on both axes so it holds the corner while either scrolls. */
.cmp-table thead th:first-child,
.cmp-table tbody th:first-child {
  position: sticky;
  left: 0;
  background: var(--surface-default);
  border-right: var(--border-w-small) solid var(--border-default);
}
.cmp-table thead th:first-child { z-index: 3; background: var(--surface-raised); } /* the corner */
.cmp-table thead th:not(:first-child) { z-index: 2; }
.cmp-table tbody th:first-child { z-index: 1; }
.cmp-table tbody tr:hover th:first-child { background: var(--surface-raised); }

.cmp-style {
  font-weight: var(--weight-demi);
  white-space: nowrap;
}
.cmp-style a { color: var(--text-heading); text-decoration: underline; text-underline-offset: 3px; }
.cmp-style a:hover { color: var(--text-action); }

.cmp-num { white-space: nowrap; font-variant-numeric: tabular-nums; }
/* The honest-limits column. Set apart because it is the reason the table
   exists — no comparable publishes what its chains are bad at. */
.cmp-watch { color: var(--text-muted); }

.len-table { min-width: 640px; }
.len-unit { font-size: var(--type-caption); color: var(--text-muted); margin-left: 1px; }
.len-alt { color: var(--text-muted); }
.len-name { color: var(--text-muted); font-style: italic; white-space: nowrap; }

/* ── Per-style editorial block, under a style page's variant grid ── */
.style-copy {
  max-width: 72ch;
  margin-top: var(--space-16);
  padding-top: var(--space-10);
  border-top: var(--border-w-medium) solid var(--border-default);
}
.style-copy-lead {
  font-size: var(--type-body);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
  margin-bottom: var(--space-6);
}
.style-copy-list { display: flex; flex-direction: column; gap: var(--space-5); }
.style-copy-item dt {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}
.style-copy-item dd {
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}

/* ── The honest-limits callout ────────────────────────────────────
   Shared by the product page and the style listing pages. Tinted as a
   warning only when the style actually has something to warn about;
   otherwise it reads as the reassurance it is. */
.pdp-note {
  margin-top: var(--space-6);
  padding: var(--space-4);
  border-radius: var(--radius-base);
  background: var(--surface-raised);
  border-left: var(--border-w-large) solid var(--border-strong);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
.pdp-note-warn { background: var(--surface-warning); border-left-color: var(--border-warning); }
.pdp-note-tag {
  display: block;
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}
.pdp-note-warn .pdp-note-tag { color: var(--text-warning); }

/* ── Guide cards (/chains/guide/) ─────────────────────────────────
   One card per stocked length, each drawing the configurator's bust
   with that length lit. Same asset as the "Find by fit" filter. */
.guide-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* single-column mobile needs the floor dropped too */
  gap: var(--space-5);
}
/* minmax(0, 1fr), not 1fr: the card head holds a nowrap name, and a long one
   ("Long matinee") gives its column a wide min-content floor that a bare 1fr
   respects — so that column grew, and its bust with it. minmax(0, …) drops the
   floor and keeps every column, and every bust frame, the same size. */
@media (min-width: 768px)  { .guide-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .guide-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.guide-card {
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
/* Hover zooms the bust, exactly like the configurator on "Find by fit" — the
   .chain-stage:hover rule already does the scale, so the guide cards just need
   to NOT suppress it. Simple and effective; no per-chain emphasis. */
.guide-card-body { display: flex; flex-direction: column; gap: var(--space-3); }

/* The length and its classical name share one line — cm on the left, the name
   on the right — so the head is a single row instead of a stacked pair, which
   takes height out of the card. */
.guide-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.guide-card-cm {
  font-size: var(--type-h6);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h6);
  color: var(--text-heading);
  white-space: nowrap;
}
.guide-card-cm-u {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-regular);
  color: var(--text-muted);
  margin-left: 1px;
}
.guide-card-in {
  font-size: var(--type-caption);
  font-weight: var(--weight-regular);
  color: var(--text-subtle);
  margin-left: var(--space-2);
}
/* The classical name is the headline of the card — larger than the cm and in
   the brand maroon, so Choker / Princess / Matinee reads as the thing named. It
   may wrap (only "Long matinee" might, on a narrow card): kept
   right-aligned so a second line tucks under the first rather than forcing the
   card wider than its column. */
.guide-card-name {
  font-size: var(--type-h5);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h5);
  color: var(--text-action);
  text-align: right;
  min-width: 0;
}
.guide-sit { display: flex; flex-direction: column; gap: var(--space-2h); }
.guide-sit dt {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.guide-sit dd {
  font-size: var(--type-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-body);
}
.guide-card-note {
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: var(--border-w-small) solid var(--border-default);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
}

.guide-prose { max-width: 72ch; }
.guide-list {
  max-width: 72ch;
  margin: var(--space-5) 0;
  padding-left: var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-2h);
}
.guide-list li {
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
.guide-list strong { font-weight: var(--weight-demi); color: var(--text-heading); }

.guide-cta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-5);
  margin-top: var(--space-2);
}

/* ── Gauge guide ──────────────────────────────────────────────────
   Reuses the length guide's card, head and cropped bust. Only three
   cards, so the grid goes three-across a step earlier (md, not lg). */
@media (min-width: 768px) { .guide-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.gauge-card-range {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-demi);
  color: var(--text-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.gauge-card-body-text {
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
/* The "useful for" note — the whole reason the page exists, so it is set apart
   with a tinted rule the way the layering note is on the length guide. */
.gauge-card-use {
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
  background: var(--surface-raised);
  border-left: var(--border-w-large) solid var(--border-strong);
  border-radius: 0 var(--radius-base) var(--radius-base) 0;
  padding: var(--space-3) var(--space-4);
}
.gauge-card-use-tag {
  display: block;
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}
/* The wearer convention, quiet — it is context, not the headline (rule 2). */
.gauge-card-wearer {
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: var(--border-w-small) solid var(--border-default);
  font-size: var(--type-caption);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
.gauge-caveat { text-align: center; color: var(--text-subtle); font-style: italic; }

/* The measured-density column. Banded rather than free text so the eye can sort
   the table without reading it; the band comes from chain-copy.js and is
   verified against inventory by the test suite, not chosen for looks. */
.cmp-heft { white-space: nowrap; font-weight: var(--weight-demi); }
.cmp-heft[data-band="Dense"]  { color: var(--text-heading); }
.cmp-heft[data-band="Middle"] { color: var(--text-body); }
.cmp-heft[data-band="Light"]  { color: var(--text-muted); }
.cmp-heft::before {
  content: '';
  display: inline-block;
  width: var(--space-2); height: var(--space-2);
  margin-right: var(--space-2);
  border-radius: var(--radius-round);
  vertical-align: baseline;
  background: currentColor;
}
.cmp-heft[data-band="Light"]::before  { opacity: 0.35; }
.cmp-heft[data-band="Middle"]::before { opacity: 0.65; }

/* ── The bust as a static illustration (product page) ─────────────
   Displayed exactly like the guide cards and the configurator: the art is
   sliced and anchored at the top (xMidYMin), showing the neck and upper chest
   and running the lower torso off the bottom. That means the same short frame
   (90.91%, the configurator's aspect) rather than the full figure — shorter,
   and cropped/centred the same way everywhere. The only difference from the
   guide cards is a heavier stroke, set in JS for this smaller frame.

   min-height:0 releases the configurator's 200px floor (which has length
   buttons to match); the aspect spacer below decides the height instead. */
.chain-stage-illus { pointer-events: none; min-height: 0; }
.chain-stage-illus::before { padding-top: 90.91%; }
/* The svg uses the base .chain-stage positioning (slice-fill, flush bottom) — no
   override needed now that it slices like everything else. */
/* It is a picture, not a target — no hover zoom. */
.chain-stage-illus:hover svg { transform: none; }

/* The homepage's card MEDIA — the guide bust, the Gallery deck, the Instagram reel —
   is not here. It lives in shop/index.html beside the markup it dresses, for the reason
   written at the top of that file's <style>: the document is served must-revalidate and
   this sheet is not, so a returning visitor can pair new markup with a four-hour-old
   stylesheet. The generic .cat-card chrome above is shared and stable; the per-card
   treatments change with their markup and travel with it.
   (shop/index.html also carries the hover rules that replace .cat-card-img img's scale —
   those wells animate their frames, and an animation beats a plain declaration.) */

/* ══ Learn hub ═════════════════════════════════════════════════════════════════
   A hub of cards plus long-form articles, built from the existing card, pill and
   spacing tokens. Content lives in shop/learn/content.js; this only dresses it. */

.learn-head { padding: 0; max-width: 68ch; }
.learn-lead {
  margin-top: var(--space-4);
  font-size: var(--type-body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-muted);
}

.learn-group { margin-bottom: var(--space-12); }
/* Closed folds are a stack of headings. The open one earns the section's full gap back. */
.learn-group.is-fold { margin-bottom: var(--space-4); }
.learn-group.is-fold:has(.learn-fold[aria-expanded="true"]) { margin-bottom: var(--space-12); }
.learn-group.is-fold .learn-group-title {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}
.learn-fold {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
  margin: 0;
  padding: 0 0 var(--space-3);
  border: 0;
  border-bottom: var(--border-w-small) solid var(--border-default);
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: pointer;
  /* Safari on a phone paints the native rounded button unless appearance is
     cleared. Desktop never did, so the heading has to opt out to stay the
     same rule and type at every width. */
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
}
.learn-fold:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
/* The count sits in the title, in the same disc as the nav. .count-bubble owns the
   shape and colour; this only keeps the tracked-out heading from spacing the digit. */
.learn-fold-name {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.learn-fold .count-bubble {
  letter-spacing: 0;
  text-transform: none;
}
.learn-fold-chev {
  flex: 0 0 auto;
  width: 0.45em;
  height: 0.45em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.1em) rotate(45deg);
}
.learn-fold[aria-expanded="true"] .learn-fold-chev {
  transform: translateY(0.15em) rotate(-135deg);
}
.learn-fold-body { margin-top: var(--space-5); }
.learn-group-title {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-5);
  border-bottom: var(--border-w-small) solid var(--border-default);
}

/* ── The cards ── */
/* Two columns from 640, not 480. At 500 a two-up card was 226px wide and its blurb had 178px to
   sit in — about twenty characters a line, so every card ran to three or four lines at the one
   width where the grid was tightest. Below 640 a card takes the full width instead, which is the
   only honest thing to do with a card that narrow.

   minmax(0, …), not 1fr: `1fr` is `minmax(auto, 1fr)` and never shrinks below its content, and
   the card now holds a flex row with a fixed-width mark in it (CLAUDE.md). */
.learn-cards { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
@media (min-width: 640px)  { .learn-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .learn-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.learn-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-6);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-default);
  text-decoration: none;
  transition: box-shadow var(--transition-base), transform var(--transition-base),
              border-color var(--transition-base);
}
.learn-card:hover {
  box-shadow: var(--shadow-lg);
  transform: translateY(-3px);
  border-color: var(--text-subtle);
}
.learn-card-title {
  font-size: var(--type-h5);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h5);
  color: var(--text-heading);
}
/* The link is part of the sentence, in the sentence's own type. The whole card is a link, so
   the words are a cue rather than a control — set apart in size and letter-spacing they read as
   a button on a surface that is already pressable, and cost the card a row to do it. Colour is
   the only difference, and only so the eye finds the end of the line. */
.learn-card-more { color: var(--text-action); white-space: nowrap; }

/* A READING MEASURE, so the line length barely changes with the column. A card is 288px wide in
   a three-up row and 568px wide as a single column, and one sentence cannot be two lines in both
   — uncapped it ran to two lines on a desktop and one on a phone-and-a-half. Capped, the text
   wraps at about the same place everywhere and the card keeps its shape. */
.learn-card-blurb {
  max-width: 30ch;
  margin-top: var(--space-2);
  font-size: var(--type-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
}
.learn-card-cta {
  /* The FEATURE card keeps the set-apart label: it is the one card with a call to action of its
     own rather than a whole surface to press.
     A BLOCK at every width, so it takes its own line and starts on the same left edge as the copy
     above it. It rode the end of the sentence for a while; at full width that put it a long way
     out to the right of a line it was meant to follow. nowrap because letter-spaced small caps
     broke as "HOW / IT WORKS" and read as two things. */
  display: block;
  white-space: nowrap;
  margin-top: var(--space-4);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-action);
}

/* The featured topic spans the whole row and carries an extra line — it is the one card on
   this page we actually want clicked, and a lone card in a three-column grid reads as an
   afterthought rather than a headline. */
.learn-card-feature {
  grid-column: 1 / -1;
  background: var(--surface-raised);
  border-color: var(--text-subtle);
}
/* Copy on the left, the object on the right — the arrangement the article itself opens with, so
   the card is a small preview of the page behind it rather than a different design. */
.learn-card-feature {
  display: grid;
  /* `auto`, not a percentage. A quarter of the card made the tag 307px tall against 176px of
     copy, so the TAG set the card's height and left 131px of dead space above and below the
     text. The object should be as big as the words allow, not the other way round. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(var(--space-4), 3vw, var(--space-8));
}
.learn-card-feature-copy { display: flex; flex-direction: column; align-items: flex-start; }
/* The 62ch measure below is for a paragraph. This card's copy is ONE short line with the call to
   action riding its end, and the cap left no room for it — the label wrapped to a line of its
   own, which is the arrangement it was moved out of. The card's own width is the constraint. */
.learn-card-feature .learn-card-detail { max-width: none; }
.learn-card-feature .learn-card-title { font-size: var(--type-h4); line-height: var(--lh-h4); }
/* Decoration inside a link. The whole card is one click target, and the tag's drag handler would
   otherwise compete with it — a drag ending on the tag would still navigate. Not hoverable, not
   draggable; it turns on its own, which is all it is here for.

   Sized as a PERCENTAGE, unlike every other slot on the site, and deliberately. The house pattern
   fixes the box at `calc(368px * scale)` and shrinks the model with a transform, because the
   portal animates the tag between slots and resizing a model-viewer mid-flight reallocates (and
   clears) its WebGL buffer. None of that applies to a card that never moves — and a unitless
   scale cannot be derived from a column width in CSS, so the transform approach can only ever
   approximate "a quarter of the card" with hard-coded steps. Here the slot simply fills its
   column, so 25% means 25% at every width. */
.learn-card-tag {
  pointer-events: none;
  /* Right-justified against the card's edge, and sized so its height lands just under the copy
     block's. 160px wide is 181px tall, against roughly 176px of text — close enough that the
     card is still sized by its words. */
  justify-self: end;
  width: 160px;
  aspect-ratio: 368 / 416;
}
.learn-card-tag model-viewer {
  display: block;
  width: 100%;
  height: 100%;
  background: transparent;
  --poster-color: transparent;
}
.learn-card-detail {
  margin-top: var(--space-3);
  max-width: 62ch;
  font-size: var(--type-body);
  /* Unitless: this line wraps. A fixed --lh-* under it is the overlap bug from tokens.css §leading. */
  line-height: var(--leading-normal);
  color: var(--text-body);
}

/* ── Pills: policies, and "keep reading" ── */
.learn-links { display: flex; flex-wrap: wrap; gap: var(--space-2h); }
.learn-pill {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2h) var(--space-4);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-chip);
  background: var(--surface-raised);
  font-size: var(--type-body-sm);
  color: var(--text-body);
  text-decoration: none;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}
.learn-pill:hover { border-color: var(--text-action); color: var(--text-action); }

/* ── Article prose ── */
/* ONE MEASURE FOR THE WHOLE LEARN SECTION.
   The hub, the five articles and the affiliate page are read as a sequence, and they used to be
   three different widths: the hub at .shop-container's 1200px, a plain article at 760px, and the
   affiliate page at 1040px. Clicking between them resized the page under the reader, which reads
   as three sites rather than one section.

   1040px is the affiliate page's width and now the section's, named once here. It is narrower
   than the catalogue's 1200px on purpose — that width is right for a grid of product cards and
   wide for a page read left to right.

   Prose still gets a reading measure inside that frame (--learn-measure, below); what lines up
   between pages is the frame — breadcrumb, headings and figures — not the paragraph. */
.learn-measure,
.learn-article {
  /* The section's frame, and the reading measure inside it. Both named here so the hub, a prose
     article and the affiliate page cannot drift apart again. */
  --learn-frame: 1040px;
  /* IN REM, NOT CH. `ch` resolves against the element's OWN font size, so the same declaration
     capped a 16px paragraph at 694px and a 32px heading at twice that — the heading ran to the
     frame while the prose under it stopped short, which is the page appearing to change width as
     you read down it. 43rem is the same measure the paragraphs already had, now shared by every
     block that takes it. */
  --learn-measure: 43rem;
  max-width: var(--learn-frame);
}

/* Running text keeps a reading measure; headings, figures and the rich blocks use the frame.
   LEFT-ANCHORED, not centred: the paragraph shares its left edge with the breadcrumb and the H1
   above it, which is what makes the column read as one page rather than a box floating in it.
   (The centring warning in the rich-article notes below is about a different case — there the
   full-width hero made a capped child look off-centre. Here every prose block is capped alike.) */
.learn-article:not(.is-rich) :is(.learn-h2, .learn-p, .learn-list, .learn-note, .learn-faq) {
  max-width: var(--learn-measure);
}

/* ── A COMPILED ARTICLE (.is-doc) ─────────────────────────────────────────────
   Centred, and ONE WIDTH FOR EVERYTHING.

   Scoped to `.is-doc` and not to `:not(.is-rich)`, which is the mistake the first version made:
   the six hand-written topics are not prose articles. /learn/pricing/, /learn/orders/, /learn/925/
   and /learn/values/ lay their content out in TWO COLUMNS (`{ two: [...] }` in content.js), and
   narrowing the frame under them squeezed their prose to 352px while the breadcrumb above it
   stayed at 688 — three different widths on one page. They keep the 1040px frame they were
   designed against; only the compiled manuscripts are re-laid-out.

   `margin-inline: auto` is the whole centring change. The TEXT stays left-aligned, because
   centred running text is unreadable; it is the COLUMN that moves to the middle. The breadcrumb
   and the page header are in the list, so the crumb, the H1 and the prose share one left edge
   and one right edge — the article reads as a single object rather than as three stacked
   rectangles of different widths. */
/* THE CONTAINER IS THE READING MEASURE. Nothing inside it is centred individually.
 *
 * The first version capped each block at 43rem and centred it with `margin-inline: auto` inside
 * a wider frame. Two things were wrong with that. The breadcrumb is a STICKY, FULL-BLEED bar —
 * it carries `margin-left/right: calc(var(--bleed) * -1)` so its background reaches the
 * container's gutters and content cannot scroll up through the padding beside it — and
 * `margin-inline: auto` overwrote exactly those margins. The bar then sat inside the measure and
 * applied its own 32px padding on top, so the breadcrumb TEXT started 32px right of the
 * paragraph below it. The boxes measured equal; the words did not line up.
 *
 * So: narrow the container to the measure plus its own gutters, and let every child fill it, the
 * way /learn/affiliate/ and every other page on the site already work. The bleed maths then
 * comes out right on its own — the bar's negative margin and its padding cancel, and the
 * breadcrumb lands on the same left edge as the prose at every width. Nothing needs centring
 * because there is no spare room to centre in. */
.learn-article.is-doc {
  max-width: calc(var(--learn-measure) + var(--page-px) * 2);

  /* Photographs sit INSIDE the column, never across it — narrower than the text at every width.
     `--learn-figure-w` is the same 84% the referral page uses for its two big pictures on a
     phone (see .learn-article.is-rich in the mobile block below); reusing the number rather than
     inventing a second one is what stops the two kinds of Learn page drifting apart on the one
     dimension a reader can compare across them. min() picks whichever is smaller, so the rem cap
     governs a desktop and the percentage governs a phone. */
  --learn-figure: 34rem;
  --learn-figure-portrait: 22rem;
  --learn-figure-w: 84%;
}
.learn-article.is-doc .learn-figure {
  max-width: min(var(--learn-figure), var(--learn-figure-w));
  margin-inline: auto;
}
.learn-body { padding-bottom: var(--space-10); }
.learn-h2 {
  margin: var(--space-10) 0 var(--space-4);
  font-size: var(--type-h4);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h4);
  color: var(--text-heading);
}
.learn-p {
  margin-bottom: var(--space-4);
  font-size: var(--type-body);
  line-height: var(--lh-body);
  color: var(--text-body);
}
/* Coconat's 700 shouts mid-sentence — emphasis in long-form prose sits at demi.
   Same rule as .legal-body strong. */
.learn-p strong, .learn-list strong, .learn-note strong { font-weight: var(--weight-demi); }

.learn-list { margin: 0 0 var(--space-5); padding-left: var(--space-6); }
.learn-list li {
  margin-bottom: var(--space-2h);
  font-size: var(--type-body);
  line-height: var(--lh-body);
  color: var(--text-body);
}
.learn-link { color: var(--text-action); text-decoration: underline; text-underline-offset: 3px; }
.learn-link:hover { color: var(--text-action-hover); }

/* ── Compiled articles ───────────────────────────────────────────────────────
   The blocks the markdown compiler emits (scripts/build-learn.mjs) that the six hand-written
   topics never needed: a sub-heading, an ordered list, a figure, the pull CTA, the sources list.
   Everything here inherits the same 760px measure as the prose above it. */

/* A sub-heading. `--leading-tight` and NOT --lh-h5: the --lh-* tokens are fixed heights, and a
   28px box under 24px type collapses the moment a heading wraps to a second line — which
   "Method 2 — mild soap, warm water, two minutes" does on a phone. tokens.css §line-height
   states the rule; this is the case it warns about. */
.learn-h3 {
  margin: var(--space-7) 0 var(--space-3);
  font-size: var(--type-h5);
  font-weight: var(--weight-demi);
  line-height: var(--leading-tight);
  color: var(--text-heading);
}

/* The numbers are the browser's. See the note in learn.js on why this is an <ol>. */
.learn-ol { margin: 0 0 var(--space-5); padding-left: var(--space-6); }
.learn-ol li {
  margin-bottom: var(--space-3);
  font-size: var(--type-body);
  line-height: var(--lh-body);
  color: var(--text-body);
}
.learn-ol strong, .learn-figcaption strong { font-weight: var(--weight-demi); }

/* A photograph in the column. `aspect-ratio` from the attributes rather than a fixed height, so
   the space is reserved before the file lands and the reader is never scrolled by a late image.
   width/height are mandatory in media.js for exactly this. */
.learn-figure {
  margin: var(--space-7) 0;
  max-width: var(--learn-measure);
}
.learn-figure picture { display: block; }
/* The photograph is a button because it opens the Gallery viewer. Reset the button chrome
   so the figure's own sizing — lead crop, portrait cap, grid — still measures the image. */
/* Not .learn-shot — that class is the affiliate tour's grid cell, and it pins its
   item to column 1. A photograph button with that name stacked every chain on one spot. */
.learn-figure-btn {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: zoom-in;
  text-align: inherit;
}
/* A button's min-width is auto, which is the photograph's own width. The grid column
   is already minmax(0, …); the item has to agree or the row will not shrink. */
.learn-figure-grid > .learn-figure-btn { min-width: 0; }

/* The product page draws this strip in an inline style on product.html. Learn uses the
   same windows, so the rules have to exist on a page that never loads that file.
   Clipping, not overlap: the icons are transparent, and an overlap shows the tail through. */
.learn-article .pdp-def-figure {
  --chain-strip-h: 56px;
  display: flex;
  overflow: hidden;
  height: var(--chain-strip-h);
  margin: var(--space-4) 0 var(--space-5);
}
.learn-article .pdp-strip-link {
  --chain-window-w: 224px;
  --chain-window-x: 0px;
  flex: 0 0 auto;
  display: block;
  position: relative;
  width: var(--chain-window-w);
  height: 100%;
  overflow: hidden;
}
.learn-article .pdp-strip-link img {
  position: absolute;
  top: 0;
  left: calc(-1 * var(--chain-window-x));
  width: 224px;
  max-width: none;
  height: 100%;
  display: block;
}
.learn-figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}
/* A portrait crop, capped. See figureBlock() in learn.js: the class comes from the declared
   dimensions. 26rem is about two thirds of the prose measure — enough that the photograph is
   still the thing you look at, not so much that it is the only thing on the screen. Full width
   on a phone, where 26rem is already wider than the viewport and the cap would do nothing but
   confuse the margin. */
/* The opening photograph. It used to run to the FRAME — wider than the prose under it — on the
   reasoning that a lead should be the biggest thing on the page. In place that read as a fourth
   width on a page that already had three, and it is the figure that should give way: the column
   is the article. It keeps the extra vertical room, and the 4:5 crop is still capped by height
   so a portrait hero is not a screenful of neck before the first sentence. */
.learn-article.is-doc .learn-figure.is-lead {
  margin-block: var(--space-6) var(--space-8);
}
.learn-figure.is-lead img {
  width: 100%;
  max-height: 30rem;
  object-fit: cover;
  object-position: center 30%;   /* the chain sits below centre in every 4:5 crop we hold */
}
/* Desktop swaps in the uncropped frame via <picture>. A 3:2 at the figure's width is shorter
   than the 30rem cap, so contain shows the whole photograph instead of the portrait crop. */
.learn-figure.is-lead.has-wide { --wide-aspect: 3 / 2; }
@media (min-width: 768px) {
  .learn-figure.is-lead.has-wide img {
    aspect-ratio: var(--wide-aspect);
    max-height: none;
    object-fit: contain;
    object-position: center;
  }
}

/* A set of photographs as one illustration. Runs to the FRAME like the lead, because six
   thumbnails inside a 43rem reading measure are six thumbnails. `auto-fit` rather than a fixed
   count so the row reflows to two or three per line on a phone without a second media query;
   `--grid-cols` is the desktop ceiling, set per slot in media.js. */
/* A set is still a figure, so it obeys the figure width. Six 4:5 boxes at 34rem are ~10rem
   each on a desktop, which is enough to read a box and a chain and no more than the comparison
   needs. */
.learn-article.is-doc .learn-figure.is-grid { max-width: min(var(--learn-figure), var(--learn-figure-w)); }
/* The desktop column count. DECLARED HERE with its default rather than left to a var()
   fallback: media.js sets it inline per slot, and brand.test.mjs fails a `var()` whose token is
   defined nowhere precisely because the fallback is what would make a typo invisible. */
.learn-figure.is-grid { --grid-cols: 3; }
.learn-figure-grid {
  display: grid;
  /* minmax(0, …) — `1fr` is `minmax(auto, 1fr)` and an item never shrinks below its content,
     which for an image means its intrinsic width and a row that bursts out of the page. */
  grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), minmax(0, 1fr)));
  gap: var(--space-3);
}
@media (min-width: 48rem) {
  .learn-figure-grid { grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr)); }
}
.learn-figure-grid img {
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

/* THE SELECTOR HAS TO OUT-SPECIFY THE MEASURE RULE ABOVE, which is why it is not simply
   `.learn-figure.is-portrait`. That reads as more specific and is not: the measure cap is
   `.learn-article:not(.is-rich) :is(… .learn-figure …)`, which scores 0,3,0 — the two classes
   plus the one inside :is() — against a bare compound's 0,2,0. The cap silently lost and the
   portrait ran to the full measure anyway, with the class correctly applied and the rule
   correctly written. Matching the ancestor is what makes it stick. */
/* No media query: min() already narrows this on a phone, and a portrait crop needs the inset
   there more than anything else does. */
.learn-article.is-doc .learn-figure.is-portrait {
  max-width: min(var(--learn-figure-portrait), var(--learn-figure-w));
}

.learn-figcaption {
  margin-top: var(--space-3);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
}

/* The in-article call to action. A rule on the left, like .learn-note, rather than a tinted
   panel — it is an invitation in the middle of an explanation, and a coloured box in the run of
   the prose reads as an advertisement someone has dropped into the page. */
.learn-pull {
  margin: var(--space-8) 0;
  padding-left: var(--space-5);
  border-left: var(--border-w-medium) solid var(--border-strong);
  max-width: var(--learn-measure);
}
.learn-pull-p {
  margin-bottom: var(--space-4);
  font-size: var(--type-body);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
/* `min-width: 0` on the items, not just `flex: 1`. A long label ("See how each length sits")
   floors at its own min-content width otherwise and pushes the row off a 320px screen while
   looking like it is behaving — CLAUDE.md records this one. */
.learn-pull-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0; }
.learn-pull-actions .btn { min-width: 0; }

/* The named sources. Small, muted, and set below the article rather than inside it: it is the
   inventory a reader checks the page against, not part of the argument. */
.learn-sources { margin-top: var(--space-10); max-width: var(--learn-measure); }
.learn-source-list { margin: 0; padding-left: var(--space-6); }
.learn-source-list li {
  margin-bottom: var(--space-3);
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
}

.learn-updated {
  margin-top: var(--space-8);
  font-size: var(--type-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-muted);
}

/* ── Tables ──────────────────────────────────────────────────────────────────
   Two shapes, chosen by column count in tableBlock(). See the note there.

   The frame is the scroll container, and it carries NO negative margin: `overflow-x: auto`
   establishes a scroll container on both axes, so anything reaching outside it is clipped
   rather than overlapping (CLAUDE.md). `tabindex="0"` because a scrollable region has to be
   reachable by keyboard, or the last two columns of a comparison exist only for mouse users. */
.learn-table-frame {
  overflow-x: auto;
  margin: var(--space-6) 0;
  -webkit-overflow-scrolling: touch;
}
.learn-table-frame:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
.learn-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
.learn-table th,
.learn-table td {
  padding: var(--space-3) var(--space-4) var(--space-3) 0;
  text-align: left;
  vertical-align: top;
  border-bottom: var(--border-w-small) solid var(--border-default);
}
.learn-table thead th {
  font-weight: var(--weight-demi);
  color: var(--text-heading);
  white-space: nowrap;
}
.learn-table tbody tr:last-child :is(th, td) { border-bottom: none; }

/* The fact sheet. No header row, no rules between columns — the label carries the weight and
   the pair reads as one line, which is what a two-column table of terms actually is. */
.learn-table.is-facts th[scope="row"] {
  width: 34%;
  font-weight: var(--weight-demi);
  color: var(--text-heading);
}
@media (max-width: 30rem) {
  /* On a phone a 34% label column leaves four words a line. Stack instead: the label on its own
     row above its value, which is how a definition list would have read all along. */
  .learn-table.is-facts,
  .learn-table.is-facts tbody,
  .learn-table.is-facts tr { display: block; }
  .learn-table.is-facts :is(th, td) { display: block; width: auto; padding-right: 0; }
  .learn-table.is-facts th[scope="row"] { width: auto; padding-bottom: var(--space-1h); border-bottom: none; }
  .learn-table.is-facts td { padding-top: 0; }
}

/* ── Find a guide ────────────────────────────────────────────────────────────
   A filter over the cards already on the page, not a search index. See mountFind() in learn.js.
   Added by script, so a reader without JS gets the full grid and no dead input. */
.learn-find { margin: var(--space-8) 0 var(--space-6); }
.learn-find-label {
  display: block;
  margin-bottom: var(--space-2h);
  font-size: var(--type-body-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.learn-find-input {
  width: 100%;
  max-width: 28rem;
  padding: var(--space-3) var(--space-4);
  font-family: inherit;
  font-size: var(--type-body);
  line-height: var(--leading-normal);
  color: var(--text-body);
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-md);
}
.learn-find-input:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
.learn-find-count {
  margin: var(--space-2h) 0 0;
  min-height: 1.25em;      /* reserved, so the tag row does not jump as the count appears */
  font-size: var(--type-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-muted);
}
.learn-find-tags { display: flex; flex-wrap: wrap; gap: var(--space-2h); margin-top: var(--space-3); }
.learn-tag {
  padding: var(--space-1h) var(--space-3);
  font-family: inherit;
  font-size: var(--type-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-muted);
  background: var(--surface-raised);
  border: var(--border-w-small) solid transparent;
  border-radius: var(--radius-chip);
  cursor: pointer;
}
.learn-tag:hover { color: var(--text-body); }
.learn-tag[aria-pressed="true"] {
  color: var(--text-heading);
  border-color: var(--border-strong);
  background: var(--surface-elevated);
}

/* The pillar of a cluster, on the hub. A border rather than a fill or a scale: the card grid is
   a place to choose from, and one card painted a different colour reads as the only one that is
   for sale. */
.learn-card.is-pillar { border-color: var(--border-strong); }

/* The honest caveat. Set apart with a rule rather than a tinted box: it is a remark, not a
   warning, and a coloured panel would read as one. */
.learn-note {
  margin: var(--space-5) 0;
  padding-left: var(--space-5);
  border-left: var(--border-w-medium) solid var(--border-default);
  font-size: var(--type-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
}

/* ── Rich articles ───────────────────────────────────────────────────────────
   Opt-in, per topic (`rich: true`). The five prose articles keep the 760px column exactly as
   they had it.

   THE SHOP'S OWN WIDTH, CENTRED, FOR EVERYTHING. A rich article is `.shop-container` and nothing
   inside it narrows: the same 1200px the catalogue and every other page uses. It was capped to
   an 820px column, which `max-width` on the children left ANCHORED TO THE LEFT — so the page
   read as both narrower than the shop and pushed off-centre, which is exactly what it was.

   No block re-caps itself either. A hero at the full width above prose that stops short makes
   the page look like it resizes as you read it; one measure, top to bottom, is the whole rule
   here.

   1040px, not the catalogue's 1200: that width is right for a grid of product cards and wide
   for an article read left to right. This holds the photograph beside three steps and still
   keeps a paragraph near ninety characters. .shop-container's auto margins centre it. */
.learn-article.is-rich {
  /* The page's vertical beat, named once. Every boundary on a rich page — above the lead, either
     side of a section, either side of the rule before the closing CTA — is a multiple of this,
     and they were drifting apart because each was written out separately. */
  --learn-beat: clamp(var(--space-16), 12vh, var(--space-32));
}
/* The reference sections at the end — Getting paid, The rules — keep the section rhythm. Left
   at the prose default the page went from airy to tight halfway down, and the last third read
   as a different document. */
.learn-article.is-rich .learn-h2 { margin-top: var(--learn-beat); }
.learn-article.is-rich .learn-body > .learn-h2:first-child { margin-top: 0; }
/* The header carries no title on a rich page — the hero's H1 is the title — so the crumb would
   otherwise sit alone above a gap. */
.page-header.is-bare {
  /* Only the space BELOW is removed. The crumb keeps whatever distance from the nav .page-header
     gives it everywhere else in the shop — it is the same component in the same position, and a
     rich page is not a reason for it to sit lower here than on /chains/ or /learn/orders/. The
     air above the lead belongs to the lead, and .learn-hero's own padding provides it. */
  padding-bottom: 0;
  margin-bottom: 0;
}

/* ── The lead ──
   H1, the line under it, the buttons, and the object on the right. The same shape as the
   affiliate sign-in screen and the Share tab, because it is the same object being introduced —
   someone who reads this and then signs in should recognise the arrangement. */
.learn-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* Centred, not top-aligned. The min-height below makes this section taller than its content,
     and top-aligned that surplus all fell BELOW the buttons as one dead half-screen. Centred,
     the same surplus is split above and below and reads as the lead sitting in its own space. */
  align-items: center;
  gap: clamp(var(--space-6), 4vw, var(--space-12));
  /* The same beat as every other section, top and bottom — the lead was on a smaller rhythm of
     its own and read as a header rather than as the page's first section. The min-height is what
     makes it OWN the first screen: without it a short lead left the next heading peeking over
     the fold. Capped in px as well as vh so a very tall monitor does not turn it into an empty
     field. */
  padding-block: var(--learn-beat);
  /* Sized so the NEXT section's heading breaks the fold by a few dozen pixels. A lead that
     ends exactly at the fold looks like the end of the page; a sliver of the following heading
     is what tells the reader to keep going. Expressed as the viewport minus a fixed block
     rather than as a vh fraction, because the sliver has to stay the same size at every window
     height — a percentage grows the gap on tall screens and closes it on short ones.

     Everything stacked above that heading is subtracted by name: the nav, and one beat (the next
     section's own top padding). The beat grows with the viewport, so subtracting a flat block
     instead left the sliver at 72px on an 800px-tall window and 26px on a 1080px one — the same
     rule reading as "there is more below" on one screen and as a rendering slip on another. The
     remaining 5.7rem is the header block the crumb sits in, plus the sliver itself.

     It was a beat and a HALF while the crumb carried half a beat of its own; that override is
     gone, so this follows it down. The two numbers are coupled and always were — changing the
     header's padding without changing this one silently moves the fold.

     The cap stops a very tall monitor turning the lead into an empty field; above it the sliver
     grows again, which on a screen that tall is not a problem. */
  min-height: min(calc(100vh - var(--nav-h) - var(--learn-beat) - 5.7rem), 900px);
}
.learn-hero-copy { max-width: none; }
.learn-hero-h {
  font-size: var(--type-h2);
  line-height: var(--lh-h2);
  color: var(--text-heading);
  margin: 0 0 var(--space-2h);
  text-wrap: balance;
}
/* The H2 is a statement, not a section label — it finishes the H1's sentence, so it sits close
   to it and reads at body scale rather than as another heading shouting underneath. */
.learn-hero-sub {
  font-size: var(--type-h5);
  font-weight: var(--weight-regular);
  /* Same fix as .learn-cta-sub below: a fixed 20px line box under 24px text. It only ever showed
     on the closing sub because this one usually sits on one line, which is exactly how a bug
     like this survives. */
  line-height: var(--leading-snug);
  color: var(--text-action);
  margin: 0 0 var(--space-4);
}
.learn-hero-p {
  font-size: var(--type-body);
  line-height: var(--lh-body);
  color: var(--text-body);
  margin: 0 0 var(--space-6);
}
.learn-hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0; }

/* Same slot mechanics as the portal and the account panel: the element is one fixed size and
   every appearance is a transform of it, because resizing a <model-viewer> reallocates its
   drawing buffer and that CLEARS it. A new surface adds a scale, never a size. */
.learn-hero-tag {
  position: relative;
  justify-self: center;
  --aff-tag-scale: .68;
  width: calc(368px * var(--aff-tag-scale));
  height: calc(416px * var(--aff-tag-scale));
  justify-self: center;
}
.learn-hero-tag model-viewer {
  position: absolute; top: 0; left: 0;
  width: 368px; height: 416px;
  transform-origin: 0 0;
  transform: scale(var(--aff-tag-scale));
  display: block;
  background: transparent;
  --poster-color: transparent;
  cursor: grab;
  touch-action: pan-y;
}
.learn-hero-tag model-viewer:active { cursor: grabbing; }

/* ── The split control, shared ───────────────────────────────────────────────
   The card, its heading, the two figures and the slider — ONE component, used by the affiliate
   portal and by /learn/affiliate/, which demonstrates it to someone who has never signed in.

   Here rather than in affiliate.css because the learn page does not load that stylesheet, and a
   matching slider inside a different box is still a different control.

   ⚠ THIS BLOCK HAS BEEN DELETED BY ACCIDENT THREE TIMES, always the same way: a bulk edit that
   replaced a RANGE of this file between two comment markers, with this sitting inside the range.
   It takes the portal's split card down with it, not just the learn page's. If you are rewriting
   the learn-article rules below, edit them by name, not by span. */
.aff-panel {
  padding: var(--space-6); margin-bottom: var(--space-6);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card); background: var(--surface-default);
}
.aff-panel-title { font-size: var(--type-h5); font-weight: var(--weight-bold);
  color: var(--text-heading); margin-bottom: var(--space-2); }
.aff-panel-sub { font-size: var(--type-body-sm); color: var(--text-muted);
  line-height: var(--lh-body); margin-bottom: var(--space-5); max-width: 60ch; }

/* The card is white (--surface-default); the two halves inside it are the raised tint. That
   contrast is what makes the figures read as insets rather than as more card. */
.aff-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0 var(--space-3);
}
.aff-split-half {
  padding: var(--space-3);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-raised);
  text-align: center;
}
.aff-split-pct {
  font-size: var(--type-h4);
  font-weight: var(--weight-bold);
  line-height: 1.1;
  color: var(--text-heading);
  font-variant-numeric: tabular-nums;
}
.aff-split-label {
  font-size: var(--type-caption);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: var(--space-1);
}
/* The guide spans both halves — it describes the trade, not either side of it. It also RESERVES
   its height: the notes are different lengths, so without a floor the card grew and shrank as
   the slider moved and the Save button walked up and down under the cursor. */
.aff-split-guide { grid-column: 1 / -1; min-height: 3rem; }
.aff-split-guide b { display: block; font-size: var(--type-body-sm); color: var(--text-heading); }
.aff-split-guide span { font-size: var(--type-caption); color: var(--text-muted); }

/* A native range is ~16px tall, which is a thin thing to catch with a thumb on a phone. The
   track keeps its look; the element gets height and vertical padding so the grabbable band is a
   comfortable one. */
.aff-slider {
  width: 100%;
  accent-color: var(--surface-action);
  margin: 0;
  height: 44px;
  padding-block: 14px;
  background: transparent;
  box-sizing: border-box;
}
/* Above the track, not below: these are the options being chosen between, so they belong on the
   approach to the control rather than as a footnote under it. */
.aff-slider-ticks {
  display: flex;
  justify-content: space-between;
  margin-bottom: var(--space-1);
  font-size: var(--type-caption);
  color: var(--text-subtle);
  font-variant-numeric: tabular-nums;
}

/* ── Sections that own a screen ──────────────────────────────────────────────
   Each of these is meant to be met on its own: arrive at it, take it in, move on. The vertical
   rhythm is what does that — enough air above and below that the previous section has visibly
   ended and the next one has not started crowding. `clamp` rather than a fixed gap so a laptop
   does not get the spacing designed for a 27" monitor.

   They alternate sides down the page: the photograph left, the split card right. Two sections
   with the object on the same side read as a template; alternating reads as a rhythm, and it is
   also what keeps the eye moving rather than settling into one column. */
.learn-sec {
  display: grid;
  /* 2x1, equal halves, for every section. The uniformity IS the design: four sections that each
     invent their own proportions read as four layouts, however carefully each one is judged. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* TOP-aligned, always. A section heading floated to the vertical middle of a tall asset sits
     in the middle of nothing; aligned to the asset's top edge the two columns start together
     and the eye has one line to begin on. */
  align-items: start;
  gap: clamp(var(--space-6), 4vw, var(--space-12));
  /* Enough that a section is arrived at rather than stumbled into.
   *
   * PADDING, not margin. Adjacent margins collapse to the larger of the two, so two sections
   * each asking for 96px of space between them got 96px, not 192 — which is why the first pass
   * at this looked barely different. Padding does not collapse, so the gap is the sum.
   *
   * The ceiling is --space-32 (128px): 16vh is 173px on a 1080-tall screen, and clamped at
   * --space-24 the preferred value could never win. Sum of two paddings gives roughly a third
   * of a viewport between sections, which is what puts the next heading below the fold. */
  padding-block: var(--learn-beat);
}
.learn-sec-h {
  margin-top: 0;
  font-size: var(--type-h3);
  line-height: var(--lh-h3);
  color: var(--text-heading);
  margin: 0 0 var(--space-4);
  text-wrap: balance;
}
.learn-sec-p {
  font-size: var(--type-body);
  line-height: var(--lh-body);
  color: var(--text-body);
  margin: 0;
}

/* ── The sequence ──
   A FLAT grid: the heading, the three steps and the figure are all direct children, which is
   what lets the figure hold its own column on a desktop and drop between step one and step two
   on a phone with nothing but `order`. Two columns, photograph on the LEFT. */
.learn-steps-sec.has-figure { row-gap: var(--space-6); }
.learn-steps-sec .learn-steps-figure { grid-column: 1; grid-row: 1 / span 4; }
.learn-steps-sec .learn-sec-copy,
.learn-steps-sec .learn-step { grid-column: 2; }
/* The full 5:4 frame, uncropped — the image was cut to this shape on disk, so the page shows all
   of it rather than cropping a crop. */
.learn-steps-figure { margin: 0; }
.learn-steps-figure img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--radius-card);
}

.learn-step { padding-top: var(--space-4); border-top: var(--border-w-medium) solid var(--border-default); }
/* The number leads the title on one line, rather than sitting above it as a label. It is part of
   the heading text, so a screen reader reads "1 You place your first order" — which is where the
   sequence lives now that these are not list items. */
.learn-step-h {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  /* A step heading that wraps should break evenly rather than orphan its last word under the
     number. The text beside the numeral is an anonymous flex item, and balance applies to it. */
  text-wrap: balance;
  font-size: var(--type-body);
  font-weight: var(--weight-bold);
  line-height: var(--lh-body);
  color: var(--text-heading);
  margin: 0 0 var(--space-2);
}
.learn-step-n {
  flex: none;
  font-size: var(--type-h4);
  line-height: 1;
  color: var(--text-action);
  font-variant-numeric: tabular-nums;
}
.learn-step-p {
  font-size: var(--type-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
  margin: 0;
}

/* ── The split ──
   Copy left, the portal's card right. */
/* The card carries the portal's own padding, so it needs no extra placement. */
.learn-split { margin: 0; }

/* ── Copy on the asset's baseline ──
   For the two sections whose asset is a framed object — the split card and the portal video —
   the copy sits at the BOTTOM of its column, so its last line lands level with the frame's
   bottom edge. Top-aligned they left a growing wedge of white under the text as the asset got
   taller, and the wedge read as a gap rather than as space. Bottom-aligned, the whitespace
   gathers above the heading instead, where it does the heading a favour.

   The photograph section stays top-aligned: its copy is a numbered sequence that has to start
   level with the picture it explains. */
.learn-split-sec .learn-sec-copy,
.learn-shots .learn-sec-copy {
  /* Not `end`, which pinned only the bottom. The copy spans the asset's full height and its
     three tiers are pushed apart: the heading's cap-height starts level with the frame's top
     edge, the small line's baseline lands on its bottom edge, and the middle tier floats in
     between. Bottom-alignment alone left the heading starting somewhere arbitrary down the
     card, which read as a mistake rather than as a choice. */
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-5);
}
/* Their own margins would break both pins — flex `space-between` distributes what is left after
   margins, so a heading with `margin-bottom` sits above where it is meant to. */
.learn-split-sec .learn-sec-copy > *,
.learn-shots .learn-sec-copy > * { margin: 0; }
/* The middle tier is the one that carries the explanation, so it reads at body scale rather
   than shrinking to fill; the third is a caption and says so by being one. */
.learn-split-sec .learn-sec-foot,
.learn-shots .learn-shots-foot {
  font-size: var(--type-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
}

/* ── What the portal shows ──
   Both screenshots are in the markup; CSS picks the one that matches the device. Showing both
   at once had the reader comparing two pictures instead of looking at the one in their hand. */
/* Joins the alternation: the portal on the left, the copy on the right. */
.learn-shots .learn-sec-copy { grid-column: 2; grid-row: 1; }
.learn-shot { margin: 0; width: 100%; grid-column: 1; grid-row: 1; }


.learn-shot img,
.learn-shot .learn-tour-canvas {
  display: block;
  width: 100%;
  height: auto;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
}
/* The tour's ONLY element. Its <video> source is created in script and never enters the
   document — see mountTour() — so there is nothing here to hide, only something to paint on.
   The poster is the canvas's own background: it shows through until the first frame lands, and
   stays put if a browser declines to decode at all. */
.learn-tour-canvas {
  /* The poster comes from the figure's own custom properties, one per theme, so the still is
     already the right one before mountTour runs — and stays right under reduced motion, where
     no video is ever fetched and the poster is the whole asset.

     DECLARED HERE with a `none` default even though the real values arrive inline on the
     element: the undefined-token test reads the stylesheets and the inline <style> blocks, and
     it cannot see a custom property set in an element's style attribute — so without these two
     lines it reports a rule that is silently doing nothing. Which is exactly what it would be
     if learn.js ever stopped emitting them. */
  --tour-poster: none;
  --tour-poster-dark: none;
  background-image: var(--tour-poster);
  background-size: cover;
  background-position: top center;
}
[data-theme="dark"] .learn-tour-canvas { background-image: var(--tour-poster-dark); }
/* Fullscreen is requested on the FIGURE, so the figure has to behave like a viewer rather than
   like a block that happens to be full-screen: without this the canvas kept its laid-out width
   and sat small against a flat field. */
.learn-shot:fullscreen {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--surface-page);
}
.learn-shot:fullscreen .learn-tour-canvas {
  width: auto;
  height: 100%;
  max-width: 100%;
  border: 0;
  border-radius: 0;
}
.learn-shot-mobile { display: none; }
.learn-shot-mobile img { max-width: 22rem; margin-inline: auto; }
.learn-shots-foot {
  justify-self: start;
  margin: 0;
  font-size: var(--type-caption);
  color: var(--text-subtle);
}

/* The split card is .aff-panel — the portal's, verbatim. This only places it. */

/* ── Closing call to action ── */
.learn-cta {
  margin: var(--space-12) 0 var(--space-6);
  padding-top: var(--space-8);
  border-top: var(--border-w-medium) solid var(--border-default);
}
/* No rule and no star on a rich page. The five prose articles keep the border, because there a
   line is the only thing separating a wall of text from the closing ask. Here the closing
   section is a full pairing with an object in it and a beat of space on either side, which
   already reads as a new section — the rule was drawing a boundary that the layout had made. */
.learn-article.is-rich .learn-cta {
  margin-top: var(--learn-beat);
  padding-top: var(--learn-beat);
  border-top: 0;
}
/* Paired: copy on the left, the object on the right, the same two columns and the same gap as
   the lead. The section keeps its own top border and star; only the inside changes. */
.learn-cta.is-paired {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(var(--space-6), 4vw, var(--space-12));
}
.learn-cta-tag { justify-self: center; }
.learn-cta-sub {
  font-size: var(--type-h5);
  font-weight: var(--weight-regular);
  /* A unitless multiplier, not --lh-body. The --lh-* tokens are FIXED heights each cut to pair
     with one --type-* size, and tokens.css says so directly: use them for headings and
     single-line labels, and a --leading-* for anything that runs to a second line. --lh-body is
     20px, so against 24px --type-h5 text the line box was SMALLER THAN THE FONT and three lines
     of this sub overlapped each other. */
  line-height: var(--leading-snug);
  color: var(--text-action);
  margin: 0 0 var(--space-5);
  max-width: 34ch;
}
.learn-cta-h {
  font-size: var(--type-h3);
  line-height: var(--lh-h3);
  color: var(--text-heading);
  margin: 0 0 var(--space-3);
}
.learn-cta-p {
  font-size: var(--type-body);
  line-height: var(--lh-body);
  color: var(--text-body);
  margin: 0 0 var(--space-6);
  max-width: 46ch;
}
/* The closing pair matches the lead's: equal halves, same two labels. Capped near the
   paragraph's own measure, because the closing section spans the full article width and two
   true halves of 1040px would be a pair of enormous slabs. */
.learn-cta-actions {
  display: flex;
  gap: var(--space-3);
  margin: 0;
  max-width: min(42ch, 100%);
}
/* Both button rows behave identically: equal halves when there is room for two, stacked when
   there is not. They are a pair of equal offers ("browse" or "claim"), and letting the labels
   decide the widths made the shorter one look like the lesser option.
///
   `min-width: 0` is the load-bearing line, and it is not optional. A flex item will not shrink
   below its own min-content width, so `flex: 1 1 0` LOOKS like it forces halves and does not:
   on a narrow phone "CLAIM ACCOUNT" held its 171px and pushed 35px of the page off the side,
   and at 375 the pair came out 160/171 and only appeared even. Releasing that floor is what
   makes the halves real.

   With the floor gone the label needs somewhere to go, hence `white-space: normal` — on the
   narrowest phones it sets on two lines inside the button, and both buttons stretch to the same
   height because they are flex siblings. Two even buttons with a wrapped label reads better than
   one wide button beside a narrow one, and much better than a page that scrolls sideways. */
.learn-hero-actions .btn,
.learn-cta-actions .btn {
  flex: 1 1 0;
  min-width: 0;
  white-space: normal;
  justify-content: center;
  text-align: center;
}
.learn-hero-actions { display: flex; gap: var(--space-3); margin: 0; }
.learn-cta-foot {
  margin: var(--space-6) 0 0;
  font-size: var(--type-body-sm);
  color: var(--text-muted);
}

@media (max-width: 768px) {
  /* Title and sub across the top, then the explaining line and the object side by side, then
     the buttons across the bottom. `display: contents` on the copy wrapper is what makes this
     possible without a second markup shape: it dissolves, and its children become grid items of
     the hero itself, so the paragraph can sit beside the tag while the H1 above it still spans
     the full width. Stacked, the tag was a 300px interruption between the reader and the
     buttons; beside the line it explains, it earns the space it takes. */
  .learn-hero {
    /* Even halves. The line and the object are a pair here, not a caption with a thumbnail. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
    column-gap: var(--space-4);
    row-gap: var(--space-4);
    /* Less air above the title, more below the pair. On a phone the space over the H1 is dead
       scrolling before anything has been said, while the gap before the buttons is what stops
       them reading as part of the paragraph. Desktop keeps the symmetric beat; a phone does
       not have the height to spend it evenly. */
    padding-block: var(--space-5) var(--learn-beat);
    margin-bottom: var(--space-10);
  }
  .learn-hero-copy { display: contents; }
  /* Rows named, not left to auto-placement. The tag is the LAST child in the DOM (it follows the
     copy wrapper), so placing it in column 2 without a row put it on a row of its own below the
     buttons — column 2 of row four, which is nowhere near the line it belongs to. Every item
     gets its row here; the pairing is the whole point of the layout and cannot be left to
     source order. */
  .learn-hero .page-eyebrow { grid-column: 1 / -1; grid-row: 1; }
  .learn-hero-h { grid-column: 1 / -1; grid-row: 2; }
  .learn-hero-sub { grid-column: 1 / -1; grid-row: 3; }
  .learn-hero-p { grid-column: 1; grid-row: 4; margin-bottom: 0; align-self: center; }
  /* The two columns are exact halves; the tag sits centred in its half rather than filling it
     edge to edge. It cannot simply be given `width: 100%` — the slot's box is derived from the
     one element size and one scale on purpose, because RESIZING a model-viewer reallocates and
     clears its WebGL drawing buffer, so the tag is scaled, never resized. A scale is a unitless
     number and cannot be computed from a column width in CSS, hence the step below rather than
     a calc: .44 overflowed a 320px phone's half by 26px and pushed the page sideways. */
  .learn-hero-tag {
    grid-column: 2;
    grid-row: 4;
    --aff-tag-scale: .36;
    align-self: center;
    justify-self: center;
  }
  /* Set apart from the pair above it, so the two buttons read as the next move rather than as
     the last line of the paragraph. */
  .learn-hero-actions { grid-column: 1 / -1; grid-row: 5; margin-top: var(--space-5); }
  /* Both keyed to the viewport, because both have a line count that must not change.
     At the flat 40px the capitalised title broke to THREE lines and pushed the pair and the
     buttons down the screen, which is the opposite of the reduced top spacing above. 9.2vw puts
     "The Referral / Keychain" on two lines from 320px up. */
  .learn-hero-h { font-size: min(2.25rem, 9.2vw); line-height: var(--lh-h2); }
  /* Sized so the justified line is ALREADY almost the full measure: at 16px the natural width
     was 255 of 358 and justify had 100px to distribute across four gaps, which reads as broken
     spacing rather than as a set line. At 5.6vw it is 351 of 358 and the gaps are a few pixels.
     Justification only looks deliberate when it barely has to do anything. */
  .learn-hero-sub { font-size: min(1.375rem, 5.6vw); }

  /* Less air on a phone. 12vh reads as a deliberate pause on a desktop, where the eye takes in
     a whole section at once; on a 844px screen the same rule is 200px of dead scrolling between
     things the reader is already meeting one at a time. */
  .learn-sec {
    grid-template-columns: 1fr !important;
    row-gap: var(--space-5);
    padding-block: var(--space-10);
  }
  .learn-sec-h { font-size: var(--type-h4); line-height: var(--lh-h4); }

  /* The photograph lands BETWEEN step one and step two — a flat grid is what makes that one
     `order` rather than a second copy of the image in the markup. Inset from the text column so
     it reads as a picture placed in the run of steps rather than as a full-bleed break in it. */
  .learn-steps-sec .learn-steps-figure,
  .learn-steps-sec .learn-sec-copy,
  .learn-steps-sec .learn-step { grid-column: 1; grid-row: auto; }
  /* Ordered by the step's OWN number, not by its position among siblings. `:nth-of-type`
     counts by tag, and the copy block is a <div> like the steps are — so it took index 1 and
     pushed every step one place along, which put step three above the heading. The number is in
     the markup; the CSS reads it. */
  .learn-steps-sec .learn-sec-copy { order: 1; }
  .learn-steps-sec .learn-step[data-step="1"] { order: 2; }
  /* The photograph and the portal tour are the page's two big pictures and they sit one screen
     apart, so a phone reads them as a pair. At full width the tour was noticeably wider than the
     keychain shot above it and the two looked like different kinds of thing. One variable, used
     by both, so they cannot drift apart again. */
  .learn-article.is-rich { --learn-figure-w: 84%; }
  .learn-steps-sec .learn-steps-figure {
    order: 3;
    width: var(--learn-figure-w);
    margin-inline: auto;
    padding-block: var(--space-2);
  }
  .learn-steps-sec .learn-step[data-step="2"] { order: 4; }
  .learn-steps-sec .learn-step[data-step="3"] { order: 5; }

  /* The closing section pairs on a phone too, the same way the lead does: title and line across
     the top, then the buttons, with the object beside the line. */
  .learn-cta.is-paired {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--space-4);
    row-gap: var(--space-4);
  }
  .learn-cta-copy { display: contents; }
  .learn-cta-h { grid-column: 1 / -1; grid-row: 1; }
  /* Body size beside the tag, not h5. The lead's equivalent line is set at body size in the
     same half-column, and at 24px this one broke into seven short ragged lines that read as a
     different component rather than as the same arrangement repeated. */
  .learn-cta-sub {
    grid-column: 1;
    grid-row: 2;
    font-size: var(--type-body);
    margin-bottom: 0;
    align-self: center;
  }
  .learn-cta-tag { grid-column: 2; grid-row: 2; --aff-tag-scale: .36; align-self: center; }
  .learn-cta-actions { grid-column: 1 / -1; grid-row: 3; margin-top: var(--space-5); }
  .learn-cta-foot { grid-column: 1 / -1; grid-row: 4; }

  /* Above 400px the half is wide enough for the bigger tag. */
  @media (min-width: 400px) {
    .learn-hero-tag,
    .learn-cta-tag { --aff-tag-scale: .44; }
  }

  /* The featured card keeps the pairing on a phone, at half the width rather than a quarter —
     a quarter of 358px is 90px, at which the tag is a smudge rather than an object. */
  /* Four rows: the title across the top, then the opening line with the object to its right,
     then the rest of the copy at full width, then the link. Only the FIRST sentence shares a row — pairing the
     whole copy block with the tag left it twelve ragged lines deep in a half column, and running
     the whole thing full width wasted the object entirely. `display: contents` dissolves the
     wrapper so its children can be placed on the card's own grid. */
  .learn-card-feature { grid-template-columns: minmax(0, 1fr) 50%; column-gap: var(--space-4); }
  .learn-card-feature-copy { display: contents; }
  .learn-card-feature .learn-card-title { grid-column: 1 / -1; grid-row: 1; }
  .learn-card-tag {
    grid-column: 2;
    grid-row: 2;
    width: 100%;
    /* Half the card, but not without limit: this breakpoint runs to 768px, where half is 327px
       and the tag became a 370px-tall slab with a sentence beside it. The cap only engages above
       roughly 470px, so every phone still gets its true half. */
    max-width: 220px;
    /* Against the card's right edge, as on desktop. Text left, object right at both sizes. */
    justify-self: end;
    align-self: center;
  }
  /* The SUBHEAD is what pairs with the object on a phone — it is the promise, and it is one
     short line, so it sits in a half column without going ragged. The keychain sentence then has
     the card's full width beneath, and the label its own line under that. */
  .learn-card-feature-head { grid-column: 1; grid-row: 2; align-self: center; margin-top: 0; }
  .learn-card-detail { grid-column: 1 / -1; grid-row: 3; margin-top: var(--space-4); }
  /* No override needed: the label is a block at every width now. */

  /* The phone screenshot, on a phone. */
  .learn-shot-desktop { display: none; }
  .learn-shot-mobile {
    display: block;
    width: var(--learn-figure-w);
    margin-inline: auto;
  }
  /* And the section's desktop placement has to be UNDONE, not merely overridden by the
     single-column template. `grid-column: 2` on the copy does not fold into column 1 — it
     creates an implicit second column, and the shot in column 1 is then auto-sized against it.
     While the asset was an <img> this stayed invisible: the image's `max-width: 22rem` gave
     column 1 an intrinsic width to keep. A <video> carries only `width: 100%`, so it resolved
     against a column that had nothing to be 100% of and collapsed to 2px — the tour was
     playing, correctly, at no size at all. The steps section had this reset from the start;
     this one was missed. */
  .learn-shots .learn-sec-copy,
  .learn-shots .learn-shot { grid-column: 1; grid-row: auto; }
  .learn-shots .learn-sec-copy { order: 1; }
  .learn-shots .learn-shot { order: 2; }
  /* The caption belongs UNDER the thing it captions. On desktop it is the bottom of a column
     standing beside the asset and reads as its baseline; stacked on a phone that same position
     put it above the asset, where "example figures, from a test account" annotated the
     paragraph instead of the screenshot it is about. Pulled out of the copy flow and placed
     after. Same reasoning for the split card's closing line below. */
  .learn-shots .learn-sec-copy { display: contents; }
  .learn-shots .learn-sec-h { order: 1; }
  .learn-shots .learn-sec-p { order: 1; }
  .learn-shots .learn-shots-foot { order: 3; }
  .learn-split-sec .learn-sec-copy { display: contents; }
  .learn-split-sec .learn-sec-h { order: 1; }
  .learn-split-sec .learn-sec-p { order: 1; }
  .learn-split-sec .learn-split { order: 2; }
  .learn-split-sec .learn-sec-foot { order: 3; }
}

/* ── FAQ ── */
.learn-faq { border-top: var(--border-w-small) solid var(--border-default); }
.learn-faq-item { border-bottom: var(--border-w-small) solid var(--border-default); }
.learn-faq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) 0;
  cursor: pointer;
  list-style: none;
  font-size: var(--type-body);
  font-weight: var(--weight-demi);
  color: var(--text-heading);
}
.learn-faq-q::-webkit-details-marker { display: none; }
/* A + that becomes a − when open. Drawn in CSS so it needs no icon file and inherits colour. */
.learn-faq-mark { position: relative; width: 12px; height: 12px; flex-shrink: 0; }
.learn-faq-mark::before,
.learn-faq-mark::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: var(--border-w-medium);
  background: var(--text-action);
  transition: transform var(--transition-base);
}
.learn-faq-mark::after { transform: rotate(90deg); }
.learn-faq-item[open] .learn-faq-mark::after { transform: rotate(0deg); }
.learn-faq-a { padding-bottom: var(--space-4); }
.learn-faq-a .learn-p:last-child { margin-bottom: 0; }

/* ── Values ── */
/* minmax(0, 1fr), not 1fr. `1fr` is `minmax(auto, 1fr)`, so a column never shrinks below its
   content's min-content width — and once each card carried a flex row with the mark pinned to the
   right, that floor became copy + gap + 83px and the column grew wider than the grid. The card
   overhung the page on a phone by 15px with nothing in the layout looking wrong. CLAUDE.md. */
.value-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); margin: var(--space-6) 0; }
@media (min-width: 768px) { .value-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.value-card {
  padding: var(--space-6);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-raised);
}

/* ── Two columns, for any learn page ─────────────────────────────────────────
   Prose on the left, the page's objects on the right. Used by /learn/pricing/ and
   /learn/orders/, and meant for the rest as they are rewritten — one layout for the section
   rather than a grid invented per page.

   FULL FRAME, not the reading measure: this is the same width the values grid takes, which is
   what makes the three pages line up. The prose inside is narrower by consequence of the column,
   not by a second cap.

   Below the breakpoint the rails are `display: contents`, so every block becomes a direct child
   of this grid and the inline `order` learn.js writes puts them back into the sequence the
   content file lists them in. `order` is inert on a block container (CLAUDE.md), so the rails
   have to disappear rather than merely stack. */
.learn-two { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
.learn-two-main, .learn-two-side { display: contents; }
/* One spacing mechanism at a time. Stacked, the blocks are grid children and the GAP spaces
   them; their own trailing margins would be added on top of it and the rhythm would double
   between two paragraphs while staying single around a card, which has no margin of its own. */
@media (max-width: 899px) { .learn-two-item > :last-child { margin-bottom: 0; } }
@media (min-width: 900px) {
  .learn-two {
    grid-template-columns: minmax(0, 1fr) minmax(0, 23rem);
    gap: clamp(var(--space-8), 4vw, var(--space-12));
    align-items: start;
  }
  .learn-two-main, .learn-two-side { display: block; }
  /* The rail travels with the reader: the rate and the ladder are what the prose beside them is
     about, and on a long page they would otherwise scroll away from it. */
  /* Parks below the nav and the breadcrumb bar, both of which are sticky above it. */
  .learn-two-side { position: sticky; top: calc(var(--nav-h) + var(--crumb-h) + var(--space-6)); }
  .learn-two-side > .learn-two-item + .learn-two-item { margin-top: var(--space-6); }
}
/* The blocks keep their own rhythm, so the wrapper adds none of its own — but only the LAST
   item drops its trailing margin. Applied to every item it removed the space BETWEEN paragraphs
   as well, and two paragraphs then read as one. */
.learn-two-main > .learn-two-item:last-child > :last-child { margin-bottom: 0; }

/* ── An information card ─────────────────────────────────────────────────────
   The filled panel the section uses for anything that is a fact rather than a sentence: the
   discount ladder, where we ship, what delivery costs. One definition so the three read as one
   family. */
.learn-panel, .price-tiers {
  padding: var(--space-6);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-raised);
}
.learn-panel-title {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
}
.learn-panel-figure {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-size: var(--type-h3);
  font-weight: var(--weight-bold);
  line-height: 1.1;
  color: var(--text-heading);
  font-variant-numeric: tabular-nums;
}
.learn-panel-unit {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-demi);
  color: var(--text-action);
  letter-spacing: normal;
}
.learn-panel-line {
  margin-top: var(--space-2);
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-action);
  font-weight: var(--weight-demi);
}
/* The destination selector on the Delivery panel.
 *
 * A native <select> does the work and a styled face is what is seen.
 *
 * That split is not decoration. A select's closed state cannot carry a custom caret and cannot
 * be transformed on hover — the browser draws it — and its popup is drawn by the OS. But it is
 * also the only control that is keyboard-operable everywhere, announces itself correctly, and
 * opens the native wheel picker on a phone, which beats any list we could build at this size.
 * So the select stays, transparent, on top; the face below it is what the reader looks at.
 *
 * No underline. The caret says it opens, and the lift on hover confirms it — an underline in the
 * middle of a sentence reads as a link, and this does not navigate.
 */
.ship-to-label {
  color: var(--text-muted);
  font-weight: var(--weight-regular);
  margin-right: 0.3em;
}
.ship-to {
  position: relative;
  display: inline-block;
  vertical-align: baseline;
}
.ship-to-face {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  /* The lift is the whole hover affordance, so it is the only thing that moves. Small on
     purpose: this sits inside a line of prose, and anything larger shoves the line around. */
  transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1), color 160ms ease;
}
.ship-to-flag {
  /* Emoji flags sit high in their em box; nudged down so the glyph's optical centre lines up
     with the text beside it rather than the box it is drawn in. */
  font-size: 1.05em;
  line-height: 1;
  transform: translateY(0.02em);
}
.ship-to-caret {
  color: var(--text-muted);
  transition: color 160ms ease, transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
  flex-shrink: 0;
}
/* The control itself: present, focusable, invisible. `inset: 0` rather than a fixed size so it
   always covers whatever the face grew to — a long country name must not leave dead edges. */
.ship-to-select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  font: inherit;
  /* Safari draws a native control here regardless of opacity on some versions. */
  -webkit-appearance: none;
  appearance: none;
  border: none;
  background: none;
}
.ship-to:hover .ship-to-face { transform: translateY(-2px); }
.ship-to:hover .ship-to-caret { color: var(--text-action); transform: translateY(1px); }

/* Focus has to be visible on the FACE, because the control it belongs to is transparent — the
   browser's own ring is drawn where nobody can see it. :focus-visible so a mouse click does not
   leave a ring behind, and :has() so the state can reach a sibling. */
.ship-to:has(.ship-to-select:focus-visible) .ship-to-face {
  transform: translateY(-2px);
  outline: 2px solid var(--text-action);
  outline-offset: 3px;
  border-radius: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .ship-to-face, .ship-to-caret { transition: none; }
  .ship-to:hover .ship-to-face,
  .ship-to:hover .ship-to-caret,
  .ship-to:has(.ship-to-select:focus-visible) .ship-to-face { transform: none; }
}

/* The popup list is drawn by the OS and inherits nothing from the page, so it needs its own
   colours or it renders white-on-white in dark mode on some platforms. */
.ship-to-select option { color: var(--text-body); background: var(--surface-raised); }

.learn-panel-note {
  margin-top: var(--space-4);
  font-size: var(--type-caption);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

/* Name/value pairs inside a panel. A <dl>, because that is what it is. */
.learn-facts { margin-top: var(--space-5); display: grid; gap: var(--space-3); }
.learn-facts dt {
  font-size: var(--type-caption);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.learn-facts dd {
  margin-top: 2px;
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-body);
}

/* The countries, as flags. auto-fill rather than a fixed count, so the card holds the same
   number of columns whether it sits in the rail or runs the full width on a phone. */
.flag-grid {
  margin-top: var(--space-4);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: var(--space-2) var(--space-3);
  list-style: none;
  padding: 0;
}
.flag-item { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
/* The glyph is a regional-indicator pair. A platform without the flag draws the two letters,
   which is still the right answer — it is never the only thing carrying the meaning. */
.flag-glyph { font-size: var(--type-body); line-height: 1; }
.flag-name {
  font-size: var(--type-body-sm);
  color: var(--text-body);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Priced by weight ────────────────────────────────────────────────────────
   ONE CARD, TWO PAGES. On the maroon ground, because what it carries is the thing no comparable
   shop publishes: on a product page the piece's own sum, on /learn/pricing/ the rate that sum is
   built from. Uses the --*-on-action tokens, which exist for exactly this surface.

   It lived inline in product.html until the pricing page needed the same object. Rather than a
   second set of rules that would drift, both names are on one definition — STYLE-STANDARDS §7 is
   explicit that a parallel component is how this codebase ended up with four tab patterns.
   product.html still decides where the card sits in its own grid. */
.pdp-priced, .price-rate {
  background: var(--surface-action);
  border-radius: var(--radius-card);
  padding: var(--space-8) var(--space-6);
  display: flex; flex-direction: column; align-items: center;
  text-align: center; gap: var(--space-2);
}
@media (min-width: 640px) { .pdp-priced, .price-rate { padding: var(--space-10); } }
.pdp-priced-title, .price-rate-title {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-on-action-subtle);
}
.pdp-priced-lead, .price-rate-lead {
  font-size: var(--type-h6);
  text-wrap: balance;
  font-weight: var(--weight-bold);
  line-height: var(--lh-h6);
  color: var(--text-on-action);
}
.pdp-priced-sum, .price-rate-sum {
  font-size: var(--type-body-lg);
  line-height: var(--lh-body-lg);
  color: var(--text-on-action);
  letter-spacing: var(--tracking-wide);
  padding: var(--space-3) var(--space-5);
  margin-top: var(--space-2);
  border: var(--border-w-small) solid var(--border-on-action);
  border-radius: var(--radius-base);
  background: var(--surface-on-action-subtle);
}
/* The sum in two registers: the multiplication small and quiet on top, the money large
   underneath. The working is evidence, not the headline — a reader who wants to check it will
   find it, and one who does not is looking at the totals. */
.pdp-priced-working {
  display: block;
  font-size: var(--type-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-on-action-muted);
}
.pdp-priced-totals {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-top: var(--space-1h);
}
/* Appearance of the struck figure on THIS surface; the strike itself is .price-was.
   The DIGITS stay subtle — they are the superseded number — but the RULE takes the full
   strength of the price it points at, so the mark is as present as the figure that won. At
   --text-on-action-subtle the whole thing receded together and the strike read as a scratch on
   the card rather than as a correction. The other two surfaces do the same thing with the
   discount ramp; this one has no ramp on it, so the paid price's own colour is the match. */
.pdp-priced-was {
  font-size: var(--type-body);
  line-height: var(--leading-snug);
  font-weight: var(--weight-regular);
  color: var(--text-on-action-subtle);
  --strike-ink: var(--text-on-action);
}
.pdp-priced-paid {
  font-size: var(--type-h5);
  line-height: var(--lh-h5);
  font-weight: var(--weight-bold);
  color: var(--text-on-action);
}
/* Inside the note's sentence. It inherits the note's size and colour and is marked only by the
   underline, which is what keeps it reading as prose that happens to be clickable rather than
   as a second element parked under the card. */
.pdp-priced-link {
  color: var(--text-on-action);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.pdp-priced-link:hover { text-decoration-thickness: 2px; }
.pdp-priced-note, .price-rate-note {
  font-size: var(--type-caption);
  line-height: var(--leading-normal);
  color: var(--text-on-action-muted);
  max-width: 52ch;
  margin-top: var(--space-2);
}

/* ── The struck price, and the step that struck it ───────────────────────────
   ONE definition, three surfaces: the weight card on the product page, the recommendation
   cards under it, and the Find-by-fit results. STYLE-STANDARDS §7 — a second strike that
   "matches" would be the fourth-tab-pattern mistake again.

   Bottom-left to top-right, not the browser's horizontal line-through. A horizontal rule
   through a price reads as a redaction; a rising diagonal reads as a correction, and it is
   the shape every shop that discounts uses. `text-decoration` cannot draw it, so the line is
   a pseudo-element and `text-decoration: none` turns off the one <s> would otherwise add.

   CORNER TO CORNER, via a gradient — and this comment used to say CSS could not do that.
   It was wrong, and a fixed -14deg was the workaround. The angle is constant but the boxes are
   not, so the RISE varies with how wide the number is: measured on the cards, "60€" rose 6.5px
   and "186€" 7.6px where ~14px was needed to reach both corners, so short prices got a line
   through their middle and only long ones looked struck from corner to corner.

   What I had missed is what a corner keyword means. For `to bottom right` the gradient line is
   angled so that the perpendicular through it — which is the direction the colour bands run —
   passes through the two corners NEIGHBOURING that one: top-right and bottom-left. So the band
   at the 50% stop runs precisely along the bottom-left-to-top-right diagonal, at whatever angle
   that box's own proportions make it, with no angle written down anywhere.

   Thickness is measured ALONG the gradient line, which is perpendicular to the band, so the
   stops give a true perpendicular width rather than a distorted one. var() resolves inside a
   gradient, so --strike-ink still themes it.

   currentColor, so it inherits: beige on the maroon card, muted grey on a product card, and
   both themes, without a second colour rule anywhere.

   MECHANICS ONLY — no colour, no size. Those belong to the surface: .pdp-priced-was is beige on
   maroon at body size, .pcard-was is muted grey at small-body size. Putting them here instead would
   not merely be untidy, it would BREAK: this block sits ~3000 lines below .pcard-was, both are
   one class deep, so at equal specificity the later rule wins whatever the card asked for. That
   ordering trap has cost this codebase a session before (CLAUDE.md, the `order` bug). */
.price-was {
  position: relative;
  display: inline-block;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.price-was::after {
  content: '';
  position: absolute;
  /* Flush with the glyphs. The old -4% overhang is what put a stub of line in the empty space
     before the first digit — the leading gap that made the strike look like it began somewhere
     other than the corner of the number. */
  left: 0;
  right: 0;
  /* The DIGITS' band, not the line box. A line box carries descender space below the baseline
     and leading above the caps, so corner-to-corner of THAT would start well under the numerals
     and finish well above them. 0.86em is a shade taller than the cap height, which puts the
     ends just past the corners of the figure rather than inside it. */
  top: 50%;
  height: 0.86em;
  transform: translateY(-50%);
  /* currentColor by default, so the weight card's strike stays with its own text. The surfaces
     that sit beside a ramp-coloured price override --strike-ink to that same red: the mark
     reads as the thing the discount did, while the struck FIGURE stays muted and does not add
     another red to the row.

     Thickness is in px because it is measured along the gradient line, perpendicular to the
     band — 1.5px of true stroke. It was proportional when the rule was a rotated box; a
     gradient stop cannot take an em and still mean the same thing at both ends. */
  background-image: linear-gradient(to bottom right,
    transparent calc(50% - 0.75px),
    var(--strike-ink, currentColor) calc(50% - 0.75px),
    var(--strike-ink, currentColor) calc(50% + 0.75px),
    transparent calc(50% + 0.75px));
  /* Decorative: the price beside it carries the meaning, and the sentence in .sr-only says
     "less 10%" in words. An announced "strikethrough" here would be noise. */
  pointer-events: none;
}

/* The step, on the shoulder of the price that won.
   RELATIVE, not absolute. Absolute took it out of flow, so the centred row above laid itself
   out as though the bubble were not there and the bubble hung outside the card's border.
   Relative keeps its width in the line — the row centres on price-plus-bubble, which is what
   is actually on screen — while `top` lifts it clear of the digits without touching the line
   box, because at half the price's size it is far shorter than the line it sits in.

   MECHANICS AND PROPORTION ONLY, like .price-was. Size is in `em` so one definition works over
   a 28px price on the weight card and a 12px one in the buy block. Colour is per surface: the
   weight card sits on maroon and uses the on-action tokens, the buy blocks sit on beige and
   take the discount ramp, so the bubble is the same red as the price it annotates. */
.price-off-bubble {
  position: relative;
  top: -0.72em;
  margin-left: 0.2em;
  display: inline-block;
  padding: 0.1em 0.5em;
  border-radius: var(--radius-round);
  font-size: 0.46em;
  font-weight: var(--weight-demi);
  line-height: 1.5;
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
  border: var(--border-w-small) solid transparent;
}
/* On the maroon weight card. */
.pdp-priced-paid > .price-off-bubble {
  background: var(--surface-on-action-hover);
  color: var(--text-on-action);
  border-color: var(--border-on-action);
}

/* ── The reward box ──────────────────────────────────────────────────────────
   ONE reward, at three moments, so it is one object with one look:

     .auth-reward         signed out, inside the sign-in card (laid out in auth.css)
     .learn-signup-offer  at the foot of /learn/pricing/
     .notif-code          signed in, the coupon revealed in the Messages feed

   The first two were already one rule. .notif-code was a fourth near-copy of the same dashed
   box — same anatomy, different border token — which meant the discount a customer was OFFERED
   and the discount they were GIVEN did not look related. They do now.

   MAROON TINT, NOT WHITE. The panel header these sit under is --surface-action, and the count
   on it is beige-on-maroon. The reward's colour in this shop is maroon; a white box said
   "generic aside" while the header two rows up said "reward". The tint is the quietest way to
   say the same word — it is a fill, not a filled block, so it never competes with the primary
   button, which is the objection that kept the earlier solid-maroon version out.

   Dashed, not solid: it is an aside — a thing on offer — rather than another panel of the
   page's own content, and a broken edge says that before the copy does. */
.auth-reward,
.learn-signup-offer,
.notif-code {
  border: var(--border-w-small) dashed var(--surface-action);
  border-radius: var(--radius-base);
  background: var(--surface-action-subtle);
}
/* --text-muted is --beige-900 (#7a6e68). It clears 4.5:1 on white, which is what these boxes
   used to be, but only reaches 4.42:1 on --maroon-50 — and every line of copy in here is
   caption or body-sm, so 4.5 is the bar. One step darker takes it to 4.97:1. Scoped with
   :not([data-theme="dark"]) rather than [data-theme="light"] so it still holds on any page
   that ships without the attribute; dark keeps the theme's own value, which passes at 5.36:1
   on the dark tint and needs no help. */
:root:not([data-theme="dark"]) .auth-reward,
:root:not([data-theme="dark"]) .learn-signup-offer,
:root:not([data-theme="dark"]) .notif-code {
  --text-muted:  #726660;
  --text-subtle: #726660;
}
/* In dark the tint composites to #2d1e22 and lifts 1.03:1 off the card — the fill is doing
   nothing and the dashed edge IS the box, so it cannot also be maroon-on-maroon. Beige-700 at
   7.43:1 reads as a light edge without reaching the brightness of the heading inside it; the
   brand beige at full strength made the frame louder than the offer. */
[data-theme="dark"] .auth-reward,
[data-theme="dark"] .learn-signup-offer,
[data-theme="dark"] .notif-code {
  border-color: var(--beige-700);
}
/* THE COUNT INVERTS ON A DARK GROUND, exactly as it already does on the maroon panel header
   (.notif-head .count-bubble, below). The disc is --surface-action: 13.02:1 on the light tint,
   1.09:1 on the dark one — not dim, gone, leaving the beige numeral floating with no chip
   under it. Flipping it to the light value restores 12.39:1, which is the light theme's
   relationship rather than a new one. Same fix, same reason, on the feed row's bubble. */
[data-theme="dark"] .auth-reward .count-bubble,
[data-theme="dark"] .learn-signup-offer .count-bubble,
[data-theme="dark"] .notif-item-bubble {
  background: var(--beige-500);
  color: var(--surface-action);
}
/* The offer row's own layout, matching .auth-reward's in auth.css: copy left, count right. */
.learn-signup-offer {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
}
.learn-signup-offer .count-bubble { margin-top: var(--space-0h); margin-left: auto; }

/* The CARD keeps its own look — the raised surface and the solid card edge it always had.
   Only the offer inside it is the white dashed box, which is the arrangement on the sign-in
   card: a panel holding a thing on offer, not a panel that IS one. */
.learn-signup {
  display: grid;
  gap: var(--space-3);
  margin: var(--space-8) 0 var(--space-4);
  padding: var(--space-6);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-raised);
}
.learn-signup-title {
  font-size: var(--type-h6);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h6);
  color: var(--text-heading);
}
.learn-signup-text {
  margin-top: var(--space-1h);
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
/* Second column, first row: the same side the count sits on in the nav, on the notifications
   header, on a message row and in the sign-up card. */
/* Full width of the card. It is the only action on it and the last thing on the page, so there
   is nothing for it to sit beside — shrunk to its label it read as one of two choices. */
.learn-signup-btn { justify-self: stretch; }

/* ── The pricing page ────────────────────────────────────────────────────────
   The gram rate is the loudest thing on the page because it is the number the page exists to
   publish. Everything else here is arranged to keep it that way. */
/* The mark sits beside the H1, not in the card: inside it the ink would have to be restated in
   the on-action tokens and would compete with the figure it introduces. `.page-header-row` does
   the placing — see headerHTML() in learn.js. */
.learn-head-mark { --vi-box: 88px; }
/* The title block takes the room; the mark keeps its size. min-width:0 or a long title refuses
   to wrap and pushes the mark off the row (CLAUDE.md). */
.page-header-row > .learn-head { flex: 1 1 auto; min-width: 0; }
/* The header badge is the full component, at the size /chains/guide/ uses it. */
.learn-head-mark.is-badge { width: auto; height: auto; align-self: center; }
/* …and NOT on a phone, where the title carries the stamp itself (titleHTML in learn.js). Two
   badges an inch apart is the same object drawn twice, and on the narrowest screen the full one
   — "Sterling Silver | 925" — is what squeezes the title it sits beside. The wide screens keep
   both: there the full badge is a header object and the one in the sentence is punctuation. */
@media (max-width: 767px) {
  .learn-head-mark.is-badge { display: none; }
}
/* The stamp set INTO a heading, sized to it: the same arrangement as the homepage's claim
   (home.css, .h-925). Ems throughout, so it tracks a title that is clamped against the viewport
   rather than needing a size of its own at every breakpoint. */
.badge-925.learn-title-925 {
  display: inline-flex;
  align-items: center;
  vertical-align: baseline;
  padding: 0.1em 0.3em;
  font-size: 0.78em;
  line-height: 1;
}
.learn-title-925 .badge-925-num { font-size: inherit; }

/* Wide artwork in a square box reads at half the mass. The van's drawing is twice as wide as it
   is tall, so it fitted the 83px box by WIDTH and rendered 83x42 against the balance's 75x83 —
   3,445 square pixels against 6,235. A 1.35x box puts the two within a hundred of each other. */
.learn-head-mark.is-wide { width: calc(var(--vi-box) * 1.35); }

/* The shared .page-header-row stacks below 480 so the 925 badge is never squeezed. A learn
   page's mark is the page's own object and belongs beside the title at every width, so this row
   opts out and lets the TITLE wrap around it instead — "How we price" breaking after "we" is a
   better outcome than the mark dropping to a line of its own. */
@media (max-width: 480px) {
  .learn-header-row { flex-direction: row; align-items: center; flex-wrap: nowrap; }
  .learn-head-mark { --vi-box: 52px; }
}
@media (min-width: 481px) and (max-width: 767px) { .learn-head-mark { --vi-box: 68px; } }

/* Fills its rail. It carried a reading-measure cap when it sat in a single column; in the
   right-hand rail the column is the constraint and a second one would leave it short of the
   ladder beneath it. */
.price-rate { width: 100%; }
/* Larger than a product page's sum: there the card shows one piece's worked example, here the
   rate is the subject of the page. Same inset box, same tokens. */
.price-rate-sum {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: center;
  /* No gap: these are flex siblings and a gap here would reopen the space between the value and
     its symbol, which the house format does not have (formatEUR in money.js). The unit spaces
     itself instead. */
  gap: 0;
  /* Sized to the RAIL, not the viewport. It was clamped against 7vw, which on a 1280 desktop
     asked for ~90px inside a 370px card — the figure broke and the euro sign landed on a line of
     its own. The card is a fixed-width object now, so the figure is too. */
  font-size: var(--type-h3);
  font-weight: var(--weight-bold);
  line-height: 1.1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* An OPTICAL nudge, not a space. Coconat's € has almost no left side bearing, and at this size
   its crossbar collided with the final 0 — "10.50€" rendered as though the two glyphs were one.
   Em-based so it holds through the clamp above. The house format is still symbol-after-value
   with no space (see formatEUR in money.js); this is kerning, not punctuation. */
.price-rate-cur { margin-left: 0.08em; }
.price-rate-unit {
  margin-left: var(--space-2);
  font-size: var(--type-h6);
  font-weight: var(--weight-demi);
  color: var(--text-on-action-subtle);
  letter-spacing: normal;
}
/* Before /api/config answers there is no rate, and this page must not invent one. */
.price-rate-pending { font-size: var(--type-h6); font-weight: var(--weight-regular); }

/* The ladder is a .learn-panel (defined above with the other information cards); what is left
   here is the table inside it.

   THE SCROLL CONTAINER IS THE PANEL, not the table. Three columns of numbers have a min-content
   width the column cannot go under, and at 320 the table pushed the whole page sideways. Nothing
   is clipped vertically here, which is the usual cost of overflow-x (CLAUDE.md). */
.price-tiers { overflow-x: auto; }
.price-table {
  width: 100%;
  min-width: 18rem;
  border-collapse: collapse;
  font-size: var(--type-body-sm);
}
.price-table-cap {
  margin-bottom: var(--space-4);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: left;
}
.price-table th, .price-table td {
  padding: var(--space-3) var(--space-3);
  border-bottom: var(--border-w-small) solid var(--border-default);
  text-align: left;
}
.price-table th:first-child, .price-table td:first-child { padding-left: 0; }
.price-table th:last-child, .price-table td:last-child { padding-right: 0; }
.price-table thead th {
  padding-top: 0;
  font-size: var(--type-caption);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.price-table tbody th {
  font-weight: var(--weight-demi);
  color: var(--text-heading);
  white-space: nowrap;
}
.price-table td { color: var(--text-body); font-variant-numeric: tabular-nums; }
.price-table tbody tr:last-child th, .price-table tbody tr:last-child td { border-bottom: 0; padding-bottom: 0; }
.price-off { color: var(--text-action); font-weight: var(--weight-demi); }

/* The feature card's subhead: the promise, under the name of the thing. */
.learn-card-feature-head {
  margin-top: var(--space-2);
  font-size: var(--type-h6);
  font-weight: var(--weight-demi);
  line-height: var(--lh-h6);
  color: var(--text-action);
}

/* A hub card's mark, and the row it lives on.
   The mark sits beside the TITLE, which splits that row; the blurb below then has the card's full
   width, and the link rides the end of it rather than taking a line of its own. That is the
   shortest arrangement the three pieces have: nothing on this card occupies a row alone.

   ONE CELL FOR ALL MARKS, so they share a centre. The drawings are a tall balance, a squarish
   scroll, a van half as tall as it is wide, a bust and a chain link: right-aligned at their
   natural widths their centres sat at five different places, which reads as five cards laid out
   slightly differently. A fixed 64x52 box with the artwork centred puts every centre in the same
   spot, and each drawing still fits by whichever side reaches first.

   The -sm assets are drawn for this size. See build_small.py: a line drawn to read at 85px is
   under a device pixel at 30 and simply is not drawn. */
.learn-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}
/* min-width:0 or a long title refuses to shrink and pushes the mark out of the card (CLAUDE.md). */
.learn-card-head > .learn-card-title { flex: 1 1 auto; min-width: 0; }
.learn-card-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 64px;
  height: 52px;
  color: var(--text-action);
  /* The card is an <a>, so the mark must not eat the click or the hover. */
  pointer-events: none;
}
/* The chain link brings its own greys — it is the catalogue's icon, not a mark in the brand's
   ink, and recolouring it to one tone would flatten the shading that makes it read as metal. */
.learn-card-mark > svg { display: block; max-width: 100%; max-height: 100%; }
/* The badge sizes itself; the cell only centres it. */
.learn-card-mark.is-badge > .badge-925 { pointer-events: none; }
/* Left half of the catalogue icon, at the same mass as the care card's shortened anchor.
   The cell is 64px; drawing the trace at 128px and clipping shows a short run of links
   without recolouring the greys. min/max width hold the flex item at the cell, or the
   img's min-content pushes the title out of the card. */
.learn-card-mark.is-chain {
  overflow: hidden;
  place-items: center start;
  min-width: 64px;
  max-width: 64px;
}
.learn-card-mark.is-chain .chain-icon {
  width: 128px;
  max-width: none;
  height: auto;
  max-height: none;
  margin: 0;
}

/* ── The animated value marks ────────────────────────────────────────────────
   One mark per value. Two are inlined SVG rigs (the artwork traced once, the moving parts
   transformed from here); two are alpha sprites stepped through a mask, for motion no transform
   reproduces. Both routes paint with `currentColor`/`background: currentColor`, so ONE
   declaration below sets the ink for all four and the theme token does the rest — no second
   asset for dark mode, no colour baked into a file.

   The ink is --text-action, the same maroon as the line under each title, so the mark reads as
   part of the card rather than as clip art dropped into it.

   NOTHING HERE RUNS ON ITS OWN. Every animation is armed paused and plays exactly once, and
   `--vi-run` is the switch: it is set on .value-icon and inherits, which is how one property
   reaches both a sprite span and the groups inside an inlined SVG. learn.js decides when — a
   sequence then hover on a pointer, scroll position on a touch screen. Four marks looping
   forever is four things competing with the sentence beside them. */
.value-head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  /* The mark sets the row's height and the copy centres against it, so the four cards agree on
     where the body text starts however long a title wraps. */
  min-height: var(--vi-box);
  margin-bottom: var(--space-4);
}
/* min-width:0 or a long title refuses to wrap and pushes the mark off the card — `flex: 1` alone
   floors at min-content width, which is the trap recorded in CLAUDE.md. */
.value-head-copy { flex: 1 1 auto; min-width: 0; }
.value-icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: var(--vi-box);
  height: var(--vi-box);
  color: var(--text-action);
  --vi-box: 83px;
  --vi-run: paused;
}
@media (max-width: 767px) { .value-icon { --vi-box: 68px; } }

/* `meet` (the default) fits each drawing inside the box whatever its proportions — the watch is
   tall, the scroll is narrow, and they must still occupy one common square or the cards do not
   line up. */
.vicon { display: block; max-width: 100%; max-height: 100%; }
.vicon-sprite {
  /* FIT INSIDE THE BOX, whichever way the cell is proportioned — the same thing `meet` does for
     the rigs. `height: 100%` alone sizes the width off the aspect ratio and a cell wider than it
     is tall then overhangs the box, which on a phone was real horizontal page overflow rather
     than a clipped edge. --vi-ar is the cell's width over its height. */
  width:  min(100%, calc(100% * var(--vi-ar)));
  height: min(100%, calc(100% / var(--vi-ar)));
  /* Defaults for the sprite's shape and source, overridden per icon by the inline style
     content.js writes. Declared here too so no rule below refers to a property nothing defines,
     which is a silently dead rule and what the token audit in tests/unit catches.
     `--vi-steps` is one less than `--vi-cells`: stepping 0%->100% across N cells lands on cells
     0..N-1 and finishes on the last, which is the frame each mark rests on. */
  --vi-src: none;
  --vi-cells: 24;
  --vi-steps: 23;
  --vi-ar: 1;
  /* One pass by default. A mark with `cycle` runs the strip twice with the second pass reversed,
     which is how the scroll rolls shut and opens again and ends on the frame it started on. */
  --vi-dur: 3.2s;
  --vi-iter: 1;
  --vi-dir: normal;
  -webkit-mask-image: var(--vi-src);
          mask-image: var(--vi-src);
  /* The strip is one row of `--vi-cells`; a cell is therefore the element's own width, which is
     what keeps this in percentages and lets the box resize at the breakpoint above. */
  -webkit-mask-size: calc(var(--vi-cells) * 100%) 100%;
          mask-size: calc(var(--vi-cells) * 100%) 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  background: currentColor;
  /* Longhands throughout: the `animation` shorthand resets direction, iteration count and play
     state to their initial values, so any of them written above it would be silently discarded. */
  animation-name: vi-hand;
  animation-duration: var(--vi-dur, 3.2s);
  animation-timing-function: steps(var(--vi-steps));
  animation-iteration-count: var(--vi-iter, 1);
  animation-direction: var(--vi-dir, normal);
  animation-fill-mode: both;
  animation-play-state: var(--vi-run, paused);
}
@keyframes vi-hand {
  from { -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
  to   { -webkit-mask-position: 100% 0%; mask-position: 100% 0%; }
}

/* Honesty — the balance settles. The beam turns about its pivot and each pan follows the end it
   hangs from: down and INWARD, because a beam end travels on an arc, not a vertical line. The
   spire is not in any of these groups, which is the whole repair — it is drawn once and stays. */
.vicon-scale .vi-beam {
  transform-origin: 388px 222px;
  animation: vi-beam 4s ease-in-out 1 both;
  animation-play-state: var(--vi-run, paused);
}
.vicon-scale .vi-pan-l { animation: vi-pan-l 4s ease-in-out 1 both; }
.vicon-scale .vi-pan-r { animation: vi-pan-r 4s ease-in-out 1 both; }
.vicon-scale :is(.vi-pan-l, .vi-pan-r) { animation-play-state: var(--vi-run, paused); }
@keyframes vi-beam {
  0%   { transform: rotate(0deg); }
  8%   { transform: rotate(8deg); }
  27%  { transform: rotate(27deg); }
  45%  { transform: rotate(12deg); }
  56%  { transform: rotate(0deg); }
  70%  { transform: rotate(-11.5deg); }
  82%  { transform: rotate(-4deg); }
  92%  { transform: rotate(1.5deg); }
  100% { transform: rotate(0deg); }
}
@keyframes vi-pan-l {
  0%   { transform: translate(0px, 0px); }
  8%   { transform: translate(2.0px, -28.67px); }
  27%  { transform: translate(22.51px, -93.52px); }
  45%  { transform: translate(4.5px, -42.83px); }
  56%  { transform: translate(0px, 0px); }
  70%  { transform: translate(4.14px, 41.07px); }
  82%  { transform: translate(0.5px, 14.37px); }
  92%  { transform: translate(0.07px, -5.39px); }
  100% { transform: translate(0px, 0px); }
}
@keyframes vi-pan-r {
  0%   { transform: translate(0px, 0px); }
  8%   { transform: translate(-2.0px, 28.67px); }
  27%  { transform: translate(-22.51px, 93.52px); }
  45%  { transform: translate(-4.5px, 42.83px); }
  56%  { transform: translate(0px, 0px); }
  70%  { transform: translate(-4.14px, -41.07px); }
  82%  { transform: translate(-0.5px, -14.37px); }
  92%  { transform: translate(-0.07px, 5.39px); }
  100% { transform: translate(0px, 0px); }
}

/* Orders — the van holds still and the wind goes past it.
   The truck is a brand mark and reads as one standing still; what says "in transit" is the air
   behind it. Three streaks, each on its own delay so they do not move as a comb, three passes of
   one second inside the three seconds the brief asked for. */
.vicon-truck :is(.vi-wind-1, .vi-wind-2, .vi-wind-3) {
  animation-name: vi-wind;
  animation-duration: 1s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1);
  animation-iteration-count: 3;
  animation-fill-mode: both;
  animation-play-state: var(--vi-run, paused);
}
.vicon-truck .vi-wind-2 { animation-delay: 0.12s; }
.vicon-truck .vi-wind-3 { animation-delay: 0.24s; }
/* Ends at opacity 0, so the mark rests as a clean truck with no streak frozen beside it. */
@keyframes vi-wind {
  0%   { opacity: 0; transform: translateX(90px); }
  25%  { opacity: 0.85; }
  70%  { opacity: 0.85; }
  100% { opacity: 0; transform: translateX(-60px); }
}

/* Timelessness — twelve hours in four seconds, and back where it started.
   ONE turn of the hour hand and twelve of the minute hand, both on the same ease-in-out: time
   speeds up through the first six hours and slows through the last six, which is the brief, and
   because 4320 is 12 x 360 the two hands stay in step and the dial returns to 10:10 exactly. */
.vicon-watch .vi-hour,
.vicon-watch .vi-min {
  transform-origin: 482.25px 562.5px;
  animation-duration: 4s;
  animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
  animation-iteration-count: 1;
  animation-fill-mode: both;
  animation-play-state: var(--vi-run, paused);
}
.vicon-watch .vi-hour { animation-name: vi-hour; }
.vicon-watch .vi-min  { animation-name: vi-min; }
@keyframes vi-hour { from { transform: rotate(0deg); }   to { transform: rotate(360deg); } }
@keyframes vi-min  { from { transform: rotate(0deg); }   to { transform: rotate(4320deg); } }

/* learn.js already parks every mark on its last frame and never starts the sequence when this
   is set. The rule is here as well so the stylesheet does not rely on a script to hold still. */
@media (prefers-reduced-motion: reduce) {
  .value-icon { --vi-run: paused; }
}

.value-title {
  font-size: var(--type-h5);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
}
.value-line {
  margin-top: var(--space-1);
  font-size: var(--type-body-sm);
  font-weight: var(--weight-demi);
  color: var(--text-action);
}
.value-body {
  margin-top: var(--space-3);
  font-size: var(--type-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
}
.learn-more { padding-bottom: var(--space-16); }
/* On a rich page the closing section is a full pairing with an object in it, not a couple of
   lines, so the related-reading strip needs real distance from it or it reads as part of the
   call to action rather than as what comes after the page has finished. */
.learn-article.is-rich .learn-more { padding-top: var(--learn-beat); }

/* ══ Lookbook / Gallery ═══════════════════════════════════════════════════════
   Compact filter bar + three caption layouts (Line / Stack / Stamp) switched
   with .seg. Chips stay .chip-option. Gender uses the finder recipient ramps
   (.badge-rec-girl/woman/boy/man). */ 

.look-filters {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.look-filter-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  min-width: 0;
}
.look-filter-k {
  flex: 0 0 2.75rem;
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-subtle);
  line-height: 1;
}
.look-chip-group {
  flex: 0 0 auto;
  width: auto;
  display: grid;
  gap: var(--space-1);
  align-items: start;
}
.look-filter-row-style .look-chip-group { grid-template-columns: repeat(3, var(--look-chip)); }
.look-filter-row-who .look-chip-group {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  width: 100%;
}
.look-filter-row-gauge .look-chip-group { grid-template-columns: repeat(3, var(--look-chip)); }

/* Style and gauge stay 44px squares. Who is taller-same, wider for the words. */
.look-chip { width: var(--look-chip); }
.look-chip .chip-option-label {
  box-sizing: border-box;
  width: var(--look-chip);
  height: var(--look-chip);
  min-height: var(--look-chip);
  padding: 0;
  justify-content: center;
  align-items: center;
  text-align: center;
  line-height: 1.1;
}
.look-who-chip { width: auto; }
.look-who-chip .chip-option-label {
  width: 100%;
  padding: 0 var(--space-3);
}

/* ── The chips: one ladder each, three rungs ─────────────────────────────────
   Every chip here goes rest → hover → chosen, and the only thing that changes down the
   ladder is how much fill it has. Chosen used to be the brand maroon, which made a picked
   filter the darkest thing on a white card — louder than the photographs it was filtering.
   A chip is a state, not a call to action.

   STYLE, GAUGE and SHOW ride the beige the filter button is made of, so the card and its
   controls are one material. WHO keeps its rose and steel, because those four are the only
   chips whose colour carries meaning, but each shifts a step lighter: what used to be the
   resting fill is now the hover, which leaves the four legible at a glance without four
   coloured blocks competing with the grid behind them. */
.look-chip:not(.look-who-chip) .chip-option-label {
  background: var(--surface-raised);
}
/* Hover IS the chosen fill, so hovering shows you what picking would look like rather than
   an intermediate value that means nothing on its own. Dark keeps its own three rungs — see
   the override below — because there the ladder lightens, and a hover as bright as the
   chosen state pulls the eye across the whole card. */
.look-chip:not(.look-who-chip):hover .chip-option-label {
  background: var(--look-chip-on);
}
[data-theme="dark"] .look-chip:not(.look-who-chip):hover .chip-option-label {
  background: var(--surface-elevated);
}
.look-chip:not(.look-who-chip) input:checked + .chip-option-label {
  background: var(--look-chip-on);
  border-color: var(--look-on-border);
  color: var(--look-chip-on-ink);
  -webkit-text-fill-color: var(--look-chip-on-ink);
}

/* ── The two values the whole card's chosen state hangs on ───────────────────
   --look-chip-on   the chosen fill, one rung past hover
   --look-on-border the chosen edge, THE SAME on every chip so that "chosen" reads as one
                    state and not as four differently-outlined ones

   Dark inverts both. In light the ladder darkens as it is chosen and the edge darkens with
   it; on a dark page darkening is what the card is already made of, so the ladder lightens
   instead and the edge lightens with it. --text-action is the border there for the same
   reason it is the link colour: it is the maroon that has been lifted to read on black. */
.look-filters {
  /* One step lighter than beige-600. Step by LUMINANCE, not by the number in the name — the
     beige scale is not monotonic, because 500 is the brand anchor and sits lighter than 400.
     Darkest to lightest it runs 600, 400, 500, 300, 200, so the rung below 600 is 400. */
  --look-chip-on:     var(--beige-400);
  --look-chip-on-ink: var(--text-heading);
  --look-on-border:   var(--border-action);
}
[data-theme="dark"] .look-filters {
  --look-chip-on:     #453539;
  --look-chip-on-ink: var(--beige-500);
  --look-on-border:   var(--text-action);
}

/* Same rose / steel ramps as Find by fit (.chip-girl etc.), each moved one rung lighter:
   rest is quieter than it was, hover is what rest used to be, and chosen is the finder
   value exactly — so the ramp still ends where the rest of the shop expects it to. */
.look-who-chip.chip-girl .chip-option-label {
  background: var(--surface-default);
  border-color: var(--maroon-100);
  color: var(--maroon-500);
  -webkit-text-fill-color: var(--maroon-500);
}
.look-who-chip.chip-woman .chip-option-label {
  background: var(--maroon-50);
  border-color: var(--maroon-200);
  color: var(--maroon-600);
  -webkit-text-fill-color: var(--maroon-600);
}
.look-who-chip.chip-boy .chip-option-label {
  background: var(--surface-default);
  border-color: var(--blue-100);
  color: var(--blue-600);
  -webkit-text-fill-color: var(--blue-600);
}
.look-who-chip.chip-man .chip-option-label {
  background: var(--blue-100);
  border-color: var(--blue-300);
  color: var(--blue-800);
  -webkit-text-fill-color: var(--blue-800);
}
/* Hover is the chosen fill, as it is for the beige chips. Man carries its chosen INK too:
   blue-800 on blue-400 measures 4.07, so the pairing has to travel together. */
.look-who-chip.chip-girl:hover  .chip-option-label { background: var(--maroon-200); }
.look-who-chip.chip-woman:hover .chip-option-label { background: var(--maroon-300); }
.look-who-chip.chip-boy:hover   .chip-option-label { background: var(--blue-200); }
.look-who-chip.chip-man:hover   .chip-option-label {
  background: var(--blue-400);
  color: var(--text-heading);
  -webkit-text-fill-color: var(--text-heading);
}

/* ── WHO on a dark page ──────────────────────────────────────────────────────
   The brand tints are FIXED across themes, so the light-mode ramps put two big pale blocks
   on a near-black card while the other two were dark ink on a dark fill — Boy was the same
   value as the card it sat on. Neither half was wrong on its own; together they read as a
   row that had been half-inverted.
   Here the hue is carried by translucency over the card, so all four rest dark and gain the
   colour as they are chosen, and the INK is the light end of each hue rather than the deep
   end. Two ramps, four chips, one direction. */
[data-theme="dark"] .look-who-chip.chip-girl  .chip-option-label,
[data-theme="dark"] .look-who-chip.chip-woman .chip-option-label {
  color: var(--maroon-300);
  -webkit-text-fill-color: var(--maroon-300);
  border-color: rgba(192, 125, 142, 0.38);      /* --maroon-300 */
}
[data-theme="dark"] .look-who-chip.chip-boy .chip-option-label,
[data-theme="dark"] .look-who-chip.chip-man .chip-option-label {
  color: var(--blue-300);
  -webkit-text-fill-color: var(--blue-300);
  border-color: rgba(151, 179, 196, 0.38);      /* --blue-300 */
}
[data-theme="dark"] .look-who-chip.chip-girl  .chip-option-label { background: rgba(192, 125, 142, 0.08); }
[data-theme="dark"] .look-who-chip.chip-woman .chip-option-label { background: rgba(192, 125, 142, 0.16); }
[data-theme="dark"] .look-who-chip.chip-boy   .chip-option-label { background: rgba(151, 179, 196, 0.08); }
[data-theme="dark"] .look-who-chip.chip-man   .chip-option-label { background: rgba(151, 179, 196, 0.16); }

[data-theme="dark"] .look-who-chip.chip-girl:hover  .chip-option-label { background: rgba(192, 125, 142, 0.18); }
[data-theme="dark"] .look-who-chip.chip-woman:hover .chip-option-label { background: rgba(192, 125, 142, 0.26); }
[data-theme="dark"] .look-who-chip.chip-boy:hover   .chip-option-label { background: rgba(151, 179, 196, 0.18); }
[data-theme="dark"] .look-who-chip.chip-man:hover   .chip-option-label { background: rgba(151, 179, 196, 0.26); }

/* Chosen: the most colour, and the light ink brightened a step so it still leads the fill. */
[data-theme="dark"] .look-who-chip.chip-girl  input:checked + .chip-option-label,
[data-theme="dark"] .look-who-chip.chip-woman input:checked + .chip-option-label {
  color: var(--maroon-200);
  -webkit-text-fill-color: var(--maroon-200);
}
[data-theme="dark"] .look-who-chip.chip-boy input:checked + .chip-option-label,
[data-theme="dark"] .look-who-chip.chip-man input:checked + .chip-option-label {
  color: var(--blue-200);
  -webkit-text-fill-color: var(--blue-200);
}
[data-theme="dark"] .look-who-chip.chip-girl  input:checked + .chip-option-label { background: rgba(192, 125, 142, 0.30); }
[data-theme="dark"] .look-who-chip.chip-woman input:checked + .chip-option-label { background: rgba(192, 125, 142, 0.40); }
[data-theme="dark"] .look-who-chip.chip-boy   input:checked + .chip-option-label { background: rgba(151, 179, 196, 0.30); }
[data-theme="dark"] .look-who-chip.chip-man   input:checked + .chip-option-label { background: rgba(151, 179, 196, 0.40); }
.look-who-chip.chip-girl input:checked + .chip-option-label {
  background: var(--maroon-200);
  border-color: var(--maroon-200);
  color: var(--maroon-500);
  -webkit-text-fill-color: var(--maroon-500);
}
.look-who-chip.chip-woman input:checked + .chip-option-label {
  background: var(--maroon-300);
  border-color: var(--maroon-300);
  color: var(--maroon-600);
  -webkit-text-fill-color: var(--maroon-600);
}
.look-who-chip.chip-boy input:checked + .chip-option-label {
  background: var(--blue-200);
  border-color: var(--blue-200);
  color: var(--blue-600);
  -webkit-text-fill-color: var(--blue-600);
}
/* The one chip whose ink had to change with the fill. blue-400 is the darkest of the four
   chosen fills, and blue-800 on it measures 4.07 — under AA. --text-heading takes it to 5.69.
   Measured, not eyeballed: it is the only pairing on this card that did not already clear. */
.look-who-chip.chip-man input:checked + .chip-option-label {
  background: var(--blue-400);
  border-color: var(--blue-400);
  color: var(--text-heading);
  -webkit-text-fill-color: var(--text-heading);
}
.look-filter-row-who .look-who-chip input:checked + .chip-option-label {
  border-color: var(--look-on-border);
}
/* The shop .chain-icon carries a left margin and a 2.38em length so it
   sits on a title line. In a square chip that margin throws it off-centre. */
.look-style-chip .chain-icon {
  display: block;
  margin: 0;
  width: 28px;
  max-height: 11px;
  height: auto;
}
.look-gauge-chip .chip-thickness-bar { height: 16px; }
.look-age-chip .chip-option-label {
  min-width: 44px;
  justify-content: center;
  font-variant-numeric: tabular-nums;
}
.look-ico { display: block; flex-shrink: 0; }

/* Parks under the breadcrumb bar and collapses as you scroll, the same move the chain
   configurator makes: the count folds away first — it is a fact you read once — and the
   funnel stays, because that is the control you come back to. Same --crumb-h the bar
   publishes, so the two cannot drift apart.
   The gutter bleed is the crumb bar's, for the same reason: without it the grid scrolls up
   through the page padding either side of a bar meant to be blocking it. */
/* The toolbar's own wrapper holds nothing but the toolbar, and a sticky element may only
   travel inside its parent — so parked there it came unstuck a few pixels later and the
   funnel scrolled off with the grid. `display: contents` drops the wrapper out of the box
   tree, which makes .shop-container the parent and gives the bar the whole page to travel.
   Same fix, same reason, as lifting the breadcrumb out of .page-header. */
/* The toolbar is an ordinary row and scrolls away with the page. It is only the FUNNEL that
   stays, and it stays the way the collapsed configurator does: lifted out of the flow and
   floated over the grid, not parked in a bar of its own. A sticky bar would have to reserve a
   strip under the breadcrumbs for the rest of the scroll, and the point of collapsing to an
   icon is to stop paying for the row. */
.look-toolbar {
  /* The funnel's size, declared HERE rather than on the menu: the menu leaves the flow when it
     pins, and the row still has to know how much space it was taking. */
  --look-funnel: 44px;

  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  /* Holds the row open once the funnel is lifted out of it. Without this the row collapsed to
     the height of the count line — 44px to 20px — and the whole grid jumped 24px up at the
     moment of pinning. Reserving the height means nothing below moves. */
  min-height: var(--look-funnel);
}

.look-count {
  font-size: var(--type-body-sm);
  color: var(--text-muted);
  margin: 0;
  /* Inset by the same gap the funnel keeps at the other end, so the row reads as evenly
     spaced rather than pressed against one edge and floated off the other. */
  margin-left: var(--config-gap);
  margin-right: auto;
}

.look-filter-menu {
  position: relative;
  z-index: var(--z-raised);
  /* The same inset it keeps once pinned, so the button does not slide sideways at the moment
     it lifts out of the row — and so the card, which opens one padding to the right of it,
     lands flush with the content edge in both states rather than overhanging at rest. */
  margin-right: var(--config-gap);
}
/* Pinned, the gap is in `right` instead — chrome.js measures it from the container. */
.look-filter-menu.is-pinned { margin-right: 0; }
/* Floated over the grid once its row has scrolled past the chrome. `top` and `right` are
   written by chrome.js: the edge it aligns to is the content container's, and a fixed element
   has no ancestor that can supply it.

   IT KEEPS --z-raised FROM THE BASE RULE. Pinning changes where the button IS, not how
   important it is, and it used to jump to --z-overlay (200) here — above --z-nav (100), so the
   funnel floated on top of the nav bar and, once the mobile drawer landed, on top of the menu
   itself, covering the first row. Nothing on this page needs it above the nav: the grid it
   floats over is normal flow, and the lightbox that must cover it is --z-modal (400). */
.look-filter-menu.is-pinned {
  position: fixed;
}
/* Open, the funnel does not move at all — the CARD is placed around it. It used to be
   re-positioned into the card's corner, which meant the thing you had just clicked jumped out
   from under the cursor. Only its stacking changes, so it stays above the card it opened and
   remains the way to close it again. */
/* 2 and 1, not --z-overlay and --z-overlay + 1. .look-filter-menu sets a z-index, so it is a
   stacking context and these two only ever ranked against each other INSIDE it — the page-level
   token was never doing page-level work here, and reading it as though it were is what put the
   pinned rule above the nav. Local problem, local numbers. */
.look-filter-menu[open] .look-filter-toggle {
  z-index: 2;
}
/* Three steps of one fill rather than three different treatments: at rest the button is a
   tint of the page, hover deepens it, and open deepens it again. A control that is doing
   something should look more present than one that is merely available, and on a page that is
   almost entirely photographs a border alone did not carry that.
   The ladder is declared here, on the component, because there is no third elevated surface
   token — and it is overridden whole for dark, where the beige scale is not what the page is
   made of. Same shape as .chain-stage's colour block above. */
/* White, like the configurator card — the filter is the same kind of object, a panel of
   controls, and the two should not be two different materials. Hover warms it a step; open
   drops the border entirely, because by then the button is sitting ON the white card it
   opened and an outline around it would draw a box inside a box. */
.look-filter-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--look-funnel);
  min-width: var(--look-funnel);
  padding: var(--space-1h) var(--space-3);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-base);
  background: var(--surface-default);
  color: var(--text-body);
  cursor: pointer;
  list-style: none;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.look-filter-toggle:hover { background: var(--surface-raised); }
.look-filter-menu[open] .look-filter-toggle {
  background: transparent;
  border-color: transparent;
}
.look-filter-toggle:focus-visible {
  outline: var(--border-w-medium) solid var(--border-focus);
  outline-offset: 2px;
}
.look-filter-toggle::-webkit-details-marker { display: none; }
.look-filter-funnel { display: block; flex: none; }
/* Overlaid on the funnel's top-right corner, the way the cart and account counts sit on their
   icons in the nav — a count belongs ON the thing it counts for, not beside it. Shape and
   colour come from .count-bubble, the same one those use; this only says where it sits. It
   carried its own near-copy of that styling before, which is how it ended up a rounded
   rectangle while every other count in the shop was a circle. */
.look-filter-badge {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
}
/* On a dark card the maroon coin all but vanishes, so it flips to the nav's treatment —
   beige disc, maroon figure — which is what the cart and account counts already are.
   --surface-action for the ink, NOT --text-action: the nav's own rule uses the latter, and in
   the dark THEME that resolves to maroon-300, which measures 2.49 on beige. --surface-action
   is maroon-500 in both themes and gives 11.32 for the same look. */
[data-theme="dark"] .look-filter-badge {
  background: var(--text-on-action);
  color: var(--surface-action);
  -webkit-text-fill-color: var(--surface-action);
}
.look-filters {
  --look-chip: 44px;
  position: absolute;
  /* Up and left by one padding, so the funnel — which has not moved — is exactly at the
     card's content top-right, level with Reset at the content left. The card's own outer
     edges then land where the funnel was inset from, which is why the pinned inset below and
     this padding are the same value. */
  top: calc(var(--space-4) * -1);
  right: calc(var(--space-4) * -1);
  z-index: 1;   /* inside .look-filter-menu's stacking context — under the toggle, over nothing else */
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: stretch;
  column-gap: var(--space-2);
  row-gap: var(--space-4);
  width: min(22rem, calc(100vw - 2 * var(--page-px)));
  margin: 0;
  /* Row 1 is the header the funnel sits in, and is exactly its height. */
  grid-template-rows: var(--look-funnel) auto auto;
  padding: var(--space-4);
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-lg);
}
.look-filter-row {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  min-width: 0;
}
.look-filter-k { flex: none; min-height: 1em; }

/* Row 1 is the header. The count is at the left — it is the answer the filters produced, and
   the toolbar's copy of it has scrolled away by the time the card is opened from a pinned
   funnel. Reset and the funnel are the two controls, together at the right: clear, then close.
   Reset carries the funnel's width as a right margin because the funnel is the <summary> and
   is not in this grid to be laid out beside. */
.look-filter-count {
  grid-column: 1;
  grid-row: 1;
  align-self: center;
  margin: 0;
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}
.look-filter-reset {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  margin-right: calc(var(--look-funnel) + var(--space-3));
}
.look-filter-row-style { grid-column: 1; grid-row: 2; }
.look-filter-row-who   { grid-column: 2; grid-row: 2; }
.look-filter-row-gauge { grid-column: 1; grid-row: 3; }
.look-filter-row-show  { grid-column: 2; grid-row: 3; }

/* Two chips over one axis, sized like the gauge and style chips beside them. */
.look-filter-row-show .look-chip-group { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.look-show-chip { width: 100%; min-width: 0; }
.look-show-chip .chip-option-label {
  width: 100%;
  justify-content: center;
  padding: 0 var(--space-2);
  height: var(--look-chip);
  min-height: var(--look-chip);
  box-sizing: border-box;
}

.look-filter-reset {
  align-self: center;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.look-filter-reset:hover { color: var(--text-action); }

.look-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-7) var(--space-4);
  padding-bottom: var(--space-16);
}
@media (min-width: 640px) {
  .look-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .look-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-8) var(--space-5); }
}

.look-card { min-width: 0; }

.look-card-well {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  border: 0;
  padding: 0;
  cursor: zoom-in;
  color: inherit;
}
.look-card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
}
/* No .is-mirrored rule, deliberately. The over-45 shoot ships mirrored in the WebP itself —
   see MIRRORED in scripts/prep-model-shots.py. Turning a photograph round in CSS put the
   orientation in four places that had to agree and did not: the grid honoured it, the shot
   page never asked for it, and the fullscreen image writes `transform` inline for pan and
   zoom, which beats the sheet, so its rule could never fire at all. If a shot faces the wrong
   way, re-encode the asset; do not add a transform here. */

.look-card-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  column-gap: var(--space-3);
  row-gap: var(--space-1);
  align-items: stretch;
  padding: var(--space-3) 0 0;
}
.look-card-name {
  grid-column: 1;
  margin: 0;
  font-family: var(--font-base);
  font-size: var(--type-h6);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  line-height: var(--lh-h6);
}
.look-card-name a {
  color: inherit;
  text-decoration: none;
}
.look-card-name a:hover { color: var(--text-action); }
.look-card-name .chain-icon {
  width: 2.4em;
  max-height: 0.72em;
  margin-left: var(--space-2);
  margin-right: 0;
}
.look-card-spec {
  grid-column: 1;
  margin: 0;
  font-size: var(--type-body-sm);
  color: var(--text-muted);
  white-space: nowrap;
}
.look-card-star {
  display: inline-block;
  margin: 0 var(--space-1h);
  font-size: inherit;
  line-height: 1;
  color: inherit;
}
.look-card-star::before { content: var(--brand-star); }
.look-card-more {
  color: inherit;
  font-size: inherit;
  font-weight: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
}
.look-card-more:hover { color: var(--text-action-hover); }

/* ── Instagram on /gallery/ ──────────────────────────────────────────────
 *
 * Shaped like the profile it mirrors, not like the rest of the shop. It was six
 * .look-cards, which meant "@auravox.jewelry" set in Coconat six times down a phone over
 * captions cut to one line — the repeated part shouted, the differing part hidden. A
 * profile grid says the same thing in a shape the reader already knows how to scan.
 *
 * Only the panel is Instagram-shaped. Everything inside it — buttons, chips, the
 * fullscreen viewer — is ours.
 */

/* One tab bar, two panels, and only one of them in the flow. `display: none` rather than
   `hidden`, because the inactive panel keeps its ids for the hash router. */
.gallery-panel { display: none; }
.gallery-panel.active { display: block; }

/* One grid, six slots placed by hand, and only the placement changes with the viewport —
   no wrapper div that exists on one shape and is inert on the other, and nothing rendered
   twice. The name is its own child rather than the first line of .ig-identity precisely so
   it can sit above the counts on one shape and above the bio on both.
 *
 * Phone: name and counts stack beside the avatar; the handle, bio, and a full-width Follow
 * follow underneath. Wide: a third column opens on the right for the handle and Follow, and
 * the whole identity column rides beside the avatar rather than under it. */
.ig-profile {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-5);
  row-gap: var(--space-3);
  margin: 0 0 var(--space-6);
}
.ig-profile > .ig-avatar   { grid-column: 1; grid-row: 1 / span 2; }
/* Pulled together across the row boundary: the name reads as the label on the counts, not
   as a heading floating half an avatar above them. */
.ig-profile > .ig-name     { grid-column: 2; grid-row: 1; align-self: end; }
.ig-profile > .ig-stats    { grid-column: 2; grid-row: 2; align-self: start; }
.ig-profile > .ig-handle   { grid-column: 1 / -1; grid-row: 3; }
.ig-profile > .ig-identity { grid-column: 1 / -1; grid-row: 4; }
.ig-profile > .ig-follow   { grid-column: 1 / -1; grid-row: 5; margin-top: var(--space-2); }
.ig-profile > .ig-note     { grid-column: 1 / -1; grid-row: 6; }
.ig-handle {
  margin: 0;
  min-width: 0;
  font-size: var(--type-body);
  font-weight: var(--weight-demi);
  color: var(--text-heading);
  overflow-wrap: anywhere;
}
.ig-handle a { color: inherit; text-decoration: none; }
.ig-handle a:hover { color: var(--text-action); }

.ig-avatar {
  flex: 0 0 auto;
  width: 5.5rem;
  height: 5.5rem;
  border-radius: var(--radius-round);
  object-fit: cover;
  background: var(--surface-raised);
  border: var(--border-w-small) solid var(--border-default);
}
/* No avatar yet — the monogram, not a broken frame. */
.ig-avatar-fallback {
  display: grid;
  place-items: center;
  font-family: var(--font-coconat);
  font-size: var(--type-h4);
  color: var(--text-muted);
}
.ig-stats {
  display: flex;
  /* min-width: 0 is what makes these share the row rather than push it wide: a flex item
     floors at its own min-content width without it, and "74 followers" does not shrink. */
  min-width: 0;
  flex: 1 1 0;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.ig-stat {
  font-size: var(--type-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-muted);
  white-space: nowrap;
}
.ig-stat b {
  display: block;
  font-size: var(--type-h6);
  line-height: var(--lh-h6);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
}
.ig-identity { display: grid; gap: var(--space-1); min-width: 0; }
.ig-name {
  margin: 0;
  font-size: var(--type-body);
  font-weight: var(--weight-demi);
  line-height: var(--leading-snug);
  color: var(--text-heading);
}
.ig-bio, .ig-site {
  margin: 0;
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
  /* The bio arrives with its own newlines. Instagram keeps them; collapsing them turns a
     three-line bio into one long line of separators. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ig-site a { color: var(--text-action); text-decoration: none; }
.ig-site a:hover { text-decoration: underline; }
.ig-follow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  /* Larger than a .btn's --type-label eyebrow. This is the one action in the panel and it
     sits beside a 136px portrait on a desktop; at 10px it read as a caption on the avatar
     rather than a button. The height goes up with it so the label keeps its own room. */
  font-size: var(--type-body-sm);
  letter-spacing: var(--tracking-wide);
  min-height: 3rem;
  padding-inline: var(--space-7);
}
.ig-follow svg { width: 1.4em; height: 1.4em; flex: 0 0 auto; }
.ig-note {
  margin: 0;
  font-size: var(--type-caption);
  line-height: var(--leading-relaxed);
  color: var(--text-muted);
}

/* Three across, hairline gaps, square. The tiles are the page's densest thing and they are
   meant to be: a profile grid is read as a block, not card by card. */
.ig-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 3px;
  padding-bottom: var(--space-16);
}
.ig-grid.is-empty { display: block; }
.ig-tile {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--surface-raised);
  cursor: zoom-in;
}
.ig-tile.is-pending { cursor: default; }
.ig-tile-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ig-tile-kind {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: block;
  width: 1.25rem;
  height: 1.25rem;
  color: #fff;
  /* Over a photograph, so a shadow rather than a colour token: the tile beneath can be any
     brightness and there is no surface to sit the glyph on without covering the picture. */
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.55));
  pointer-events: none;
}
.ig-tile-kind svg { width: 100%; height: 100%; display: block; }

.ig-empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
}
.ig-empty-link { color: var(--text-action); }

/* From 640 the profile takes the desktop shape — a bigger avatar with the identity beside
   it — and the grid earns a real gutter and rounded corners like everything else here. */
@media (min-width: 640px) {
  /* THE BAND, and the rule that makes it one.
   *
   * Three things share a centre line here — the avatar, the count row, and the Follow
   * button — with the name hung above the counts and the handle below the button. Grid
   * cannot centre a middle row against a spanning neighbour on its own: `1fr auto 1fr`
   * looks like it should, but a spanning item's height is distributed across its tracks
   * rather than handed to the flexible ones, so the fr rows do not end up equal.
   *
   * So rows 1 and 3 are equal BY CONSTRUCTION — one --ig-cap each — and the avatar spans
   * all three and centres itself. Equal caps plus equal row gaps put the centre of row 2
   * exactly on the centre of the span, and therefore on the avatar's centre. `minmax`
   * rather than a fixed height so a name that wraps grows its row instead of clipping;
   * the centring drifts a few pixels in that case, which is the right way to fail.
   *
   * Row 3 carries nothing. It is the name's counterweight and that is its whole job —
   * delete it and row 2 stops being the middle of the span.
   *
   * The handle and the description both leave the band and start on their own rows under
   * the avatar, spanning, exactly as they do on a phone. */
  .ig-profile {
    --ig-cap: 2.25rem;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-rows:
      minmax(var(--ig-cap), auto)   /* 1 — the name                                */
      auto                          /* 2 — counts and Follow: the centre line       */
      minmax(var(--ig-cap), auto)   /* 3 — empty: the name's counterweight          */
      auto auto auto;               /* 4, 5, 6 — handle, description, sample note   */
    align-items: start;
    column-gap: var(--space-7);
    row-gap: var(--space-2);
  }
  .ig-profile > .ig-avatar   { grid-column: 1; grid-row: 1 / span 3; align-self: center; }
  .ig-profile > .ig-name     { grid-column: 2; grid-row: 1; align-self: end; }
  .ig-profile > .ig-stats    { grid-column: 2; grid-row: 2; align-self: center; }
  .ig-profile > .ig-follow   {
    grid-column: 3;
    grid-row: 2;
    width: auto;
    justify-self: end;
    align-self: center;
    margin-top: 0;
  }
  .ig-profile > .ig-handle   { grid-column: 1 / -1; grid-row: 4; text-align: left; }
  .ig-profile > .ig-identity { grid-column: 1 / -1; grid-row: 5; margin-top: var(--space-2); }
  .ig-profile > .ig-note     { grid-column: 1 / -1; grid-row: 6; }
  /* The name carries the band now that the description has left it. */
  .ig-name { font-size: var(--type-h5); line-height: var(--leading-tight); }
  .ig-avatar, .ig-avatar-fallback { width: 8.5rem; height: 8.5rem; }
  .ig-grid { gap: var(--space-2); }
  .ig-tile { border-radius: var(--radius-md); }
}
@media (min-width: 1000px) {
  .ig-grid { gap: var(--space-3); }
}

.look-card-shop {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  padding: 0 var(--space-3);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-lg);
  background: transparent;
  color: var(--text-heading);
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
  line-height: 1;
  transition: border-color var(--transition-fast), color var(--transition-fast),
              background var(--transition-fast);
}
.look-card-shop:hover {
  border-color: var(--border-action);
  color: var(--text-action);
  background: var(--surface-action-subtle);
}

.look-card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
}
.look-body-line .look-card-tags { margin-top: var(--space-1h); }

.look-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--type-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  -webkit-text-fill-color: currentColor;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-badge);
  padding: 2px var(--space-2);
  background: var(--surface-raised);
  line-height: 1.4;
}
.look-tag .look-ico { width: 11px; height: 11px; }

/* Wearer: same ramps as the finder recipient tags. */
.look-tag-who.badge-rec-girl,
.look-tag-who.badge-rec-woman,
.look-tag-who.badge-rec-boy,
.look-tag-who.badge-rec-man { border-width: var(--border-w-small); }

/* Age stamp — bottom left, wearer ramp fill. */
.look-stamps {
  position: absolute;
  left: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
  pointer-events: none;
}
.look-stamp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-2);
  border-radius: var(--radius-badge);
  border: var(--border-w-small) solid var(--border-default);
  background: var(--surface-default);
  color: var(--text-heading);
  -webkit-text-fill-color: currentColor;
  font-size: var(--type-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  line-height: 1.4;
}
.look-stamp .look-ico { width: 11px; height: 11px; }
/* Fill wins over the default stamp ground — same ramps as finder recipient tags. */
.look-stamp.badge-rec-girl  { background: var(--maroon-200); border-color: var(--maroon-300); color: var(--maroon-600); -webkit-text-fill-color: var(--maroon-600); }
.look-stamp.badge-rec-woman { background: var(--maroon-300); border-color: var(--maroon-400); color: var(--maroon-700); -webkit-text-fill-color: var(--maroon-700); }
.look-stamp.badge-rec-boy   { background: var(--blue-300);   border-color: var(--blue-400);   color: var(--blue-700);   -webkit-text-fill-color: var(--blue-700); }
.look-stamp.badge-rec-man   { background: var(--blue-400);   border-color: var(--blue-500);   color: var(--blue-800);   -webkit-text-fill-color: var(--blue-800); }

@media (max-width: 479px) {
  .look-grid { gap: var(--space-6) var(--space-3); }
}

/* ── Fullscreen ───────────────────────────────────────────────── */
.look-lb {
  --lb-aspect: 1.5;
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Photo stage: same reason the home macros sit on #000 — the well is not a page surface. */
  background: rgb(0 0 0 / 0.92);
  padding: 5vh var(--space-10);
}
.look-lb[hidden] { display: none; }
.look-lb-stage {
  position: relative;
  height: 90vh;
  max-height: 90vh;
  max-width: min(100%, calc(90vh * 0.8));
}
/* A hero with an uncropped frame. The 0.8 above is the 4:5 crop; --lb-aspect is the real
   frame (3:2 landscape, or 2:3 for the rolo). Mobile keeps the crop and this rule does not
   apply there — the phone query below sizes the stage to the viewport instead. */
@media (min-width: 768px) {
  .look-lb.is-wide .look-lb-stage {
    max-width: min(100%, calc(90vh * var(--lb-aspect)));
  }
}
.look-lb-zoom {
  overflow: hidden;
  line-height: 0;
  touch-action: none;
  cursor: zoom-in;
}
.look-lb-zoom.is-zoomed { cursor: zoom-out; }
/* Scoped to the ZOOM box, not to the stage. The stage also holds the caption chips, and a chain
   icon is an <img> too — under `.look-lb-stage img` it inherited height:90vh and drew the link
   pattern a foot tall across the photo. The zoom box contains exactly one image, the photo. */
.look-lb-zoom img {
  display: block;
  width: auto;
  height: 90vh;
  max-width: 100%;
  max-height: 90vh;
  object-fit: contain;
  transform-origin: 0 0;
  will-change: transform;
}
.look-lb-stage .look-stamps,
.look-lb-shop { z-index: 2; }

/* Fullscreen carries the chain rather than the wearer — see lbMeta() in lookbook.js. The chip
   itself is unchanged, so the corner keeps one shape at both sizes.

   The measurements stay cased as they are written everywhere else in the shop: "1.5mm · 45cm",
   not "1.5MM · 45CM". The uppercase micro-label is right for a NAME and wrong for a unit, and
   the style chip above it still carries it, so the two lines read as label and value. */
.look-lb-spec {
  text-transform: none;
  letter-spacing: var(--tracking-normal);
}
/* z-index on the SHARED rule, not on .look-lb-close alone.
 *
 * The three buttons and the stage are all positioned with z-index:auto, so they paint in DOM
 * order — and prev comes BEFORE the stage while next comes after it. On a desktop the padding
 * gutters keep the photo away from the edges and that never showed; on a phone the padding is
 * 0 and the stage is full-bleed, so the image painted straight over the back arrow. It was not
 * missing on mobile, it was underneath the photo — invisible and untappable, while the forward
 * arrow beside it worked. Lifting every button out of that stack fixes both at once. */
.look-lb-btn {
  position: absolute;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border: 0;
  border-radius: var(--radius-base);
  background: rgb(0 0 0 / 0.45);
  color: #fff;
  cursor: pointer;
}
.look-lb-close { top: var(--space-3); right: var(--space-3); }
.look-lb-prev { left: var(--space-2); top: 50%; transform: translateY(-50%); }
.look-lb-next { right: var(--space-2); top: 50%; transform: translateY(-50%); }
.look-lb-btn:disabled { opacity: 0.3; cursor: default; }
.look-lb-shop {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  left: auto;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-4);
  border: var(--border-w-small) solid rgb(255 255 255 / 0.7);
  border-radius: var(--radius-base);
  background: transparent;
  color: #fff;
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
}
.look-lb-shop:hover {
  background: rgb(255 255 255 / 0.12);
  border-color: #fff;
}

/* ── Fullscreen: reels, and a caption under them ─────────────────────────
 *
 * The lookbook's fullscreen is a photograph and two chips over it. Instagram's is a
 * photograph OR a reel, with a caption that is the whole point of opening it — so the
 * captioned slide is laid out as media OVER text rather than text over media. A <video>
 * draws its own control bar along the bottom edge of the frame, and an overlaid caption
 * would sit on the scrubber.
 *
 * `.has-caption` is written by lbShow(), so the lookbook's slides keep the geometry they
 * were tuned with — `height: 90vh` on the image and all — and nothing about this rule can
 * reach them. */
.look-lb-video {
  display: block;
  width: auto;
  height: 90vh;
  max-width: 100%;
  max-height: 90vh;
  background: #000;
}
.look-lb-video[hidden] { display: none; }
/* The player owns its gestures — tapping a control must not also toggle the pan/zoom
   transform the photo path uses. */
.look-lb.has-caption .look-lb-zoom { touch-action: auto; cursor: default; }

.look-lb.has-caption .look-lb-stage {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* Explicit, not `max-height`: the mobile rule below sets `height: auto` on the stage, and
     a percentage max-height on the media inside an auto-height parent resolves to none —
     the photo would keep its full size and push the caption off the screen. */
  height: 90vh;
  max-height: 90vh;
}
.look-lb.has-caption .look-lb-zoom {
  flex: 1 1 auto;
  /* minmax(auto, …) is the default and would floor this at the media's own height, so the
     caption would be pushed off the bottom of the screen instead of the photo shrinking. */
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.look-lb.has-caption .look-lb-zoom img,
.look-lb.has-caption .look-lb-video {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

/* The caption, in the shape of the comment it is: handle, then the words, then how long
   ago. Same order and same emphasis as under the post itself. */
.look-lb-caption {
  flex: 0 0 auto;
  max-height: 26vh;
  overflow-y: auto;
  padding: 0 var(--space-3);
  color: rgb(255 255 255 / 0.92);
}
/* With a caption the action button joins the column instead of floating in the corner —
   an absolutely positioned button over a block of running text collides with it at some
   caption length, and the length is the poster's to choose, not ours. */
.look-lb.has-caption .look-lb-shop {
  position: static;
  flex: 0 0 auto;
  align-self: flex-end;
  margin-right: var(--space-3);
}
/* The slide counter is the one chip a captioned slide carries, and .look-stamps parks at
   the stage's bottom-left — which is now where the caption is. It moves to the top, and to
   the CENTRE rather than a corner: the stage is wider than the photograph inside it (the
   zoom box centres the media), so a chip pinned to the stage's left edge floats in the
   dark a hand's width away from the picture it is counting. */
.look-lb.has-caption .look-stamps {
  top: var(--space-3);
  bottom: auto;
  left: 50%;
  transform: translateX(-50%);
  align-items: center;
}
.look-lb-caption[hidden] { display: none; }
.ig-cap-text {
  margin: 0;
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  /* Instagram keeps the poster's line breaks; collapsing them runs a three-line caption
     into one. `anywhere` because a caption can be a single 40-character hashtag. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.ig-cap-who {
  font-weight: var(--weight-bold);
  color: #fff;
  text-decoration: none;
}
.ig-cap-who:hover { text-decoration: underline; }
.ig-tag { color: rgb(160 200 255); }
.ig-cap-when {
  margin: var(--space-1) 0 0;
  font-size: var(--type-caption);
  color: rgb(255 255 255 / 0.6);
}

@media (max-width: 639px) {
  .look-lb.has-caption .look-lb-stage { padding-bottom: var(--space-3); }
  .look-lb-caption { max-height: 22vh; }
}

@media (max-width: 639px) {
  .look-lb { padding: 0; }
  .look-lb-stage {
    max-width: 100%;
    width: 100%;
    height: auto;
  }
  .look-lb-zoom img {
    width: 100%;
    height: auto;
    max-height: none;
    object-fit: contain;
  }
}

/* ── Shot story — centred deck, faded peeks, copy below ───────── */
.look-story { padding-bottom: var(--space-16); }
@media (min-width: 640px) {
  .look-story {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 2.2fr) minmax(0, 1fr);
    column-gap: var(--space-2);
    max-width: 64rem;
    margin-inline: auto;
  }
  .look-story .look-deck {
    grid-column: 1 / -1;
    max-width: none;
    width: auto;
  }
  .look-story-col {
    grid-column: 2;
    max-width: none;
    margin: 0;
  }
}

.look-deck {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2.2fr) minmax(0, 1fr);
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) auto var(--space-8);
  max-width: 64rem;
}
.look-deck-slot {
  display: block;
  width: 100%;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  appearance: none;
  -webkit-appearance: none;
}
.look-deck-slot.is-side {
  opacity: 0.38;
  cursor: pointer;
}
.look-deck-slot.is-side .look-card-well {
  cursor: pointer;
  transform: scale(0.92);
}
.look-deck-slot.is-main .look-card-well { cursor: zoom-in; }
.look-deck-slot.is-empty { visibility: hidden; }

.look-deck-btn {
  position: absolute;
  top: 50%;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border: 0;
  border-radius: var(--radius-base);
  background: var(--surface-default);
  color: var(--text-heading);
  box-shadow: var(--shadow-md);
  cursor: pointer;
  transform: translateY(-50%);
}
.look-deck-prev { left: var(--space-2); }
.look-deck-next { right: var(--space-2); }
.look-deck-btn:hover { background: var(--surface-raised); }

.look-story-col .browse-section {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  max-width: none;
}
.look-story-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  width: 100%;
  max-width: 36rem;
  margin: 0 auto;
  text-align: left;
}
.look-story-head { text-align: left; margin: 0; }
.look-story-title {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-3);
  margin: 0;
  font-size: var(--type-h4);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
  line-height: var(--lh-h4);
}
.look-story-title .chain-icon {
  width: 2.6em;
  max-height: 0.72em;
  margin-left: 0;
  margin-right: 0;
}
.look-story-spec {
  margin: var(--space-2) 0 0;
  font-size: var(--type-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--text-muted);
}
.look-story-kicker {
  display: flex;
  justify-content: flex-start;
  margin-top: var(--space-4);
  margin-bottom: 0;
}
/* Badge is left-aligned here; open the tip from its left edge so the
   bubble does not run off the viewport on a phone. */
.look-story-kicker .tip-wrap .tip-bubble {
  left: 0;
  right: auto;
}
.look-story-about,
.look-story-copy,
.look-story-block {
  display: flex;
  flex-direction: column;
  margin: 0;
}
.look-story-about { gap: var(--space-4); }
.look-story-copy { gap: var(--space-6); }
.look-story-block { gap: var(--space-2); }
.look-story-about .pdp-note { margin-top: 0; }
.look-story-h {
  margin: 0;
  font-size: var(--type-h6);
  font-weight: var(--weight-bold);
  line-height: var(--lh-h6);
  color: var(--text-heading);
}
.look-story-p,
.look-story-def dd {
  margin: 0;
  font-size: var(--type-body-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
.look-story-defs {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
}
.look-story-def dt {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 var(--space-1);
}
.look-story-style-icon {
  display: block;
  width: min(224px, 49%);
  max-height: none;
  margin: 0;
}
.look-story-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
}
.look-story-back {
  font-size: var(--type-label);
  font-weight: var(--weight-demi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none;
}
.look-story-back:hover { color: var(--text-action); }
.look-story-actions .look-card-shop {
  grid-column: auto;
  grid-row: auto;
  align-self: center;
  margin-left: auto;
  min-height: 44px;
}

@media (max-width: 639px) {
  .look-deck {
    grid-template-columns: 1fr;
    gap: 0;
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
  }
  .look-deck-slot.is-side { display: none; }
  .look-deck-slot.is-main .look-card-well { border-radius: 0; }
  /* Same overlay as the lookbook lightbox: the photo is the surface. */
  .look-deck-btn {
    width: 44px;
    height: 44px;
    background: rgb(0 0 0 / 0.45);
    color: #fff;
    box-shadow: none;
  }
  .look-deck-btn:hover { background: rgb(0 0 0 / 0.6); }
  .look-deck-prev { left: var(--space-2); }
  .look-deck-next { right: var(--space-2); }
  .look-story-col { max-width: none; }
}

/* ── Notification system ─────────────────────────────────────────────────────
   ONE component with two states — the signed-out OFFER and the signed-in FEED — because the
   account page is one place whether or not you have an account yet. They share the panel, the
   header, the row anatomy and the spacing scale, so signing in changes what the panel says
   rather than replacing it with a different-looking thing.

   Signed OUT, the reward is not a panel at all: it renders inside the sign-up card as
   .auth-reward (shop/auth.css). It was a standalone panel above the card, then a panel below
   it — both of which made the reason to fill in the form a separate thing to read. It belongs
   beside the form.

   Signed IN, this panel is the feed. Three decisions worth stating:

   1. ROWS, NOT CARDS. Items are rows with a status gutter. Five rewards should read as a list;
      five cards read as five interruptions. State is carried by a marker and by text colour,
      not by a different surface per row.

   2. THREE STATES, NOT TWO. unread → claimed → spent. The bubble counts unread only: claiming
      is the action the "1" pointed at. A used code stays in the list as history (`spent`),
      muted, so it does not compete with anything still to claim.

   3. ONE BUBBLE EVERYWHERE. The count in this header is .count-bubble from brand/tokens.css —
      the same object as the badge on the nav icon and the one beside the reward in the sign-up
      card. It had grown a near-copy here at a different size.

   The panel borrows .auth-card's surface, border, radius and padding rhythm rather than
   inventing its own — an early version put a white card against a beige one. */

.notif-panel {
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-raised);
  overflow: hidden;
}

/* Header: eyebrow on the left, count on the right. The eyebrow matches the uppercase micro-label
   used for field labels and section headers throughout the shop. */
.notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  background: var(--surface-action);
  color: var(--text-on-action);
}
.notif-head-label {
  font-size: var(--type-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: inherit;
}
/* On a maroon header the default count-bubble (maroon on beige) would vanish. Invert it:
   beige fill, maroon figure — the same contrast as the nav bubble, flipped for this ground. */
.notif-head .count-bubble {
  background: var(--text-on-action);
  color: var(--surface-action);
}

/* ── A row ───────────────────────────────────────────────────────────────────
   Grid rather than flex: the status gutter is a fixed track, so every title in the list starts
   on the same vertical line whether or not its row has a marker. */
.notif-item {
  display: grid;
  /* marker | content | bubble. The third track is `auto`, so it costs nothing on the rows that
     have no bubble — a claimed message keeps the same left edge as an unread one. */
  grid-template-columns: var(--space-4) minmax(0, 1fr) auto;
  gap: 0 var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-w-small) solid var(--border-default);
}
.notif-item:last-child { border-bottom: none; }

/* The marker. An unread reward gets a filled accent dot; a completed one (claimed, or a
   spent code) fills the brand green — `--icon-success`, the same ink as the New hallmark —
   so the row reads as done rather than as a leftover ring. A text token is the wrong map
   for a filled mark. */
.notif-mark {
  grid-row: 1 / span 2;
  width: var(--space-2);
  height: var(--space-2);
  margin-top: var(--space-1h);
  border-radius: var(--radius-round);
  background: var(--surface-action);
  justify-self: center;
}
.notif-item.is-claimed .notif-mark,
.notif-item.is-spent .notif-mark {
  background: var(--icon-success);
  border: 0;
}

/* ── Not yet earned ──────────────────────────────────────────────────────────
   A hollow marker — the same dot, drawn as an outline. Read down the list it is the tell that
   the chain stops here: filled means happened, hollow means next. The row is NOT dimmed as a
   whole. `opacity` on a disabled control says "this is not for you", and this one very much is;
   it is a reward being advertised, not a button that cannot be pressed. So the marker and the
   lock carry the state and the title keeps its normal weight and colour. */
.notif-item.is-locked .notif-mark {
  background: transparent;
  border: var(--border-w-medium) solid var(--border-default);
}
/* Sits where a bubble would, in the same third track — the two never appear on one row, because
   a locked reward has nothing to count. */
.notif-lock {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: start;
  display: inline-flex;
  margin-top: 1px;
  color: var(--text-subtle);
}
.notif-lock svg { display: block; }

.notif-title {
  font-size: var(--type-body-sm);
  font-weight: var(--weight-bold);
  line-height: var(--lh-body);
  color: var(--text-heading);
  margin: 0;
}
.notif-item.is-claimed .notif-title { color: var(--text-muted); }

.notif-body {
  grid-column: 2;
  font-size: var(--type-caption);
  line-height: var(--lh-body);
  color: var(--text-muted);
  margin: var(--space-1) 0 0;
}

/* Action and revealed code both live in the content column, indented to the title. */
.notif-foot { grid-column: 2; margin-top: var(--space-3); }

/* 44px, the minimum comfortable hit target, and full width on a phone — the row is the whole
   reachable area rather than a small button floating in it. */
.notif-action {
  width: 100%;
  min-height: 44px;
}

/* The reward itself. Letter-spaced and boxed so it reads as something to copy rather than as a
   word in the sentence above it, and selectable on a phone without a long-press fight. */
/* Two rows, not one. The label, the code and a 44px copy button do not fit on one line at
   360–390px: the code broke mid-word ("WELCOME2AU / RA"), which is unreadable for the one piece
   of text on this card that has to be read exactly. The label sits above; the code and the
   button share the row beneath, where the code has the full width. */
.notif-code {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  min-width: 0;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  /* The border, radius and fill come from "The reward box" above — this is the same object as
     the offer the customer was shown before they had an account. Layout only, in here. */
}
/* The code takes the room and the copy button is pinned to its right. Was flex-wrap + space-
   between, which dropped the button onto its own line the moment a code was long. */
.notif-code code {
  grid-column: 1;
  min-width: 0;
  font-family: var(--font-base);
  font-size: var(--type-body-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--text-action);
  overflow-wrap: anywhere;
  user-select: all;
}
.notif-code .btn-copy { grid-column: 2; grid-row: 1 / span 2; }

/* Never break the code across lines. It is the one string on this card read character by
   character, and "WELCOME2AU / RA" is not a coupon code. nowrap plus a font-size that steps
   down on the narrowest phones keeps it on one line at 320px, where the chip has about 190px
   of room after the card padding and the 44px copy button. */
.notif-code code { white-space: nowrap; }
/* On the narrowest phones the code takes the FULL chip width and the copy button moves up
   beside the label. Sharing a row with a 44px button left it 128px for a string that needs 164
   — nowrap alone just turned a wrap into an overflow. */
@media (max-width: 359px) {
  .notif-code code {
    grid-column: 1 / -1;
    font-size: var(--type-caption);
    letter-spacing: var(--tracking-normal);
  }
  .notif-code .btn-copy { grid-row: 1; }
}
.notif-code-label {
  grid-column: 1;
  font-size: var(--type-label);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-subtle);
}

/* Said on every claimed reward, because claiming now APPLIES the discount rather than handing
   over a string to type. A customer who claims and then does not see why the total dropped will
   assume something is wrong. */
.notif-applied {
  margin: 0 0 var(--space-2);
  font-size: var(--type-caption);
  line-height: var(--lh-caption);
  color: var(--text-success);
}

.notif-empty {
  padding: var(--space-8) var(--space-5);
  font-size: var(--type-body-sm);
  line-height: var(--lh-body);
  color: var(--text-muted);
  text-align: center;
}

/* ── Spent ──────────────────────────────────────────────────────────────────
   A reward whose code has actually been used. It stays in the list — it is history the customer
   may want — but stops counting and stops competing for attention. */
/* ── Spent ───────────────────────────────────────────────────────────────────
   A reward that has been used is history, and it should read as settled rather than as
   something still on the list. Three changes, all quiet:

     the row takes a tint      so it separates from the live rows above it at a glance,
                               down a list, without anyone reading a word
     the title mutes           it is a record now, not an offer
     a tick replaces the count where the bubble sat on an unclaimed row and the lock sits on a
                               locked one — one corner, three answers

   The tint is --surface-success, not a grey: this is a GOOD end state — the discount was
   actually had. A grey row reads as expired, which is the opposite fact.

   That token and not --badge-new-fill, which is the flag over photography: the hallmark badges
   are deliberately FIXED and do not invert, so a fixed light green here would stay light green
   on a dark theme and glare. --surface-success is translucent (10% light, 18% dark) and sits on
   whatever ground it lands on. */
.notif-item.is-spent {
  background: var(--surface-success);
}
.notif-item.is-spent .notif-title { color: var(--text-muted); }
.notif-item.is-spent .notif-code { opacity: 0.7; }
.notif-done {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: start;
  display: inline-flex;
  margin-top: 1px;
  color: var(--icon-success);
}
.notif-done svg { display: block; }

/* The feed lives in the Messages tab now. No extra offset — the tab strip is the separator. */


/* ── Copy button ─────────────────────────────────────────────────────────────
   Shared by the affiliate link buttons and the claimed reward code (window.copyButtonHTML in
   ui.js). Square, quiet, and the same size wherever it appears — the affiliate portal had two
   different copy buttons with the word "Copy" in them at two different sizes, and the account
   page had none. 44px is the comfortable hit target; the icon inside stays 16px. */
.btn-copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-base);
  background: var(--surface-default);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast),
              background var(--transition-fast);
}
.btn-copy:hover {
  color: var(--text-heading);
  border-color: var(--border-strong);
  background: var(--surface-raised);
}
.btn-copy:focus-visible,
.btn-remove:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* Remove, as an X. Same square box as .btn-copy so a row of actions is a row of equal boxes,
   and red only on hover — a destructive control should be legible as one when you reach for it,
   not shout on a row you are only reading. */
.btn-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-base);
  background: var(--surface-default);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast),
              background var(--transition-fast);
}
.btn-remove:hover {
  color: var(--text-error);
  border-color: var(--border-error);
  background: var(--surface-error);
}
/* Confirmation reads as success, not as a new resting state — it reverts after 1.6s. */
.btn-copy.is-done,
.btn-copy.is-done:hover {
  color: var(--text-success);
  border-color: var(--border-success);
  background: var(--surface-success);
}


/* ── First-login setup dialog ────────────────────────────────────────────────
   The shared .modal (brand/tokens.css) with one change: full-bleed on a phone. This is raised
   the instant a code is accepted and it is the only thing on screen at that moment — a small
   card floating in a dimmed page understates that, and on a 390px screen the padding around it
   buys nothing. Above 600px it goes back to being the centred card everything else uses. */
/* Top-aligned on a phone, not centred. .modal-overlay centres its child, which on a short
   viewport with the keyboard up pushed the dialog's heading above the fold — you landed on the
   middle of a form you had not read the top of, and had to scroll UP to find out what it was
   asking. Anchoring to the top means the first thing on screen is the first thing to read. */
.modal-overlay.modal-setup { padding: 0; align-items: flex-start; }
.modal-setup .modal {
  max-width: none;
  height: 100%;
  max-height: none;
  border-radius: 0;
}
@media (min-width: 600px) {
  .modal-overlay.modal-setup { padding: var(--space-5); align-items: center; }
  .modal-setup .modal {
    max-width: 520px;
    height: auto;
    max-height: 90vh;
    border-radius: var(--radius-card);
  }
}
/* The body is the whole point of this dialog, so it gets room to breathe rather than the
   compact spacing a confirmation modal wants. */
.modal-setup .modal-body { display: grid; gap: var(--space-4); align-content: start; }

/* A checkbox and a sentence, on one baseline.
   `align-items: start` rather than center: the sentence runs to three lines on a phone and a
   centred box would float halfway down it, pointing at nothing. `margin-top` nudges the box
   onto the first line's optical centre — the box is 16px and the line box is 20px, so the
   two do not agree on where the middle is. The label is not uppercased or tracked like
   .field-label because this one is read as a sentence, not scanned as a field name. */
.field-check {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-3); align-items: start;
  font-size: var(--type-body-sm); line-height: var(--leading-normal);
  color: var(--text-muted); cursor: pointer;
}
.field-check input[type="checkbox"] {
  width: 16px; height: 16px; margin: 2px 0 0; flex: none;
  accent-color: var(--surface-action); cursor: pointer;
}
.field-check:hover { color: var(--text-body); }

/* The first-login dialog's consent row.
   Label left, switch hard right — the same shape as .aff-set-row and as the Newsletter setcard,
   because a toggle that sits on the left on one screen and the right on another stops reading
   as the same control. `align-items: start` keeps it on the first line: the note runs to three
   lines on a phone, and a centred switch would float halfway down it pointing at nothing. */
.setup-consent {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-4); align-items: start;
}
.setup-consent > .switch { justify-self: end; }
.setup-consent-title {
  margin: 0; font-size: var(--type-body-sm); color: var(--text-heading);
  line-height: var(--leading-normal);
}
.setup-consent-note {
  margin: var(--space-1) 0 0; font-size: var(--type-caption);
  line-height: var(--leading-normal); color: var(--text-muted);
}

/* ── The switch ──
   A real checkbox, restyled: it keeps the checkbox's keyboard behaviour, its :focus-visible
   ring and its place in the accessibility tree, which none of a div-with-a-click-handler does.
   The input is hidden from SIGHT only — never `display: none`, which would take it out of the
   tab order and out of the tree with it.

   Was `.aff-switch` in affiliate.css. It moved when account settings needed the same control:
   an `aff-` prefix on a component used outside the affiliate portal reads as "this belongs to
   that page" to the next person, and a second copy under a second name is how two toggles end
   up looking subtly different. */
.switch { flex: none; display: inline-flex; align-items: center; cursor: pointer; }
.switch input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  margin: 0;
}
.switch-track {
  width: 2.75rem; height: 1.5rem;
  border-radius: 999px;
  background: var(--surface-default);
  border: var(--border-w-small) solid var(--border-default);
  position: relative;
  transition: background var(--transition-base), border-color var(--transition-base);
}
.switch-track::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 3px;
  width: 1.05rem; height: 1.05rem;
  transform: translateY(-50%);
  border-radius: 50%;
  background: var(--text-muted);
  transition: transform var(--transition-base), background var(--transition-base);
}
.switch input:checked + .switch-track {
  background: var(--surface-action);
  border-color: var(--surface-action);
}
.switch input:checked + .switch-track::after {
  transform: translateY(-50%) translateX(1.2rem);
  background: var(--text-on-action);
}
.switch input:focus-visible + .switch-track { box-shadow: var(--shadow-focus); }

.modal-setup .modal-footer { justify-content: space-between; }


/* The unread count, on the message itself.
   Right-justified and brand red — the same .count-bubble as the panel header and the nav icon,
   so the "1" you tapped in the nav is the "1" on the header is the "1" on the row that is
   actually waiting for you. It is the last of the three to clear, which is the point: the row
   is where the thing to do lives. */
.notif-item-bubble { grid-column: 3; grid-row: 1; justify-self: end; align-self: start; }


/* ── Welcome splash ──────────────────────────────────────────────────────────
   Shown once, edge to edge, the moment a password is saved from the first-login dialog. Brand
   red with no chrome on it: this is the one screen in the shop that is purely a greeting, so
   anything else on it — a card edge, a header, a close button in the corner — would make it
   look like another thing to deal with rather than a moment.

   Dismisses on any tap or key, and after a beat on its own; it must never be something you can
   get stuck behind. */
/* ⚠ [hidden] FIRST, and this is not belt-and-braces.
   Any author `display` beats the UA stylesheet's [hidden] { display: none }, so a fixed,
   inset-0 element with display:flex stays laid out when hidden — invisible at opacity 0 and
   swallowing every click on the page beneath it. The account page was entirely unclickable
   until a hit test named this element. It is the second time in this codebase (.auth-reward was
   the first), which is why the blanket rule below exists. */
.welcome-splash {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-8);
  text-align: center;
  background: var(--surface-action);
  color: var(--text-on-action);
  opacity: 0;
  transition: opacity var(--transition-slow);
}
.welcome-splash.open { opacity: 1; }
.welcome-splash-name {
  font-size: var(--type-h3);
  line-height: var(--lh-h3);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  text-transform: uppercase;
}
.welcome-splash-line {
  font-size: var(--type-body-lg);
  line-height: var(--lh-body-lg);
  /* The beige on maroon the footer already uses, rather than a flat white. */
  color: var(--text-on-action);
  opacity: 0.85;
}
@media (prefers-reduced-motion: reduce) {
  .welcome-splash { transition: none; }
}


/* ── [hidden] means hidden ───────────────────────────────────────────────────
   The UA stylesheet's `[hidden] { display: none }` has the weakest possible specificity, so ANY
   author rule that sets `display` on the same element silently defeats it. Twice now that has
   shipped an element which was invisible but still laid out: .auth-reward stayed visible in
   sign-in mode however many times JS set .hidden = true, and .welcome-splash — fixed, inset 0 —
   covered the whole account page and ate every click while showing nothing.

   Neither was caught by a test that clicked through the DOM, because element.click() skips hit
   testing. Only a real mouse click finds it, which is what tests/browser now uses.

   `!important` is deliberate: the whole point is that nothing should be able to beat it. If an
   element needs to be laid out while hidden, it does not want the `hidden` attribute. */
[hidden] { display: none !important; }


/* ── Settings cards ──────────────────────────────────────────────────────────
   ONE component for /account.html and /affiliate. Both pages let the same person edit the same
   account — the name, the email and the password on each screen write to the same two endpoints
   — so two visual systems for that was two ways to render one thing. It lived inline in
   account.html, which is why the affiliate portal could not use it.

   Every card reads the same way: what it is, what it is set to, a note, and one button that
   opens the form ([data-open] / [data-close], wired in each page's script). Two columns at
   900px, the breakpoint both pages already change shape at.
*/
.setcard-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}
.setcard-col { display: grid; gap: var(--space-5); align-content: start; }
@media (min-width: 900px) {
  .setcard-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}
/* Title and its action share the top row, the action pushed right.
   
   They used to stack: title, value, note, then the button on a row of its own — four rows for
   two facts and one control, and a card mostly made of gap. Putting the control on the title's
   row takes a whole row out of every card on both settings screens, and puts the thing you came
   to press on the edge your eye already tracks down. */
.setcard {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0 var(--space-3);
  padding: var(--space-4) var(--space-5);
  border: var(--border-w-small) solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--surface-raised);
}
/* Everything that is not the top-row action runs the full width beneath it. */
.setcard > *:not(.setcard-h):not(.setcard-actions) { grid-column: 1 / -1; }
.setcard-h { grid-column: 1; }
/* The opening control rides the title row; the ones inside a form do not — a Save and a Cancel
   belong under the fields they apply to. */
.setcard > .setcard-actions { grid-column: 2; grid-row: 1; margin-top: 0; }
.setcard-h {
  margin: 0;
  font-size: var(--type-h6);
  font-weight: var(--weight-bold);
  color: var(--text-heading);
}
.setcard-note {
  margin: var(--space-2) 0 0;
  font-size: var(--type-caption);
  line-height: var(--lh-body);
  color: var(--text-muted);
}
.setcard-value {
  margin: var(--space-2) 0 0;
  font-size: var(--type-body-sm);
  color: var(--text-heading);
  word-break: break-all;
}
.setcard-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.setcard .field { margin-top: var(--space-3); }


/* ── Full-screen image viewer ──
   Behaviour and markup live in shop/lightbox.js; both the product page and the cart use it.
   The ids keep their pdp- prefix: they were already in the wild and renaming buys nothing. */
/* ── Lightbox ─────────────────────────────────────────────── */
#pdp-lightbox {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(0,0,0,0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}
/* Brand-system icon button adapted for dark overlay */
.pdp-lb-btn {
  position: fixed;
  z-index: 2;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-base);
  background: rgba(255,255,255,0.10);
  border: var(--border-w-small) solid rgba(255,255,255,0.16);
  color: rgba(255,255,255,0.85);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}
.pdp-lb-btn:hover { background: rgba(255,255,255,0.22); color: #fff; }
.pdp-lb-btn:focus-visible { outline: var(--border-w-medium) solid var(--border-focus); outline-offset: 2px; }
.pdp-lb-close { top: var(--space-5); right: var(--space-5); }
.pdp-lb-prev  { top: 50%; left:  var(--space-5); transform: translateY(-50%); }
.pdp-lb-next  { top: 50%; right: var(--space-5); transform: translateY(-50%); }
.pdp-lb-btn:disabled { opacity: 0.25; pointer-events: none; }
#pdp-lb-img {
  max-width: 100vw;
  max-height: 100vh;
  object-fit: contain;
  cursor: zoom-in;
  user-select: none;
  -webkit-user-drag: none;
  transition: transform 220ms ease;
}
#pdp-lb-img.lb-zoomed { cursor: zoom-out; }
/* Gallery chips, only while a gallery photograph is fullscreen. lightbox.js sizes
   this host to the picture, so the shared .look-stamps corner sits on the photo
   rather than in the gutter. Studio slides and the cart leave it hidden. */
#pdp-lb-stamps {
  position: absolute;
  z-index: 2;
  pointer-events: none;
}
#pdp-lightbox .look-stamps { z-index: 2; }

/* ── Review card ──────────────────────────────────────────────────────────────────────────
   One quote, on two pages: the product page lists it under the questions and the homepage
   runs it in the band under the anchor viewer. reviews.js writes the same markup for both,
   so the card is styled once, here. It was two copies — a <style> in product.html and the
   same rules again under .h-reviews in home.css — and two copies of a component is how a
   portrait ends up one size on one page and another on the next. Each page keeps only its
   layout: the product page's rule between reviews, the homepage's rows and zoom. */
.rev-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}
.rev-by {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
.rev-who { display: flex; flex-direction: column; min-width: 0; }
.rev-portrait {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
  background: var(--border-default);
}
.rev-name { font-weight: var(--weight-demi); color: var(--text-heading); }
.rev-job {
  font-size: var(--type-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
.rev-scoreline { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
.rev-stars { display: inline-flex; gap: 2px; }
.rev-star { width: 1.15rem; height: 1.15rem; display: block; }
.rev-star.is-off { color: var(--text-subtle); }
.rev-score {
  font-size: var(--type-label);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
}
.rev-body,
.rev-body p { margin: 0; }
.rev-body {
  quotes: none;
  font-size: var(--type-body);
  line-height: var(--leading-normal);
  color: var(--text-body);
  overflow-wrap: anywhere;
}
