/* components.css — shared components on the tokens in tokens.css.

   Loaded by base.html after style.css and before base.css, so a rule here
   restyles the same class in style.css without renaming it (templates,
   scripts and tests read those names). Redo 0 (2026-10-13) rebuilt the
   components on the new palette:

   - ONE primary button. Every filled call to action (.btn-primary,
     .btn-gradient, .btn-gradient-lg) is the same object: the accent
     gradient (pink to violet, tokens.css "second light"), dark text, 48px
     tall. One per screen is the rule the layouts keep
     (Von Restorff; docs/UX-AUDIT.md §1).
   - Everything else is quiet: secondary is an outline, ghost is text.
   - Fields carry a 3:1 edge (WCAG 1.4.11), never a glow.
   - Status is a neutral chip with a coloured word or dot, never a tinted fill.

   Contrast figures are WCAG 2.x ratios (see the tokens.css header and
   docs/design/COLOR-AUDIT.md). */

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.btn,
.btn-gradient-lg,
.btn-ghost-lg,
.btn-ghost-sm {
  font-family: var(--font-body);
  font-weight: var(--w-body-strong);
  letter-spacing: -.005em;
  border-radius: var(--radius-control);
  text-shadow: none;
  transition:
    background-color var(--dur-hover) var(--ease),
    border-color var(--dur-hover) var(--ease),
    color var(--dur-hover) var(--ease),
    transform var(--dur-hover) var(--ease);
}

/* The primary. The accent gradient under --accent-ink text: 7.3:1 on the
   pink end, 7.1:1 on the violet. The flat accent stays underneath as the
   colour a context that drops images (forced colours) falls back to. */
.btn-primary,
.btn-gradient,
.btn-gradient-lg,
.vx-btn-primary {
  background: var(--accent);
  background-image: var(--gradient-accent);
  color: var(--accent-ink);
  border: 1px solid transparent;
  box-shadow: none;
  min-height: 48px;
  padding: 12px 24px;
  font-size: var(--text-body);
  font-weight: var(--w-body-strong);
  text-shadow: none;
}
.btn-gradient-lg { padding: 14px 28px; }
/* A small primary (table rows, inline forms) keeps its size class: 40px,
   still above the 24px target floor (WCAG 2.5.8). */
.btn-sm.btn-primary,
.btn-sm.btn-gradient { min-height: 40px; padding: 8px 16px; font-size: var(--text-small); }
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover:not(:disabled),
  .btn-gradient:hover:not(:disabled),
  .btn-gradient-lg:hover,
  .vx-btn-primary:hover {
    background: var(--accent-hover);
    background-image: var(--gradient-accent-hover);
    box-shadow: none;
    opacity: 1;
    transform: none;
  }
}
@media (hover: none), (pointer: coarse) {
  .btn-primary:hover:not(:disabled),
  .btn-gradient:hover:not(:disabled),
  .btn-gradient-lg:hover {
    transform: none;
    box-shadow: none;
    opacity: 1;
  }
}

/* Secondary: an outline with a 3:1 edge. */
.btn-secondary,
.btn-ghost-lg {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--line-control);
  box-shadow: none;
}
/* Ghost: text that reads as a control, for the rest. */
.btn-ghost,
.btn-ghost-sm {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--line);
}
@media (hover: hover) and (pointer: fine) {
  .btn-secondary:hover:not(:disabled),
  .btn-ghost-lg:hover,
  .btn-ghost:hover:not(:disabled),
  .btn-ghost-sm:hover {
    background: var(--surface-2);
    border-color: var(--line-control);
    color: var(--ink);
  }
}
@media (hover: none), (pointer: coarse) {
  .btn-secondary:hover:not(:disabled) {
    border-color: var(--line-control);
    color: var(--ink);
  }
}

/* Danger is an outline in the error colour: a destructive action never
   competes with the one filled primary. */
.btn-danger {
  background: transparent;
  color: var(--error);
  border: 1px solid currentColor;
  text-shadow: none;
}
@media (hover: hover) and (pointer: fine) {
  .btn-danger:hover:not(:disabled) { background: var(--surface-2); opacity: 1; }
}
.btn-approve { color: var(--success); }
.btn-reject  { color: var(--error); }

.btn-subtitle { opacity: 1; font-weight: var(--w-body); color: inherit; }

/* Focus: one ring everywhere, offset onto the ground so it shows around a
   pink button too. */
