/* GENERATED by `pnpm css:public` from kj.css — do not hand-edit. */

/* =====================================================================
   1. FONT
   ===================================================================== */

@font-face {
  font-family: "Archivo";
  src: url("/s/archivo-var.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-stretch: 88% 125%;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched fallback. Kills the layout shift during the swap.
   Values tuned against Helvetica Neue; re-measure if the stack changes. */
@font-face {
  font-family: "Archivo Fallback";
  src: local("Helvetica Neue"), local("Helvetica"), local("Arial");
  size-adjust: 97%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Big Shoulders Display";
  src: url("/s/big-shoulders-display-var.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched fallback, tuned against Arial Narrow — the closest common
   system-installed condensed face (Big Shoulders Display is condensed, so
   Helvetica Neue is the wrong comparison here, unlike Archivo above).
   Values computed from real a-z advance-width averages (fontTools hmtx
   read) for both faces, not guessed or copied from the Archivo block:
   size-adjust scales the fallback so its average glyph width matches Big
   Shoulders Display's; the override values are pre-scale, so they're
   divided by size-adjust to land at the real face's ascent/descent ratio
   once size-adjust is applied. Re-measure if either font file changes. */
@font-face {
  font-family: "Big Shoulders Display Fallback";
  src: local("Arial Narrow"), local("Arial");
  size-adjust: 64%;
  ascent-override: 155%;
  descent-override: 34%;
  line-gap-override: 0%;
}


/* =====================================================================
   2. TOKENS — DARK
   ===================================================================== */

:root {
  color-scheme: dark light;

  /* Surfaces — Vault Steel: hue-locked cool blue-grey, OKLCH. Redesign
     (superseded the warm hue-33-36° ramp; see design-craft-local.md).
     Round-1 critique caught the first pass of these values (L=9-23%)
     rendering as functionally pure black in sRGB — OKLCH lightness at
     very low L with this hue/chroma clips the R/G channels toward zero
     before the intended steel cast becomes visible. Retuned higher
     (L=18-29%) so the hue actually reads at every step, canvas included. */
  --canvas: oklch(18% 0.010 240); --surface: oklch(21% 0.012 240);
  --surface-2: oklch(25% 0.014 240); --surface-3: oklch(29% 0.016 240);
  --surface-2-a: oklch(25% 0.014 240 / 86%);   /* backdrop-filter fill */
  --overlay: oklch(10% 0.010 240 / 62%);
  /* Glass — pills/tabs/buttons' frosted treatment. Lower alpha than
     surface-2-a on purpose: that token needs to stay legibly opaque behind
     scrolling content (nav/buybar); this one wants real see-through glass
     character. --glass-border is the light catching the bevel edge — the
     one place a light-toned line appears in the dark theme on purpose. */
  --glass-bg: oklch(32% 0.016 240 / 55%);
  --glass-border: rgba(240,244,255,.20);
  --canvas-grad: radial-gradient(ellipse 120% 60% at 50% -10%, var(--surface) 0%, var(--canvas) 62%);

  /* Borders */
  --border-subtle: oklch(24% 0.014 240); --border: oklch(32% 0.016 240);
  --border-strong: oklch(58% 0.020 240);   /* 3.74:1 on surface-2 — meets WCAG 1.4.11. 3.30:1 on
                                               surface-3 (hover/active only, not a text or control
                                               boundary — same excused case as before, see
                                               frontend-a11y.md's rulings log) */

  /* Text — cool steel-grey, not warm bone. Deliberate hue break from the
     old ramp: text now shares the surface family's hue instead of sitting
     off it. */
  --text: oklch(94% 0.010 240);        /* 15.78:1 canvas, 14.86:1 surface */
  --text-2: oklch(74% 0.012 240);      /* 8.16:1 canvas, 7.69:1 surface */
  --text-3: oklch(60% 0.014 240);      /* 4.77:1 canvas · 4.50:1 surface · 4.06:1 surface-2 ·
                                           3.58:1 surface-3 — surface-2/surface-3 both excused
                                           per frontend-a11y.md's rulings log (text-3 there is
                                           meta/tertiary text only, never primary content) */
  --text-placeholder: var(--text-3);   /* solid, not alpha — the earlier alpha-composited
                                           placeholder measured only 2.98:1 in practice; a solid
                                           color measures true regardless of what's behind it */

  /* Accent — gold, a LINE, never a field. Unchanged brand color, kept as
     hex deliberately (not converted to oklch — nothing to gain from
     converting an already-correct, brand-verbatim value). */
  --accent: #C9A130;          /* brand gold unchanged. 7.72:1 new canvas, 6.56:1 new surface-2 */
  --accent-hover: #E3C56B; --accent-line: rgba(201, 161, 48, 0.34);
  --accent-bg: rgba(201, 161, 48, 0.12); --accent-ring: rgba(201, 161, 48, 0.24);
  --text-on-accent: var(--canvas);   /* 7.72:1 on accent fill */
  --text-glass: oklch(94% 0.010 240 / 92%);   /* --text, glass-translucent — .btn--primary */

  /* Danger — brand oxblood, lifted for dark (#8E2029 alone is only 2.22:1 here).
     Unchanged hex; re-verified against the new steel surfaces below. */
  --danger: #E0736A;          /* 6.11:1 new canvas, 4.58:1 new surface-3 — text/icon use */
  --danger-hover: #EE8A81; --danger-fill: #8E2029; --danger-fill-hover: #A5252F;
  --danger-bg: rgba(224, 115, 106, 0.12); --danger-border: rgba(224, 115, 106, 0.38);

  /* Success — re-hued to verdigris (was bronze-green ~150°), a state
     signal now clearly split from accent's value signal. Hue-distinct
     from gold for protan/deutan readers, as before. */
  --success: oklch(68% 0.090 165);      /* 6.80:1 new canvas, 5.78:1 new surface-2 */
  --success-bg: oklch(68% 0.090 165 / 12%); --success-border: oklch(68% 0.090 165 / 34%);

  /* Elevation — dark strategy unchanged: a luminous top edge + wide black
     ambient. Only the highlight hue changes, cool white (240,244,255) not
     warm (255,252,244) — a warm highlight would mismatch the cool steel
     surface it's lighting. Every alpha/blur/spread/offset is byte-identical
     to the pre-redesign values. */
  --shadow-1: inset 0 1px 0 rgba(240,244,255,.045), 0 1px 2px rgba(0,0,0,.60);
  --shadow-2: 0 -1px 0 rgba(240,244,255,.06), 0 -12px 32px rgba(0,0,0,.70);
  --shadow-3: 0 0 0 1px rgba(240,244,255,.06), 0 16px 40px -8px rgba(0,0,0,.80);
  --glow-accent: 0 0 0 1px rgba(201,161,48,.32), 0 8px 28px -12px rgba(201,161,48,.35);
  --shadow-logo: 0 18px 40px rgba(0, 0, 0, 0.55);   /* was index.ts:115 */

  /* The crest ring — used at :focus-visible and the confirmation thumbnail ONLY */
  --ring-crest: 0 0 0 2px var(--canvas), 0 0 0 4px var(--accent);

  --font: "Archivo", "Archivo Fallback", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: "Big Shoulders Display", "Big Shoulders Display Fallback", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Type scale — bimodal by design: 1.125 ratio across the UI band (many
     near-equal small sizes should read as one instrument), a hard break
     to display sizes. --fs-md (16px) is the floor for EVERY input — iOS
     Safari zooms the viewport below 16px, costing a tap to undo. */
  --fs-micro: 0.6875rem; --fs-xs: 0.75rem; --fs-sm: 0.875rem; --fs-md: 1rem;
  --fs-lg: 1.125rem; --fs-xl: 1.25rem; --fs-2xl: 1.4375rem; --fs-3xl: 1.75rem;
  --fs-4xl: 2.25rem; --fs-5xl: 3rem;   /* 5xl: confirmation + public hero/coming-soon headline */
  --fs-6xl: 4rem;   /* continues 4xl→5xl's 1.333x step forward; the new display band's largest size */
  --lh-tight: 1.05; --lh-snug: 1.22; --lh-normal: 1.5; --lh-relaxed: 1.6;
  --track-display: -0.022em; --track-tight: -0.01em; --track-normal: 0;
  --track-display-condensed: -0.005em;   /* lighter than --track-display — Big Shoulders Display
                                             is already condensed; that value was tuned for
                                             Archivo at --wd-expanded */
  --track-label: 0.06em; --track-slab: 0.18em;   /* the slab micro-label */
  --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-bold: 700;
  --wd-narrow: 88%; --wd-normal: 100%; --wd-wide: 112%; --wd-expanded: 125%;

  /* Spacing — 4px base */
  --space-3xs:2px; --space-2xs:4px; --space-xs:8px; --space-sm:12px; --space-md:16px;
  --space-lg:24px; --space-xl:32px; --space-2xl:48px; --space-3xl:64px; --space-4xl:96px;
  --gutter: 20px;   /* not 16 — generous edge reads as "expensive" */

  /* Radii — buttons and inputs SHARE --r-md so a form reads as one machined object */
  --r-xs:3px; --r-sm:6px; --r-md:10px; --r-lg:14px; --r-pill:999px;
  --bw:1px; --bw-thick:2px;
  --w-form:34rem; --w-prose:44rem; --w-list:76rem;
  --z-base:0; --z-raised:10; --z-sticky:100; --z-banner:200; --z-overlay:300; --z-toast:400;

  /* Touch */
  --tap-min:44px; --tap-comfortable:52px; --tap-action:64px; --tap-hero:104px;
  --safe-b: env(safe-area-inset-bottom, 0px); --safe-t: env(safe-area-inset-top, 0px);
  --actionbar-h: calc(var(--tap-action) + var(--space-xl) + var(--safe-b));

  --dur-instant:90ms; --dur-fast:140ms; --dur:200ms; --dur-slow:320ms; --dur-slower:560ms;
  --ease-out: cubic-bezier(.22,1,.36,1); --ease-in-out: cubic-bezier(.65,0,.35,1);
  --ease-spring: cubic-bezier(.34,1.56,.64,1);   /* confirmation only */
}


/* =====================================================================
   3. TOKENS — LIGHT
   ===================================================================== */

/* Light — Vault Steel's ivory/ledger counterpart, anchored near the brand's
   own cream (#F3ECDC) rather than porcelain. Pure-white cards unchanged.
   Round-1 critique (two independent passes) flagged the first pass of this
   ramp (chroma 0.010-0.025) as landing on the same warm-cream signature as
   a known AI-generated-page cluster, and as warmer than the dark theme is
   cool — not "two tunings of one ramp." Chroma cut roughly 3x across every
   step (0.005-0.010) — still warm-leaning, brand-anchored, but reading as
   a near-neutral paper grey rather than a saturated cream. Lightness
   values are unchanged, so this is a saturation fix, not a new ramp. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light dark;
    --canvas: oklch(94% 0.008 85); --surface: oklch(100% 0 0);
    --surface-2: oklch(98% 0.005 85); --surface-3: oklch(95% 0.007 85);
    --surface-2-a: oklch(98% 0.005 85 / 88%); --overlay: oklch(18% 0.010 85 / 38%);
    --glass-bg: oklch(100% 0 0 / 65%); --glass-border: rgba(25,21,16,.20);
    --canvas-grad: radial-gradient(ellipse 120% 60% at 50% -10%, var(--surface) 0%, var(--canvas) 62%);
    --border-subtle: oklch(90% 0.006 85); --border: oklch(85% 0.007 85);
    --border-strong: oklch(58% 0.010 85);   /* 4.04:1 on surface-2, 3.70:1 on surface-3 */
    --text: oklch(18% 0.010 85);        /* 15.77:1 canvas — brand/colors.txt-adjacent, near-black */
    --text-2: oklch(42% 0.010 85);      /* 7.10:1 canvas */
    --text-3: oklch(52% 0.009 85);      /* 4.62:1 canvas */
    --text-placeholder: var(--text-3);  /* solid, not alpha — see the dark block's note */
    --accent: #7D5D18;          /* 5.10:1 new canvas — #C9A130 is only 2.4:1 on white, unusable as text */
    --accent-hover: #6A4E12; --accent-line: rgba(125,93,24,.42);   /* ~1.9:1 on surface — was
                                                                       .26 (~1.46:1), 32% weaker
                                                                       than dark's --accent-line
                                                                       at the same role; matched */
    --accent-bg: rgba(125,93,24,.09); --accent-ring: rgba(125,93,24,.18);
    --text-on-accent: var(--surface);   /* 6.09:1 on accent fill */
    --text-glass: oklch(18% 0.010 85 / 92%);   /* --text, glass-translucent — .btn--primary */
    --danger: #8E2029;          /* 7.37:1 new canvas — brand red, unchanged, re-verified */
    --danger-hover: #741A22; --danger-fill: #8E2029; --danger-fill-hover: #A5252F;
    --danger-bg: rgba(142,32,41,.08); --danger-border: rgba(142,32,41,.30);
    --success: oklch(38% 0.080 165);    /* 8.06:1 canvas */
    --success-bg: oklch(38% 0.080 165 / 9%); --success-border: oklch(38% 0.080 165 / 28%);
    /* Light strategy: real layered shadows tinted with the warm black —
       a pure-black shadow on a warm page reads grey and dead. Byte-
       identical to the pre-redesign values; only the dark strategy's
       highlight hue changes (see the dark block's comment) — the black
       ambient component in both strategies is hue-neutral either way. */
    --shadow-1: 0 1px 2px rgba(25,21,16,.06), 0 1px 1px rgba(25,21,16,.04);
    --shadow-2: 0 -1px 0 var(--border-subtle), 0 -8px 24px rgba(25,21,16,.08);
    --shadow-3: 0 12px 32px -8px rgba(25,21,16,.18), 0 2px 6px rgba(25,21,16,.08);
    --glow-accent: 0 0 0 1px rgba(125,93,24,.18), 0 8px 24px -12px rgba(125,93,24,.22);
    --shadow-logo: 0 18px 40px rgba(25, 21, 16, 0.22);
  }
}

/* Manual override (D9). Placed AFTER the media query above — deliberately.
   :root[data-theme="light"] and the media query's :root:not([data-theme="dark"])
   have identical specificity (0-2-0); CSS resolves the tie by source order,
   last-declared wins. Placed here, "Always light" wins unconditionally
   regardless of the visitor's OS setting. */
:root[data-theme="light"] {
  color-scheme: light dark;
  --canvas: oklch(94% 0.008 85); --surface: oklch(100% 0 0);
  --surface-2: oklch(98% 0.005 85); --surface-3: oklch(95% 0.007 85);
  --surface-2-a: oklch(98% 0.005 85 / 88%); --overlay: oklch(18% 0.010 85 / 38%);
  --glass-bg: oklch(100% 0 0 / 65%); --glass-border: rgba(25,21,16,.20);
  --canvas-grad: radial-gradient(ellipse 120% 60% at 50% -10%, var(--surface) 0%, var(--canvas) 62%);
  --border-subtle: oklch(90% 0.006 85); --border: oklch(85% 0.007 85);
  --border-strong: oklch(58% 0.010 85);   /* 4.04:1 on surface-2, 3.70:1 on surface-3 */
  --text: oklch(18% 0.010 85);        /* 15.77:1 canvas — brand/colors.txt-adjacent, near-black */
  --text-2: oklch(42% 0.010 85);      /* 7.10:1 canvas */
  --text-3: oklch(52% 0.009 85);      /* 4.62:1 canvas */
  --text-placeholder: var(--text-3);  /* solid, not alpha — see the dark block's note */
  --accent: #7D5D18;          /* 5.10:1 new canvas — #C9A130 is only 2.4:1 on white, unusable as text */
  --accent-hover: #6A4E12; --accent-line: rgba(125,93,24,.42);
  --accent-bg: rgba(125,93,24,.09); --accent-ring: rgba(125,93,24,.18);
  --text-on-accent: var(--surface);   /* 6.09:1 on accent fill */
    --text-glass: oklch(18% 0.010 85 / 92%);   /* --text, glass-translucent — .btn--primary */
  --danger: #8E2029;          /* 7.37:1 new canvas — brand red, unchanged, re-verified */
  --danger-hover: #741A22; --danger-fill: #8E2029; --danger-fill-hover: #A5252F;
  --danger-bg: rgba(142,32,41,.08); --danger-border: rgba(142,32,41,.30);
  --success: oklch(38% 0.080 165);    /* 8.06:1 canvas */
  --success-bg: oklch(38% 0.080 165 / 9%); --success-border: oklch(38% 0.080 165 / 28%);
  --shadow-1: 0 1px 2px rgba(25,21,16,.06), 0 1px 1px rgba(25,21,16,.04);
  --shadow-2: 0 -1px 0 var(--border-subtle), 0 -8px 24px rgba(25,21,16,.08);
  --shadow-3: 0 12px 32px -8px rgba(25,21,16,.18), 0 2px 6px rgba(25,21,16,.08);
  --glow-accent: 0 0 0 1px rgba(125,93,24,.18), 0 8px 24px -12px rgba(125,93,24,.22);
  --shadow-logo: 0 18px 40px rgba(25, 21, 16, 0.22);
}

:root[data-theme="dark"] {
  color-scheme: dark light;
  --canvas: oklch(18% 0.010 240); --surface: oklch(21% 0.012 240);
  --surface-2: oklch(25% 0.014 240); --surface-3: oklch(29% 0.016 240);
  --surface-2-a: oklch(25% 0.014 240 / 86%);
  --overlay: oklch(10% 0.010 240 / 62%);
  --glass-bg: oklch(32% 0.016 240 / 55%);
  --glass-border: rgba(240,244,255,.20);
  --canvas-grad: radial-gradient(ellipse 120% 60% at 50% -10%, var(--surface) 0%, var(--canvas) 62%);
  --border-subtle: oklch(24% 0.014 240); --border: oklch(32% 0.016 240);
  --border-strong: oklch(58% 0.020 240);   /* 3.74:1 on surface-2 — meets WCAG 1.4.11. 3.30:1 on
                                               surface-3 (hover/active only, not a text or control
                                               boundary — same excused case as before) */
  --text: oklch(94% 0.010 240);        /* 15.78:1 canvas, 14.86:1 surface */
  --text-2: oklch(74% 0.012 240);      /* 8.16:1 canvas, 7.69:1 surface */
  --text-3: oklch(60% 0.014 240);      /* 4.77:1 canvas · 4.50:1 surface · 4.06:1 surface-2 ·
                                           3.58:1 surface-3 (surface-2/surface-3 excused, see
                                           frontend-a11y.md) */
  --text-placeholder: var(--text-3);   /* solid, not alpha — see the :root block's note */
  --accent: #C9A130;          /* brand gold unchanged. 7.72:1 new canvas, 6.56:1 new surface-2 */
  --accent-hover: #E3C56B; --accent-line: rgba(201, 161, 48, 0.34);
  --accent-bg: rgba(201, 161, 48, 0.12); --accent-ring: rgba(201, 161, 48, 0.24);
  --text-on-accent: var(--canvas);  /* 7.72:1 on accent fill */
  --text-glass: oklch(94% 0.010 240 / 92%);   /* --text, glass-translucent — .btn--primary */
  --danger: #E0736A;          /* 6.11:1 new canvas, 4.58:1 new surface-3 — text/icon use */
  --danger-hover: #EE8A81; --danger-fill: #8E2029; --danger-fill-hover: #A5252F;
  --danger-bg: rgba(224, 115, 106, 0.12); --danger-border: rgba(224, 115, 106, 0.38);
  --success: oklch(68% 0.090 165);      /* 6.80:1 new canvas, 5.78:1 new surface-2 */
  --success-bg: oklch(68% 0.090 165 / 12%); --success-border: oklch(68% 0.090 165 / 34%);
  --shadow-1: inset 0 1px 0 rgba(240,244,255,.045), 0 1px 2px rgba(0,0,0,.60);
  --shadow-2: 0 -1px 0 rgba(240,244,255,.06), 0 -12px 32px rgba(0,0,0,.70);
  --shadow-3: 0 0 0 1px rgba(240,244,255,.06), 0 16px 40px -8px rgba(0,0,0,.80);
  --glow-accent: 0 0 0 1px rgba(201,161,48,.32), 0 8px 28px -12px rgba(201,161,48,.35);
  --shadow-logo: 0 18px 40px rgba(0, 0, 0, 0.55);
}


/* =====================================================================
   4. RESET + BASE
   ===================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;            /* dvh second: pre-15.4 Safari keeps vh */
  background-color: var(--canvas);
  background-image: var(--canvas-grad);
  background-attachment: fixed;
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-md);
  font-weight: var(--fw-regular);
  font-stretch: var(--wd-normal);
  line-height: var(--lh-normal);
  letter-spacing: var(--track-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-variant-numeric: slashed-zero;
}

h1, h2, h3, h4, p, figure, fieldset { margin: 0; }
fieldset { border: 0; padding: 0; min-width: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, svg, video { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; margin: 0; }
button { background: none; border: 0; padding: 0; }
table { border-collapse: collapse; width: 100%; }

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
@media (hover: hover) {
  a:hover { color: var(--accent-hover); }
}

::selection { background: var(--accent-bg); }
::placeholder { color: var(--text-placeholder); opacity: 1; }

/* Keyboard focus is the brand mark's geometry: the concentric double-rule
   of the seal. First of exactly two uses of --ring-crest. */
/* outline, not box-shadow — round 2's glassmorphism pass gave several
   components (chips, ghost/primary buttons, inputs) their own decorative
   box-shadow, which silently overwrote this rule's box-shadow-based ring
   wherever it was declared later/at equal-or-higher specificity, leaving
   focus invisible on the primary CTA and every filter chip. outline is a
   wholly separate paint layer — it can never be clobbered by box-shadow.
   The offset stands in for --ring-crest's inner canvas-colored ring. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
:focus:not(:focus-visible) { outline: none; }

/* Windows High Contrast drops box-shadow entirely — without this, the
   `outline: none` above leaves no visible focus indicator at all.
   WCAG 2.4.7 / 2.4.11. */
@media (forced-colors: active) {
  :focus-visible { outline: 3px solid Highlight; outline-offset: 2px; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}


/* =====================================================================
   5. LAYOUT PRIMITIVES
   ===================================================================== */

.wrap {
  width: 100%;
  max-width: var(--w-form);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-top: calc(var(--space-lg) + var(--safe-t));
}
.wrap--wide  { max-width: var(--w-list); }
.wrap--prose { max-width: var(--w-prose); }

/* Reserves room for the fixed action bar so the last field is never
   trapped underneath it. */
.wrap--form { padding-bottom: calc(var(--actionbar-h) + var(--space-xl)); }

/* Today screen: header pinned top, button stack optically centred,
   orphan line pinned bottom. The screen ends ~180px short of the fold
   if the stack sits at the top; centring makes the emptiness read as
   deliberate rather than unfinished. */
.today {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
.today__main {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-sm);
  padding-block: var(--space-xl);
}
.today__foot {
  padding-bottom: calc(var(--space-lg) + var(--safe-b));
}

.stack             { display: flex; flex-direction: column; }
.stack > * + *     { margin-top: var(--space-md); }
.stack--lg > * + * { margin-top: var(--space-lg); }
.stack--xl > * + * { margin-top: var(--space-xl); }

.two-up {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-sm);
}

.hidden { display: none !important; }

/* The standard clip-rect visually-hidden pattern — NOT display:none.
   Load-bearing on the two file inputs in New Card (§2.3): display:none
   breaks <label for> click-to-open-picker association on some mobile
   browsers, silently killing the primary capture path. */
.vis-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--gutter); top: -100px;
  z-index: var(--z-toast);
  padding: var(--space-sm) var(--space-md);
  background: var(--surface-2);
  border-radius: var(--r-md);
  color: var(--text);
  text-decoration: none;
}
.skip-link:focus { top: calc(var(--space-sm) + var(--safe-t)); }


