/* ============================================================
   _base.css — the editorial shell every preview card lives in.
   Newsprint, ivory, filing-code furniture. No 12-radius.
   Enforced by variable + selector; cards cannot opt out.
   ============================================================ */

@import url("../contract.css");
@import url("../derived.css");
@import url("./themes.css");

/* Editorial palette (--color, --backgroundColor, --red, --hair,
   --hairline, --border, --grey-300, --grey-500, --grey-700,
   --fg-1..--fg-4, --bg-1..--bg-3) lives in contract.css + derived.css.
   Do not redeclare here — those two files are the source of truth.
   The legacy --ink/--ivory/--ink-2/--ivory-2/--bg/--fg aliases were
   deleted in PR #168; tests/no-deprecated-aliases.test.mjs gates
   re-introduction. */

html, body {
  margin: 0;
  background: var(--backgroundColor);
  color: var(--color);
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-body);
  -webkit-font-smoothing: antialiased;
  /* Body color/background transition softens theme switches — matches
     johnny-5-rebuild's globals.css:85-86. */
  transition: background-color 0.4s ease, color 0.4s ease;
}
body { padding: 0; }

/* ---------- NEWSPRINT GRAIN ----------
   Real material instead of flat color. SVG <feTurbulence> noise tiled
   at 240×240, applied as a fixed top-layer overlay at ~5% opacity. The
   layer sits above everything (pointer-events:none) so clicks pass
   through; opacity is low enough that text contrast isn't affected.

   Why this matters: AI-generated design defaults to flat fills. Paper,
   newsprint, halftone screens — anything with real grain — is the
   single most reliable visual signal that a surface was *made*, not
   templated. Cost: one fixed pseudo-element + a single tiled SVG.

   Implementation: ::after on body so it's above all body content; SVG
   uses fractalNoise (smoother than turbulence) with numOctaves=2 and a
   fixed seed for determinism; feColorMatrix maps to ink with alpha
   knocked to 0.4 inside the SVG, then the whole layer wears 0.05 alpha
   globally — total ~2% effective per pixel. Visible as texture in
   negative space, invisible over body type.
   ---------- */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='2' seed='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.5 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 240px 240px;
  background-repeat: repeat;
  opacity: 0.05;
}

/* ---------- CARD ----------
   CONTRACT — one card. `.card` is the ONLY canonical content-sheet
   primitive; pages never fabricate a `.tile`/`.panel`/`.box` sibling.

   · What it is — a sheet of editorial paper, not a SaaS tile. A bounded
     unit of content with a 3px ink top rule and a hairline bottom rule;
     it reads like a clipping, not a UI card.
   · Render — fixed 900px width, sharp corners (`--radius-sharp`, never a
     ≥12px radius — DOCTRINE.md§3.1), NO box-shadow, NO soft border, NO
     decorative gradient. The top rule is the only loud edge.
   · Reveal — `.card` is one of reveal.js's observed targets: it fades in
     (opacity + blur + translateY) when it enters the viewport, and
     setting [data-revealed] on the card is what triggers every reveal-
     gated descendant inside it (the `.dag-mark`, draw-in link
     underlines, etc.). A card is therefore a reveal SCOPE, not just a
     box.
   · Contexts it must work in — block-level; centered in flow via its
     own margin. Light/dark via --backgroundColor/--color. Cards do NOT
     nest as a default layout move (cards-inside-cards is a banned
     pattern — DOCTRINE.md§3.5 layout).
   · Reduced motion — the reveal entrance drops its travel/blur via the
     global prefers-reduced-motion rule; the card snaps to its end-state.

   This is the ONLY card rule. Pages never redeclare or fork it. */
.card {
  box-sizing: border-box;
  width: 900px;
  min-height: 120px;
  margin: 24px;
  padding: 40px 48px 44px;
  background: var(--backgroundColor);
  border: 0;
  border-top: var(--rule-strong);
  border-bottom: var(--rule);
  border-radius: var(--radius-sharp);
  position: relative;
}

/* ---------- REVEAL (canonical entrance motion) ----------
   Any element marked with `data-reveal` (or the legacy `.card` /
   `.page` targets that reveal.js already observes) fades in via
   opacity + filter:blur(6px) + translateY(20px) over 400ms when
   reveal.js sets [data-revealed]. Used by .card and any section
   that opts in via data-reveal. Reduced-motion drops the travel
   via the global prefers-reduced-motion rule below. */
.card,
[data-reveal] {
  opacity: 0;
  filter: blur(6px);
  transform: translateY(20px);
  transition: opacity 400ms ease-out,
              filter 400ms ease-out,
              transform 400ms ease-out;
}
.card[data-revealed],
[data-reveal][data-revealed] {
  opacity: 1;
  filter: none;
  transform: translateY(0);
}

/* The masthead row used to render `Nº XX · Page · italic phrase` above
   each card (via .card::before reading data-nr/data-masthead). Removed
   2026-05 — the .label below already identifies each card, and the
   "Nº" + italic phrase decoration was the most templated, "AI-design-demo"
   element in the system. data-nr / data-masthead attributes preserved on
   markup for backward compat; they no longer render. */

/* ---------- WORDMARK (the brand mark — same everywhere) ----------
   The Subconscious wordmark renders identically on every page that
   includes _base.css. Markup contract:

     <a href="..." class="wordmark no-link">
       <svg class="dag-mark" viewBox="0 0 60 42" aria-hidden="true">…</svg>
       <span><b>subconscious</b>.ai</span>
     </a>

   Pages do NOT redeclare wordmark styling. If you find yourself adding
   .wordmark { font-family: ... } on a page, you're working against the
   system. Stop and read this comment. */
.wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: 12px;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--fs-body);
  letter-spacing: -0.045em;
  text-transform: lowercase;
  line-height: 1;
  color: var(--color);
}
.wordmark b { font-weight: 800; }
.wordmark .dag-mark { width: 22px; height: 16px; flex-shrink: 0; }

/* ---------- LABEL (replaces the uppercase kicker tic) ---------- */
.label {
  font-family: var(--font-mono);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--color);
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin: 0 0 18px;
}
.label::before {
  content: "";
  width: 6px; height: 6px;
  background: var(--red);
  border-radius: var(--radius-dot);
  display: inline-block;
  flex-shrink: 0;
}
.label .sub {
  font-family: var(--font-italic);
  font-style: italic;
  font-weight: 400;
  text-transform: none;
  letter-spacing: -0.01em;
  font-size: 14px;
  color: var(--grey-500);
  margin-left: 6px;
}

/* ---------- HEAD (headline inside the card) ---------- */
.head {
  font-family: var(--font-body);
  font-weight: 200;
  font-size: 56px;
  line-height: 0.95;
  letter-spacing: -0.045em;
  margin: 0 0 18px;
}
.head em {
  font-family: var(--font-italic);
  font-style: italic;
  font-weight: 400;
  opacity: 0.35;
}

/* ---------- LEDE (opening paragraph below .head) ----------
   Canonical recipe. Pages may override only font-size, max-width,
   or bottom margin via a 1-line scoped rule. Do not redeclare the
   full block — that's the DRY violation we just cleaned up. */
.lede {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 18px;
  line-height: 1.5;
  letter-spacing: -0.012em;
  max-width: 62ch;
  color: color-mix(in srgb, var(--color) 70%, var(--backgroundColor));
  margin: 0 0 32px;
}
.lede em {
  font-family: var(--font-italic);
  font-style: italic;
  font-weight: 400;
  color: var(--grey-500);
}

/* ---------- FOOTNOTE (mono footnote near body copy) ---------- */
.footnote {
  font-family: var(--font-mono);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--grey-500);
  max-width: 52ch;
  line-height: 1.8;
}
.footnote sup { color: var(--red); margin-right: 4px; }

/* ---------- PROOF (mono meta block, footnote-style) ---------- */
.proof {
  font-family: var(--font-mono);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--grey-500);
  line-height: 1.8;
  margin-top: 16px;
  padding-top: 14px;
  border-top: var(--rule-muted);
}
.proof b { color: var(--color); font-weight: 400; }
.proof .r { color: var(--red); }

/* ---------- ROW ---------- */
.row { display: flex; gap: 0; flex-wrap: wrap; align-items: stretch; }

/* ---------- HAIRLINE ---------- */
.hair { height: 1px; background: var(--hair); margin: 20px 0; }

/* ============================================================
   FORM PRIMITIVES — opinionated input set.

   The doctrine: forms are evidence collection, not toy widgets.
   No glassmorphism, no soft shadows, no rounded pills. 1px hairline,
   2px sharp radius, ink/ivory cascade, uppercase mono labels.

   .field            vertical stack: label → input → help. gap: --sp-2.
   .field--inline    horizontal: label · input · help inline.
   .label--required  appends a red asterisk to the label text.
   .input            text / email / number / tel / textarea / select.
   .input--error     red border + uses --focus-ring-destructive on focus.
   .input--ok        subtle green tint via --chart-2 (positive validation).
   .help             meta-sized helper text below the input.
   .help--error      red helper text (use with .input--error sibling).
   .fieldset         legend treatment + canonical inset.

   Native <input type="checkbox"> / <input type="radio"> stay native and
   theme via accent-color. No custom toggle widgets in the contract —
   pages compose their own if they need them.
   ============================================================ */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.field > label {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color) 70%, var(--backgroundColor));
  line-height: 1;
}
.field > label.label--required::after {
  content: "*";
  color: var(--red);
  margin-left: 4px;
}