.btn:focus-visible,
.btn-gradient-lg:focus-visible,
.btn-ghost-lg:focus-visible,
.btn-ghost-sm:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  box-shadow: none;
}

/* ── Fields ───────────────────────────────────────────────────────────────── */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="date"],
input[type="search"],
input[type="url"],
input[type="tel"],
textarea,
select {
  background-color: var(--surface-1);
  color: var(--ink);
  border: 1px solid var(--line-control);
  border-radius: var(--radius-control);
  min-height: 44px;
  font-size: max(16px, var(--text-body));   /* 16px stops iOS zooming on focus */
  box-shadow: none;
  transition:
    border-color var(--dur-hover) var(--ease),
    background-color var(--dur-hover) var(--ease);
}
textarea { min-height: 88px; }
input:focus,
textarea:focus,
select:focus {
  border-color: var(--ink);
  box-shadow: none;
}
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
input::placeholder,
textarea::placeholder { color: var(--muted); opacity: 1; }
label { color: var(--ink); font-weight: var(--w-body-strong); }
input[type="checkbox"],
input[type="radio"] { accent-color: var(--ink); }

/* A <label> inside a .filters-label paragraph took the global
   `label { font-size: 14px }` instead of the paragraph's 11px caption size,
   so "COLLABORATION" in uppercase tracking measured 126px in a 90px column
   and the panel's overflow cut it. Inherit the caption instead. */
.filters-label label {
  display: inline;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
}
/* The pair's two columns are 90px each at the 220px panel. .mkt-select's
   12px + 34px padding left 42px for text, so "Any key" showed as "Any ke".
   6px + 20px (a 10px chevron 6px from the edge) leaves 62px inside the
   1px borders at 13px; the widest options measure 61.4px ("Invite only")
   and 58.4px ("G# minor"). */
.filters-pair .mkt-select {
  font-size: 13px;
  padding: 9px 20px 9px 6px;
  background-position: right 6px center;
  background-size: 10px 10px;
}

/* ── File input ───────────────────────────────────────────────────────────── */
/* The platform's grey "Choose File" button, restyled as a secondary button
   on every file input. */
input[type="file"] {
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--muted);
  max-width: 100%;
}
input[type="file"]::file-selector-button {
  font: inherit;
  font-weight: var(--w-body-strong);
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--line-control);
  border-radius: var(--radius-control);
  padding: 8px 14px;
  margin-right: 12px;
  cursor: pointer;
  transition: border-color var(--dur-hover) var(--ease), background-color var(--dur-hover) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  input[type="file"]::file-selector-button:hover { background: var(--surface-2); }
}

/* ── Chips ────────────────────────────────────────────────────────────────── */
/* Neutral; the selected one is inverted (ink fill, ground text), which is an
   active state without spending the accent. */
.chip {
  background: var(--surface-2);
  color: var(--muted);
  border: 1px solid transparent;
  transition:
    border-color var(--dur-hover) var(--ease),
    color var(--dur-hover) var(--ease),
    background-color var(--dur-hover) var(--ease);
}
.chip.is-selected {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
  text-shadow: none;
  font-weight: var(--w-body-strong);
}
@media (hover: hover) and (pointer: fine) {
  .chip:hover:not(.is-selected) { color: var(--ink); border-color: var(--line); }
}
@media (hover: none), (pointer: coarse) {
  .chip:hover:not(.is-selected) { border-color: transparent; color: var(--muted); }
}

/* ── Cards, modals, toasts, empty states ──────────────────────────────────── */
/* A card is a surface, not a box: no glow, a hairline only at rest. */
.card {
  background: var(--surface-1);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-card);
  box-shadow: none;
  transition: border-color var(--dur-ui) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .card:hover { border-color: var(--line); box-shadow: none; transform: none; }
}
@media (hover: none), (pointer: coarse) {
  .card:hover { transform: none; box-shadow: none; }
}
.modal-box {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-float);
}
.vx-toast {
  background: var(--surface-3);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-float);
  color: var(--ink);
}
.empty-state-sub,
.empty-state p { color: var(--muted); }

/* ── Section tabs ─────────────────────────────────────────────────────────── */
.sec-tab {
  font-size: var(--text-nav);
  letter-spacing: var(--track-nav);
  color: var(--muted);
  transition: color var(--dur-hover) var(--ease), background-color var(--dur-hover) var(--ease);
}
/* Active: ink text with the accent underline, the same indicator as the nav,
   drawn in the gradient. The account tabs (Profile / Settings) are the same
   control and carry the same mark. */