/* =====================================================================
   6. THE SLAB LABEL  (signature device)

   Taken from the printed label bar on a graded-card slab:
   micro-caps, wide-tracked, tabular numerals. Appears in exactly four
   places — Today's header, the confirmation header, the <details>
   summary, and list-row meta. Never decorative.
   ===================================================================== */

.slab {
  font-size: var(--fs-micro);
  font-weight: var(--fw-semibold);
  font-stretch: var(--wd-normal);
  letter-spacing: var(--track-slab);
  text-transform: uppercase;
  color: var(--accent);
  line-height: 1;
  font-variant-numeric: tabular-nums slashed-zero;
}
.slab--quiet { color: var(--text-3); }

.slabstrip {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding-bottom: var(--space-xs);
  border-bottom: var(--bw) solid var(--accent-line);
}

.rule {
  height: var(--bw);
  border: 0;
  margin: 0;
  background: var(--border-subtle);
}
.rule--gold { background: var(--accent-line); }


/* =====================================================================
   7. TYPE UTILITIES
   ===================================================================== */

.title {
  font-family: var(--font-display);
  /* A flat --fs-3xl (28px) never let the display face reach display
     size outside the hero — at UI-band sizes a condensed poster face
     just reads as a narrower sans, which defeats the point of using it.
     Grows to --fs-5xl at wide viewports. */
  font-size: clamp(var(--fs-3xl), 1.3rem + 2vw, var(--fs-5xl));
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-display-condensed);
  line-height: var(--lh-snug);
  text-wrap: balance;
}
.title--hero {
  font-size: var(--fs-4xl);
  line-height: var(--lh-tight);
}