.input {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: var(--sp-3) var(--sp-4);
  background: var(--backgroundColor);
  color: var(--color);
  border: 1px solid var(--color);
  border-radius: var(--radius-control);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: 1.4;
  letter-spacing: var(--tracking-body);
  transition: border-color var(--dur-fast) var(--ease-out-quart),
              outline-color var(--dur-fast) var(--ease-out-quart);
}
.input:focus {
  outline: none;
}
.input:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}
.input::placeholder {
  color: var(--grey-500);
  opacity: 1;
}
textarea.input {
  min-height: 6em;
  resize: vertical;
  font-family: var(--font-mono);  /* mono for free-form notes */
}
select.input {
  background-image:
    linear-gradient(45deg, transparent 50%, var(--color) 50%),
    linear-gradient(135deg, var(--color) 50%, transparent 50%);
  background-position:
    calc(100% - 18px) 50%,
    calc(100% - 13px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--sp-7);
}
.input--error {
  border-color: var(--red);
}
.input--error:focus-visible {
  outline: var(--focus-ring-destructive);
}
.input--ok {
  border-color: var(--chart-2);
}

.help {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  letter-spacing: 0.06em;
  color: var(--grey-500);
  line-height: 1.5;
  margin: 0;
}
.help--error { color: var(--red); }

.fieldset {
  border: 0;
  padding: 0;
  margin: 0 0 var(--sp-6) 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.fieldset > legend {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--fs-h2);
  letter-spacing: var(--tracking-h2);
  color: var(--color);
  margin: 0 0 var(--sp-3);
  padding: 0;
}

/* .input--seam — the editorial intake variant.
   CONTRACT — one underline-field treatment. `.input--seam` is the ONLY
   canonical "field as a line" variant; pages never hand-roll an
   underline input.

   · What it is — the editorial intake field: a field is a LINE, not a
     box. Transparent ground, a single 1px ink baseline, no border, no
     radius. The boxed `.input` is the system default; `.input--seam` is
     the variant long-form intake forms reach for ("underline, not box"
     — components-forms.html §1).
   · Render — display-font; placeholder is italic grey; the baseline
     turns signal-red on focus (the gap-marker, here marking "this is
     where you write"). `select.input--seam` drops the canonical caret
     triangle — the consuming page supplies its own glyph.
   · Element — native `<input>` / `<select>` / `<textarea>` ONLY. The
     doctrine bans custom field widgets; this is a restyle of a real
     form control, not a div-based fake.
   · Contexts it must work in — block-level (the field stretches to its
     container); textarea variant stays in display font and is
     vertically resizable; disabled state dashes the baseline and greys
     the text; the `.input--error` companion turns the baseline red.
     Light/dark via --color/--red/--grey-300.
   · Reduced motion — the focus transition is a border-color change
     governed by --dur-fast, which the global reduced-motion rule clamps
     to ~1ms; the red focus signal still appears, just without easing.

   This is the ONLY seam-field rule. Pages never redeclare or fork it. */
.input--seam {
  padding: 0 0 10px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--color);
  border-radius: 0;
  font-family: var(--font-display);
}
.input--seam:focus-visible {
  outline: none;
  border-bottom-color: var(--red);
}
.input--seam:disabled {
  color: var(--grey-300);
  border-bottom-style: dashed;
}
.input--seam::placeholder {
  font-family: var(--font-italic);
  font-style: italic;
  color: var(--grey-300);
}
textarea.input--seam {
  resize: vertical;
  min-height: 96px;
  padding-top: 8px;
  font-family: var(--font-display);  /* seam textareas stay in display, not mono */
}
/* seam selects drop the canonical triangle — the caret is supplied by the
   consuming page (e.g. a mono `↓` glyph on a positioned wrapper). */
select.input--seam {
  background-image: none;
  padding-right: 0;
}
.input--seam.input--error { border-bottom-color: var(--red); border-color: transparent; }

/* .choice — native checkbox / radio.
   CONTRACT — one checkbox/radio treatment. `.choice` is the ONLY
   canonical class for a binary/single-select form control; pages never
   build a div-based toggle as a default move.

   · What it is — a native `<input type="checkbox">` or
     `<input type="radio">`, themed (not replaced) with contract tokens.
     Per DECISIONS.md: native form elements, styled — never reinvented.
     The doctrine bans custom toggle widgets from the public contract.
   · Render — themed purely via `accent-color: var(--color)`, plus a
     fixed 16×16 box. No custom checkmark SVG, no pseudo-element fake.
     The treatment is identical to the bare-element rule it shares, so
     adding `.choice` is a no-op visual change — it exists as an explicit
     hook (e.g. for a visually-hidden input inside a page-local custom
     control that still needs the accent token).
   · Element — native checkbox/radio inputs ONLY.
   · Contexts it must work in — inline beside a label, or stacked in a
     `.fieldset`; the native control supplies focus ring, keyboard, and
     indeterminate behavior for free. Light/dark via the --color
     accent, which inverts with data-mode.
   · Reduced motion — native controls carry no custom motion; nothing to
     suppress.

   This is the ONLY choice rule. Pages never redeclare or fork it.
   Consumers needing a richer visual variant compose it page-locally
   around a native input — they do not fork this rule. */
input[type="checkbox"],
input[type="radio"],
.choice {
  accent-color: var(--color);
  width: 16px;
  height: 16px;
  margin: 0;
}

/* .ds-range — native `<input type="range">`, themed (not replaced).
   CONTRACT — one slider treatment. `.ds-range` is the ONLY canonical
   class for a numeric scrubber; pages never build a div+drag-handle
   slider as a default move.

   · What it is — a native `<input type="range">`, themed with contract
     tokens. Per DECISIONS.md and DOCTRINE.md§3.7: native form elements,
     styled — never reinvented. The doctrine bans custom slider widgets
     from the public contract (mobile-slider parity is the wedge — the
     native element gets touch, pointer, and keyboard right by default,
     a hand-rolled JS slider gets all three wrong without 200+ LOC).
   · Render — 2px ink rule for the track, a sharp 16px ink-filled
     thumb centered on the rule (no shadow, no rounded corners,
     no glow). `touch-action: manipulation` kills iOS's 300ms tap
     delay so the thumb tracks the finger.
   · Element — native `<input type="range">` ONLY.
   · Cross-browser — both `-webkit-` and `-moz-` track/thumb pseudo-
     elements are styled; Chromium/Safari/Firefox render identically.
   · Disabled — `opacity: 0.4` + `cursor: not-allowed`, no separate
     greyed track (the rule's `var(--color)` already dims).
   · Reduced motion — no custom motion; the native scrub is the only
     movement and that is user-driven.

   This is the ONLY range rule. Pages never redeclare or fork it. */
.ds-range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 16px;
  background: transparent;
  touch-action: manipulation;
  cursor: ew-resize;
  margin: 0;
}
.ds-range:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

/* Track — 2px ink rule, both engines. */
.ds-range::-webkit-slider-runnable-track {
  height: 2px;
  background: var(--color);
  border: 0;
}
.ds-range::-moz-range-track {
  height: 2px;
  background: var(--color);
  border: 0;
}

/* Thumb — sharp 16px ink square, both engines.
   `margin-top: -7px` centers the thumb on the 2px track in Webkit;
   Firefox centers natively. */
.ds-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  background: var(--color);
  border: 0;
  border-radius: 0;
  margin-top: -7px;
  cursor: ew-resize;
}
.ds-range::-moz-range-thumb {
  width: 16px;
  height: 16px;
  background: var(--color);
  border: 0;
  border-radius: 0;
  cursor: ew-resize;
}

.ds-range:disabled {
  cursor: not-allowed;
  opacity: 0.4;
}

/* ============================================================
   OVERLAYS — canonical modal / drawer / toast.

   Promoted from preview/components-overlays.html (was page-local). The
   doctrine in that page is the source of truth; this block is the
   canonical implementation that consumers (and the React kit) get for free.

   .modal-overlay       backdrop (ivory-2 + 8% ink hatch — never blurred glass)
   .modal               body container, default is md (540px)
   .modal--sm           420px (compact confirm)
   .modal--lg           720px (form-bearing modals)
   .modal--destructive  flips top rule to red; pairs with .modal-eyebrow--destructive
   .modal-eyebrow       small-caps mono header row with red dot lede
   .modal-close         × button in the top-right
   .modal-actions       bottom action row, right-aligned

   .drawer              right-side panel (380px default)
   .drawer--wide        480px when forms live inside

   .toast-rail          top-right column where toasts stack
   .toast               single receipt; tone via .toast--success/info/warn/error
   .toast-dot           leading status dot
   .toast-close         × button
   ============================================================ */

/* Positioning is scoped to [data-state] — Radix Dialog sets it on the
   Content element it renders. Preview pages that demo modals INLINE inside
   a `.stage` container (no data-state attr) keep their natural in-flow
   layout via page-local rules. The React kit (always Radix-driven) gets
   fixed-positioning automatically. */