.sec-tab.active,
.acct-tab.active {
  color: var(--ink);
  box-shadow: none;
  background: var(--gradient-accent-x) center bottom / 100% 2px no-repeat, rgb(var(--ink-rgb) / .07);
}

/* ── Badges, pills, status ────────────────────────────────────────────────── */
.pill,
.badge {
  background: var(--surface-2);
  border: 1px solid transparent;
  color: var(--muted);
  transition: background-color var(--dur-hover) var(--ease), color var(--dur-hover) var(--ease);
}
/* New name, added beside the old ones: a neutral status chip whose meaning is
   carried by a dot and a word in the semantic colour. */
.vx-status {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--muted);
  font-size: var(--text-small); font-weight: var(--w-body-strong);
}
.vx-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.vx-status--ok   { color: var(--success); }
.vx-status--warn { color: var(--warning); }
.vx-status--err  { color: var(--error); }

/* ── Audio player ─────────────────────────────────────────────────────────── */
/* The play button and the played part of the waveform are the accent
   gradient; the rest of the waveform is muted ink (static/js/audio_player.js).
   The played tint keeps the old 28% on the pink end and 32% on the violet,
   which is that much lighter, so the two ends read as one strength. */
.vx-player-play { background: var(--accent); background-image: var(--gradient-accent); color: var(--accent-ink); }
.vx-player-progress { background: linear-gradient(90deg, rgb(var(--accent-rgb) / .28), rgb(var(--accent-2-rgb) / .32)); }
.vx-player-divider { opacity: 1; color: var(--muted); }
.vx-player-error { color: var(--error); }

/* ── Cover placeholder and avatar initials ────────────────────────────────── */
/* No art yet: a quiet surface with the initials in ink, not a gradient tile
   pretending to be artwork. */
.cover-placeholder-text,
.cover-placeholder-genre { color: var(--ink); text-shadow: none; }
.cover-placeholder-genre { color: var(--muted); }
.voc-avatar-fallback { color: var(--ink); }
.num { color: var(--ink); text-shadow: none; }

/* ── Credibility line ─────────────────────────────────────────────────────── */
.cred-dot { opacity: 1; color: var(--muted); }

/* ── Emphasis that used to be gradient text ───────────────────────────────── */
/* Emphasis comes from weight and size, not from colour clipped to type. */
.headline-accent,
.voc-stat-value {
  background: none;
  color: var(--ink);
  -webkit-text-fill-color: currentColor;
}

/* ── Signal assistant button ──────────────────────────────────────────────── */
/* A floating pill on the dark ground, edged, lettered and iconed in the
   accent gradient so it reads as Voxen's own (the founder's call,
   docs/adr/0017). The border is the gradient painted on the border-box
   under a solid padding-box. style.css sets these with !important, so
   restating them needs the same. */