.lede {
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
  color: var(--text-2);
  text-wrap: pretty;
}

.meta {
  font-size: var(--fs-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums slashed-zero;
}

.quiet {
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--text-3);
  text-wrap: pretty;
}

.num {
  font-variant-numeric: tabular-nums slashed-zero;
  font-stretch: var(--wd-expanded);
  letter-spacing: var(--track-tight);
}


/* =====================================================================
   8. CONTROLS — BUTTONS + FORM FIELDS
   ===================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--tap-comfortable);
  padding-inline: var(--space-lg);
  border: var(--bw) solid transparent;
  border-radius: var(--r-md);
  background: transparent;
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  font-stretch: var(--wd-normal);
  letter-spacing: var(--track-tight);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  touch-action: manipulation;              /* kills the 300ms dbl-tap delay */
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color     var(--dur-fast) var(--ease-out),
              color            var(--dur-fast) var(--ease-out),
              transform        var(--dur-instant) var(--ease-out),
              box-shadow       var(--dur-fast) var(--ease-out);
}
.btn:active { transform: translateY(1px); }
.btn[disabled] { cursor: default; opacity: 0.62; transform: none; }

/* PRIMARY — maximum ink. Inverts automatically with the theme:
   bone-on-black in dark (15.39:1), black-on-white in light (16.77:1).
   Never gold: a 104px field of #C9A130 reads as costume jewellery, and
   a big bright accent block is the generic dark-admin answer. */