[data-state].modal-overlay {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--color) 5%, var(--backgroundColor));
  background-image: repeating-linear-gradient(135deg,
    transparent 0 14px,
    color-mix(in srgb, var(--color) 4%, transparent) 14px 15px);
  z-index: 1000;
}

[data-state].modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  max-width: calc(100vw - 32px);
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  z-index: 1001;
  box-sizing: border-box;
}

.modal {
  width: 540px;
  background: var(--backgroundColor);
  color: var(--color);
  border-top: 3px solid var(--color);
  border-bottom: 1px solid var(--color);
  padding: var(--sp-6) var(--sp-7) var(--sp-5);
}
.modal--sm { width: 420px; }
.modal--lg { width: 720px; }
.modal--destructive { border-top-color: var(--red); }

/* The eyebrow is one inline row: the red dot, the mono label, and a
   quiet aside clause. Plain inline flow; the label is a bare text
   node, so no flex item is involved. */
.modal-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--color);
  margin: 0 0 var(--sp-4);
}
.modal-eyebrow::before {
  content: "";
  width: 6px;
  height: 6px;
  background: var(--red);
  border-radius: 50%;
  display: inline-block;
  margin-right: var(--sp-3);
  vertical-align: 0.08em;
}
/* Aside: a readable supporting clause. Body font, not the editorial
   italic, so the modal holds two type families (mono label + body),
   not three. */
.modal-eyebrow .modal-eyebrow-aside {
  font-family: var(--font-body);
  font-weight: 400;
  text-transform: none;
  letter-spacing: -0.01em;
  font-size: 13px;
  color: var(--grey-500);
  margin-left: var(--sp-3);
}
.modal--destructive .modal-eyebrow { color: var(--red); }
.modal--destructive .modal-eyebrow::before { background: var(--red); }

.modal-close {
  position: absolute;
  top: var(--sp-4);
  right: var(--sp-5);
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--grey-500);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.modal-close::before { content: "✕ "; color: var(--red); }
.modal-close:hover { color: var(--color); }
.modal-close:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

.modal-title {
  font-family: var(--font-body);
  font-weight: 200;
  font-size: 36px;
  line-height: 1;
  letter-spacing: -0.035em;
  margin: 0 0 var(--sp-3);
  color: var(--color);
}
/* Emphasis in a modal title is a weight step in the same family,
   not a second typeface: the modal is a product surface, restrained. */
.modal-title em {
  font-style: normal;
  font-weight: 400;
}
.modal-description {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--fs-body);
  line-height: 1.5;
  letter-spacing: -0.012em;
  margin: 0 0 var(--sp-4);
  max-width: 50ch;
  color: var(--color);
}
.modal-description em {
  font-family: var(--font-italic);
  font-style: italic;
  color: var(--grey-500);
}

.modal-actions {
  display: flex;
  gap: var(--sp-3);
  justify-content: flex-end;
  align-items: center;
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid color-mix(in srgb, var(--color) 8%, transparent);
}

/* Same scoping pattern as .modal — fixed-positioning is opt-in via
   [data-state] (Radix-driven). Preview demos render inline via page-local
   rules; the React kit gets the right-side panel for free. */
[data-state].drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  max-width: calc(100vw - 32px);
  z-index: 1001;
  overflow-y: auto;
  box-sizing: border-box;
}

.drawer {
  width: 380px;
  background: var(--backgroundColor);
  color: var(--color);
  border-left: 3px solid var(--color);
  padding: var(--sp-5) var(--sp-6);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.drawer--wide { width: 480px; }

.drawer-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--color);
  gap: var(--sp-3);
}
.drawer-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--red);
}
.drawer-title {
  font-family: var(--font-body);
  font-weight: 200;
  font-size: 30px;
  letter-spacing: -0.035em;
  line-height: 1.05;
  margin: 0;
  color: var(--color);
}
.drawer-title em {
  font-family: var(--font-italic);
  font-style: italic;
  font-weight: 400;
  opacity: 0.35;
}
.drawer-body {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: -0.012em;
  flex: 1 1 auto;
  overflow-y: auto;
}
.drawer-footer {
  margin-top: auto;
  display: flex;
  gap: var(--sp-2);
  justify-content: flex-end;
  padding-top: var(--sp-3);
  border-top: 1px solid color-mix(in srgb, var(--color) 8%, transparent);
}

/* Toast viewport — scoped to [role="region"] which Radix Toast.Viewport sets
   automatically. Preview demos use a plain <div class="toast-rail"> for the
   teaching layout and need their own positioning. */
[role="region"].toast-rail {
  position: fixed;
  top: var(--sp-5);
  right: var(--sp-5);
  z-index: 1100;
  width: min(380px, calc(100vw - 32px));
  pointer-events: none;
  /* Reset Radix's default list styling on the <ol> Viewport renders. */
  margin: 0;
  padding: 0;
  list-style: none;
}
[role="region"].toast-rail > * { pointer-events: auto; }

.toast-rail {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.toast {
  background: var(--backgroundColor);
  color: var(--color);
  border-top: 3px solid var(--color);
  border-bottom: 1px solid var(--color);
  padding: var(--sp-3) var(--sp-4) var(--sp-3);
  display: grid;
  grid-template-columns: 18px 1fr auto;
  gap: var(--sp-3);
  align-items: baseline;
}
[role="region"] .toast { box-sizing: border-box; }
.toast-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color);
  margin-top: 6px;
  flex-shrink: 0;
}
.toast-body {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 14px;
  line-height: 1.4;
  letter-spacing: -0.012em;
  color: var(--color);
}
.toast-title {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  display: block;
  margin-bottom: 2px;
  color: var(--color);
}
.toast-description {
  display: block;
}
.toast-description em {
  font-family: var(--font-italic);
  font-style: italic;
  color: var(--grey-500);
}
.toast--warn .toast-title,
.toast--error .toast-title { color: var(--red); }
.toast--info .toast-title { color: var(--grey-500); }

.toast-close {
  font-family: var(--font-mono);
  font-size: var(--fs-meta);
  color: var(--grey-500);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.toast-close:hover { color: var(--color); }
.toast-close:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

.toast--success { border-top-color: var(--color); }
.toast--success .toast-dot { background: var(--color); }

.toast--info { border-top-color: var(--grey-500); }
.toast--info .toast-dot { background: var(--grey-500); }

.toast--warn { border-top-color: var(--red); }
.toast--warn .toast-dot { background: var(--red); }

.toast--error { border-top-color: var(--red); }
.toast--error .toast-dot { background: var(--red); }

@media (prefers-reduced-motion: reduce) {
  /* No bespoke animation here, but Radix Dialog/Toast may add transitions
     via data-state. Keep them snap-to-end so motion-sensitive users still
     see the same final layout. */
  .modal-overlay,
  .modal,
  .drawer,
  .toast {
    animation: none !important;
    transition: none !important;
  }
}

/* ============================================================
   LAYOUT PRIMITIVES — canonical full-page composition.

   CONTRACT — three primitives compose every full page; pages never
   fabricate their own width-clamp or section wrapper. Consumers stack
   `.section` blocks inside a `.rail` — that is the whole page-layout
   vocabulary. These replace the page-local classes landing.html and
   feature.html used to invent.

   .rail            1200px max-width content rail (live-site default)
   .rail--narrow     920px variant for interior pages (deeper prose)
   .topbar          sticky top bar; hairline border, opaque background
   .section         full-section block, min-height 80vh, vertical center
   .section--tinted same as .section, with the canonical 4% color-mix tint
   ============================================================ */

/* ---------- RAIL (the page width-clamp) ----------
   CONTRACT — one width-clamp. `.rail` is the ONLY canonical max-width
   container; pages never hand-roll a `max-width: Npx; margin: 0 auto`.

   · What it is — the horizontal content rail: caps content at 1200px,
     centers it, and supplies the responsive side gutter. Everything on a
     full page lives inside exactly one rail.
   · Render — `box-sizing: border-box` is load-bearing: it keeps the
     clamp gutter INSIDE the width so `width:100%` + padding never
     overflows the viewport. Pages must satisfy
     `scrollWidth === clientWidth` at 360 / 390 / 414 (the mobile-overflow
     audit).
   · Contexts it must work in — block-level only; full-width down to
     mobile (the gutter is `clamp(20px, 1.6vw, ...)`, never zero). One
     variant: `.rail--narrow` (920px) for prose-heavy interior pages.
     `.section--tinted` deliberately breaks out of the rail gutter with
     negative margins — that is the one sanctioned exception.
   · Reduced motion / light-dark — no motion, no color of its own; it is
     pure geometry and is theme-agnostic.

   This is the ONLY rail rule. Pages never redeclare or fork it. */
.rail {
  /* box-sizing: border-box keeps padding INSIDE the width — without it,
     `width: 100%` + 20px+20px padding totals 100vw + 40px and pushes 40px
     past the viewport at narrow widths (caught by the mobile-overflow
     audit). Pages must scrollWidth === clientWidth at 360/390/414. */
  box-sizing: border-box;
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding-left: clamp(20px, 1.6vw, 1.6vw);
  padding-right: clamp(20px, 1.6vw, 1.6vw);
}
.rail--narrow {
  max-width: 920px;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: clamp(14px, 1.1vw, 1.1vw) 0;
  background: var(--backgroundColor);
  border-bottom: 1px solid color-mix(in srgb, var(--color) 8%, transparent);
}

/* ----------------------------------------------------------------
   .mast — canonical editorial masthead (3-col newspaper-style header).

   Used by index.html, color-system, type-system, doctrine, components-nav
   — i.e. every page-level surface that's NOT a sticky live-site .topbar.

   Promoted to canonical from four near-identical page-local declarations
   (.sys-mast / .mast / .masthead) that all painted the same chassis under
   different names. Same border-top 3px / border-bottom 1px, same mono
   uppercase 10px 0.14em, same padding 14px/0/10px.

   Slot order (positional, no class needed on children):
     :first-child       wordmark + DAG mark, left-aligned
     :nth-child(2)      page title / nº, centered, slightly looser tracking
     :last-child        edition info / date, right-aligned

   For the 4-column nav variant (wordmark · navlinks · date · CTA),
   add `.mast--nav`. -------------------------------------------------- */
.mast {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 24px;
  border-top: 3px solid var(--color);
  border-bottom: 1px solid var(--color);
  padding: 14px 0 10px;
  margin-bottom: 64px;
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--grey-500);
}
.mast > :nth-child(2) { text-align: center; letter-spacing: 0.18em; }
.mast > :last-child:not(:first-child):not(:nth-child(2)) { text-align: right; }