.signal-fab.signal-toggle {
  background:
    linear-gradient(var(--surface-3), var(--surface-3)) padding-box,
    var(--gradient-accent) border-box !important;
  color: var(--ink) !important;
  border: 1px solid transparent !important;
  box-shadow: var(--shadow-float) !important;
  text-shadow: none;
  font-weight: var(--w-body-strong);
}
.signal-fab .signal-fab-label {
  background: var(--gradient-accent);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
@media (hover: hover) and (pointer: fine) {
  .signal-fab.signal-toggle:hover {
    transform: none;
    background:
      linear-gradient(var(--surface-3), var(--surface-3)) padding-box,
      var(--gradient-accent-hover) border-box !important;
  }
}

/* The hint bubble above the pill (signal.js rotates its text), a message
   bubble whose tail points down at the pill. The widget is the pill's
   height while the panel is closed; the bubble is hidden while it is open.
   The text fades out and in over --dur-fade, which is 0s under reduced
   motion (tokens.css). */
.signal-hint {
  position: absolute;
  bottom: calc(100% + 0.75rem);
  right: 0;
  white-space: nowrap;
  padding: 0.45rem 0.8rem;
  border-radius: 1rem;
  border: 1px solid transparent;
  background:
    linear-gradient(var(--surface-3), var(--surface-3)) padding-box,
    var(--gradient-accent) border-box;
  color: var(--ink);
  box-shadow: var(--shadow-float);
  font-size: 0.8rem;
  pointer-events: none;
}
/* The point: an inline SVG (base.html) hanging from the bubble's bottom
   edge, like a speech bubble's. Its top row sits on the bubble's 1px bottom
   border (offsets count from inside the border; the SVG's bottom 10
   units hang below it, so bottom: -10px; it ends 4px short of the right
   edge, so right: 4px); the fill hides that stretch of border and the edge stroke leaves
   it tangentially, so the outline flows into the point. It sits at the
   bubble's right end, its right side running up into the bottom-right
   corner's curve (base.html has the geometry). The bubble is right-aligned
   with the Cue button, so the point (18px in) is over the desktop pill and
   the phone's 48px circle (centre 24px in). Under the
   point the bubble's 135deg gradient is about two-thirds of the way to
   violet, so the stroke mixes the two ends in that ratio. */
.signal-hint-tail { position: absolute; right: 4px; bottom: -10px; overflow: visible; pointer-events: none; }
.signal-hint-tail-fill { fill: var(--surface-3); }
.signal-hint-tail-edge { fill: none; stroke: color-mix(in srgb, var(--accent) 35%, var(--accent-2)); stroke-width: 1; }
.signal-hint-text { display: inline-block; transition: opacity var(--dur-fade) var(--ease-io); }
.signal-hint.is-fading .signal-hint-text { opacity: 0; }
@media (hover: none), (pointer: coarse) {
  .signal-fab.signal-toggle:hover { transform: none; }
}

/* ── Signal panel: chooser, chat, bug report (docs/adr/0017) ─────────────── */
/* The panel holds one view at a time (signal.js); each view is a column that
   fills the panel and scrolls inside it. The height cap is the viewport less
   160px: the widget's 80px offset above the phone's bottom bar (base.css),
   the 48px button, the widget's 8px gap and a 24px top margin, so the form
   never runs off the top of a short phone screen. 520px on a desktop is the
   old 500px chat plus the header's back/new-chat row. */
.signal-panel { display: flex; flex-direction: column; max-height: min(520px, calc(100dvh - 160px)); }
.signal-view { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.signal-header .signal-back,
.signal-header .signal-new-chat,
.signal-header .signal-close {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
  min-width: 32px; min-height: 32px; border-radius: var(--radius-sm);
}
.signal-new-chat-label { font-size: var(--text-small); }
.signal-chooser { gap: var(--space-2); padding: var(--space-3); overflow-y: auto; }
.signal-choice {
  display: flex; align-items: flex-start; gap: var(--space-3); width: 100%;
  min-height: 44px; padding: var(--space-3); text-align: left; cursor: pointer;
  background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
}
@media (hover: hover) and (pointer: fine) {
  .signal-choice:hover { border-color: var(--line-control); }
}
.signal-choice-icon { display: inline-flex; flex: 0 0 auto; color: var(--accent); padding-top: 2px; }
.signal-choice-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.signal-choice-title { font-weight: var(--w-body-strong); font-size: var(--text-small); }
.signal-choice-sub { color: var(--muted); font-size: var(--text-small); line-height: 1.4; }
/* The chat's messages shrink first when the panel is capped, so the input row
   stays on screen; the view scrolls as a last resort. The 160px floor for an
   empty chat applies only from a 480px-tall viewport: the cap there is 320px,
   which holds the header (~56px), 160px of messages, the "Still stuck?" link
   (~30px) and the input row (~50px), ~296px. */
.signal-chat { overflow-y: auto; }
.signal-chat .signal-messages { flex: 1 1 auto; min-height: 0; }
@media (min-height: 480px) {
  .signal-chat .signal-messages { min-height: 160px; }
}
.signal-msg-notice { color: var(--muted); }
.signal-msg-link { align-self: flex-start; color: var(--accent); font-size: var(--text-small); }
.signal-escalate {
  align-self: flex-start; margin: 0 var(--space-3) var(--space-2);
  background: none; border: none; padding: var(--space-1) 0; cursor: pointer;
  color: var(--accent); font-size: var(--text-small); text-decoration: underline;
}
.signal-report, .signal-thanks { gap: var(--space-2); padding: var(--space-3); overflow-y: auto; }
.signal-field-label { font-size: var(--text-small); font-weight: var(--w-body-strong); color: var(--ink); }
.signal-field {
  width: 100%; min-width: 0; min-height: 40px; padding: var(--space-2) var(--space-3);
  background: var(--surface-2); color: var(--ink);
  border: 1px solid var(--line-control); border-radius: var(--radius-sm);
  font: inherit; font-size: var(--text-small);
}
.signal-report-desc { resize: vertical; min-height: 96px; }
.signal-report-note { margin: 0; color: var(--muted); font-size: var(--text-small); line-height: 1.4; overflow-wrap: anywhere; }
.signal-report-error { margin: 0; color: var(--error); font-size: var(--text-small); }
.signal-report .btn { align-self: flex-start; }
.signal-thanks { align-items: flex-start; }
.signal-thanks-title { margin: 0; font-weight: var(--w-body-strong); color: var(--ink); }

/* ── Questionnaire (_components/questionnaire.html) ───────────────────────── */
/* Native fieldsets and inputs, styled from the tokens. Without JS every step
   shows; questionnaire.js hides all but the current one with [hidden]. The
   bar fills with --ink, not the accent: the accent is rationed to the primary
   action (test_responsive_a11y 7k), and here that is the Save button. */
.vx-q { max-width: 640px; font-family: var(--font-body); color: var(--ink); }
.vx-q-progress { margin-bottom: var(--space-block); }
.vx-q-count {
  margin: 0 0 8px;
  font-size: var(--text-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted);
}
.vx-q-bar {
  appearance: none;
  display: block;
  width: 100%;
  height: 6px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  overflow: hidden;
}
.vx-q-bar::-webkit-progress-bar { background: var(--surface-3); border-radius: var(--radius-pill); }
.vx-q-bar::-webkit-progress-value { background: var(--ink); border-radius: var(--radius-pill); transition: width var(--dur-ui) var(--ease); }
.vx-q-bar::-moz-progress-bar { background: var(--ink); border-radius: var(--radius-pill); }
.vx-q-step { border: 0; margin: 0 0 var(--space-block); padding: 0; min-width: 0; }
.vx-q-step[hidden] { display: none; }
.vx-q-legend {
  padding: 0;
  margin-bottom: 8px;
  font-family: var(--font-display);
  font-weight: var(--w-display);
  font-size: var(--text-h3);
  letter-spacing: var(--track-heading);
  line-height: 1.25;
}
.vx-q-optional { font-family: var(--font-body); font-weight: var(--w-body); font-size: var(--text-small); color: var(--muted); letter-spacing: 0; }
.vx-q-hint { margin: 0 0 16px; font-size: var(--text-small); color: var(--muted); }
.vx-q-options { display: grid; gap: 10px; }
.vx-q-option {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface-1);
  cursor: pointer;
  transition: border-color var(--dur-hover) var(--ease), background var(--dur-hover) var(--ease);
}
.vx-q-option:hover { border-color: var(--line-control); }
.vx-q-option:has(input:checked) { border-color: var(--accent); background: var(--surface-2); }
.vx-q-option:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vx-q-option input { margin: 3px 0 0; accent-color: var(--accent); flex: none; }
.vx-q-option-body { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: baseline; }
.vx-q-option-label { font-weight: var(--w-body-strong); font-size: var(--text-body); }
.vx-q-option-hint { grid-column: 2; font-size: var(--text-small); color: var(--muted); }
.vx-q kbd, .vx-q-key {
  font-family: var(--font-body);
  font-size: var(--text-label);
  min-width: 1.6em;
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--muted);
  text-align: center;
}
.vx-q-text {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line-control);
  border-radius: var(--radius-control);
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-body);
  resize: vertical;
}
.vx-q-text:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.vx-q-error { margin: 10px 0 0; color: var(--error); font-size: var(--text-small); font-weight: var(--w-body-strong); }
.vx-q-step [aria-invalid="true"] { border-color: var(--error); }
.vx-q-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.vx-q-nav-spacer { flex: 1 1 auto; }
.vx-q-nav [hidden] { display: none; }
.vx-q-keys { margin: 16px 0 0; font-size: var(--text-small); color: var(--faint); }
@media (hover: none), (pointer: coarse) { .vx-q-keys, .vx-q-key { display: none; } }