/* Solid, not glass — round 2's critique made the case directly: this is
   the site's one maximum-contrast, maximum-trust surface (the actual
   purchase/signup action), translucency works against both the stated
   "maximum ink" intent and the numbers above, and glass belongs on the
   secondary surfaces (chips, tabs, ghost buttons) instead. */
.btn--primary {
  background: var(--text);
  color: var(--canvas);
}
@media (hover: hover) {
  .btn--primary:hover { box-shadow: var(--glow-accent); }
}

/* Today's "+ Add a Card". Full width, thumb-height, dominant. */
.btn--hero {
  width: 100%;
  min-height: var(--tap-hero);
  border-radius: var(--r-lg);
  font-size: var(--fs-xl);
  font-stretch: var(--wd-wide);
  letter-spacing: var(--track-display);
}

/* The fixed-bar PUBLISH. Uppercase + slab tracking so it reads as the
   terminal action, not just another button. */
.btn--action {
  min-height: var(--tap-action);
  border-radius: var(--r-md);
  font-size: var(--fs-lg);
  font-stretch: var(--wd-wide);
  letter-spacing: var(--track-slab);
  text-transform: uppercase;
}

.btn--ghost {
  border-color: var(--glass-border);
  background: var(--glass-bg);
  /* padding-box, not the default border-box — otherwise the element's
     own (nearly opaque) fill paints under the border too, and a
     translucent border blended against its own near-match fill is
     effectively invisible. padding-box lets the border composite against
     what's actually behind the element instead. */
  background-clip: padding-box;
  backdrop-filter: blur(10px) saturate(1.4);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
  color: var(--text);
  box-shadow: var(--shadow-1), inset 0 1px 0 var(--glass-border);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .btn--ghost { background: var(--surface); border-color: var(--border); }
}
.btn--ghost:active { background: var(--surface-3); }
@media (hover: hover) {
  .btn--ghost:hover {
    border-color: var(--border-strong);
    background: var(--surface-2);
  }
}

/* QUIET — "Save for later", "Add another card", "Start fresh".
   Deliberately not a peer of primary. */
.btn--quiet {
  min-height: var(--tap-min);
  padding-inline: var(--space-sm);
  color: var(--text-2);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-normal);
}
@media (hover: hover) {
  .btn--quiet:hover { color: var(--text); text-decoration: underline; }
}

/* DANGER.
   NOTE: there is no filled danger button in this system, on purpose.
   Rule 3 removes every confirmation dialog, so destructive actions are
   quiet and reversible. A loud red fill would be theatre for an action
   that undoes itself for 30 seconds. */