/* 4-col nav variant: wordmark · navlinks · date · CTA */
.mast--nav {
  grid-template-columns: auto 1fr auto auto;
  gap: 32px;
  padding: 14px 20px;
  background: var(--backgroundColor);
}
.mast .navlinks { display: flex; gap: 24px; }
.mast .navlinks a { color: var(--grey-500); text-decoration: none; padding-bottom: 2px; }
.mast .navlinks a.on { color: var(--color); border-bottom: 1px solid var(--color); }
.mast .navlinks a:hover { color: var(--color); }

/* Masthead CTA — the compact bordered link in the 4-col .mast--nav
   variant. Smaller and quieter than the .b button engine (a masthead
   sits above content, not inside it); fills ink on hover. The trailing
   .arr glyph travels NE, matching the directional motion vocabulary. */
.mast .cta {
  font-family: var(--font-display); font-weight: 300; font-size: 13px;
  letter-spacing: -0.012em; padding: 8px 14px;
  border: 1px solid var(--color); color: var(--color);
  transition: background var(--dur-fast) var(--ease-causal),
              color var(--dur-fast) var(--ease-causal);
}
.mast .cta:hover { background: var(--color); color: var(--backgroundColor); }
.mast .cta .arr {
  font-family: var(--font-mono); margin-left: 6px;
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-causal);
}
.mast .cta:hover .arr { transform: translate(3px, -3px); }
@media (prefers-reduced-motion: reduce) {
  .mast .cta .arr { transition: none; }
  .mast .cta:hover .arr { transform: none; }
}

/* ---------- PAGER (numbered pagination row) ----------
   CONTRACT — one pagination row. `.pager` is the ONLY numbered-page
   navigation primitive. Pages never hand-roll a pagination strip.

   · What it is — a mono-caps pagination row: `prev · numbered pages ·
     next`, the numbered pages held in a `.nums` flex group. A link role,
     not a button — it carries no border, fill, or sweep.
   · Render — quiet grey mono caps; page links sharpen to ink on hover.
     The current page (`.now`) carries a 2px red underbar — the SAME
     current-marker the masthead navlinks and the `.tab` strip use; one
     "you are here" marker across the system.
   · Anchors inside reset the signature-link treatment — no blur, no
     dotted border, no draw-in ::after — so the row reads as furniture,
     not prose. This reset is intentional and part of the contract.
   · Contexts it must work in — block-level, sits below content above a
     hairline; light/dark via --grey-500/--color/--red. The current
     marker is `.now`, never a fork.
   · Reduced motion — only the hover color transition exists; it is
     dropped under reduced motion (see the @media block below).

   This is the ONLY pager rule. Pages never redeclare or fork it.
   Catalogued in components-nav.html §F. */
.pager {
  display: flex; align-items: baseline; justify-content: space-between;
  padding-top: 18px; border-top: 1px solid var(--hair); margin-top: 18px;
  font-family: var(--font-mono); font-size: 10px;
  text-transform: uppercase; letter-spacing: 0.14em; color: var(--grey-500);
}
.pager .nums { display: flex; gap: 14px; }
.pager a {
  color: var(--grey-500); text-decoration: none;
  border: 0; filter: none; opacity: 1; padding: 0;
  transition: color var(--dur-fast) var(--ease-causal);
}
.pager a:hover { color: var(--color); }
.pager .now { color: var(--color); border-bottom: 1px solid var(--red); padding-bottom: 1px; }
@media (prefers-reduced-motion: reduce) {
  .pager a { transition: none; }
}

/* ---------- SECTION (the full-page vertical block) ----------
   CONTRACT — one section block. `.section` is the ONLY canonical
   full-height page band; pages never hand-roll a `min-height: 100vh;
   display: flex` story block.

   · What it is — a vertical content band sized to the viewport: at
     least 80vh tall, its children vertically centered, with the
     canonical inter-block rhythm. Full pages are a stack of `.section`s
     inside one `.rail`.
   · Render — `min-height: 80vh` (not 100vh — leaves a sliver of the
     next band visible, signalling scroll); column flex, content
     centered; `padding` and `gap` use clamp() so the rhythm holds from
     mobile to desktop.
   · Contexts it must work in — block-level, stacked inside a `.rail`;
     works at every width via the clamp()s. One variant:
     `.section--tinted` adds the canonical 4% color-mix tint and breaks
     out of the rail gutter with negative margins so the tint runs
     edge-to-edge.
   · Reduced motion / light-dark — no motion of its own; sections opt
     into the reveal entrance via `data-reveal` if wanted. The tinted
     variant's color-mix tracks the active theme automatically.

   This is the ONLY section rule. Pages never redeclare or fork it. */
.section {
  min-height: 80vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(60px, 6vw, 6vw) 0;
  gap: clamp(24px, 1.8vw, 1.8vw);
}
.section--tinted {
  background: color-mix(in srgb, var(--color) 4%, var(--backgroundColor));
  /* break out of the rail's padding when nested inside one */
  margin-left: calc(-1 * clamp(20px, 1.6vw, 1.6vw));
  margin-right: calc(-1 * clamp(20px, 1.6vw, 1.6vw));
  padding-left: clamp(20px, 1.6vw, 1.6vw);
  padding-right: clamp(20px, 1.6vw, 1.6vw);
}

/* ----------------------------------------------------------------
   Every Layout primitives — l-* namespace.

   CONTRACT — when a page needs a responsive layout that fits one of
   Heydon Pickering's Every Layout primitives (Stack, Cluster, Sidebar,
   Switcher, Cover, Center, Reel, Imposter, Box, Icon — see
   every-layout.dev), use the recipe verbatim under the `.l-<primitive>`
   class name. Hand-rolled `grid-template-columns: repeat(N, 1fr)` +
   `@media (max-width: ...)` is the antipattern this replaces; the
   primitives use intrinsic sizing so layout decisions live at the
   container, not the viewport. DOCTRINE.md§2.2 carries the full list +
   the `@container` vs `@media` rule.

   The l-* namespace is canonical reserved territory. Only canonical
   recipes ship here; promoting a new primitive follows §2.10 (fourth-
   occurrence rule). New entries are registered in DOCTRINE.md§1. ------ */

/* .l-switcher — the canonical Every Layout Switcher.
   · Above the threshold, children share width equally on one row
     (`flex: 1` via large-positive basis); below it, they stack to a
     single column. No `@media` query — the container's own width drives
     the fold, so the same primitive works inside a card, a sidebar, or
     a full rail with no rewrite.
   · The fold threshold is `--l-switcher-threshold` (default `30rem` /
     ~480px), overridable per-instance. The `* 999` multiplier locks the
     binary choice — at one pixel above the threshold flex-basis goes
     hugely negative and `flex-grow: 1` takes over; one pixel below, it
     becomes hugely positive and each child fills `100%`.
   · Wrap gap is the canonical `clamp(16px, 1.4vw, 1.4vw)`. Override on
     the instance if a denser/sparser composition wants different rhythm.
   · Reduced motion / light-dark — no motion, no color of its own; pure
     intrinsic geometry, theme-agnostic.
   See every-layout.dev/layouts/switcher/ for the canonical recipe. */
.l-switcher {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(16px, 1.4vw, 1.4vw);
}
.l-switcher > * {
  flex-grow: 1;
  flex-basis: calc((var(--l-switcher-threshold, 30rem) - 100%) * 999);
}

/* ============================================================
   Motion vocabulary — system-wide.
   Causal motion: arrows have direction, motion follows the axis.
   The CONTROL itself stays still; pseudo-elements and descendants move.
   Any preview card can opt into directional motion via [data-dir].
   ============================================================ */