/* ── Buttons, redesign (ui-overhaul-v1) ───────────────────────────────────── */
/* Three kinds on one base: primary (the accent fill, one per screen),
   outline (a 3:1 edge that inverts on hover) and text. A pill, 44px at the
   least (WCAG 2.5.8 needs 24; Fitts). New names, so no existing page moves
   until its group is restyled. Use <a> to go somewhere, <button> to act. */
.vx-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--target-min);
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: var(--w-body-strong);
  line-height: 1.2;
  letter-spacing: -.005em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-2) var(--ease),
    border-color var(--dur-2) var(--ease),
    color var(--dur-2) var(--ease);
}
.vx-btn-lg { min-height: 3.5rem; padding: var(--space-4) var(--space-6); font-size: var(--fs-lead); }
.vx-btn:focus-visible {
  outline: var(--focus-width) solid var(--focus-ring);
  outline-offset: var(--focus-offset);
}
.vx-btn:disabled,
.vx-btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }

/* Primary: accent fill, --accent-ink text (7.3:1). .vx-btn-primary is also in
   the shared primary rule above; this restates the pill shape and padding. */
.vx-btn.vx-btn-primary {
  background: var(--accent);
  background-image: var(--gradient-accent);
  color: var(--accent-ink);
  border-color: transparent;
  min-height: var(--target-min);
  padding: var(--space-3) var(--space-5);
  font-size: var(--fs-body);
}
.vx-btn.vx-btn-primary.vx-btn-lg { min-height: 3.5rem; padding: var(--space-4) var(--space-6); font-size: var(--fs-lead); }