.btn--danger {
  color: var(--danger);
  border-color: var(--danger-border);
}
.btn--danger:active { background: var(--danger-bg); }
@media (hover: hover) {
  .btn--danger:hover {
    background: var(--danger-bg);
    border-color: var(--danger);
  }
}

.btn--full  { width: 100%; }
.btn--block { display: flex; width: 100%; }

/* --- In-flight state (Rule 10) --- */
.btn[aria-busy="true"] {
  cursor: progress;
  pointer-events: none;
  opacity: 0.78;
}
.btn[aria-busy="true"]::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  height: 2px; width: 40%;
  background: var(--accent);
  animation: kj-indeterminate 1.1s var(--ease-in-out) infinite;
}
@keyframes kj-indeterminate {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
  .btn[aria-busy="true"]::after {
    animation: none;
    width: 100%;
    opacity: 0.5;
  }
}

/* --- Form fields — promoted from the old §11 FORM FIELDS. build-public-
       css.mjs drops §9-§18 (admin-only) from kj-public.css; these were
       unstyled on the public site (subscribe form, /shop empty states)
       until this move. --- */
.field { display: block; }
.field + .field { margin-top: var(--space-lg); }

/* 14px, NOT the 11px slab treatment. The slab micro-label is for
   METADATA ONLY — a required field label at 11px, on a phone, in a
   hurry, is a failure dressed as sophistication. */
.field__label {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-2);
}

.field__hint {
  margin-top: var(--space-xs);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--text-3);
  text-wrap: pretty;
}

.field__error {
  margin-top: var(--space-xs);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--danger);
}

.input,
.textarea,
.select {
  display: block;
  width: 100%;
  min-height: var(--tap-comfortable);
  padding: 14px var(--space-md);
  border: var(--bw) solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--text);
  font-size: var(--fs-md);        /* NEVER lower — iOS zooms below 16px */
  line-height: var(--lh-normal);
  appearance: none;
  -webkit-appearance: none;
  transition: border-color     var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              box-shadow       var(--dur-fast) var(--ease-out);
}

.input:focus,
.textarea:focus,
.select:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--surface-3);
  box-shadow: 0 0 0 3px var(--accent-ring);
}

.textarea {
  min-height: 104px;
  resize: vertical;
}

.input--invalid,
.textarea--invalid {
  border-color: var(--danger);
}

/* --- Banners — promoted from the old §13 BANNERS, same reason as the
       fields above. --- */
.banner {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: var(--bw) solid var(--border-subtle);
  border-left-width: 3px;
  border-radius: var(--r-md);
  background: var(--surface);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--text);
  text-wrap: pretty;
}

.banner--success {
  border-color: var(--success-border);
  border-left-color: var(--success);
  background: var(--success-bg);
}
.banner--error {
  border-color: var(--danger-border);
  border-left-color: var(--danger);
  background: var(--danger-bg);
}

.banner + .banner { margin-top: var(--space-sm); }

/* --- Empty state — promoted from the old §16 LIST VIEW, same reason. --- */
.empty {
  padding: var(--space-3xl) var(--space-lg);
  text-align: center;
  color: var(--text-2);
}

/* =====================================================================
   19. PUBLIC HEADER + NAV
   ===================================================================== */

/* PUBLIC SITE TOKENS — exactly two new custom properties (everything else
   in §19-27 reuses §2-3's existing ramp: no new colors, elevation, spacing,
   radii, durations, or keyframes). Defined inside §19 (not in a standalone
   block above it) so build-public-css.mjs's §19-27 extraction — keyed on
   the `/* ====` divider immediately preceding this section's own header —
   actually carries these into kj-public.css; a prior standalone block here
   was silently dropped from the public bundle, breaking --nav-h and
   --track-caps on every public page while remaining defined (and
   therefore invisible to review) in kj.css itself. */
:root {
  /* Sticky-nav offset — one combined row (crest + links) at every
     viewport now; used by the sticky gallery `top`, [id] scroll-margin-top,
     and the hero's layout. */
  --nav-h: 56px;
  /* --track-display (-0.022em) is wrong-signed for all-caps text;
     --track-normal (0) is acceptable but not ideal for wide-tracked caps.
     Used by the hero eyebrow, nav labels, and .btn--action-weight caps. */
  --track-caps: 0.02em;
}
@media (min-width: 900px) {
  :root {
    --nav-h: 64px;
  }
}

.sitehead {
  position: relative;
}

.sitenav {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  height: var(--nav-h);
  padding-inline: var(--gutter);
  background: var(--surface-2-a);
  backdrop-filter: blur(14px) saturate(1.25);
  -webkit-backdrop-filter: blur(14px) saturate(1.25);
  border-bottom: var(--bw) solid var(--border);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sitenav { background: var(--surface-2); }
}
/* The crest does real layout work at the row's own start at every
   viewport now, not centered alone in its own otherwise-empty band above
   the nav (the previous two-row split — see design-craft-local.md). The
   wordmark span stays hidden below 480px, where it would crowd the four
   nav links; the crest alone still identifies the brand there. */
.sitenav__brand {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: 0 0 auto;
  text-decoration: none;
}
/* Circular, with a themed ring — the crest's own cream/gold/oxblood face
   is fixed art, warmer than either theme's surfaces; a hard rectangular
   edge with no mediation read as a sticker dropped onto an unrelated
   background. The ring doesn't change the crest's colors, just gives it
   a boundary that belongs to the same object system as the rest of the
   chrome (same border token .filter/.btn use). */
.sitenav__brand img {
  border-radius: 50%;
  outline: var(--bw) solid var(--border);
  outline-offset: -1px;
}
.sitenav__brand span {
  display: none;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
}
@media (min-width: 480px) {
  .sitenav__brand span { display: inline; }
}
.sitenav__links {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: flex-end;
  /* Tight at base — four links plus their own glass-pill padding, plus
     the crest, overflowed a 390px viewport by ~27px at --space-md. Only
     ≥900px (real room, and only 3-4 links visible at once) gets the
     roomier gap. */
  gap: var(--space-3xs);
}
.sitenav__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-2xs);
  border: var(--bw) solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-2);
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color     var(--dur-fast) var(--ease-out),
              color            var(--dur-fast) var(--ease-out);
}
@media (hover: hover) {
  .sitenav__link:hover {
    color: var(--text);
    background: var(--glass-bg);
    background-clip: padding-box;
    border-color: var(--glass-border);
  }
}
.sitenav__link[aria-current="page"] {
  color: var(--accent);
  background: var(--glass-bg);
  background-clip: padding-box;
  border-color: var(--glass-border);
  box-shadow: inset 0 1px 0 var(--glass-border);
}

.announce {
  padding: var(--space-xs) var(--gutter);
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--text-2);
  background: var(--surface);
  border-bottom: var(--bw) solid var(--border-subtle);
}

@media (min-width: 900px) {
  .sitenav { gap: var(--space-2xl); }
  .sitenav__links { gap: var(--space-sm); }
  .sitenav__link { padding-inline: var(--space-sm); }
}


/* =====================================================================
   20. PUBLIC PAGE FURNITURE
   ===================================================================== */

[id] { scroll-margin-top: calc(var(--nav-h) + var(--space-md)); }