:root {
  /* Easing curves. Restrained deceleration — no bounce, no elastic. */
  --ease-causal: cubic-bezier(0.7, 0, 0.25, 1);     /* matches button engine */
  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);  /* secondary, smoother  */

  /* Duration tokens */
  --dur-fast: 200ms;     /* hover, focus feedback */
  --dur-base: 380ms;     /* state changes, sweeps */
  --dur-slow: 1100ms;    /* entrances, draw-ins, strikes — one knob for the lot */

  /* Direction tokens (defaults — overridden by [data-dir] on the element).
     -1 = up/left, 0 = none, 1 = down/right. */
  --ax: 1;
  --ay: -1;  /* default: ne — east-ish, up-ish */
}

/* Direction tokens — any element with [data-dir] opts into the engine. */
[data-dir="e"]  { --ax:  1; --ay:  0; }
[data-dir="ne"] { --ax:  1; --ay: -1; }
[data-dir="n"]  { --ax:  0; --ay: -1; }
[data-dir="nw"] { --ax: -1; --ay: -1; }
[data-dir="w"]  { --ax: -1; --ay:  0; }
[data-dir="sw"] { --ax: -1; --ay:  1; }
[data-dir="s"]  { --ax:  0; --ay:  1; }
[data-dir="se"] { --ax:  1; --ay:  1; }

/* Reduced motion — global tier-1/tier-2 floor.
   See DOCTRINE.md§4.2 for the full posture (three tiers: essential feedback,
   brand-narrative snap-to-end-state, decorative loops slow-or-stop).
   This block clamps every var(--dur-*) and any direct animation/transition
   to ~1ms so motion that uses the tokens snaps to its end-state cleanly.
   Per-page tier-3 affordances (spinners, live pulses, skeleton shimmers)
   override locally to slow or stop while preserving the visual signal. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 1ms;
    --dur-base: 1ms;
    --dur-slow: 1ms;
  }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

/* ---------- LINK (signature: recessive-at-rest, sharpen-on-hover) ----------
   CONTRACT — one prose-link treatment, applied by element. Every bare
   <a> in the system IS the signature link: there is no `.link` class to
   remember (the class exists only as an explicit alias). Pages never
   restyle the bare anchor.

   · What it is — the subconscious.ai signature link: a recessive dotted
     underline at rest, resolving to a solid-ink-weight line on hover.
     Reads as "not-quite-resolved until you engage it" — without ever
     touching the glyphs (an earlier `filter: blur(0.7px)` implementation
     hurt body-size legibility on 1× displays; #396).
   · Render — `border-bottom: 1px dotted` at 35% currentColor + `opacity:
     0.78` at rest; hover flips the border to a solid line at full
     currentColor and lifts opacity to 1. The glyphs themselves never
     change rendering — no filter, no transform. Colors are
     currentColor-derived so the link repaints correctly under any theme.
   · Three link ROLES, one vocabulary — the bare link is the default; the
     two sharp variants below (`a.cite`, `a.editorial`) are deliberate
     counterparts, not forks. Pick the role, never invent a fourth.
   · Contexts it must work in — inline in prose (the common case), inside
     a heading, trailing a quote, inside charts (`a.red` tints the line
     signal-red for source links). The `.link--draw` opt-in swaps the
     dotted border for a scaleX(0)→(1) ::after that draws in on
     [data-revealed] — the same drawing choreography as the strike and
     the doctrine bars; one drawing language across every line.
   · Reduced motion — the opacity/border transition is suppressed and the
     draw-in ::after snaps to scaleX(1) (see the @media block below).
   · Opt out — `.no-link` strips the treatment for purely structural
     anchors (nav tiles, card-wrapping links) that should not read as
     prose links. `a.has-hover`/`a.has-focus`/`a.is-disabled` are
     state-demo pins for a11y galleries, never product markup.

   This is the ONLY bare-link rule. Pages never redeclare or fork it. */
/* Recessive-at-rest link recipe: the underline is visibly *fuzzy* at rest
   and *sharpens* on hover/focus. The fuzz lives on a decorative ::after
   pseudo-element so `filter: blur()` only touches decoration — never the
   glyphs. PR #398 removed an earlier `filter: blur()` on the <a> itself
   because it softened body-text legibility on 1× displays; this recipe
   restores the fuzzy→crisp contrast without that cost.

   At rest: glyphs sharp, opacity 0.85; the underline is a 4px-wide
   repeated radial-dot pattern (currentColor dots on transparent),
   blurred 0.8px. On hover/focus: the ::after blur drops to 0, opacity
   to 1 — a sharp, resolved dotted line. The glyphs themselves never
   gain a filter, never transform, never shift in weight. currentColor
   lets every state repaint correctly under any active theme.

   History: PR #439 collapsed this to a native `text-decoration` with
   only a color contrast transition — but native text-decoration cannot
   blur, and `components-links.html`'s own signature-link doctrine
   ('dotted 1px · blur 0.8px at rest · sharpens on hover · opacity .85
   → 1') requires blur. The page that documents the doctrine was
   shipping the anti-pattern. This restores the doctrine; the variants
   (a.cite, a.editorial, a.link--draw) keep their own underline chrome
   and suppress the inherited ::after at lines 1393–1399 below. */
a, a.link {
  color: var(--color);
  position: relative;
  cursor: pointer;
  text-decoration: none;
}
a::after, a.link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 2px;
  /* Dotted underline rendered as a tiled radial-gradient — ~1.4px dot,
     ~2.6px gap — so we can apply `filter: blur()` independently of the
     text. native `text-decoration` cannot blur; this can. */
  background: radial-gradient(
    circle,
    currentColor 0 1px,
    transparent 1.2px
  ) 0 0 / 4px 2px repeat-x;
  opacity: 0.85;
  filter: blur(0.8px);
  pointer-events: none;
  transition:
    filter var(--dur-base) var(--ease-causal),
    opacity var(--dur-base) var(--ease-causal);
}

a:hover::after, a.link:hover::after,
a:focus-visible::after, a.link:focus-visible::after {
  filter: blur(0);
  opacity: 1;
}

a.no-link {
  text-decoration: none;
  opacity: 1;
}
/* a.no-link must also suppress the inherited fuzzy `a::after` — without
   this, a structural anchor wrapping a card would render a phantom
   blurred dotted line near its bottom edge. */
a.no-link::after { content: none; }
/* Pages that want the editorial scaleX draw-in (the doctrine bar that
   paints under each link when the card reveals) opt in with `.link--draw`.
   Uses ::after so it composites under the dotted border. */
a.link--draw {
  position: relative;
  border-bottom: 0;
  padding-bottom: 1px;
}
a.link--draw::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: color-mix(in srgb, currentColor 40%, transparent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-slow) var(--ease-out-quart);
}
[data-revealed] a.link--draw::after { transform: scaleX(1); }
/* red-accent variant (source links inside charts, uplift callouts).
   currentColor on a::after already paints the underline red via the
   color: var(--red) cascade — no per-state override needed. */
a.red { color: var(--red); }

/* citation mark — the §2 link role from components-links.html.
   CONTRACT — one citation treatment. `a.cite` is the ONLY way to mark a
   source reference. Pages never fork it or hand-roll a "[1]"-style tag.

   · What it is — a mono red uppercase tag that TRAILS a claim or a
     signature link and points at its evidence. The smallest, quietest
     link role; it reads as a footnote marker, not a CTA.
   · Render — mono caps, 0.78em, signal-red, no border-bottom (the
     trailing ↗ glyph is the affordance, not an underline). On hover the
     text underlines and the glyph travels NE.
   · Contexts it must work in — inline only, and never standalone: it
     must trail a host (a claim, a stat, a signature link). Pair the
     trailing glyph with `.arr` so it travels NE on hover, matching the
     system's directional motion vocabulary. Works in light/dark via the
     --red token.
   · Reduced motion — the glyph travel is dropped; the hover underline
     survives (see the @media block below).

   This is the ONLY citation rule. Pages never redeclare or fork it. */
a.cite {
  font-family: var(--font-mono); font-size: 0.78em;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--red); border-bottom: 0;
  margin-left: 4px;
}
a.cite:hover { text-decoration: underline; }
a.cite .arr {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-causal);
}
a.cite:hover .arr { transform: translate(3px, -3px); }
@media (prefers-reduced-motion: reduce) {
  a.cite .arr { transition: none; }
  a.cite:hover .arr { transform: none; }
}

/* editorial link — the sharp counterpart to the soft signature link.
   CONTRACT — one editorial-link treatment. `a.editorial` is the ONLY
   sharp inline link role. Pages never fork it.

   · What it is — the sharp counterpart to the soft bare link: a solid
     1px ink underline at rest (no blur, no dotted line), flipping to
     signal-red on hover. Used where a link must read as deliberate and
     resolved — an inline link inside prose, or trailing a quote — and
     the citation mark is too small to carry the weight.
   · Render — display-font, solid underline, optional trailing `.arr`
     glyph; hover flips text + underline + glyph to --red, no motion
     (the sharpness IS the signal — it does not blur or draw).
   · Contexts it must work in — inline in prose, trailing a quote or a
     pull-stat. `display: inline-flex` keeps an optional arrow glyph
     baseline-aligned. Light/dark via --color and --red.
   · Reduced motion — no motion to suppress; the hover is a pure color
     change, already reduced-motion-safe.

   This is the ONLY editorial-link rule. Pages never redeclare or fork it.
   Catalogued in components-buttons.html §05. */
