/* button.css — THE button. ONE canonical source, shared VERBATIM by the shell
   (site) and the private line. Edit the canonical in packages/shell/ and run
   tools/sync-shared.sh; never edit a copy. Pairs with tokens.css.

   Self-contained like door-menu.css: it references only tokens BOTH hosts define
   (with inline fallbacks for anything one host lacks) and uses LITERAL px for the
   type sizes — the private line has no --t-* ramp, so this shared file is the one
   sanctioned exception to the raw-px ban.

   The button runs FULL ROW (William, 2026-08-05): width: 100% of whatever
   column or cell holds it, in BOTH hosts — the old layout-neutral law made
   every row restate the width and they drifted. The row still owns columns
   and placement (.btn-row's grid; a .btn-row > .btn--full-row spans all its
   columns — layout.css). The curl bar is exempt (width: auto — it hugs its
   code); .btn--full / .btn--half are the opt-in caps.

   AND NEVER WIDER THAN THE ONE WIDTH (William, 2026-08-08): the base carries
   max-width --btn-w-full, so a wide column (a desktop split body, a stacked
   section) stops stretching the button at the private line width; auto inline
   margins keep a capped button centred in its track. .btn--full is thereby
   the base behaviour restated; .btn--half stays the narrower opt-in.

   Reference spec = the private line sign-in button: 44px tall, 11/18 padding, a 1px
   --line-2 ring, 14px Fira Code / 500 / 0.06em, and a tip-less flashlight beam. */