.section { padding-block: var(--space-3xl); position: relative; }
@media (min-width: 900px) {
  .section { padding-block: var(--space-4xl); }
}
/* Vault seam — the one new sitewide secondary device. A doubled hairline
   wherever two .section elements are already adjacent (home, shop, blog,
   shows), for free — no page-module markup changes needed anywhere. */
.section + .section::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: linear-gradient(
    to bottom,
    var(--border) 0, var(--border) 1px,
    transparent 1px, transparent 2px,
    var(--border) 2px, var(--border) 3px
  );
}

.pagehead {
  padding-bottom: var(--space-lg);
  margin-bottom: var(--space-xl);
  border-bottom: var(--bw) solid var(--accent-line);
}
.pagehead .title { margin-bottom: var(--space-xs); }

.backlink {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-2);
  text-decoration: none;
}
@media (hover: hover) {
  .backlink:hover { color: var(--text); text-decoration: underline; }
}

/* .filters-wrap is the fade's actual host — round 1's critique found the
   original mask-image (applied directly to the scroll container) erased
   a focused chip's ring at rest and at the scroll end, AND that the
   scroller's own missing top padding clipped every ring's top arc
   regardless of the mask. A mask on the scroll container masks its
   content; these two fixed, non-scrolling overlay gradients on the OUTER
   wrapper don't — they paint on top, at the true edges, and never touch
   a chip's own box-shadow ring. */
.filters-wrap {
  position: relative;
}
.filters-wrap::before,
.filters-wrap::after {
  content: "";
  position: absolute;
  top: 0; bottom: var(--space-md);
  width: 20px;
  pointer-events: none;
  z-index: 1;
}
.filters-wrap::before { left: 0; background: linear-gradient(to right, var(--canvas), transparent); }
.filters-wrap::after  { right: 0; background: linear-gradient(to left, var(--canvas), transparent); }
.filters {
  display: flex;
  gap: var(--space-xs);
  overflow-x: auto;
  /* Top padding reserves room for a focused chip's box-shadow ring — an
     overflow-x:auto container promotes overflow-y to auto per spec, so
     with no top padding the ring's top arc was clipped at every chip,
     every scroll position, not just at the edges. */
  padding: 4px 4px var(--space-md);
  margin-bottom: var(--space-lg);
  border-bottom: var(--bw) solid var(--border-subtle);
  -webkit-overflow-scrolling: touch;
}
.filter {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-md);
  border: var(--bw) solid var(--glass-border);
  border-radius: var(--r-pill);
  /* Glass pill — translucent surface + blur, not a flat bordered label.
     saturate(1.4) keeps whatever's behind the chip (cards, gold accents)
     from washing out under the blur, the way plain blur alone tends to.
     background-clip:padding-box keeps the border compositing against the
     page instead of the chip's own near-opaque fill (border-box's
     default would blend it with itself and erase it). */
  background: var(--glass-bg);
  background-clip: padding-box;
  backdrop-filter: blur(10px) saturate(1.4);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
  box-shadow: var(--shadow-1), inset 0 1px 0 var(--glass-border);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color     var(--dur-fast) var(--ease-out),
              color            var(--dur-fast) var(--ease-out);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .filter { background: var(--surface-2); }
}
@media (hover: hover) {
  .filter:hover { border-color: var(--border-strong); color: var(--text); }
}
.filter.chip--live {
  /* State signal, not a value signal — verdigris, not gold. Gold stays
     reserved for value/rarity/primary-action (see design-craft-local.md). */
  color: var(--success);
  border-color: var(--success-border);
  /* background-color, not the `background` shorthand — the shorthand
     resets background-clip to its initial border-box, silently undoing
     the base .filter rule's `background-clip: padding-box` and making
     border-color composite against this chip's own fill instead of the
     page behind it (the exact bug that rule exists to prevent). */
  background-color: var(--success-bg);
  backdrop-filter: blur(10px) saturate(1.4);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
}


/* =====================================================================
   21. HERO
   ===================================================================== */

.hero {
  display: grid;
  gap: var(--space-xl);
  align-items: center;
}
.hero__media {
  /* Below 900px, .hero has no grid-template-columns, so .hero__media was
     free to grow to the full single-column width — at tablet widths that
     let the 5:7 slabframe blow up to ~1100px tall. Capped at the same
     420px the desktop grid column already allows, and centered so the
     leftover width doesn't strand it against the left edge with a wide
     empty gap on the right. `width: 100%` is required — without it, a
     grid/flex item with `margin-inline: auto` and no explicit width
     shrinks to fit-content instead of stretching, collapsing this to the
     slabframe bar text's own ~153px content width at every viewport. */
  width: 100%;
  max-width: 420px;
  margin-inline: auto;
}
.hero__title {
  font-family: var(--font-display);
  font-size: clamp(var(--fs-3xl), 6vw, var(--fs-6xl));
  letter-spacing: var(--track-display-condensed);
}
.hero__eyebrow {
  letter-spacing: var(--track-caps);
}
/* Fills the statement hero's media slot when there's no product to show
   yet (empty catalog) — same crest asset the coming-soon page uses. */
.hero__media--brand {
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero__crest {
  width: clamp(160px, 40vw, 280px);
  height: auto;
  margin-inline: auto;
  filter: drop-shadow(var(--shadow-logo));
}
@media (min-width: 900px) {
  .hero {
    grid-template-columns: 1fr minmax(320px, 420px);
    gap: var(--space-4xl);
  }
  .hero__media { order: 0; }
}


/* =====================================================================
   22. SLAB FRAME + PRODUCT CARD
   ===================================================================== */

.slabframe {
  /* --border-strong, not --border — this frame is the house signature
     element (the 5:7 graded-slab silhouette), and --border measured
     ~1.3-1.5:1 against the page, the faintest edge on screen for the one
     shape the whole visual system is built around. */
  border: var(--bw) solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  background: var(--surface);
  overflow: hidden;
}
/* The bar sits above the 5:7 face inside the same border, so the visible
   outer silhouette is ~1.47:1, not the true 5:7 — this modifier puts the
   ratio on the OUTER box instead, and the face absorbs exactly the bar's
   height via flex. Applied on grid cards and the home hero card only —
   never on .slabframe--natural, which is sized to a real image's
   intrinsic dimensions on purpose. */
.slabframe--ratio-outer {
  aspect-ratio: 5 / 7;
  display: flex;
  flex-direction: column;
}
.slabframe--ratio-outer .slabframe__img {
  aspect-ratio: auto;
  flex: 1 1 auto;
  min-height: 0;
}
.slabframe__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  min-height: 26px;
  padding-inline: var(--space-sm);
  font-size: var(--fs-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-slab);
  text-transform: uppercase;
  color: var(--accent);
  font-variant-numeric: tabular-nums slashed-zero;
  border-bottom: var(--bw) solid var(--accent-line);
}
.slabframe__img {
  display: block;
  width: 100%;
  aspect-ratio: 5 / 7;
  object-fit: cover;
  background: var(--surface-3);
}
.slabframe--natural .slabframe__img { aspect-ratio: auto; }
/* A coverless entry's placeholder is a bare <div>, not an <img> with
   intrinsic dimensions — aspect-ratio:auto (from --natural above, and
   from --ratio-outer's .slabframe__img reset) has nothing to size itself
   against and collapses to zero height. Forces the 5:7 ratio back
   regardless of either modifier. */