a.editorial {
  font-family: var(--font-display);
  font-weight: 300; font-size: 17px;
  letter-spacing: -0.015em;
  color: var(--color);
  display: inline-flex; align-items: baseline; gap: 10px;
  padding: 0 0 3px;
  filter: none; opacity: 1;
  border-bottom: 1px solid var(--color);
  transition: color 200ms, border-color 200ms;
}
a.editorial .arr {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.95em;
  display: inline-block;
  transition: color 200ms;
}
a.editorial:hover { filter: none; color: var(--red); border-bottom-color: var(--red); }
a.editorial:hover .arr { color: var(--red); }

/* (a.no-link is defined alongside the bare-<a> rule above — single source.) */

/* Variants that carry their own underline (or intentionally none) must
   suppress the inherited fuzzy `a::after`. Without this, `a.editorial`
   renders BOTH its sharp `border-bottom` AND the bare-<a> fuzzy
   pseudo-element underneath — two underlines stacked. Same hazard for
   `a.cite`, where the trailing ↗ glyph is the affordance by doctrine
   (DOCTRINE.md §1) and a fuzzy line contradicts it. */
a.editorial::after,
a.cite::after { content: none; }

/* Static state demos for <a> — the same convention as .b's .has-hover /
   .has-focus: pin a register the screenshot can't capture live. Used by
   a11y.html's state matrix. .is-disabled is demo-only (links have no real
   disabled attribute); it greys the line to the disabled token.

   .has-hover and .has-focus pin the *resolved* underline (the crisp solid
   2px form), not the at-rest dotted form — that's the whole point of a
   hover/focus demo. Mirror the :hover / :focus-visible rule. */
a.has-focus {
  outline: var(--focus-ring);
  outline-offset: 3px;
  opacity: 1;
}
a.has-focus::after,
a.has-hover::after {
  /* Mirror the :hover / :focus-visible end-state: sharpened dotted line. */
  filter: blur(0);
  opacity: 1;
}
a.has-hover {
  opacity: 1;
}
a.is-disabled {
  color: var(--grey-300);
  cursor: not-allowed;
}
a.is-disabled::after {
  /* Disabled demo: dots stay blurred AND go ghost (no sharpen path). */
  opacity: 0.35;
}

/* .site-btn was a fabricated class in #149 that duplicated .b[data-dir]'s
   role. Removed in #150 — pages compose buttons from the canonical
   .b[data-dir] engine instead. The 8-direction sweep is the system's
   characteristic motion; keep it. */

@media (prefers-reduced-motion: reduce) {
  /* The pseudo-element underline transitions blur+opacity; drop them under
     reduced-motion and snap to the resolved (sharp) state. The dotted
     underline + sharp glyphs read the link clearly without animation. The
     `.link--draw` opt-in still needs scaleX(1) so its drawn-line variant
     doesn't sit invisibly. */
  a::after, a.link::after { transition: none; filter: blur(0); opacity: 1; }
  a.link--draw::after { transform: scaleX(1) !important; }
}

/* ---------- MONO ---------- */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ---------- DAG MARK (canonical brand ornament) ----------
   CONTRACT — one brand mark. `.dag-mark` is the ONLY canonical causal
   ornament; pages never redraw the DAG glyph or fork its animation.

   · What it is — the Subconscious brand mark: the Pearl backdoor
     scenario rendered as a 3-node DAG. C (confounder, top, dashed) with
     grey confounder paths down to T and O, and one signal-red T→O
     causal arrow. The mark embodies the doctrine — "find the causal
     arrow despite confounding."
   · Render — pure SVG; paste the canonical markup below verbatim and
     add `class="dag-mark"`. Three sizes via modifier: `.dag-mark`
     (60×42, default), `.dag-mark--xs` (24×14, confounder dropped — just
     T→O), `.dag-mark--lg` (120×84). Strokes use `vector-effect:
     non-scaling-stroke` so weight holds at every size. The only red
     stroke is the T→O arrow — the one-red-arrow rule.
   · REVEAL-GATED — this is the load-bearing behavior. The mark renders
     INVISIBLE at rest (arrows at `stroke-dashoffset`, nodes at
     `scale(0)`) and draws in only when an ANCESTOR carries
     [data-revealed]. The mark has NO trigger of its own — reveal.js
     observes `.mast`, `.card`, `.page`, and `.struck` (SELECTOR in
     reveal.js), and the mark draws when whichever of those wraps it is
     revealed. THEREFORE: the mark must be placed inside a container that
     reveal.js observes, AND that container must be on-screen / get
     revealed wherever the mark is expected to be visible. A `.dag-mark`
     dropped outside a `.mast`/`.card`/`.page`/`.struck` scope, or inside
     one that never enters the viewport, stays permanently invisible.
     The mast carries the mark on every page precisely because the mast
     is always near the top and always observed.
   · Stagger — arrows draw via stroke-dashoffset, nodes pop via scale
     (no bounce — DOCTRINE.md§3.3); the red T→O arrow draws LAST so the
     eye lands on the causal claim.
   · Reduced motion — snaps to the final drawn state (full DAG visible),
     no animation (see the @media block below).
   · Light/dark — node fills/strokes are --color/--backgroundColor/
     --grey-500/--red; the mark tracks the theme automatically.

   This is the ONLY dag-mark rule. Pages never redeclare or fork it.

   The Pearl backdoor scenario rendered as a 3-node DAG:
     C (confounder, top, dashed)
       ↘  ↙
     T → O   ← red causal arrow
   Pages that want the brand mark drop the SVG snippet below and add
   class="dag-mark" to it. Three sizes via modifier classes:

     .dag-mark              60×42 default — corner/section watermark
     .dag-mark--xs          24×14         — inline ornament, no confounder
     .dag-mark--lg          120×84        — section divider

   The mark embodies the doctrine: the only red arrow is T→O (the causal
   claim we measure). Confounder paths and the latent C node are grey. The
   visual reads as "find the causal arrow despite confounding" — which is
   what Subconscious's RCT-on-LLMs methodology does.

   Animation: arrows draw via stroke-dashoffset, nodes pop via scale. The
   T→O red arrow draws *last* — it's the punchline. All gated on
   [data-revealed] from any ancestor; reduced-motion snaps to final.

   Canonical SVG markup (paste into the page where the mark should appear):

     <svg class="dag-mark" viewBox="0 0 60 42" aria-hidden="true">
       <line class="dm-a" data-arr="1" x1="29" y1="9"  x2="13" y2="29"/>
       <line class="dm-a" data-arr="2" x1="31" y1="9"  x2="47" y2="29"/>
       <line class="dm-a dm-causal" data-arr="3" x1="13" y1="32" x2="47" y2="32"/>
       <circle class="dm-n dm-n-c" data-node="c" cx="30" cy="6" r="2.4"/>
       <circle class="dm-n dm-n-t" data-node="t" cx="10" cy="32" r="2.4"/>
       <circle class="dm-n dm-n-o" data-node="o" cx="50" cy="32" r="2.4"/>
     </svg>
*/
.dag-mark {
  display: inline-block;
  width: 60px;
  height: 42px;
  vertical-align: middle;
  flex-shrink: 0;
  overflow: visible;
}
.dag-mark--xs { width: 24px; height: 14px; }
.dag-mark--lg { width: 120px; height: 84px; }

/* Nodes: latent C is dashed grey; intervened T is filled ink; outcome O
   is open ink. The pattern matches causality.html's existing vocabulary
   (.n-lat / .n-int / .n-obs) so the corner mark and the full DAG diagrams
   share a visual language. */
.dag-mark .dm-n {
  stroke-width: 0.8;
  vector-effect: non-scaling-stroke;
}
.dag-mark .dm-n-c {
  fill: var(--backgroundColor);
  stroke: var(--grey-500);
  stroke-dasharray: 1.4 1;
}
.dag-mark .dm-n-t {
  fill: var(--color);
  stroke: var(--color);
}
.dag-mark .dm-n-o {
  fill: var(--backgroundColor);
  stroke: var(--color);
}

/* Arrows: confounder paths are thin grey; the T→O causal arrow is red,
   slightly heavier. Only one red arrow per mark — the doctrinal rule. */