@property --btn-beam { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
/* Typed so consumers (colour mixes, gradients) animate when the state moves —
   the same trick the beam angle uses. */
@property --curl-state { syntax: "<percentage>"; inherits: true; initial-value: 33%; }

/* ── THE STRENGTH LADDER (William, 2026-08-11) ──────────────────────────────
   The one system every rank reads from — steps on a shared scale, so the whole
   button family moves together from one place. Ring strength is a % of --set
   (mixed over the ground); words are a % of --ink. Defined on :root so it stays
   INHERITABLE — a surface can re-point --set to recolour the whole set (e.g.
   body[data-surface="retreat"] { --set: var(--hibiscus); }).
     PRIMARY   (.btn--primary)    — dark chip · --set ring · STRIKE · square glow. Loud.
     SECONDARY (.btn--secondary)  — glass · --ring-2 --set ring · the calm crisp beam.
     TERTIARY  (.btn--tertiary)   — glass · monochrome --ink hairline · receded words.
     CURL      (.btn--curl)       — the utility bar, matched to the secondary.
   Old markers (.btn--invert / .btn--beam / .btn--ghost / legacy .btn.primary)
   still render via compat aliases; the old weather-keyed / solid systems are gone. */
:root {
  --set:        var(--page-primary, var(--accent, #3029E7));  /* the ONE set colour — follows the page's primary (indigo default, hibiscus on scent); private line falls back to the hex */
  --ring-lit:   100%;   /* primary ring at rest, and EVERY rank's hover/focus/press */
  --ring-2:      66%;   /* secondary ring at rest */
  --ring-3:      40%;   /* tertiary ring at rest */
  --words-soft:  60%;   /* receded words (tertiary at rest) */
  --beam-rest:   0.9;   /* the traveling beam at rest */
  --beam-lit:    1;     /* the beam, lit */
}
/* …but RE-RESOLVE --set on the BODY, where the per-surface primary lives. The
   :root line above resolves var(--page-primary) at :root — where it is always the
   DEFAULT (hard rain) — and inherits that one colour down already-substituted, so
   the body[data-surface] override never reached the button (every primary rendered
   blue, William 2026-08-11). nav.js stamps data-surface on <body> and tokens.css
   re-points --page-primary on body[data-surface]; declaring --set here makes its
   var(--page-primary) read THAT value, so a scent/retreat button goes hibiscus, a
   coffee button lime, etc. — the whole set follows the page's one primary. */
body { --set: var(--page-primary, var(--accent, #3029E7)); }
/* A thinned --set colour washes out over the one white, so light mode LIFTS the
   only SET-coloured floor that dilutes (the secondary ring, which the curl rides).
   The monochrome tertiary (--ink) reads the same on both grounds, so no lift. */
html.is-light { --ring-2: 78%; }

/* ── Base ─────────────────────────────────────────────────────── */
.btn {
  appearance: none;
  position: relative;      /* host for the beam ::before */
  isolation: isolate;      /* keep the beam's blend to the button */
  /* The beam's ONE colour — spark (the glyph's bolt blue). Variants re-pin it
     (the scent CTA wears hibiscus). Fallback for the private line, which has no
     --spark token. */
  --beam: var(--spark, #046AEF);
  display: inline-flex;
  width: 100%;             /* the full-row law — every button spans its column */
  max-width: var(--btn-w-full, 420px);  /* …and never wider than the ONE width (William, 2026-08-08) */
  align-items: center;
  /* Content sits CENTRED as one group: a mark rides tight to its label (the
     11px gap), the pair centred in the button — the app-wide standard for
     buttons + icons (William, 2026-08-01). The curl bar keeps its own layout
     via an auto margin, which overrides this. */
  justify-content: center;
  gap: 11px;
  min-height: var(--btn-h, 44px);   /* the private line falls back to 44px (no --btn-h there) */
  padding: 11px 18px;
  margin-block: 0;
  margin-inline: auto;     /* a capped button stays centred in a wider track; in an exact-width track this resolves to zero */
  /* EVERY button carries the resting ring — the ONE construction all buttons
     share, so none can lose it. The colour is the private line EXISTING MEMBERS
     stroke: the site names it --btn-stroke (tokens.css); the private line has no
     such token and falls back to its own --line-2, the same colour. */
  border: 1px solid var(--btn-stroke, var(--line-2));
  border-radius: 0;
  font: inherit;
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: var(--track-title);
  text-transform: lowercase;   /* buttons read lowercase across the site */
  text-decoration: none;
  line-height: 1;
  color: var(--ink-2);   /* REST is receded; lights to full --ink on hover/focus (the interaction standard) */
  background: transparent;
  cursor: pointer;
  flex: 0 0 auto;
  min-width: 0;
  box-sizing: border-box;
  transition: background var(--dur-base, 200ms) var(--ease, ease), border-color var(--dur-base, 200ms) var(--ease, ease), color var(--dur-base, 200ms) var(--ease, ease), transform var(--dur-instant, 80ms) var(--ease-out, ease-out);
}
/* The press — one pixel of give under the pointer, back on release. */
.btn:active { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) { .btn:active { transform: none; } }
/* Interaction standard (William, 2026-08-02): REST is receded; a button INTENSIFIES
   to full ink + a brighter ring on interaction. Keyboard focus lights on EVERY device;
   pointer hover is gated to hover-capable devices so a tap never sticks lit. On touch,
   the movement comes from the always-on ambient beam (below). */
.btn:focus-visible {
  border-color: var(--line-strong, var(--line-2));
  color: var(--ink);
  outline: none;
}
@media (hover: hover) {
  /* Bare buttons intensify on hover; the ranked variants override this with their
     own hover. The curl runs its own --curl-state system, so it is excluded. */
  .btn:not(.btn--curl):hover {
    border-color: var(--line-strong, var(--line-2));
    color: var(--ink);
  }
}
.btn > * { position: relative; z-index: 1; } /* label + mark ride above the beam */
.btn > svg { flex: 0 0 auto; }

/* ── Marks ────────────────────────────────────────────────────── */
/* Site weather mark: in the DOM so the stroke map (:has([data-wx-mark])) colours
   the border, but hidden unless a button opts back in with --glyph. */
.btn > .btn__mark {
  display: none;
  flex: 0 0 auto;
  object-fit: contain;
  pointer-events: none;
  /* Every mark (34–41px) pulls in with negative block margins so no glyph
     can lift its button above the ONE --btn-h height. */
  margin-block: -11px;
}
/* ── App-wide button law (William, 2026-07-30) ──────────────────────────
   A button carries TEXT ONLY — no weather mark, no cloud/stars, no arrow.
   (The curl bar's copy icon is exempt — it IS the button's purpose.)
   Card CTAs are first-class: a .btn MAY live inside a card body (.shc-body).
   The old "no .btn in a content card" ban was removed (William, 2026-08-01). */
.btn > .btn__mark,
.btn.btn--glyph > .btn__mark,
.btn > .btn__cloud,
.btn > .btn__cloud-stars,
.btn > .btn__go:not(.btn__curl-icon) { display: none !important; }
/* NO CLOUDS ON BUTTONS (William, 2026-08-06): the 2026-08-05 symbol-button
   standard (all-segments master shown, plus its PFP counter-rule) is
   withdrawn — the text-only law above holds with no exceptions. Marks stay
   in the DOM for the stroke map, hidden; the data-wx-light key still
   colours the stroke and words (the affiliation), it just lights no glyph. */
/* The site's inlined scentline mark gets the same pull-in as the private line's. */
.btn--glyph > .btn__mark[data-wx-mark="scentline"] { margin: -11px 0; }

/* ── Width caps — opt-in narrowing of the full-row base ───────── */
/* The base is already 100%; the token caps it so it never sprawls on desktop;
   centred in whatever column holds it. */
.btn.btn--full { max-width: var(--btn-w-full, 420px); margin-inline: auto; }
.btn.btn--half { max-width: var(--btn-w-half, 204px); margin-inline: auto; }

/* ── Words ────────────────────────────────────────────────────── */
/* The words box SHRINKS to its text (flex: 0 1 auto), so it never grows to fill
   the button and shove a mark to the far edge — the mark stays tight to the
   label and the pair centres together (see .btn justify-content). It still
   centres its own wrapped lines. The curl bar and any trailing arrow keep their
   own layout via auto margins, so this never reaches them. */
.btn .btn__words {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35em;
  min-width: 0;
  flex: 0 1 auto;
  justify-content: center;
}
.btn .btn__lead {
  font-family: "Fira Code", ui-monospace, monospace;
  font-size: var(--t-body);
  font-weight: 500;
  text-transform: lowercase;
  line-height: 1;
  white-space: nowrap;
}
/* The seat-count figure runs larger; its negative block margins keep the
   24px digits from lifting the row above --btn-h (same trick as the marks). */
.btn .btn__num { font-size: var(--t-head, 24px); line-height: 1; margin-block: -2px; }
/* Optional trailing guide arrow (→) — matches .m-go */
.btn .btn__go:not(.btn__curl-icon) {
  flex: 0 0 auto;
  margin-left: auto;
  padding-left: 10px;
  font-size: var(--t-label);
  line-height: 1;
  width: 16px;
  text-align: center;
  transform: translateX(0);
  transition: transform 160ms ease, opacity 160ms ease;
}
.btn:hover .btn__go:not(.btn__curl-icon),
.btn:focus-visible .btn__go:not(.btn__curl-icon) { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) { .btn .btn__go { transition: none; } }


/* ── --invert — THE PRIMARY (promoted from the lab, William 2026-08-11): a dark
   chip carrying ONE --set ring at full strength, a SHARP-CORNERED square glow,
   and the lightning STRIKE. The loudest rank. ── */
.btn--primary, .btn--invert {
  background: var(--vanta, #000000);
  color: var(--white, #EFEFEF);
  border-color: transparent;
  border-image: none;
}
.btn--primary::after, .btn--invert::after {
  content: "";
  position: absolute;
  inset: -1px;
  border: 1px solid color-mix(in srgb, var(--set) var(--ring-lit), transparent);
  pointer-events: none;
  /* The glow follows the button's shape with SHARP corners: a stack of HARD
     spread-shadows (0 blur, growing spread, fading alpha). Radius is 0, so each
     is a square halo stepping outward; the strike's brightness pulses the stack. */
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--set) 60%, transparent),
    0 0 0 2px color-mix(in srgb, var(--set) 40%, transparent),
    0 0 0 4px color-mix(in srgb, var(--set) 22%, transparent),
    0 0 0 6px color-mix(in srgb, var(--set) 10%, transparent),
    0 0 0 8px color-mix(in srgb, var(--set) 4%,  transparent);
  animation: btnStrike 3s steps(1, end) infinite;
}
/* The dark chip means the words are ALWAYS white — pinned across every state on
   BOTH grounds (the generic :hover/:active would flip them to --ink = black on
   the white ground; the extra .btn beats that). */
.btn.btn--primary:not(.btn--curl):hover,
.btn.btn--primary:not(.btn--curl):focus-visible,
.btn.btn--primary:not(.btn--curl):active,
.btn.btn--invert:not(.btn--curl):hover,
.btn.btn--invert:not(.btn--curl):focus-visible,
.btn.btn--invert:not(.btn--curl):active { color: var(--white, #EFEFEF); background: var(--vanta, #000000); border-color: transparent; }
.btn--primary:hover::after,
.btn--primary:focus-visible::after,
.btn--invert:hover::after,
.btn--invert:focus-visible::after {
  animation: none;
  filter: brightness(1.4);
  /* hover BLOOMS the square halo — same hard-cornered stack, wider + brighter */
  box-shadow:
    0 0 0 1px  color-mix(in srgb, var(--set) 72%, transparent),
    0 0 0 3px  color-mix(in srgb, var(--set) 48%, transparent),
    0 0 0 5px  color-mix(in srgb, var(--set) 28%, transparent),
    0 0 0 8px  color-mix(in srgb, var(--set) 13%, transparent),
    0 0 0 12px color-mix(in srgb, var(--set) 5%,  transparent);
}
@keyframes btnStrike {
  0%    { filter: brightness(1); }
  1.67% { filter: brightness(2.4); }
  3%    { filter: brightness(1); }
  5.33% { filter: brightness(2.4); }
  7.33% { filter: brightness(1.4); }
  9%    { filter: brightness(1); }
  100%  { filter: brightness(1); }
}
@media (prefers-reduced-motion: reduce) { .btn--primary::after, .btn--invert::after { animation: none; filter: none; } }
/* seatStrike — PRESERVED (referenced by the 3D mark + nav/07-rails.js by name);
   no longer used by the button, kept so nothing that names it breaks. */
@keyframes seatStrike {
  0%    { filter: brightness(1); }
  1.67% { filter: brightness(0.5); }
  3%    { filter: brightness(1); }
  5.33% { filter: brightness(0.5); }
  7.33% { filter: brightness(0.9); }
  9%    { filter: brightness(0.5); }
  100%  { filter: brightness(0.5); }
}

/* ── --quiet — FOLDED INTO THE SYSTEM BUTTON (William, 2026-08-06: the
   private line's buttons match the system buttons first). The old dress — white
   --lit stroke, full ink, no beam — rested LOUDER than the system button and
   broke the one interaction standard (REST is receded). The class stays in
   the private line markup; its spec is now the .btn base: receded ring, receded
   words, the ambient beam. .done — the enrolled state, private line-only. ── */
.btn.done { border-color: var(--line-2); color: var(--dim, var(--ink-3)); cursor: default; }
.btn.done::before { display: none; }
.btn.done svg { color: var(--spark); }

/* ── btn--curl — the copy bar: same 1px ring and dim mono command, but its OWN
   slimmer build (William, 2026-07-29). It sheds the button's --btn-h for the
   sleeker --curl-h. It runs the SAME full-row width + cap as every button
   (William, 2026-08-08 — the old hug-the-code exemption left the bar a
   different length to the button above it); on a column too narrow for the
   command, the code clips with an ellipsis (the copy action still carries
   the whole command). ── */
.btn.btn--curl {
  position: relative;
  isolation: isolate;
  min-height: var(--curl-h, 36px);   /* slimmer than a button */
  padding: 0 14px;                    /* tighter — the min-height sets the bar */
  /* The WHOLE bar runs in the one accent — command, stroke, prompt, icon,
     cursor all inherit this colour (the scent design, taken app-wide
     2026-08-05). A page re-pins --spark to move the whole bar at once.
     STATE: rest reads THE shared button rest knob (--btn-symbol-rest, 15%),
     so the bar and the buttons above it always rest at the same opacity;
     hover, keyboard focus and a successful copy light the full colour
     (--curl-state, the one knob). */
  --curl-state: var(--btn-symbol-rest, 15%);
  color: color-mix(in srgb, var(--spark, #046AEF) var(--curl-state), transparent);
  border-color: color-mix(in srgb, var(--spark, #046AEF) var(--curl-state), transparent);
  letter-spacing: 0;
  /* the ONE button weight — the private line lead's 500 */
  font-weight: 500;
  transition: color var(--dur-base, 200ms) var(--ease, ease), border-color var(--dur-base, 200ms) var(--ease, ease), --curl-state var(--dur-base, 200ms) var(--ease, ease);
}
.btn.btn--curl:hover,
.btn.btn--curl:focus-visible,
.btn.btn--curl.is-copied { --curl-state: 100%; }
.btn--curl code {
  /* The house code face, stated on the element: the browser's own
     code { font-family: monospace } rule beats inheritance from .btn, so
     without this line every curl bar silently rendered the system mono
     (found 2026-08-05 — only the scent page, which forces the face at page
     scope, showed the intended Fira Code). */
  font-family: var(--font-code, "Fira Code", ui-monospace, monospace);
  /* One step down the ramp: Fira Code runs wider than the old system-mono
     fallback did, and 13px read cramped in the bar (William, 2026-08-05). */
  font-size: var(--t-small);
  letter-spacing: var(--track-micro);
  color: inherit;
  white-space: nowrap;
  /* On a column narrower than the command (the smallest native-app width)
     the code clips honestly with an ellipsis instead of poking out of the
     bar; the data-copy action still carries the whole command. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-variant-ligatures: none;
}
.btn--curl code i { font-style: normal; color: inherit; }
.btn--curl code .c-dim { color: inherit; }
.btn--curl code .curl-slash { color: inherit; }   /* one bar, one colour, one state */
/* The terminal cursor after the command — the machine at rest. It blinks on
   the same one-second beat as the bar clock's colons; the copy action fills
   it with the accent while the icon lands with a small pop. */
.btn--curl .curl-cursor {
  display: inline-block;
  width: 0.55ch;
  height: 1em;
  margin-left: 0.35ch;
  vertical-align: text-bottom;
  background: currentColor;
  opacity: 0.55;
  animation: rs-cursor-blink 1s steps(1, end) infinite;
}
.btn--curl.is-copied .curl-cursor { background: currentColor; opacity: 1; }
@keyframes rs-cursor-blink { 50% { opacity: 0; } }
.btn--curl.is-copied .btn__curl-icon svg { animation: rs-check-pop var(--dur-base, 200ms) var(--ease-out, ease-out); }
@keyframes rs-check-pop { from { transform: scale(0.6); } }
@media (prefers-reduced-motion: reduce) {
  .btn--curl .curl-cursor { animation: none; }
  .btn--curl.is-copied .btn__curl-icon svg { animation: none; }
}
.btn__curl-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  /* Sized at --curl-icon — the glyph sits inside --curl-h with no pull-in. */
  color: inherit; /* the bar's one accent */
  transition: color 140ms ease;
}
/* The icon inherits the bar's colour and state; no separate tones. */
.btn__curl-icon svg { display: block; width: var(--curl-icon, 20px); height: var(--curl-icon, 20px); }
.btn.btn--curl .btn__curl-icon.btn__go {
  margin-left: auto;
  padding-left: 12px;
  /* No box width/height here: with the global border-box, a fixed width would
     eat the 12px padding into the glyph and squish it (a square viewBox rendered
     narrow). The svg rule above sizes the glyph at --curl-icon; this inline-flex
     span just shrink-wraps to it + the gap, so the icon stays square at any size. */
  color: inherit; /* the bar's one accent */
  transform: none;
}
.btn.btn--curl:hover .btn__curl-icon.btn__go,
.btn.btn--curl:focus-visible .btn__curl-icon.btn__go { transform: none; }

/* ── Ambient ring sweep — the ALWAYS-ON motion (the interaction standard's "alive
   on every device"): every button carries a FAINT beam, so touch — where there is
   no hover — still shows subtle movement. The curl bar + opted-in .btn--beam wear it
   brighter; hover intensifies it further (pointer devices only). One conic sweep,
   rain→lightning. (William, 2026-08-02) ── */
/* Beam standard (William, 2026-08-05): the beam lives on the CURL BAR and the
   opted-in .btn--beam doors (the private line EXISTING CLIENTS, William
   2026-08-09). Un-opted buttons carry no ambient sweep; their life is the
   33-to-full state light. */
.btn.btn--curl::before,
.btn.btn--beam::before,
.btn.btn--secondary::before {
  content: "";
  position: absolute;
  inset: -1px;
  box-sizing: border-box;   /* base.css resets only `*`, not `*::before` — the gradient-border trick needs border-box */
  clip-path: inset(0);       /* hard-clip to the border-box so no sub-pixel of the sweep bleeds past the 1px stroke (white ground) */
  border-radius: inherit;
  padding: 1px;
  /* CRISP segment — a clean 26° arc with a hard cut (a few degrees of AA so it
     never jags). The old ~42° fade tail read as a blur spilling off the stroke. */
  background: conic-gradient(from var(--btn-beam),
    var(--beam) 0deg,
    var(--beam) 26deg,
    transparent 30deg,
    transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
  z-index: 0;
  opacity: 0.3;   /* faint at rest — tunable */
  animation: btnBeam 4.6s linear infinite;
  transition: opacity var(--dur-base, 200ms) var(--ease, ease);
}
.btn.btn--curl::before,
.btn.btn--beam::before { opacity: 0.7; }   /* the curl's + opted-in beams read brighter than the old ambient did */
@media (hover: hover) {
  .btn.btn--curl:hover::before,
  .btn.btn--beam:hover::before { opacity: 0.85; }     /* pointer hover intensifies the beam */
}
@keyframes btnBeam {
  0%   { --btn-beam: 0deg;   animation-timing-function: cubic-bezier(0.5, 0, 1, 0.55); }
  22%  { --btn-beam: 26deg;  animation-timing-function: linear; }
  78%  { --btn-beam: 334deg; animation-timing-function: cubic-bezier(0, 0.45, 0.5, 1); }
  100% { --btn-beam: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .btn.btn--curl::before,
  .btn.btn--beam::before,
  .btn.btn--secondary::before { animation: none; opacity: 0; }   /* no beam when motion is reduced */
}

/* ═══ THE RANKED SYSTEM — THE canonical buttons, NAMED BY RANK (William, 2026-08-11).
   The old weather-keyed / solid / stroke-map systems are gone; every button is one
   rank. Old markers still render via the compat aliases noted, so existing markup
   keeps working while it migrates to the rank names.
     PRIMARY   .btn--primary    (alias .btn--invert)              — dark chip · strike · square glow
     SECONDARY .btn--secondary  (aliases .btn--beam, legacy .btn.primary) — glass door
     TERTIARY  .btn--tertiary   (alias .btn--ghost, no beam)      — monochrome
     CURL      .btn--curl
   ═══════════════════════════════════════════════════════════════════════════ */

/* SECONDARY / the doors — glass · a --ring-2 --set ring + full-ink words at rest ·
   bloom to the full ring on interaction. --set follows the page primary, so a
   scent door is hibiscus on its own. .btn--secondary carries the crisp beam (wired
   into the ::before above); .btn--beam and legacy .btn.primary are compat aliases.
   Each excludes the loud .btn--primary so a primary is never also a door. */
.btn--secondary:not(.btn--primary),
.btn--beam:not(.btn--curl):not(.btn--primary),
.btn.primary:not(.btn--primary) {
  --beam: var(--set);
  border-color: color-mix(in srgb, var(--set) var(--ring-2), transparent);
  color: var(--ink);
}
.btn--secondary:not(.btn--primary):hover,
.btn--secondary:not(.btn--primary):focus-visible,
.btn--secondary:not(.btn--primary):active,
.btn--beam:not(.btn--curl):not(.btn--primary):hover,
.btn--beam:not(.btn--curl):not(.btn--primary):focus-visible,
.btn--beam:not(.btn--curl):not(.btn--primary):active,
.btn.primary:not(.btn--primary):hover,
.btn.primary:not(.btn--primary):focus-visible,
.btn.primary:not(.btn--primary):active {
  border-color: color-mix(in srgb, var(--set) var(--ring-lit), transparent);
  color: var(--ink);
  background: transparent;
}
.btn--secondary:not(.btn--primary)::before,
.btn--beam:not(.btn--curl):not(.btn--primary)::before { opacity: var(--beam-rest); }
.btn--secondary:not(.btn--primary):hover::before,
.btn--secondary:not(.btn--primary):focus-visible::before,
.btn--secondary:not(.btn--primary):active::before,
.btn--beam:not(.btn--curl):not(.btn--primary):hover::before,
.btn--beam:not(.btn--curl):not(.btn--primary):focus-visible::before,
.btn--beam:not(.btn--curl):not(.btn--primary):active::before { opacity: var(--beam-lit); }

/* TERTIARY — the quiet default, MONOCHROME (our white + darks, --ink): a --ink
   hairline at --ring-3 with words receded to --words-soft, waking to full --ink on
   hover. .btn--ghost (no beam) is the compat alias. Press stays glass. */
.btn--tertiary:not(.btn--primary):not(.btn--secondary),
.btn.btn--ghost:not(.btn--beam):not(.btn--secondary):not(.btn--primary) {
  border-color: color-mix(in srgb, var(--ink) var(--ring-3), transparent);
  color: color-mix(in srgb, var(--ink) var(--words-soft), transparent);
}
.btn--tertiary:not(.btn--primary):not(.btn--secondary):hover,
.btn--tertiary:not(.btn--primary):not(.btn--secondary):focus-visible,
.btn--tertiary:not(.btn--primary):not(.btn--secondary):active,
.btn.btn--ghost:not(.btn--beam):not(.btn--secondary):not(.btn--primary):hover,
.btn.btn--ghost:not(.btn--beam):not(.btn--secondary):not(.btn--primary):focus-visible,
.btn.btn--ghost:not(.btn--beam):not(.btn--secondary):not(.btn--primary):active {
  border-color: var(--ink);
  color: var(--ink);
  background: transparent;
}

/* CURL — part of the SET, matched to the secondary: it rests at the secondary's
   own ring strength (--ring-2, lifted on white) in the --set colour (re-pointing
   its --spark), and its beam matches (--beam-rest → --beam-lit on hover). The
   bar's colour still runs to full on hover (the existing :hover → --curl-state
   100% rule). The .btn beats the base curl-state floor. */
.btn.btn--curl { --spark: var(--set); --curl-state: var(--ring-2); }
.btn--curl::before { opacity: var(--beam-rest); }
.btn--curl:hover::before,
.btn--curl:focus-visible::before { opacity: var(--beam-lit); }
/* Keep the copy glyph dead still — anchor the code so the icon can't drift. */
.btn--curl code { flex: 1 1 auto; }
.btn--curl .btn__curl-icon { transform: none !important; }
.btn--curl .btn__curl-icon svg { animation: none !important; }

/* TAP FLARE — the mobile reaction (no hover on touch): the beam jumps to full and
   speeds its sweep while pressed. */
.btn--secondary:active::before,
.btn--beam:active::before,
.btn--curl:active::before { opacity: 1; animation-duration: 1.1s; }