.slabframe__img--placeholder {
  aspect-ratio: 5 / 7 !important;
  /* Engine-turned/guilloché hairline texture — the vault's own "no cover
     yet" material, not a broken-image stand-in. Low-contrast on purpose:
     never competes with real content once a cover exists. */
  background-image: repeating-linear-gradient(
    135deg,
    var(--border-subtle) 0px,
    var(--border-subtle) 1px,
    transparent 1px,
    transparent 9px
  );
}
/* img.ts's renderImg emits this on its unsized enhanced-transform path —
   no stored width/height to size the box against. Same collapse-to-zero
   problem --placeholder above exists to fix, so the same fallback ratio. */
.slabframe--natural .img--unsized { aspect-ratio: 5 / 7; }

.pgrid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-md);
}
@media (min-width: 640px) {
  .pgrid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1024px) {
  .pgrid { grid-template-columns: repeat(4, 1fr); }
}
/* Home's "New in the Vault" rail always fetches up to 4 items (unlike
   /shop's real paginated grid, where a ragged last row is just an honest
   reflection of the catalog's real count) — at the tablet 3-column step
   that reliably orphans a 4th card onto its own row. Hiding it there
   keeps the rail's own fixed-size teaser tidy without touching /shop's
   real grid, which should never hide real inventory. */
@media (min-width: 640px) and (max-width: 1023px) {
  .pgrid--capped > :nth-child(n + 4) { display: none; }
}

.pcard { display: block; text-decoration: none; color: inherit; }
.pcard__meta {
  margin-top: var(--space-sm);
  font-size: var(--fs-xs);
  color: var(--text-3);
}
.pcard__title {
  margin-top: var(--space-2xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-tight);
  color: var(--text);
}
.pcard__price {
  margin-top: var(--space-2xs);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums slashed-zero;
  color: var(--text-2);
}
.pcard--sold .slabframe__img {
  filter: saturate(0.35);
  opacity: 0.82;
}
/* The brand oxblood's only public use — there are no destructive actions
   on a public site, so --danger reused here keeps red entirely out of
   navigation, matching Anthony's rejection of the reference's red nav bar. */
.pcard__sold-tag {
  color: var(--danger);
  border-color: var(--danger-border);
  background: var(--danger-bg);
}


/* =====================================================================
   23. ENTRY DETAIL
   ===================================================================== */

.entry {
  display: grid;
  gap: var(--space-xl);
}
@media (min-width: 900px) {
  .entry {
    grid-template-columns: minmax(320px, 460px) 1fr;
    gap: var(--space-4xl);
    align-items: start;
  }
  /* .backlink and (when present) the draft-preview .banner are direct
     children of .entry alongside .entry__gallery/.entry__info — without
     this, grid auto-placement treats all four as items in a 2-column
     grid: backlink takes row 1/col 1, gallery gets pushed to row 1/col 2,
     and info falls to row 2/col 1, entirely below the fold. */
  .entry > .backlink,
  .entry > .banner {
    grid-column: 1 / -1;
  }
  .entry__gallery {
    position: sticky;
    top: calc(var(--nav-h) + var(--space-lg));
  }
}
/* The single most decision-relevant fact on a product page was rendering
   dimmer and smaller than the spec table beside it (--text-2, 18px vs the
   spec values' --text, 14px but higher-contrast) — promoted to a real
   second-largest moment on the page, after only the title. */
.entry__price {
  margin-top: var(--space-2xs);
  font-size: var(--fs-3xl);
  font-weight: var(--fw-semibold);
  color: var(--text);
  font-variant-numeric: tabular-nums slashed-zero;
}
.entry__gallery-extra {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-xs);
  margin-top: var(--space-sm);
}
.entry__gallery-extra img {
  aspect-ratio: 5 / 7;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: var(--bw) solid var(--border);
}

.spec {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  margin-top: var(--space-lg);
}
.spec > div {
  padding-block: var(--space-sm);
  border-top: var(--bw) solid var(--border-subtle);
}
.spec dt {
  font-size: var(--fs-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-slab);
  text-transform: uppercase;
  color: var(--text-3);
}
.spec dd {
  margin: var(--space-2xs) 0 0;
  font-size: var(--fs-sm);
  color: var(--text);
}

.buybar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--gutter) calc(var(--space-md) + var(--safe-b));
  background: var(--surface-2-a);
  backdrop-filter: blur(14px) saturate(1.25);
  -webkit-backdrop-filter: blur(14px) saturate(1.25);
  border-top: var(--bw) solid var(--border);
  box-shadow: var(--shadow-2);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .buybar { background: var(--surface-2); }
}
/* The CTA is the buybar's only child now, but flex-shrink:0 still keeps
   it from squishing on the narrowest phone widths instead of overflowing
   predictably. */
.buybar > .btn { flex-shrink: 0; }
/* On body, not .wrap--detail: the buybar is position:fixed and the footer
   follows .entry in the DOM, so the reserved space has to sit at the true
   end of the document (after the footer) or the fixed bar still covers the
   footer once scrolled to the bottom of the page. */
body.has-buybar { padding-bottom: calc(var(--actionbar-h) + var(--space-xl)); }
@media (min-width: 900px) {
  .buybar {
    position: static;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-top: 0;
    box-shadow: none;
    padding: var(--space-lg) 0 0;
  }
  body.has-buybar { padding-bottom: 0; }
}


/* =====================================================================
   24. ARTICLE TYPOGRAPHY
   ===================================================================== */

.article {
  max-width: var(--w-prose);
  margin-inline: auto;
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
}
.article > * + * { margin-top: 1em; }
.article p, .article div, .article pre, .article blockquote,
.article ul, .article ol, .article figure { max-width: 62ch; }
.article h1[data-demoted-heading],
.article h2 {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-display-condensed);
  line-height: var(--lh-snug);
  margin-top: 1.6em;
}
.article h3 {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-display-condensed);
  margin-top: 1.4em;
}
.article strong, .article b { font-weight: var(--fw-semibold); }
.article ul, .article ol { list-style: revert; padding-left: 1.4em; }
.article li::marker { color: var(--accent); }
.article a { text-underline-offset: 3px; }
.article blockquote {
  /* Smaller below the tablet breakpoint — at --space-md (16px) plus the
     3px rule, the indent ate ~15% of a 390px viewport and crushed the
     passage to ~34ch/line; --space-sm keeps the rule's presence without
     taking that much width back from the one passage meant to read
     slowly. */
  padding-left: var(--space-sm);
  /* Solid accent, not --accent-line's low-alpha wash — a blockquote rule
     is a deliberate mark (same role as the nav's active underline), not a
     hairline; --accent-line measured ~1.3:1 here, functionally invisible. */
  border-left: 3px solid var(--accent);
  color: var(--text-2);
}
@media (min-width: 600px) {
  .article blockquote { padding-left: var(--space-md); }
}
.article pre {
  padding: var(--space-md);
  overflow-x: auto;
  background: var(--surface-2);
  border: var(--bw) solid var(--border-subtle);
  border-radius: var(--r-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-sm);
}
.article img { border-radius: var(--r-sm); }
.article figure { margin: 0; }
.article figure > figcaption {
  margin-top: var(--space-xs);
  font-size: var(--fs-sm);
  color: var(--text-3);
  text-align: center;
}
.article del { color: var(--text-3); }