.dag-mark .dm-a {
  fill: none;
  stroke: var(--grey-500);
  stroke-width: 0.8;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.dag-mark .dm-a.dm-causal {
  stroke: var(--red);
  stroke-width: 1.4;
}

/* ---------- Animation ----------
   Arrows draw via stroke-dasharray + dashoffset. Nodes arrive with a
   smooth ease-out-quart scale-up (no bounce — DOCTRINE.md§3.3). The red
   T→O arrow draws last so the eye lands on it after the confounder path
   is established. Stagger:

     C node           settles at  100ms
     C→T arrow draws  starts      200ms (arrives ≈900ms)
     T node settles               350ms
     C→O arrow draws  starts      400ms
     O node settles               560ms
     T→O red draws    starts      640ms — the punchline arrives last

   Mechanism: `@keyframes dag-arrow-draw` + `@keyframes dag-node-pop`
   apply on `[data-revealed]` ancestors only. The keyframe form (rather
   than the earlier transition form) means the canonical DAG draw-in
   motion is a named, discoverable, shippable rule in dist/style.css —
   kit consumers and downstream apps can reference these by name without
   having to reconstruct the curves from the transition declarations
   (issue #383). At-rest (no `[data-revealed]`) the static base rules
   leave the mark hidden (`stroke-dashoffset: 60` / `scale(0)`); reveal
   triggers the keyframes once with `forwards` fill so the end state
   sticks.
*/
.dag-mark .dm-a {
  stroke-dasharray: 60;
  stroke-dashoffset: 60;
}
[data-revealed] .dag-mark .dm-a {
  animation: dag-arrow-draw var(--dur-slow) var(--ease-out-quart) forwards;
}
.dag-mark [data-arr="1"] { animation-delay: 200ms; }
.dag-mark [data-arr="2"] { animation-delay: 400ms; }
.dag-mark [data-arr="3"] { animation-delay: 640ms; }
@keyframes dag-arrow-draw {
  from { stroke-dashoffset: 60; }
  to   { stroke-dashoffset: 0; }
}

.dag-mark .dm-n {
  transform: scale(0);
  transform-origin: center;
  transform-box: fill-box;
}
[data-revealed] .dag-mark .dm-n {
  animation: dag-node-pop var(--dur-base) var(--ease-out-quart) forwards;
}
.dag-mark [data-node="c"] { animation-delay: 100ms; }
.dag-mark [data-node="t"] { animation-delay: 350ms; }
.dag-mark [data-node="o"] { animation-delay: 560ms; }
@keyframes dag-node-pop {
  from { transform: scale(0); }
  to   { transform: scale(1); }
}

/* xs variant: drop the confounder, keep just T→O. Used inline next to
   wordmarks or as a typographic ornament. */
.dag-mark--xs [data-arr="1"],
.dag-mark--xs [data-arr="2"],
.dag-mark--xs [data-node="c"] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .dag-mark .dm-a {
    stroke-dashoffset: 0 !important;
    animation: none !important;
  }
  .dag-mark .dm-n {
    transform: scale(1) !important;
    animation: none !important;
  }
}

/* ---------- THE STRIKE (.struck) ----------
   CONTRACT. One class, one behavior. `.struck` marks a stated claim
   as a lie: a signal-red line drawn through the text LEFT TO RIGHT,
   like a pen — one line at a time, top to bottom on wrapped text.
   A page writes `class="struck"`, nothing else.

   Render: native `text-decoration: line-through` is the static
   substrate — multi-line-correct, on top of the glyphs, and the
   no-JS / reduced-motion / visual-regression resting state (always a
   complete strike, never a bare word). reveal.js's strike animator
   measures each line box, lays one signal-red bar per line, and
   wipes each `transform: scaleX(0->1)` from a left origin staggered
   top-to-bottom, then hides the native line (`.struck.is-drawn`).
   Draws once when the claim scrolls into view.

   Reduced motion: no bars; the static native line-through stays.

   This is the ONLY strike rule. Pages never redeclare or fork it. */
.struck {
  position: relative;
  font-family: var(--font-italic);
  font-style: italic;
  font-weight: 400;
  color: var(--grey-500);
  text-decoration: line-through;
  text-decoration-color: var(--red);
  text-decoration-thickness: 3px;
}
/* JS-enhanced: the per-line bars carry the strike; native line hidden. */
.struck.is-drawn { text-decoration-color: transparent; }
.struck .sl {
  position: absolute;
  height: 3px;
  background: var(--red);
  transform: translateY(-50%) scaleX(0);
  transform-origin: left center;
  pointer-events: none;
}
.struck.is-drawn .sl {
  animation: struck-wipe calc(var(--dur-slow) * 0.72) var(--ease-causal) both;
}
@keyframes struck-wipe {
  from { transform: translateY(-50%) scaleX(0); }
  to   { transform: translateY(-50%) scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  /* no bars — fall back to the static native line-through */
  .struck.is-drawn { text-decoration-color: var(--red); }
  .struck .sl { display: none; }
}

/* counter.js sets `display: inline-block` on every [data-counter] span
   for tabular-nums slot reservation. inline-block creates an atomic
   inline box, which by default does NOT propagate the parent's
   text-decoration through it, so a strike across "...pay 20% more..."
   would skip the number. `text-decoration: inherit` makes data-counter
   elements participate in the parent line-through. */
[data-counter] {
  text-decoration: inherit;
}

/* ============================================================
   .b — the canonical button engine
   ---------------------------------------------------------
   CONTRACT — one engine, one motion. `.b[data-dir]` is the
   ONLY button in the system. Every clickable command surface
   — primary CTAs, ghost actions, destructive confirms, pills,
   icon buttons — is this engine plus a modifier. Pages never
   fork it, never fabricate a `.site-btn`/`.cta-btn` sibling
   (see #149/#150 — `.site-btn` was deleted for exactly that).

   · What it is — a bordered command control with a directional
     ink-sweep on hover: a ::before slab and a paired arrow
     glyph cross the control along the [data-dir] axis. The
     control box itself stays still; only the slab and arrow
     move. Cause on one side, effect on the other.
   · Opt-in — the engine is scoped to `.b[data-dir]`, never
     bare `.b`. Plain `class="b"` is intentionally NOT a button
     (doctrine.html uses `.r .b` for body text). A canonical
     button always carries a direction attribute.
   · data-dir — required. One of `e ne n nw w sw s se`. It sets
     --ax/--ay (-1..1 each) which drive (a) the sweep slab's
     resting offset, (b) the resting arrow's exit vector, (c)
     the replacement arrow's entry. The arrow travels WITH the
     named compass direction; the slab sweeps in from the
     OPPOSITE corner.
   · Element — render on a real <button> for actions and an <a>
     for navigation. The engine styles either; semantics are
     the author's job, not the engine's.
   · Contexts it must work in — inline (in a row of actions,
     `display:inline-flex`), block (a lone CTA), reduced-motion
     (slab cross-fades instead of sliding, arrows snap — see the
     @media block), light/dark (all fills are --color/--back-
     groundColor/--red and invert with data-mode automatically),
     and disabled ([disabled] or .disabled — 0.4 opacity, sweep
     pinned out, no hover response).
   · Modifiers (mutually-exclusive register, pick one):
       .primary  — filled ink at rest, sweeps to ivory on hover
       .ghost    — no border/fill at rest, sweeps to ink
       .danger   — filled signal-red (the only red-CTA license
                   in the system; red marks a destructive gap)
       .quiet    — faint grey-tint tertiary register
     Geometry/content modifiers (compose freely with a register):
       .small      — compact size
       .pill       — mono-caps compact chip for in-app chrome;
                     2px radius, uppercase Sohne Mono. Carries no
                     register of its own — compose with .primary.
       .lead-arrow — arrow leads, label trails (row-reverse)
       .icon-lead  — static SVG icon before the label
       .icon-trail — static SVG icon after the label
     State-demo modifiers (a11y/state galleries only, never
     ship in product markup):
       .has-hover  — pins the hover sweep + label flip
       .has-focus  — pins the focus ring

   REGISTER COUNT — exactly four (the four above). Settled in
   #341. `.quiet.strong` (a fifth, middle-weight grey fill) was
   removed: a fill sitting between .quiet and .primary is the
   "middle weight" Doctrine rule 1 kills. `.pill.inverse` was
   removed too — it re-implemented .primary's fill verbatim; a
   filled pill is now `.pill.primary`. `.pill` is a geometry
   modifier (see the list above), never a register.

   This is the ONLY button rule. Pages never redeclare or fork it.

   Every canonical button has [data-dir]: ne n nw w sw s se e.
   Two CSS vars per direction — --ax (-1..1) and --ay (-1..1)
   — drive (a) the sweep slab's origin, (b) the resting
   arrow's exit, and (c) the replacement arrow's entry.
   Sweep moves opposite the arrow; the control itself stays
   still. Cause one side, effect the other, crossing in the
   middle.
   ============================================================ */
.b[data-dir] {
  --ax: 1;
  --ay: -1;
  --b-dur: var(--dur-base);
  --b-ease: var(--ease-causal);

  position: relative;
  font-family: var(--font-body);
  font-weight: 300; font-size: var(--fs-body);
  letter-spacing: -0.015em;
  padding: 14px 22px 14px 26px;
  border-radius: 0;
  border: 1px solid var(--color);
  background: var(--backgroundColor);
  color: var(--color);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 14px;
  overflow: hidden;
  isolation: isolate;
  /* Recessive-at-rest, resolve-on-engage — the link doctrine applied to
     the button chassis. Soft at rest, crisp on hover/focus. Tuned to
     0.3px (vs the link's 0.8px) because button labels are larger than
     prose and the link's full strength would muddy 14-18px text. */
  filter: blur(0.3px);
  opacity: 0.86;
  transition:
    color 280ms var(--b-ease),
    filter 280ms var(--b-ease),
    opacity 280ms var(--b-ease);
}
.b[data-dir]:hover,
.b[data-dir]:focus-visible,
.b[data-dir].has-hover,
.b[data-dir].has-focus { filter: blur(0); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .b[data-dir] { filter: blur(0); opacity: 1; transition: none; }
}

.b[data-dir="e"]  { --ax:  1; --ay:  0; }
.b[data-dir="ne"] { --ax:  1; --ay: -1; }
.b[data-dir="n"]  { --ax:  0; --ay: -1; }
.b[data-dir="nw"] { --ax: -1; --ay: -1; }
.b[data-dir="w"]  { --ax: -1; --ay:  0; }
.b[data-dir="sw"] { --ax: -1; --ay:  1; }
.b[data-dir="s"]  { --ax:  0; --ay:  1; }
.b[data-dir="se"] { --ax:  1; --ay:  1; }

.b[data-dir]::before {
  content: "";
  position: absolute; inset: 0;
  background: var(--color);
  transform: translate(calc(-101% * var(--ax)), calc(-101% * var(--ay)));
  transition: transform var(--b-dur) var(--b-ease);
  z-index: -1;
}
.b[data-dir]:hover::before { transform: translate(0, 0); }
.b[data-dir]:hover { color: var(--backgroundColor); }
.b[data-dir]:active::before { transition-duration: 200ms; }

.b[data-dir]:focus-visible,
.b[data-dir].has-focus { outline: var(--focus-ring); outline-offset: 2px; }
.b[data-dir].danger:focus-visible,
.b[data-dir].danger.has-focus { outline: var(--focus-ring-destructive); }

/* .has-hover — static demonstration of the :hover register. Pins the
   sweep slab fully in and flips the label, so a11y/state galleries can
   show the hover state without a live pointer. Pairs with .has-focus. */
.b[data-dir].has-hover::before { transform: translate(0, 0); }
.b[data-dir].has-hover { color: var(--backgroundColor); }
.b[data-dir].has-hover.primary { color: var(--color); }
.b[data-dir].has-hover.quiet { color: var(--color); }
.b[data-dir].has-hover.danger { color: var(--red); }

.b[data-dir].primary { background: var(--color); color: var(--backgroundColor); }
.b[data-dir].primary::before { background: var(--backgroundColor); }
.b[data-dir].primary:hover { color: var(--color); }

/* .quiet — tertiary register; faint tint of the contract pair so it
   theme-tracks. Inverts under data-mode automatically. */
.b[data-dir].quiet {
  background: color-mix(in srgb, var(--color) 5%, var(--backgroundColor));
  border-color: var(--grey-300);
  color: var(--grey-700);
}
.b[data-dir].quiet::before { background: var(--grey-300); }
.b[data-dir].quiet:hover { color: var(--color); }

.b[data-dir].danger { background: var(--red); border-color: var(--red); color: var(--backgroundColor); }
.b[data-dir].danger::before { background: var(--backgroundColor); }
.b[data-dir].danger:hover { color: var(--red); }

/* Ghost — quiet third register. No border at rest, no fill at rest;
   on hover the sweep slab fills with ink and text inverts to ivory.
   Use for tertiary actions where the default ivory-bordered variant
   would feel too loud. Distinct from .quiet (which has a grey fill). */
.b[data-dir].ghost {
  background: transparent;
  border-color: transparent;
  color: var(--color);
}
.b[data-dir].ghost::before { background: var(--color); }
.b[data-dir].ghost:hover { color: var(--backgroundColor); }

.b[data-dir][disabled],
.b[data-dir].disabled {
  opacity: 0.4; cursor: not-allowed;
  border-color: var(--grey-300);
}
.b[data-dir][disabled]:hover::before,
.b[data-dir].disabled:hover::before { transform: translate(calc(-101% * var(--ax)), calc(-101% * var(--ay))); }
.b[data-dir][disabled]:hover,
.b[data-dir].disabled:hover { color: var(--color); }

.b[data-dir] .arr-plate {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  overflow: hidden;
  flex-shrink: 0;
}
.b[data-dir] .arr-plate .arr {
  position: absolute; left: 0; top: 0;
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono);
  font-size: 15px; line-height: 1;
  font-weight: 500;
  transition:
    transform var(--b-dur) var(--b-ease),
    opacity calc(var(--b-dur) * 0.8) var(--b-ease);
}
.b[data-dir] .arr-plate .arr.in  { transform: translate(0, 0); opacity: 1; }
.b[data-dir] .arr-plate .arr.out { transform: translate(calc(-140% * var(--ax)), calc(-140% * var(--ay))); opacity: 0; }
.b[data-dir]:hover .arr-plate .arr.in  { transform: translate(calc(140% * var(--ax)), calc(140% * var(--ay))); opacity: 0; }
.b[data-dir]:hover .arr-plate .arr.out { transform: translate(0, 0); opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .b[data-dir]::before { transform: none; opacity: 0; transition: opacity var(--b-dur) var(--b-ease); }
  .b[data-dir]:hover::before,
  .b[data-dir].has-hover::before { opacity: 1; }
  .b[data-dir] .arr-plate .arr.in  { transform: none; }
  .b[data-dir] .arr-plate .arr.out { transform: none; opacity: 0; }
  .b[data-dir]:hover .arr-plate .arr.in  { transform: none; opacity: 0; }
  .b[data-dir]:hover .arr-plate .arr.out { transform: none; opacity: 1; }
}