/* Outline: ink text, the control edge; hover inverts to an ink fill. */
.vx-btn-outline {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-control);
}
/* Text: no box at rest; still a 44px target. */
.vx-btn-text {
  background: transparent;
  color: var(--ink);
  padding-inline: var(--space-2);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 4px;
  transition: text-decoration-color var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .vx-btn.vx-btn-primary:hover:not(:disabled) { background: var(--accent-hover); background-image: var(--gradient-accent-hover); }
  .vx-btn-outline:hover:not(:disabled) { background: var(--ink); color: var(--bg); border-color: var(--ink); }
  .vx-btn-text:hover { text-decoration-color: currentColor; }
}

/* ── Greeting signal (_partials/greeting_signal.html) ─────────────────────── */
/* The dashboards' greeting visual, in the logo's gradient: a row of spectrum
   bars that breathe and a sine trace (with a fainter echo) that swells.
   Decoration only, so a phone, where the greeting already fills the width,
   does not get it, and reduced motion holds it still.
   30 bars x (4px bar + 4px gap) - the trailing gap = 236px, the strip's
   width; each bar shows its own 8px-offset slice of one 236px gradient, so
   the colour runs continuously across the strip instead of repeating. */
.greeting-signal { display: none; }
@media (min-width: 741px) {
  .greeting-signal {
    display: block;
    position: relative;
    flex: 0 0 auto;
    width: 236px;
    height: 56px;
    pointer-events: none;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
  }
}
.greeting-signal-bars {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  gap: 4px;
  opacity: .45;
}
.greeting-signal-bars > span {
  flex: 0 0 4px;
  height: 100%;
  border-radius: 2px;
  transform-origin: bottom;
  transform: scaleY(var(--h));
  background-image: linear-gradient(90deg, rgb(var(--logo-1-rgb)), rgb(var(--logo-2-rgb)), rgb(var(--logo-3-rgb)));
  background-size: 236px 100%;
  background-position: calc(var(--i) * -8px) 0;
  animation: greeting-signal-pulse 1.8s ease-in-out infinite alternate;
  animation-delay: calc(var(--i) * -110ms);
}
@keyframes greeting-signal-pulse {
  from { transform: scaleY(calc(var(--h) * .35)); }
  to   { transform: scaleY(var(--h)); }
}
.greeting-signal-wave {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.greeting-signal-line {
  fill: none;
  stroke: url(#greeting-signal-ink);
  stroke-width: 2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  transform-origin: center;
  transform-box: fill-box;
  animation: greeting-signal-swell 2.4s ease-in-out infinite alternate;
}
/* A beat behind the first, fainter: an echo that gives the line depth
   without another colour. */
.greeting-signal-line--ghost {
  opacity: .35;
  animation-delay: -1.2s;
}
@keyframes greeting-signal-swell {
  from { transform: scaleY(.3); }
  to   { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .greeting-signal-bars > span,
  .greeting-signal-line { animation: none; }
}