.postlist { display: flex; flex-direction: column; }
.postlist__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xs);
  padding-block: var(--space-lg);
  border-bottom: var(--bw) solid var(--border-subtle);
  text-decoration: none;
  color: inherit;
}
/* Row titles reuse .pcard__title's base (body-scale, --fw-medium) — right
   for a dense grid caption, wrong for the one scannable heading on a list
   page: it read as barely-bold body text, weaker than the meta line above
   it. Promoted to the display face, one step below .pagehead's H1, so the
   thing you actually click is the strongest type on the page. The
   trailing arrow is the row's only link affordance at rest — an <a> with
   text-decoration:none and inherited color otherwise gives no visual cue
   it's clickable at all, and hover-only affordance doesn't exist on the
   phone this site is used from. */
.postlist__row .pcard__title {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-display-condensed);
}
.postlist__row .pcard__title::after {
  content: '→';
  margin-left: var(--space-2xs);
  color: var(--text-3);
}
.postlist__excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.postlist__thumb {
  /* 5:7 — the house signature (real trading-card proportions), not a
     generic 3:2 landscape crop. Matches .slabframe's own ratio so a real
     cover reads as part of the same visual system as every product card. */
  aspect-ratio: 5 / 7;
  width: 96px;
  object-fit: cover;
  border-radius: var(--r-sm);
  border: var(--bw) solid var(--border);
}
/* No placeholder variant — round 2's critique made the Chanel cut: a
   coverless row renders no thumb at all (see blog.ts's postRow) rather
   than a hatched stand-in, which had no max-width below 900px and became
   the single largest, most dominant object on the page. :has() switches
   .postlist__row to its two-column layout only when a real thumb child
   is actually present, so a coverless row never needs one just to keep
   the grid from collapsing. */
@media (min-width: 900px) {
  .postlist__row:has(.postlist__thumb) {
    /* Content first, thumb second, matching postRow's actual DOM order —
       a 96px-first template silently swapped these, squeezing the title
       and excerpt into the thumb's narrow column instead. */
    grid-template-columns: 1fr 96px;
    align-items: center;
    gap: var(--space-lg);
  }
}


/* =====================================================================
   25. SHOWS
   ===================================================================== */

.showlist { display: flex; flex-direction: column; }
.showrow {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: var(--space-md);
  padding-block: var(--space-lg);
  border-bottom: var(--bw) solid var(--border-subtle);
  text-decoration: none;
  color: inherit;
}
.showdate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-right: var(--space-md);
  border-right: var(--bw) solid var(--accent-line);
  text-align: center;
}
.showdate .slab { color: var(--accent); }
/* Dropped a step from --fs-3xl — at that size the date numeral outranked
   the show title next to it (same rule this section's .pcard__title
   promotion fixes from the other direction: the title is now the
   stronger of the two, as it should be — you click the show, not the
   date). */
.showdate .num { font-size: var(--fs-xl); line-height: 1; }
.showrow--past .showdate { opacity: 0.55; }
.showrow .pcard__title {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-display-condensed);
}
.showrow .pcard__title::after {
  content: '→';
  margin-left: var(--space-2xs);
  color: var(--text-3);
}
.chip--countdown {
  min-width: 11ch;
  justify-content: center;
  font-variant-numeric: tabular-nums slashed-zero;
}


/* =====================================================================
   26. FOOTER
   ===================================================================== */

.sitefoot {
  /* Neutral, not --accent-line — the gold hairline's whole job is to mark
     a page title (every .title/h1 already carries one via §7); doubling
     it here as ordinary page-chrome made the accent's one meaningful use
     read as a decorative bracket pair around the page instead. */
  border-top: var(--bw) solid var(--border);
  padding-block: var(--space-3xl);
}
.sitefoot--minimal {
  padding-block: var(--space-xl) 0;
}
.sitefoot--minimal .sitefoot__legal {
  margin-top: 0;
  border-top: 0;
  padding-top: 0;
}
.sitefoot__grid {
  display: grid;
  gap: var(--space-xl);
}
.sitefoot__links {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.sitefoot__links a {
  color: var(--text-2);
  text-decoration: none;
  font-size: var(--fs-sm);
}
@media (hover: hover) {
  .sitefoot__links a:hover { color: var(--text); }
}
.sitefoot__sub { margin-top: var(--space-xs); }
.sitefoot__legal {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: var(--bw) solid var(--border-subtle);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}
@media (min-width: 900px) {
  .sitefoot__grid { grid-template-columns: 1fr 2fr; }
  .sitefoot__links { flex-direction: row; gap: var(--space-2xl); }
}

.subscribeform__row {
  display: flex;
  gap: var(--space-xs);
  max-width: 22rem;
}
.subscribeform__row .input { flex: 1 1 auto; min-width: 0; }
/* Without this the button's default flex-shrink let "Notify me" wrap to
   two lines before the input gave up any width — the input should be the
   one that shrinks in a tight row, never the button's own label. */
.subscribeform__row .btn { flex-shrink: 0; }
.themepick {
  min-height: var(--tap-min);
  padding: 0 var(--space-xs);
  border: 0;
  background: transparent;
  color: var(--text-3);
  font-size: var(--fs-xs);
}


/* =====================================================================
   27. COMING SOON
   ===================================================================== */

.comingsoon {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  /* .comingsoon__inner and .peek are two stacked blocks, not a row — with
     no flex-direction this defaulted to `row`, and at mobile widths their
     combined min-content width exceeded the viewport (both children hit
     the same overflow, symmetric clipping on both edges from
     justify-content:center centering the excess). */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl) var(--gutter);
}
.comingsoon__inner {
  max-width: var(--w-prose);
  text-align: center;
}
.comingsoon__crest {
  width: clamp(200px, 34vw, 300px);
  margin-inline: auto;
  filter: drop-shadow(var(--shadow-logo));
}
.comingsoon__eyebrow {
  margin-top: var(--space-lg);
  letter-spacing: var(--track-caps);
}
.comingsoon__headline {
  margin-top: var(--space-xs);
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-display-condensed);
  font-size: clamp(var(--fs-3xl), 8vw, var(--fs-6xl));
  line-height: var(--lh-tight);
}
.comingsoon__pitch {
  margin-top: var(--space-md);
}
.comingsoon__form {
  margin: var(--space-xl) auto 0;
  max-width: 22rem;
}
.peek {
  margin-top: var(--space-3xl);
}
/* Forcing 3 columns unconditionally (including at mobile, below .pgrid's
   own 2-column default) squeezed each of the 3 preview cards' slab-bar
   text below its min-content width, overflowing the viewport horizontally
   by ~60px — scoped to ≥640px, where .pgrid would already be 3-up anyway,
   this override becomes a no-op there and mobile gets .pgrid's own
   2-column default instead. */
@media (min-width: 640px) {
  /* Stays 3-up forever, not stepping to 4 like the real /shop grid —
     the teaser only ever fetches 3 items (pageSize:3 in home.ts), so a
     4th track would sit permanently empty. Capped at 282px per card
     instead (the live catalog's own measured card width at desktop),
     so the teaser doesn't keep growing past that and out-scale the crest
     and signup form it sits beneath at wide viewports. */
  .peek .pgrid { grid-template-columns: repeat(3, minmax(0, 282px)); justify-content: center; }
}
.peek .pcard { pointer-events: none; }
.peek .pcard__title { color: var(--text-2); }

@media (max-width: 899px) {
  body { background-attachment: scroll; }
}

/* Generic spacing utilities — style-src 'self' has no 'unsafe-inline', so
   an inline style="margin-top:..." is never an option on a public page;
   these exist for the handful of one-off vertical gaps that don't warrant
   their own named component class. */
.mt-xl  { margin-top: var(--space-xl); }
.mt-2xl { margin-top: var(--space-2xl); }
.ta-center { text-align: center; }