.b[data-dir].small { font-size: 12px; padding: 10px 16px 10px 20px; gap: 10px; }
.b[data-dir].small .arr-plate { width: 18px; height: 18px; }
.b[data-dir].small .arr-plate .arr { font-size: 12px; }

/* .pill — geometry modifier: mono-caps compact chip for in-app
   chrome. Not a register; compose with .primary for a filled pill. */
.b[data-dir].pill {
  border-radius: 2px;
  padding: 8px 14px;
  font-size: 11px;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 400;
  gap: 8px;
}
.b[data-dir].pill .arr-plate { width: 14px; height: 14px; }
.b[data-dir].pill .arr-plate .arr { font-size: 11px; }

.b[data-dir].lead-arrow { flex-direction: row-reverse; padding: 14px 26px 14px 22px; }
.b[data-dir].lead-arrow.pill { padding: 8px 14px; }

/* Icon-lead — static SVG icon (not an arrow) sits before the label. */
.b[data-dir].icon-lead svg {
  width: 18px; height: 18px; flex-shrink: 0;
  color: currentColor;
  transition: color 280ms var(--b-ease);
}
.b[data-dir].icon-lead.small svg { width: 14px; height: 14px; }
.b[data-dir].icon-lead { padding-left: 18px; }
.b[data-dir].icon-lead.small { padding-left: 14px; }
.b[data-dir].icon-trail svg {
  width: 18px; height: 18px; flex-shrink: 0;
  color: currentColor;
  transition: color 280ms var(--b-ease);
}
.b[data-dir].icon-trail.small svg { width: 14px; height: 14px; }

/* SVG icons inside the arrow plate (when the icon IS the arrow). */
.b[data-dir] .arr-plate .arr svg {
  width: 14px; height: 14px; display: block;
  color: currentColor;
}

/* ============================================================
   .x — the canonical close / dismiss control
   ---------------------------------------------------------
   A bare mono glyph used to dismiss an overlay, toast, alert,
   or filter pill. Not a .b variant — it carries no border, no
   fill, no sweep; it is the smallest possible "remove this".
   Default: a bare ✕ glyph. Add .labeled for the mono-caps
   "✕ Close" form (red ✕ prefix) used by modals and alerts.
   ============================================================ */
.x {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--grey-500);
  cursor: pointer;
  background: transparent;
  border: 0;
  padding: 0;
}
.x:hover { color: var(--color); }

.x.labeled {
  text-transform: uppercase;
  letter-spacing: 0.14em;
}
.x.labeled::before { content: "✕ "; color: var(--red); }

/* ============================================================
   .tab — the canonical tab control
   ---------------------------------------------------------
   A mono-caps tab in a horizontal .tabs strip. Underline marks
   the active tab in signal red. .on is the active state; .ct is
   an inline count badge. Not a .b variant — tabs select, they
   do not act.
   ============================================================ */
.tab {
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  padding: 10px 16px 10px 0;
  margin-right: 24px;
  color: var(--grey-500);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  background: transparent;
  border-left: 0; border-right: 0; border-top: 0;
  transition: color var(--dur-fast) var(--ease-causal),
              border-bottom-color var(--dur-fast) var(--ease-causal);
}
.tab:hover { color: var(--color); }
.tab.on { color: var(--color); border-bottom-color: var(--red); }
.tab .ct { color: var(--grey-300); margin-left: 6px; font-size: 9px; }

/* ============================================================
   .close — the canonical labeled close control (dark surfaces)
   ---------------------------------------------------------
   Mono-caps "✕ Close" sized for inverted overlays: the label
   tracks the background contract color so it reads on a dark
   ground, with a red ✕ prefix. Distinct from .x.labeled, which
   sits on the ivory ground and tracks --grey-500.
   ============================================================ */
.close {
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--backgroundColor);
  cursor: pointer;
  background: transparent;
  border: 0;
}
.close::before { content: "✕ "; color: var(--red); }

/* Editorial polish (raw-element overrides) lives in derived.css —
   absorbed from the short-lived preview/_base-editorial.css when
   tokens.css was retired and derived.css took over element-level
   styling. cdn/v1/all.css picks them up via derived.css in the
   build script. */
