/* ═══════════════════════════════════════════════════════════════════
   Continuous-value editors — inline number input + Update on a lever
   row; same shape on action cards (✓ Done expands the form). Same
   tokens as the binary self-report controls so the two flavours read
   as siblings, not strangers.
   ═══════════════════════════════════════════════════════════════════ */

.continuous-editor {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  font-family: var(--font-mono);
}

.continuous-editor-input-wrap {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.continuous-editor-input {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-primary);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 4px 8px;
  width: 76px;
  text-align: right;
}
.continuous-editor-input:focus {
  outline: none;
  border-color: var(--accent);
}

.continuous-editor-unit {
  font-size: 11px;
  color: var(--text-dim);
}

.continuous-editor-source {
  font-size: 10px;
  color: var(--text-dim);
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.continuous-editor-source-stale {
  color: var(--amber);
}
.continuous-editor-source-stale::before {
  content: '⚠ ';
}

/* Lever-expansion reading affordance — "Log a reading" rides in the header
   next to "Trend ↗" as a quiet tertiary link (reusing .lever-exp-trend-link),
   not a boxed button on its own row. Clicking it reveals the tuner below the
   header via the host's is-editing class. */
.lever-exp-top-actions {
  display: inline-flex;
  align-items: center;
  gap: 0;
  flex-shrink: 0;
}
/* Hairline divider between the tertiary actions so "Log a reading" and
   "Trend ↗" don't read as one run-on string. */
.lever-exp-action-sep {
  width: 1px;
  height: 11px;
  margin: 0 4px;
  background: var(--border);
}
.lever-exp-reading-editor {
  display: none;
  margin-top: var(--space-md);
}
.lever-exp-reading-host.is-editing .lever-exp-reading-editor {
  display: block;
}

.sr-btn-confirm {
  border-color: var(--accent);
  color: var(--accent);
}
.sr-btn-confirm:hover {
  background: var(--leverage-bg);
}

/* Lever-drilldown empty-state variants — banked vs unreported vs
   genuine "no tactics." Banked is positive (green tint); unreported is
   a quiet nudge to the input above. */
.lever-exp-empty-banked {
  color: var(--green);
}
.lever-exp-empty-unreported {
  color: var(--text-dim);
  font-style: italic;
}

/* Committed tag on completed-but-not-yet-banked tactics — sits next to
   the leverage badge so the user reads "I've signaled intent on this"
   without the row losing its rank position. */
.lever-exp-action-committed-tag {
  display: inline-flex;
  align-items: center;
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--accent);
  margin-right: 6px;
}
.lever-exp-action-committed .lever-exp-action-text {
  text-decoration: line-through;
  text-decoration-color: var(--border);
}

/* ═══════════════════════════════════════════════════════════════════
   Calibrate act — final welcome step. Lives inside .welcome-card so
   chrome (logo, subtitle, progress, 3D model) matches the other acts.
   No per-row Save button: typing a number commits on debounce, picking
   a select option commits on change. The single primary control is the
   "Continue" button at the bottom.
   ═══════════════════════════════════════════════════════════════════ */

.welcome-card-calibrate {
  /* Calibrate has more vertical content than other acts (4 rows + foot).
     Stretch the card and let the slot scroll if the viewport is tight. */
  max-height: min(86vh, 720px);
}

.welcome-act-calibrate {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.calibrate-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.calibrate-row {
  padding: 16px 0;
  border-bottom: 1px solid rgba(140, 154, 172, 0.18);
}
.calibrate-row:first-child { border-top: 1px solid rgba(140, 154, 172, 0.18); }

.calibrate-row-form {
  display: grid;
  grid-template-columns: minmax(86px, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: baseline;
  gap: 4px 10px;
  text-align: left;
}

.calibrate-row-name {
  grid-column: 1;
  grid-row: 1;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: 0.2px;
}

.calibrate-row-input-wrap {
  grid-column: 2;
  grid-row: 1;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  justify-content: flex-end;
}

.calibrate-row-input-wrap input[type="number"],
.calibrate-row-input-wrap select {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(140, 154, 172, 0.35);
  border-radius: 4px;
  padding: 5px 8px;
  transition: border-color 0.15s, background 0.15s;
}
.calibrate-row-input-wrap input[type="number"] {
  width: 76px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
/* Hide the native up/down spinners — they're visual noise and the user
   can just type the value. Matches the demographics row + tuner readout
   treatment elsewhere in the welcome flow. */
.calibrate-row-input-wrap input[type="number"]::-webkit-outer-spin-button,
.calibrate-row-input-wrap input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.calibrate-row-input-wrap input[type="number"]:focus,
.calibrate-row-input-wrap select:focus {
  outline: none;
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.95);
}

.calibrate-row-unit {
  font-size: 11px;
  color: var(--text-dim);
}

.calibrate-row-goal {
  grid-column: 1;
  grid-row: 2;
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.2px;
}

.calibrate-row-source {
  grid-column: 2;
  grid-row: 2;
  font-size: 9px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  text-align: right;
  color: var(--text-dim);
}
.calibrate-source-vital { color: var(--text-secondary); }

/* Pill-pair boolean (e.g. diet → "Mediterranean / DASH" vs "Standard").
   Same visual grammar as the mortality-calc visitor tool but typed in
   the welcome flow's mono vocabulary instead of Inter. Sits inside
   .calibrate-row-input-wrap so the existing right-aligned grid column
   handles placement. */
.calibrate-row-pills {
  /* Segmented control: zero gap + overlapped borders (below) fuse the
     options into one bar, signalling single-select. nowrap keeps the bar
     intact — the labels are short enough not to overflow the row. The
     explicit inline-flex lets this nest inside .calibrate-row-input-wrap
     as its own group (continuous preset rows append a trailing unit after
     it); the combined-class boolean/smoking rows are already inline-flex. */
  display: inline-flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
}
.calibrate-pill {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.4px;
  padding: 5px 10px;
  background: transparent;
  border: 1px solid rgba(140, 154, 172, 0.35);
  border-radius: 999px;
  color: var(--text-dim);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.calibrate-pill:hover {
  color: var(--text-primary);
  background: var(--bg-surface);
}
.calibrate-pill-on,
.calibrate-pill-on:hover {
  background: var(--text-primary);
  border-color: var(--text-primary);
  color: #ffffff;
}

/* Options that carry a hover/focus definition (e.g. diet → "Mediterranean")
   render a small inline ⓘ glyph after the label. The tooltip lives on the
   pill itself, so the whole segment is the affordance — no separate cell,
   no divider. The glyph just signals "there's more here". */
.calibrate-pill-hint {
  margin-left: 5px;
  font-style: italic;
  font-weight: 600;
  font-size: 9px;
  opacity: 0.65;
  vertical-align: 1px;
  cursor: help; /* the ⓘ is the tooltip target, not the whole option */
}
.calibrate-pill:hover .calibrate-pill-hint,
.calibrate-pill-on .calibrate-pill-hint {
  opacity: 0.9;
}
.calibrate-pill-hint:hover {
  opacity: 1;
}
/* Compact the define callout. The ⓘ sits well into the right half of the
   card, so the default 280px panel (right-anchored from the icon) stretches
   nearly the full width and its left edge gets clipped by .welcome-card's
   overflow:hidden. A narrower panel keeps the whole callout inside the card;
   right-anchoring guarantees it never spills off the right (the "Standard"
   segment is always to the icon's right). */
.calibrate-pill-hint[data-tooltip]::after {
  max-width: 220px;
}

/* Filled state — applied across step 1 demographics and step 3 details
   whenever a row has a value. Reads as "you've given an answer here",
   not "your answer is clinically good". Goal-state coaching lives on
   the dashboard, not in the collection flow. The row label is left
   uncolored — the green input border / green selected pill already carry
   the answered cue, matching the demographics rows. */
.calibrate-row-filled .calibrate-row-input-wrap input[type="number"],
.calibrate-row-filled .calibrate-row-input-wrap select {
  border-color: rgba(16, 185, 129, 0.45);
}
.calibrate-row-filled .calibrate-pill-on {
  background: var(--green);
  border-color: var(--green);
}

/* ── Segmented control join ──────────────────────────────────────────
   Fuse the gapless pills (above) into one bar: overlap adjacent borders
   into a single hairline, square the interior corners, and round only the
   two outer caps. Every segment is now a bare .calibrate-pill (the ⓘ rides
   inside its pill as an inline glyph), so the rules are uniform. */
.calibrate-row-pills > .calibrate-pill {
  position: relative; /* let the active/hover border sit above neighbours */
  border-radius: 0;   /* interior corners square; caps re-rounded below */
}
.calibrate-row-pills > .calibrate-pill + .calibrate-pill {
  margin-left: -1px; /* collapse the seam between adjacent segments */
}
.calibrate-row-pills > .calibrate-pill:first-child {
  border-top-left-radius: 999px;
  border-bottom-left-radius: 999px;
}
.calibrate-row-pills > .calibrate-pill:last-child {
  border-top-right-radius: 999px;
  border-bottom-right-radius: 999px;
}
/* Raise the selected / interacted segment so its (darker or green) border
   isn't clipped by the neighbour overlapping on top of it. */
.calibrate-row-pills .calibrate-pill-on,
.calibrate-row-pills .calibrate-pill:hover,
.calibrate-row-pills .calibrate-pill:focus-visible {
  z-index: 1;
}

.calibrate-continue {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: white;
  background: var(--accent);
  border: 0;
  border-radius: 4px;
  padding: 10px 28px;
  cursor: pointer;
  transition: background 0.15s, transform 0.12s;
}
.calibrate-continue:hover { background: #0e7490; }
.calibrate-continue:active { transform: scale(0.98); }

/* ─── Demographics fallback (skip-CSV path) ─────────────────────
   Inline form shown when the user opts out of bloodwork upload. The
   row shape mirrors .calibrate-row (same grid, same input styling)
   so it reads as native to the welcome aesthetic — calibrate is the
   user's nearby reference. */

.welcome-act-demographics {
  width: 100%;
  display: flex;
  flex-direction: column;
  /* No `gap` here: prompt→content spacing is owned solely by
     .welcome-act-prompt's margin-bottom (the documented single source, so
     every act matches). A flex gap stacked on top double-spaced step 1. */
}

.demographics-form { width: 100%; }

.welcome-act-demographics .welcome-act-footer,
.welcome-act-lifestyle .welcome-act-footer {
  margin-top: var(--space-lg);
}

.demographics-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.demographics-row {
  display: grid;
  grid-template-columns: minmax(86px, 1fr) auto;
  align-items: center;
  gap: 4px 10px;
}

.demographics-row-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: 0.2px;
  text-align: left;
}

.demographics-row-input-wrap {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  justify-content: flex-end;
}

.demographics-row-input-wrap input[type="number"],
.demographics-row-input-wrap input[type="date"],
.demographics-row-input-wrap select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: textfield;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 4px 8px;
  font-variant-numeric: tabular-nums;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.demographics-row-input-wrap input[type="number"]::-webkit-outer-spin-button,
.demographics-row-input-wrap input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.demographics-row-input-wrap input[type="number"] {
  width: 56px;
  text-align: right;
}
.demographics-row-input-wrap input[type="date"] {
  width: 148px;
}
.demographics-row-input-wrap input[type="date"]::-webkit-calendar-picker-indicator {
  opacity: 0.45;
  cursor: pointer;
  padding: 0 0 0 4px;
  transition: opacity 0.14s ease;
}
.demographics-row-input-wrap input[type="date"]:hover::-webkit-calendar-picker-indicator,
.demographics-row-input-wrap input[type="date"]:focus::-webkit-calendar-picker-indicator {
  opacity: 0.85;
}

.demographics-row-input-wrap select {
  min-width: 160px;
  padding-right: 24px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'><path d='M1 1L5 5L9 1' stroke='%238c9aac' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 8px center;
  cursor: pointer;
}
/* The height pair packs two short selects (ft, in) — size them to content
   instead of the wide single-select min-width (and the mobile full-width
   rule). Tapping still opens the OS-native wheel picker; this only governs
   the inline footprint. Applies on both breakpoints. */
.demographics-row-input-wrap-pair select {
  min-width: 0;
  width: 64px;
}

.demographics-row-input-wrap input:hover,
.demographics-row-input-wrap select:hover {
  border-color: rgba(140, 154, 172, 0.25);
}
.demographics-row-input-wrap input:focus,
.demographics-row-input-wrap select:focus {
  outline: none;
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.85);
}

/* Touched state for text inputs — the field's border turns green once the
   user actually edits it (vs an ICP default). The row label no longer
   recolors: the chip's own soft-default-vs-green state already signals
   answered-vs-guess, so a green label was redundant and left labels
   inconsistently colored across rows. */
.demographics-row-touched .demographics-row-input-wrap input,
.demographics-row-touched .demographics-row-input-wrap select {
  border-color: rgba(16, 185, 129, 0.45);
}

.demographics-row-input-wrap-pair {
  gap: 2px;
}

.demographics-row-unit {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.4px;
  color: var(--text-dim);
  text-transform: uppercase;
}

/* Segmented-chip groups for sex / ethnicity. Reuse the lifestyle step's
   .calibrate-pill grammar; the only delta is wrap. Five ethnicity chips
   outrun the card width, so the group wraps to a second row instead of the
   nowrap fused bar lifestyle uses. justify-content is left unset so it
   inherits flex-end (desktop, base .demographics-row-input-wrap) and
   flex-start (mobile, the 720 override) for free. */
.demographics-row-chips {
  flex-wrap: wrap;
  gap: 5px;
}
.demographics-row-chips .demo-chip {
  padding: 5px 9px;
  letter-spacing: 0.2px;
}
/* Family history breaks the 2-column row grammar. It's a multi-select tag
   cloud with long, uneven labels ("Early heart disease" vs "Stroke"), not a
   compact control — the narrow right column wraps it into a ragged staircase.
   So it spans the full card width: label on its own line, chips left-packed
   into even rows. Its chips are .fh-chip (.calibrate-pill, the toggle grammar),
   sized to match the .demo-chip groups above. */
.demographics-row-fh {
  display: block;
}
/* Family history is collapsed behind a native <details> disclosure: it's the
   optional, recall-only pillar, so it earns one fold down rather than sitting
   at full weight with the fast demographic facts. Same design on both
   breakpoints. The summary mirrors the other rows' label-left grammar with a
   chevron + answer-count on the right. */
.fh-summary {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  list-style: none;
  user-select: none;
  border-radius: 4px;
  -webkit-tap-highlight-color: transparent;
}
.fh-summary::-webkit-details-marker {
  display: none;
}
.fh-summary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent);
}
/* Summary reuses the row label; drop the standalone-row margin it would
   otherwise inherit so it centers in the grid. */
.fh-summary .demographics-row-name {
  margin: 0;
}
.fh-summary-meta {
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
/* Answered-count echo (e.g. "2 selected" / "None") so collapsing never hides
   what the user picked. Green = answered, same language as the touched borders. */
.fh-summary-count {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.2px;
  color: var(--green);
}
.fh-summary-chevron {
  width: 10px;
  height: 6px;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'><path d='M1 1L5 5L9 1' stroke='%238c9aac' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>") no-repeat center;
  transform: rotate(-90deg);
  transition: transform 0.18s ease;
}
.fh-disclosure[open] .fh-summary-chevron {
  transform: rotate(0);
}
.fh-disclosure-body {
  margin-top: 12px;
}
.demographics-row-fh .fh-chips {
  gap: 5px;
}
.demographics-row-fh .fh-chip {
  padding: 5px 9px;
  letter-spacing: 0.2px;
}
@media (prefers-reduced-motion: reduce) {
  .fh-summary-chevron {
    transition: none;
  }
}
/* Ethnicity is a fixed six-choice set. Grid it as 3×2 so adding the
   South Asian split doesn't leave flex-wrap to create ragged 2/2/2 rows. */
.demographics-row-chips-ethnicity {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, max-content));
  justify-content: end;
  max-width: none;
}
.demographics-row-chips-ethnicity .demo-chip {
  padding-inline: 7px;
  font-size: 11px;
  white-space: nowrap;
}
/* Touched answer reads green — same language as the form's input borders.
   Before touch, the default-selected chip reads as a tentative *guess*: an
   ink OUTLINE + bold dark text, no fill (overriding the base solid-dark
   .calibrate-pill-on). Fill is reserved for commitment — a ring says "our
   guess, confirm or change it", a fill (green, below) says "you chose this".
   Replaces the old --bg-surface fill, which competed with the green commit.
   Once the user picks, the chosen chip turns solid green and the label follows. */
.demographics-row:not(.demographics-row-touched) .demo-chip.calibrate-pill-on {
  background: transparent;
  border-color: var(--text-secondary);
  color: var(--text-primary);
}
.demographics-row:not(.demographics-row-touched) .demo-chip.calibrate-pill-on:hover {
  background: transparent;
  border-color: var(--text-primary);
  color: var(--text-primary);
}
.demographics-row-touched .demo-chip.calibrate-pill-on,
.demographics-row-touched .demo-chip.calibrate-pill-on:hover {
  background: var(--green);
  border-color: var(--green);
}
/* Family history has no ICP default to soften — every chip starts off and
   each tap is a user choice — so a selected fh-chip reads solid green
   directly (matching the touched sex/ethnicity chips above) rather than
   the base dark .calibrate-pill-on. */
.demographics-row-fh .fh-chip.calibrate-pill-on,
.demographics-row-fh .fh-chip.calibrate-pill-on:hover {
  background: var(--green);
  border-color: var(--green);
}

/* ─── Live LE readout ────────────────────────────────────────────────
   A single life-expectancy figure that surfaces once demographics are in
   and sharpens as bloodwork / vitals arrive. Two placements share one
   updater: anchored to the wireframe figure on desktop, inline in the card
   on phones (where the figure is hidden). See welcome.js#readoutHTML. */
/* Visually-hidden but screen-reader-available. The animated digits are
   aria-hidden noise; the real value rides this node. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.welcome-readout {
  font-family: var(--font-mono);
  text-align: center;
  pointer-events: none;
  user-select: none;
}
/* The number is the hero — no caption, no instrument chrome; in a Longevity
   Radar the figure-with-a-number-of-years reads for itself. */
.welcome-readout-value {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  line-height: 1;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  padding: 0 10px 7px;
}
.welcome-readout.is-live .welcome-readout-value::after,
.welcome-readout.is-acquiring .welcome-readout-value::after {
  content: '';
  position: absolute;
  left: 12%;
  right: 12%;
  bottom: 0;
  height: 2px;
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, rgba(8, 145, 178, 0.68), transparent);
  opacity: 0.7;
}
/* Acquiring (engaged, no value yet): the underline is fainter than live —
   the sensor is locked on but hasn't resolved a figure. */
.welcome-readout.is-acquiring:not(.is-live) .welcome-readout-value::after {
  opacity: 0.32;
}
@media (max-width: 720px) {
  .welcome-readout.is-live .welcome-readout-value::after,
  .welcome-readout.is-acquiring .welcome-readout-value::after {
    display: none;
  }
  /* No underline on mobile, so acquiring leans on a color shift instead of a
     faint opacity bump: the dashes warm from dim grey toward the live ink so
     the readout visibly "powers on." Still no number. */
  .welcome-readout.is-acquiring:not(.is-live) .welcome-readout-num {
    color: var(--text-primary);
    opacity: 0.78;
  }
  .welcome-readout.is-acquiring:not(.is-live) .welcome-readout-unit {
    opacity: 0.9;
  }
}
.welcome-readout-num {
  display: inline-flex;
  align-items: baseline;
  font-size: 64px;
  font-weight: 500;
  letter-spacing: 0.5px;
}
/* Tenths: small, raised toward the cap line of the hero integers
   (superscript feel) rather than baseline-aligned (which left the decimal
   point floating as a giant bullet). */
.welcome-readout-dec {
  font-size: 0.4em;
  align-self: flex-start;
  letter-spacing: 0;
  transform: translateY(0.22em);
}
/* Ambient "live" cue: the decimal separator pulses erratically — a
   double-dip followed by a long idle, then a faint blip, then silence.
   Reads as a live signal with noise rather than a metronome. The value
   never moves; only this one mark flickers. */
.welcome-readout-dot {
  display: inline-block;
}
.welcome-readout.is-live .welcome-readout-dot,
.welcome-readout.is-acquiring .welcome-readout-dot {
  animation: welcome-readout-pulse 3.8s ease-in-out infinite;
}
@keyframes welcome-readout-pulse {
  0%   { opacity: 1; }
  6%   { opacity: 0.12; }
  13%  { opacity: 0.88; }
  18%  { opacity: 0.08; }
  27%  { opacity: 1; }
  58%  { opacity: 1; }
  62%  { opacity: 0.48; }
  67%  { opacity: 1; }
  100% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .welcome-readout-dot { animation: none; }
}
/* Idle placeholder (no value yet): "--.- y" keeps the readout's footprint
   stable and gives the pending value a unit without adding explanatory copy. */
.welcome-readout:not(.is-live) .welcome-readout-num {
  color: var(--text-dim);
  opacity: 0.38;
  font-weight: 500;
  transition: opacity 0.45s ease;
}
.welcome-readout:not(.is-live) .welcome-readout-unit {
  color: var(--text-dim);
  opacity: 0.5;
  transition: opacity 0.45s ease;
}
/* Acquiring: dashes brighten a step — alert, awaiting resolution, no number. */
.welcome-readout.is-acquiring:not(.is-live) .welcome-readout-num {
  opacity: 0.62;
}
.welcome-readout.is-acquiring:not(.is-live) .welcome-readout-unit {
  opacity: 0.75;
}
.welcome-readout-unit {
  font-size: 22px;
  font-weight: 500;
  letter-spacing: 0.5px;
  color: #03677f;
  margin-left: 5px;
}

/* Desktop: pinned beneath the figure. The model canvas pans the subject to
   roughly the left-third of the full viewport (camera target.x ≈ 0.7, not a
   true 25vw center), so the readout anchors there too — under the feet,
   horizontally centered on the subject. Fixed-to-viewport (not the
   max-width-1400 .welcome box) so it tracks the figure on wide monitors.
   z-index 2 lifts it over the canvas without reaching the card column. */
.welcome-readout-stage {
  position: fixed;
  left: 36vw;
  bottom: 9vh;
  transform: translateX(-50%);
  z-index: 2;
  /* Fade in with the wireframe model — same delay/duration as scene.fadeIn
     in welcome.js (350ms / 1500ms) — so the figure and its projected-years
     readout arrive as one. `backwards` fill (no static opacity) holds it
     hidden through the delay then releases to its natural opacity, so the
     later .is-terminal / .is-leaving-final recedes (opacity→0 transitions)
     still take over cleanly. */
  animation: welcome-slot-fade-in 1500ms ease 350ms backwards;
}
/* Card variant is desktop-hidden — the stage variant owns desktop. The
   phone swap lives in the media query just below (co-located here so it
   wins the cascade over this base rule regardless of where the global
   720px block sits in the file). */
.welcome-readout-card {
  display: none;
}
/* Phone: the figure (and its anchored stage readout) is gone, so the card
   variant takes over inline between the progress pills and the form. */
@media (max-width: 720px) {
  .welcome-readout-stage {
    display: none;
  }
  .welcome-readout-card {
    display: block;
    margin: 4px 0 14px;
    opacity: 0;
    /* Recompacted with the rest of the mobile stagger (subtitle hidden, so
       the whole sequence moves up one beat — see the 720px shell block). */
    animation: welcome-fade-rise 900ms cubic-bezier(.2, .7, .2, 1) 1700ms forwards;
  }
  .welcome-readout-card .welcome-readout-num {
    font-size: 36px;
  }
  .welcome-readout-card .welcome-readout-unit {
    font-size: 15px;
  }
}

/* ═══════════════════════════════════════════════════════════════════
   Files step — two peer dropzones (bloodwork PDF + Apple Health) side
   by side, each with a quiet escape-hatch link / disclosure beneath.
   The two inputs are co-equal axes in the risk engine (see ETHOS.md),
   so they get equal visual weight in a 2-col grid rather than a
   stacked primary/secondary hierarchy.
   ═══════════════════════════════════════════════════════════════════ */

.welcome-act-files {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.welcome-act-files-footer {
  display: flex;
  justify-content: center;
  margin-top: var(--space-xs);
}

/* Peer row: bloodwork left, Apple Health right. Cells stretch to a
   shared height so the two cards read as equals; the base .upload-zone
   centers its content, so whichever side has less to show floats its
   icon + label in the middle of the matched card. */
.welcome-act-files-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  align-items: stretch;
}

/* Adaptive stack: the moment either zone is loaded, the row collapses
   to a single column so the loaded card gets the full slot width.
   Empty state retains the side-by-side "two co-equal inputs" reading;
   loaded state gets room to breathe (no truncation, no wasted stretch
   forcing the Apple card to grow with a tall multi-draw bloodwork). */
.welcome-act-files-row--stacked {
  grid-template-columns: 1fr;
}

/* Tighter empty-state dropzone for the side-by-side layout — the base
   .upload-zone's 210px floor would force a tall column with a tiny
   icon island in the middle. Loaded / parsing / multi-draw states grow
   past this floor naturally via their own content. */
.welcome-act-files .upload-zone-files-primary {
  padding: var(--space-md) var(--space-md);
  min-height: 124px;
}
.welcome-act-files .upload-icon {
  margin-bottom: var(--space-sm);
}

/* Grouping wrapper so a dropzone and its escape-hatch (AI link under
   bloodwork, "how to export" disclosure under Apple Health) read as a
   single unit, with a tight top-margin on the affordance below. The
   dropzone itself flex-grows to fill the wrapper so the two columns
   match heights when the row stretches them. */
.welcome-act-files-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Grid items default to min-width: auto, which lets `nowrap` content
     inside a child .upload-zone push the column wider than 1fr (the
     whole 2-col row then overflows the parent card). Force the grid
     track to honor its 1fr share instead. */
  min-width: 0;
}
.welcome-act-files-group > .upload-zone {
  flex: 1;
  min-width: 0;
}

/* Progressive disclosure for the Apple Health export steps. Sits in
   the same caption slot as `.bloodwork-ai-link` opposite — closed by
   default (one-line "how to export →"), opens to a small popover that
   floats above the summary so it doesn't reflow the rest of the slot.
   Same dim mono treatment as the AI link so the two columns read as
   typographic peers whether or not the disclosure is expanded. */
.apple-export-help {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-dim);
  letter-spacing: 0.2px;
  text-align: center;
  position: relative;
}
.apple-export-help summary {
  display: inline-block;
  cursor: pointer;
  padding: 4px 8px;
  color: var(--text-dim);
  text-decoration: underline;
  text-decoration-color: rgba(140, 154, 172, 0.35);
  text-underline-offset: 3px;
  transition: color 0.14s ease, text-decoration-color 0.14s ease;
  list-style: none;
}
.apple-export-help summary::-webkit-details-marker {
  display: none;
}
.apple-export-help summary:hover,
.apple-export-help summary:focus-visible {
  color: var(--text-secondary);
  text-decoration-color: rgba(140, 154, 172, 0.65);
  outline: none;
}
.apple-export-help[open] summary {
  color: var(--text-secondary);
  text-decoration-color: rgba(140, 154, 172, 0.65);
}
/* Floating popover. Sits above the summary; doesn't push siblings.
   Anchored to the disclosure's own column so a too-narrow grid cell
   won't force a wider popover off-screen. The downward-pointing tail
   ties it back to the summary that opened it. */
.apple-export-help p {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(280px, calc(100% + 32px));
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  color: var(--text-secondary);
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(10, 28, 44, 0.08);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 4;
}
.apple-export-help p::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: rgba(255, 255, 255, 0.96);
  filter: drop-shadow(0 1px 0 var(--border));
}
.apple-export-help em {
  font-style: normal;
  color: var(--text-primary);
}

/* Quiet escape-hatch link directly below the bloodwork dropzone. Reads
   as a fallback, not a peer of the dropzone — same dim mono treatment
   as `.upload-zone-aside` but underlined so it cues "actionable." */
.bloodwork-ai-link {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-dim);
  background: none;
  border: 0;
  padding: 4px 8px;
  cursor: pointer;
  letter-spacing: 0.2px;
  text-align: center;
  align-self: center;
  text-decoration: underline;
  text-decoration-color: rgba(140, 154, 172, 0.35);
  text-underline-offset: 3px;
  transition: color 0.14s ease, text-decoration-color 0.14s ease;
}
.bloodwork-ai-link:hover,
.bloodwork-ai-link:focus-visible {
  color: var(--text-secondary);
  text-decoration-color: rgba(140, 154, 172, 0.65);
  outline: none;
}

/* ═══════════════════════════════════════════
   5-act onboarding: single-card layout
   ═══════════════════════════════════════════
   Acts 2-4 (bloodwork / vitals / genome) each render one centered
   dropzone filling the slot width, with a row of helper links below.
   This replaces the side-by-side layout the old combined Files act
   used. The base .upload-zone's 210px floor was sized for half-width
   columns; at full slot width it dominates the card, so single-act
   dropzones get a shorter floor and tighter padding. Loaded /
   multi-draw states grow past the floor naturally via their content. */
.welcome-act-single {
  display: block;
  width: 100%;
}
/* The base .welcome-act-footer margin-top of 8px reads as cramped under
   the helper-links row in these acts — there are only a handful of
   elements in the slot, so the skip / continue button needs real
   separation to land as the primary CTA rather than a third link.
   Demographics keeps the 8px because its dense form already provides
   the visual anchor above. */
.welcome-act-bloodwork .welcome-act-footer,
.welcome-act-vitals .welcome-act-footer,
.welcome-act-genome .welcome-act-footer,
.welcome-act-key-ask .welcome-act-footer {
  margin-top: var(--space-xl);
}
.welcome-act-single .upload-zone-files-primary {
  padding: var(--space-lg) var(--space-lg);
  min-height: 148px;
}
.welcome-act-single .upload-zone-files-primary .upload-icon {
  margin-bottom: var(--space-sm);
}
/* Loaded single-file card (success check + filename + summary +
   replace) doesn't need the 210px floor either — its content reads
   fine at the shorter height and matches the empty state's footprint. */
.welcome-act-single .upload-zone-files-primary.upload-zone-success {
  min-height: 148px;
}

/* Genome instructions sub-screen — full slot replacement (same crossfade
   pattern as the manual-entry sub-screens). Sets expectations + shows
   the JSON shape; the actual recipe lives outside the card. Sized to
   fit the slot without scrolling. */
.genome-howto,
.vitals-howto {
  text-align: left;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
  max-width: 520px;
  margin: 0 auto;
}
.genome-howto-lede,
.genome-howto-pipeline {
  margin: 0 0 var(--space-md);
}
/* The global `* { padding: 0 }` reset strips the <ol>'s padding, so outside
   markers render flush against the slot edge. Restore an indent (the vitals
   steps already do this; genome's were missed — see Image: numbers hard up
   against the screen edge). */
.genome-howto-steps {
  margin: 0 0 var(--space-md);
  padding-left: 22px;
}
.genome-howto-steps li {
  margin: 0 0 var(--space-sm);
}
.genome-howto-shape-label {
  margin: var(--space-md) 0 var(--space-xs);
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.2px;
}
.genome-howto code,
.vitals-howto code {
  font-family: var(--font-mono);
  font-size: 11px;
  background: var(--bg-surface);
  padding: 1px 5px;
  border-radius: 3px;
  color: var(--text-primary);
}

/* ── Drop-target wrapper for the drill-in upload screens (the Apple Health
   export recipe, the genome prep hub). The real dropzone leads a stacked
   column above the instructions; compacted from the base .upload-zone floor so
   it shares the card with the recipe below it. ── */
.welcome-source-upload {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  /* Separate the drop target from the instructions that follow it — without
     this they crowd (the dropzone's own border ends flush against step 1). */
  margin-bottom: var(--space-lg);
}
/* The hero dropzone leads a stacked column, not a half-width peer — drop the
   base .upload-zone 210px floor to a compact card. */
.welcome-source-upload > .upload-zone-files-primary {
  padding: var(--space-lg);
  min-height: 132px;
}

/* Progressively-disclosed export recipe (Apple Health's export is short enough
   to live inline). Collapsed by default so the initial stack — dropzone, "how
   to export", or-divider, Sync — stays clean. Quiet centered summary + chevron. */
.welcome-export-help {
  text-align: left;
}
.welcome-export-help-summary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  list-style: none;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.2px;
  color: var(--text-dim);
  padding: 4px;
  transition: color 0.14s;
}
.welcome-export-help-summary::-webkit-details-marker {
  display: none;
}
.welcome-export-help-summary:hover {
  color: var(--accent);
}
.welcome-export-help-chevron {
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.16s;
}
.welcome-export-help[open] .welcome-export-help-chevron {
  transform: rotate(-135deg);
}
.welcome-export-help-steps {
  margin: var(--space-sm) 0 0;
  padding-left: 22px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.welcome-export-help-steps li {
  margin: 0 0 var(--space-xs);
}
.welcome-export-help-steps li strong {
  color: var(--text-primary);
  font-weight: 600;
}
.welcome-export-help-steps code {
  font-family: var(--font-mono);
  font-size: 11px;
  background: var(--bg-surface);
  padding: 1px 5px;
  border-radius: 3px;
  color: var(--text-primary);
}

.genome-howto a,
.vitals-howto a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: rgba(8, 145, 178, 0.4);
  text-underline-offset: 2px;
}
.genome-howto a:hover,
.vitals-howto a:hover {
  text-decoration-color: var(--accent);
}
/* Numbered export recipe — Apple's export is a one-step provider flow, so
   it reads as an ordered list rather than the genome prose pipeline. */
.vitals-howto-steps {
  margin: 0 0 var(--space-md);
  padding-left: 20px;
}
.vitals-howto-steps li {
  margin: 0 0 var(--space-sm);
}
.vitals-howto-steps li strong {
  color: var(--text-primary);
  font-weight: 600;
}
.genome-howto-code {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  margin: 0;
  white-space: pre;
  overflow-x: auto;
  color: var(--text-primary);
  text-align: left;
}

/* Genome how-to HUB — two tappable rows that branch to the pipeline and
   schema sub-screens. Each reads as a destination, not a button label:
   number chip + title/sub stack + trailing arrow. */
.genome-howto-menu {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.genome-howto-nav {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  text-align: left;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease;
}
.genome-howto-nav:hover,
.genome-howto-nav:focus-visible {
  border-color: var(--accent);
  background: rgba(8, 145, 178, 0.06);
  outline: none;
}
.genome-howto-nav-num {
  flex: none;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
}
.genome-howto-nav-text {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.genome-howto-nav-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary);
}
.genome-howto-nav-sub {
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.2px;
}
.genome-howto-nav-arrow {
  flex: none;
  color: var(--text-dim);
  font-size: 13px;
  transition: transform 160ms ease, color 160ms ease;
}
.genome-howto-nav:hover .genome-howto-nav-arrow,
.genome-howto-nav:focus-visible .genome-howto-nav-arrow {
  color: var(--accent);
  transform: translateX(2px);
}
.genome-howto-aside {
  margin: var(--space-md) 0 0;
  font-size: 11px;
  color: var(--text-dim);
  line-height: 1.55;
}
/* Schema sub-screen isn't a flex column (unlike the bloodwork AI modal,
   which gets its gap from .welcome-act-files), so space the prompt step
   off the aside explicitly. */
.welcome-act-genome-howto .ai-step {
  margin-top: var(--space-md);
}

/* Row of escape-hatch links under the dropzone. Centers up to two
   affordances horizontally with breathing room. `position: relative`
   lets the .apple-export-help / .genome-export-help popovers anchor
   above their respective summary. Drops out of layout when empty
   (e.g., genome act with no help link in loaded state). */
.welcome-act-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: var(--space-lg);
  margin-top: 8px;
  position: relative;
  min-height: 28px;
}
.welcome-act-links:empty {
  display: none;
}
/* Inside the act-links row, the manual-entry button + the export-help
   disclosure read as typographic peers — same dim mono treatment as
   the existing .welcome-link-quiet so neither competes with the
   continue/skip button below. */
.welcome-act-links .welcome-link-quiet {
  text-decoration: underline;
  text-decoration-color: rgba(107, 114, 128, 0.42);
  text-underline-offset: 3px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2px;
  color: #5f6b7a;
  /* Override the base .welcome-link-quiet's uppercase — these read
     as inline prose ("no PDF? extract with AI →"), not chrome chips. */
  text-transform: none;
}
.welcome-act-links .welcome-link-quiet:hover,
.welcome-act-links .welcome-link-quiet:focus-visible {
  color: var(--accent);
  background: rgba(8, 145, 178, 0.06);
  text-decoration-color: rgba(8, 145, 178, 0.55);
}

/* ── Unified source picker — onboarding "bring it in" surface ──
   The real upload affordance (PDF / Apple-export / genome card) is the lead
   peer; js/components/ios-source-providers.js renders the connector rows
   beneath it. They read as ONE list: the user picks a source and the system
   does the right thing — parse the file, or, for an iOS-only connector, an
   honest inline notify-me (never a faked sync). Same fake-door pattern as the
   data panel. The "+N more" door truncates the long tail. ── */
.welcome-source-providers {
  margin-top: var(--space-md);
  display: flex;
  flex-direction: column;
  text-align: left;
}
.welcome-source-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  padding: 13px 10px;
  background: none;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-family: var(--font-mono);
  transition: background 0.14s;
}
.welcome-source-row:hover,
.welcome-source-row:focus-visible {
  background: var(--bg-surface);
  outline: none;
}
/* Uniform disclosure chevron — replaces the old per-row "Sync" pill + arrow
   chrome (repetitive, enterprise-y). One quiet affordance, same on every row;
   the tap outcome (real upload vs waitlist door) is what differs, not the
   trailing label. */
.welcome-source-row::after {
  content: "";
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  margin-right: 4px;
  border-right: 1.5px solid var(--text-dim);
  border-top: 1.5px solid var(--text-dim);
  transform: rotate(45deg);
  opacity: 0.55;
  transition: border-color 0.14s, transform 0.14s, opacity 0.14s;
}
.welcome-source-row:hover::after,
.welcome-source-row:focus-visible::after {
  border-color: var(--accent);
  opacity: 1;
  transform: rotate(45deg) translate(1px, -1px);
}
.welcome-source-left {
  display: flex;
  align-items: center;
  gap: 13px;
  min-width: 0;
}
.welcome-source-name {
  font-size: 13.5px;
  font-weight: 400;
  letter-spacing: 0.1px;
  color: var(--text-primary);
}

/* HealthKit auto-sync fake door on the Apple Health export detail screen.
   The user is already in the Apple Health path, so the door is just the
   alternative verb — stacked under the dropzone/recipe as: an "or" divider row
   (the separator itself), then a modest CENTERED Sync pill. Sync is a secondary
   fake-door, deliberately NOT full-width so it never reads as a peer of the
   continue/skip primary at the bottom. Tapping opens the usual capture card. */
.welcome-sync-door {
  margin-top: var(--space-md);
}
/* The dropzone wrapper's margin-bottom supplies the gap above; drop the
   providers block's own top margin so the divider sits tight to the button. */
.welcome-source-providers-solo {
  margin-top: 0;
}
/* "or" as its own full-width row — a centered word flanked by hairlines, the
   visual break between the upload path and the sync door. */
.welcome-sync-or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 var(--space-md);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.2px;
  color: var(--text-dim);
}
.welcome-sync-or::before,
.welcome-sync-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-subtle);
}
/* The Sync door is a real, live affordance — a user may genuinely reach for it,
   so it rests in a soft-accent treatment (accent text + accent-soft tint +
   accent-tinted border) that reads as tappable, NOT the dead gray secondary
   pill it used to be. It's still NOT the filled accent primary (that's
   continue/skip's job) and NOT full-width, so it stays subordinate to Back.
   Centered under the divider; deepens to a solid accent fill on hover. */
.welcome-sync-btn-wrap {
  display: flex;
  justify-content: center;
}
/* Sync reads as a compact sibling of the upload card above it — same card
   language (soft 0.28 border, glassy fill, rounded), just smaller and
   horizontal (icon + label inline) so it stays subordinate to the primary
   dropzone. Same accent-on-hover lift the .upload-zone uses. */
.welcome-sync-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.38);
  border: 1px solid rgba(153, 164, 180, 0.28);
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-lg);
  cursor: pointer;
  backdrop-filter: blur(10px) saturate(115%);
  -webkit-backdrop-filter: blur(10px) saturate(115%);
  transition: color 160ms ease, background 160ms ease, border-color 160ms ease;
}
.welcome-sync-btn-icon {
  display: inline-flex;
  color: var(--text-dim);
  transition: color 160ms ease;
}
.welcome-sync-btn:hover,
.welcome-sync-btn:focus-visible {
  border-color: var(--accent);
  background: rgba(235, 247, 253, 0.7);
  outline: none;
}
.welcome-sync-btn:hover .welcome-sync-btn-icon,
.welcome-sync-btn:focus-visible .welcome-sync-btn-icon {
  color: var(--accent);
}

/* The drill-in screens (Apple Health export, genome prep hub) end with a
   primary "leave this screen" button. Give it clear air above so it doesn't
   read as a cramped stack with the bordered Sync door / nav above it. */
.welcome-act-vitals-howto .welcome-act-footer,
.welcome-act-genome-howto .welcome-act-footer {
  margin-top: var(--space-lg);
}

/* ── Provider logo tile — shared by the row marks and the import-door
   stack. A monogram floor with a logo.dev <img> layered over it, revealed
   only once it loads (.is-loaded). See ios-source-providers.js. ── */
.prov-logo {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  overflow: hidden;
  background: var(--bg-surface);
}
.prov-logo-fallback {
  font-family: var(--font-mono);
  font-weight: 700;
  line-height: 1;
  color: var(--text-secondary);
}
.prov-logo-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 2px;
  background: #fff;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.prov-logo.is-loaded .prov-logo-img {
  opacity: 1;
}
.prov-logo.is-loaded .prov-logo-fallback {
  visibility: hidden;
}
.welcome-source-logo {
  width: 36px;
  height: 36px;
  border: 1px solid var(--border-subtle);
  border-radius: 9px;
}
.welcome-source-logo .prov-logo-fallback {
  font-size: 14px;
}

/* The loaded upload card (multi-draw / Apple summary), when present, sits
   above the peer list in the flat picker — give it separation. */
.welcome-source-picker-flat .upload-zone-files-primary {
  margin-bottom: var(--space-sm);
}

/* ── Mobile flat-picker grid — provider tiles (logo over name) instead of a
   list. A consumer source-picker convention that reads less utilitarian than
   rows. Real-path tiles (PDF / AI / Apple) carry an accent icon; the capture
   card spans the full width below the tiles. ── */
.welcome-source-grid {
  display: grid;
  /* minmax(0, …) not the default minmax(auto, …): the tile names are nowrap,
     so an `auto` minimum would be the full string width and the tracks would
     refuse to shrink, overflowing the card horizontally. minmax(0,…) lets a
     track shrink below its content and the name ellipsize within it. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 11px;
  margin-top: var(--space-md);
}
/* Desktop affords a third column: the wider card fits 3 tiles per row, which
   lands every act (bloodwork 6, vitals 4, genome 5 tiles) at just 2 rows. That
   compactness — versus the old 148px hero + stacked rows — is what keeps the
   footer above the fold without a fixed footer. Scoped to >720px so mobile
   stays the 2-col grid, byte-identical. */
@media (min-width: 721px) {
  .welcome-source-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  /* Three narrower columns leave less room per name; tighten the side padding
     and drop the name to 11px so the longest label ("Function Health") sits on
     one line without ellipsis. */
  .welcome-source-cell {
    padding: 18px 8px 14px;
  }
  .welcome-source-cell-name {
    font-size: 11px;
    letter-spacing: 0;
  }
}
/* Whole-act drag-drop affordance (desktop). The flat grid has no big hero drop
   target, so the act itself accepts a dragged file; this is the "drop here"
   highlight while a file hovers — a soft accent ring + tint, no dashed border. */
.welcome-act-dragover {
  position: relative;
}
.welcome-act-dragover::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  box-shadow: inset 0 0 0 2px var(--accent);
  background: var(--accent-soft);
  pointer-events: none;
}
.welcome-source-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 20px 10px 15px;
  background: var(--bg-page);
  border: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  cursor: pointer;
  font-family: var(--font-mono);
  transition: box-shadow 0.16s, transform 0.16s;
}
.welcome-source-cell:hover,
.welcome-source-cell:focus-visible {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-2px);
  outline: none;
}
/* Brand logos float directly on the card — no nested box. The logo.dev <img>
   carries its own white field, seamless on the white card; a monogram
   fallback sits bare. */
.welcome-source-cell-logo {
  width: 50px;
  height: 50px;
  background: none;
  border-radius: 12px;
}
.welcome-source-cell-logo .prov-logo-fallback {
  font-size: 18px;
}
/* Action tiles (PDF / AI) keep a quiet neutral chip so they read as buttons,
   not brand marks — same family as the "+N more" glyph, so they don't pop
   against the bare-floating brand logos. Only the small line glyph carries a
   hint of accent. */
.welcome-source-cell-logo-icon {
  background: var(--bg-surface);
}
.welcome-source-cell-logo-icon svg {
  display: block;
  width: 24px;
  height: 24px;
}
.welcome-source-cell-name {
  max-width: 100%;
  font-size: 12px;
  letter-spacing: 0.1px;
  color: var(--text-secondary);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* "+N more" tile — a quiet count glyph in place of a logo. */
.welcome-source-cell-more .welcome-source-more-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 50px;
  height: 50px;
  border-radius: 12px;
  background: var(--bg-surface);
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
  transition: color 0.14s;
}
.welcome-source-cell-more:hover .welcome-source-more-glyph,
.welcome-source-cell-more:focus-visible .welcome-source-more-glyph {
  color: var(--accent);
}
/* Capture spans the full grid width, below the tiles — with clear air above
   so the waitlist card reads as a distinct response, not glued to the grid. */
.welcome-source-grid .welcome-source-capture {
  grid-column: 1 / -1;
  margin: var(--space-md) 0 var(--space-xs);
}
.welcome-source-more {
  align-self: flex-start;
  margin-top: 8px;
  padding: 4px;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2px;
  color: var(--text-dim);
  transition: color 0.14s;
}
.welcome-source-more:hover,
.welcome-source-more:focus-visible {
  color: var(--accent);
  outline: none;
}

/* Inline capture — a self-contained panel that drops in under the source
   list when a connector is tapped. Set apart as its own card (fill + border
   + breathing room) so it reads as a deliberate moment, not crammed between
   the rows above and the helper links below. No modal. */
.welcome-source-capture {
  position: relative;
  width: 100%;
  margin: var(--space-md) 0 var(--space-sm);
  padding: 14px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  text-align: left;
}
.welcome-source-capture-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 17px;
  line-height: 1;
  color: var(--text-dim);
  cursor: pointer;
  transition: color 0.14s, background 0.14s;
}
.welcome-source-capture-close:hover,
.welcome-source-capture-close:focus-visible {
  color: var(--text-secondary);
  background: var(--bg-page);
  outline: none;
}
.welcome-source-capture-sub {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-secondary);
  letter-spacing: 0.1px;
  /* Clear the absolute × in the top-right; the input/button below stay
     full-width to the card edge. */
  margin: 0 0 11px;
  padding-right: 22px;
}
/* PDF nudge — sits just under the sub, pointing to the real path that works
   today (the bloodwork PDF tile). Dim hint register, not a header. */
.welcome-source-capture-nudge {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--text-dim);
  letter-spacing: 0.1px;
  margin: -5px 0 11px;
  padding-right: 22px;
}
/* Stacked (not side-by-side) — at phone width the input + waitlist button
   crammed onto one line; stacking lets each breathe. */
.welcome-source-capture-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.welcome-source-capture-input {
  width: 100%;
  font-family: var(--font-mono);
  font-size: 13px;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-page);
  color: var(--text-primary);
}
.welcome-source-capture-input:focus {
  outline: none;
  border-color: var(--accent);
}
.welcome-source-capture-submit {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 9px 12px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: #fff;
  cursor: pointer;
  transition: background 0.14s, border-color 0.14s;
}
.welcome-source-capture-submit:hover:not(:disabled) {
  background: var(--accent-strong, #0e7490);
  border-color: var(--accent-strong, #0e7490);
}
.welcome-source-capture-submit:disabled {
  opacity: 0.6;
  cursor: default;
}
.welcome-source-capture-error {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--red);
  margin: 8px 0 0;
}
.welcome-source-capture-done {
  display: inline-flex;
  align-items: flex-start;
  gap: 7px;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.5;
  color: var(--green);
  letter-spacing: 0.1px;
  margin: 0;
  padding-right: 22px;
}
.welcome-source-capture-done svg {
  flex-shrink: 0;
  margin-top: 2px;
}
/* Success-state nudge back to the real manual path — quiet, below the ack. */
.welcome-source-capture-fallback {
  display: inline-block;
  margin-top: 12px;
  padding: 0;
  background: none;
  border: none;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1px;
  color: var(--accent);
  cursor: pointer;
  transition: color 0.14s;
}
.welcome-source-capture-fallback:hover,
.welcome-source-capture-fallback:focus-visible {
  color: var(--accent-strong, #0e7490);
  outline: none;
}

/* ── Mobile import door — the single secondary affordance on the phone
   data acts. A pill with an overlapping monogram stack of the services
   behind it (recall teaser) + label, sharing the manual→file mode toggle.
   Real brand logo SVGs would lift recall further; the monograms are an
   asset-free stand-in. ── */
.welcome-import-door {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 20px 44px 16px;
  background: var(--bg-page);
  border: 1px solid transparent;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  cursor: pointer;
  transition: box-shadow 0.16s, transform 0.16s, border-color 0.16s;
}
.welcome-import-door:hover,
.welcome-import-door:focus-visible {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-2px);
  outline: none;
}
.welcome-import-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.welcome-import-marks {
  display: inline-flex;
}
.welcome-import-mark {
  width: 36px;
  height: 36px;
  border: 2px solid var(--bg-page); /* ring separates the overlapping tiles */
  border-radius: 10px;
  margin-left: -6px; /* slight overlap — logos read as a spread set, not a clump */
}
.welcome-import-mark:first-child {
  margin-left: 0;
}
.welcome-import-mark .prov-logo-fallback {
  font-size: 14px;
}
.welcome-import-label {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.welcome-import-door:hover .welcome-import-label,
.welcome-import-door:focus-visible .welcome-import-label {
  color: var(--accent);
}

/* Genome export popover — same affordance + treatment as
   .apple-export-help: closed by default ("how to export →"), opens
   above the summary so it doesn't reflow siblings. Lists the three
   most-common DNA-test sources with one-line instructions each. */
.genome-export-help {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-dim);
  letter-spacing: 0.2px;
  text-align: center;
  position: relative;
}
.genome-export-help summary {
  display: inline-block;
  cursor: pointer;
  padding: 4px 8px;
  color: var(--text-dim);
  text-decoration: underline;
  text-decoration-color: rgba(140, 154, 172, 0.35);
  text-underline-offset: 3px;
  transition: color 0.14s ease, text-decoration-color 0.14s ease;
  list-style: none;
}
.genome-export-help summary::-webkit-details-marker {
  display: none;
}
.genome-export-help summary:hover,
.genome-export-help summary:focus-visible {
  color: var(--text-secondary);
  text-decoration-color: rgba(140, 154, 172, 0.65);
  outline: none;
}
.genome-export-help[open] summary {
  color: var(--text-secondary);
  text-decoration-color: rgba(140, 154, 172, 0.65);
}
.genome-export-help p {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(320px, calc(100vw - 64px));
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  text-align: left;
  color: var(--text-secondary);
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(10, 28, 44, 0.08);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 4;
}
.genome-export-help p::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: rgba(255, 255, 255, 0.96);
  filter: drop-shadow(0 1px 0 var(--border));
}
.genome-export-help strong {
  font-weight: 600;
  color: var(--text-primary);
}

/* The branch ask ("we didn't get X") reuses the tuner UI from details.js
   verbatim. Prompt→content spacing comes from the shared .welcome-act-prompt
   margin (no extra margin-top here, so it matches every other step); the tuner
   row rhythm comes from the shared .tuner-rows gap. */

/* Multi-draw loaded state for the bloodwork dropzone: instead of a
   single filename + summary, we render one row per confirmed draw with
   a × to remove, and an "+ add another draw" footer. Sits inside the
   green success frame the existing `.upload-zone-loaded` modifier
   already paints. */
.upload-zone-draws {
  align-items: stretch;
  /* Tightened from md/lg/sm — the inline-checkmark title (no standalone
     check island above) means less vertical chrome to pad around, and
     the 2-draws-plus-apple stacked case fits without scroll. */
  padding: var(--space-sm) var(--space-lg) var(--space-xs);
}
/* Title becomes a flex container so the checkmark sits inline with
   the label (`✓ 2 lab draws loaded`) instead of as a standalone island
   above. The green frame around the card already carries the loaded
   signal; this is a small reaffirming glyph, not a celebration. */
.bloodwork-draws-title {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: var(--text-primary);
  margin: 0 0 var(--space-xs);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.bloodwork-draws-title-check {
  color: var(--green);
  flex: none;
}
/* Cap the chip cluster so the rows sit as a content-sized stack
   centered in the card. Without this, chips stretch to fill the full
   ~340px content area and "5 markers" floats stranded in whitespace
   right of the date. 300px holds "2026-02-02  39 markers  ×" with
   modest breathing room. */
.bloodwork-draws-list {
  list-style: none;
  margin: 0 auto;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 300px;
  width: 100%;
}
.bloodwork-draw-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.6);
  border: 1px solid var(--border-subtle);
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.bloodwork-draw-date {
  color: var(--text-primary);
  font-weight: 500;
  letter-spacing: 0.3px;
  white-space: nowrap;
}
.bloodwork-draw-meta {
  color: var(--text-dim);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Right-aligned so the meta sits at the trailing edge of the row.
     On bloodwork chips that anchors the count immediately before the
     × button; on the Apple chip (no ×) it anchors at the right edge.
     Same date-left / meta-right skeleton on both. */
  text-align: right;
}
.bloodwork-draw-remove {
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 3px;
  cursor: pointer;
  transition: color 0.14s ease, background 0.14s ease;
}
.bloodwork-draw-remove:hover {
  color: var(--red);
  background: rgba(229, 99, 77, 0.1);
}
.bloodwork-draws-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--space-sm);
}

/* ── AI extraction modal ──
   A peer of pdf-review: a slot-replacement screen inside the Files
   step. Two stacked sections — prompt block with copy button, then a
   paste textarea — with the standard welcome footer underneath. */
.welcome-act-ai {
  gap: var(--space-sm);
}
.ai-modal-aside {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.55;
  color: var(--text-dim);
  text-align: center;
  margin: 0 auto;
  max-width: 440px;
  letter-spacing: 0.2px;
}
/* No-text-layer redirect — the "how to get a readable file" tip list.
   Shares the muted aside voice but left-aligned and itemized. */
.welcome-guide-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.welcome-guide-list li {
  position: relative;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.55;
  letter-spacing: 0.2px;
  color: var(--text-dim);
  padding-left: 18px;
}
.welcome-guide-list li::before {
  content: "→";
  position: absolute;
  left: 0;
  color: var(--accent);
}

.ai-step {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ai-step-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}
.ai-step-label {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
}
.ai-copy-btn {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid rgba(8, 145, 178, 0.28);
  border-radius: var(--radius-sm);
  padding: 4px 10px;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease;
}
.ai-copy-btn:hover,
.ai-copy-btn:focus-visible {
  background: rgba(8, 145, 178, 0.14);
  border-color: var(--accent);
  outline: none;
}
.ai-copy-done { color: var(--green); }

.ai-prompt-block {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-secondary);
  background: rgba(245, 248, 252, 0.85);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  margin: 0;
  max-height: 180px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
  /* The slot centers prose; code/prompt blocks must read left-aligned. */
  text-align: left;
}
.ai-paste-textarea {
  width: 100%;
  min-height: 100px;
  max-height: 180px;
  resize: vertical;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-primary);
  background: rgba(255, 255, 255, 0.85);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  box-sizing: border-box;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.ai-paste-textarea::placeholder {
  color: var(--text-dim);
  opacity: 0.7;
}
.ai-paste-textarea:focus {
  outline: none;
  border-color: var(--accent);
  background: white;
}

/* Inline ↻ replace button on a loaded-state dropzone. Lets the user
   swap files without leaving the slot, distinct from clicking the
   zone itself (which also re-opens the picker). */
.upload-zone-replace {
  position: absolute;
  top: 8px;
  right: 8px;
  background: transparent;
  border: 0;
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 3px;
}
.upload-zone-replace:hover {
  background: rgba(140, 154, 172, 0.12);
  color: var(--text-secondary);
}
.upload-zone-loaded { position: relative; }

/* Parsing state — replaces the icon + label content with a stage label
   + progress bar while the prep worker streams a .zip / .xml. Pointer
   events are dropped so the user can't kick off a second upload mid-parse. */
.upload-zone-parsing {
  cursor: default;
  pointer-events: none;
}
.upload-zone-parsing .upload-icon { display: none; }
.upload-zone-progress {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
  max-width: 280px;
  margin: 0 auto;
}
.upload-zone-progress-stage {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  letter-spacing: 0.3px;
  margin: 0;
}
.upload-zone-progress-bar {
  width: 100%;
  height: 4px;
  background: rgba(140, 154, 172, 0.18);
  border-radius: 2px;
  overflow: hidden;
}
.upload-zone-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 2px;
  transition: width 0.2s ease;
}
.upload-zone-progress-pct {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.4px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  margin: 0;
}

/* Indeterminate pulse for PDF parse (no per-page progress signal). */
@keyframes upload-progress-pulse {
  0%   { opacity: 0.35; }
  50%  { opacity: 1; }
  100% { opacity: 0.35; }
}

/* ── PDF confirmation review ──
   The PDF dropzone parses in place, then the welcome slot swaps to
   this review view. Layout mirrors the rest of the welcome acts (same
   prompt + footer) so it reads as another beat, not a modal. */
.welcome-act-pdf-review {
  gap: var(--space-sm);
}
.pdf-review-meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-dim);
  letter-spacing: 0.3px;
  margin: 0;
}
.pdf-review-lab { color: var(--text-secondary); text-transform: capitalize; }
.pdf-review-sep { color: var(--border); }

/* Editable draw date — inline in the meta row, styled to read as a field
   without breaking the row's typographic rhythm. */
.pdf-review-date-field {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.pdf-review-date-label {
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-size: 10px;
}
.pdf-review-date-input {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 2px 5px;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.85);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.pdf-review-date-input:focus {
  outline: none;
  border-color: var(--accent);
  background: white;
}

.pdf-review-scroll {
  /* ⚠ NO VERTICAL OVERFLOW on the welcome card. Every onboarding act must
     fit between the progress thread and the footer without the card itself
     scrolling — only THIS inner list scrolls. This max-height is the
     budget: when you add any fixed-height chrome to the review act (a line
     in the meta row, the editable date field, etc.), pay for it by
     trimming here, or the footer (CANCEL / LOOKS RIGHT) clips off the
     bottom of the card. History: 320 → 296 when the editable date field
     was added. */
  max-height: 296px;
  overflow-y: auto;
  /* Vertical breathing room inside the panel — top above the header,
     bottom below the disclosure. Horizontal stays 0 so dividers and the
     disclosure bar still reach the panel edges. */
  padding: 6px 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.pdf-review-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 12px;
}
.pdf-review-table thead th {
  position: sticky;
  top: 0;
  background: rgba(245, 248, 252, 0.95);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  text-align: left;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
}
.pdf-review-table tbody td {
  padding: 5px 10px;
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: middle;
}
.pdf-review-table tbody tr:last-child td { border-bottom: 0; }
/* Inset divider — kept (transparent, so row height doesn't shift) under
   the checkbox so the separator reads as a list, starting at the name,
   rather than a full-bleed table grid. */
.pdf-review-table tbody td.pdf-review-include { border-bottom-color: transparent; }

.pdf-review-include { width: 28px; }
.pdf-review-include input { cursor: pointer; }
.pdf-review-name { color: var(--text-primary); }
.pdf-review-name label { cursor: pointer; }
.pdf-review-value-wrap {
  display: inline-flex;
  align-items: center;
}
/* The value reads as plain data, not a spreadsheet cell — the field
   (border + fill) only materializes on hover/focus, signaling it's
   editable without boxing every number by default. This is the main
   de-Excel move. */
.pdf-review-value input {
  width: 72px;
  font-family: var(--font-mono);
  font-size: 12px;
  padding: 2px 5px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  color: var(--text-primary);
  text-align: right;
  font-variant-numeric: tabular-nums;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.pdf-review-value input:hover {
  border-color: var(--border);
  background: rgba(255, 255, 255, 0.7);
}
.pdf-review-value input:focus {
  outline: none;
  border-color: var(--accent);
  background: white;
}
/* Unit rides inside the value cell as muted reference text — not its own
   column. The cell is nowrap (below) so a long unit (e.g. eGFR's
   mL/min/1.73m²) stays on the value's line instead of wrapping under it;
   the name column wraps to yield the width, so the table never scrolls
   horizontally. */
.pdf-review-value { white-space: nowrap; }
.pdf-review-unit {
  margin-left: 8px;
  color: var(--text-dim);
  font-size: 10px;
  letter-spacing: 0.2px;
}

/* Row dims when its checkbox is off so the user sees what's excluded. */
.pdf-review-table tbody tr:has(input[data-row-check]:not(:checked)) {
  opacity: 0.4;
}
.pdf-review-table tbody tr:has(input[data-row-check]:not(:checked)) .pdf-review-value input {
  text-decoration: line-through;
}

/* ── Severity-first review ──
   The gate surfaces only out-of-range markers by default; in-range rows
   collapse behind a disclosure. Severity is shown with ONE quiet, neutral
   cue — a small arrow trailing the value — not a dot + arrow + border
   stack of warm color (too loud for a confirm screen). Rows are also
   pre-sorted worst-first. */

/* Left-align cell text. The welcome act centers its content, which would
   otherwise center marker/unit text and force long names to wrap. */
.pdf-review-table tbody td { text-align: left; }

/* Custom checkbox — replaces the native OS control (dated, off-theme).
   Cyan = the user's axis of control (include this marker). */
.pdf-review-include input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin: 0;
  border: 1.5px solid var(--border);
  border-radius: 4px;
  background: #fff;
  cursor: pointer;
  position: relative;
  transition: background 0.12s ease, border-color 0.12s ease;
}
/* Soft tinted "on" state — a cyan check in a lightly-tinted square,
   not a solid block of accent. Quieter when the whole list is checked
   by default, while still clearly reading as included. */
.pdf-review-include input[type="checkbox"]:checked {
  background: var(--accent-soft);
  /* Border stays neutral grey (not accent) so the "on" cue is the soft
     tint + cyan check, not a loud cyan ring. */
  border-color: var(--border);
}
.pdf-review-include input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  /* Centered via translate; the glyph's ink sits toward its lower-right
     corner, so nudge left/up to optically center it in the box. */
  left: 50%;
  top: 47%;
  width: 3px;
  height: 6.5px;
  border: solid var(--accent);
  border-width: 0 1.75px 1.75px 0;
  transform: translate(-58%, -52%) rotate(45deg);
}
.pdf-review-include input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent-soft);
  outline-offset: 1px;
}

/* Direction-only severity cue: a small NEUTRAL arrow trailing the value
   (▲/▲▲ high, ▼/▼▼ low). No color — the visible rows are already the
   flagged ones, so a loud hue here just reads as alarm on a confirm
   screen. The glyph alone carries direction + magnitude. */
.pdf-review-arrow {
  margin-left: 6px;
  font-size: 9px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  letter-spacing: -1px;
}

/* Disclosure row that reveals the in-range markers. */
.pdf-review-disclose-cell { padding: 0; }
.pdf-review-disclose {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  background: var(--bg-surface);
  border: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: pointer;
}
.pdf-review-disclose:hover { color: var(--text-secondary); }
.pdf-review-disclose-caret {
  display: inline-block;
  transition: transform 0.15s ease;
}
.pdf-review-disclose-open .pdf-review-disclose-caret { transform: rotate(90deg); }

/* "Rows we couldn't read" — verbatim PDF text, read-only. Quieter than a
   data row: dim, smaller, no controls, so it reads as informational rather
   than an editable result. */
.pdf-review-unread-cell {
  padding: 5px 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1.5;
  color: var(--text-dim);
  word-break: break-word;
}

/* Static caption over the flagged group. Unfilled (vs the in-range
   disclosure's surface bar) so it reads as a quiet label, not a frozen
   header band — the flagged rows are always shown, so it's inert (no
   caret, no hover). Leads with a small flag glyph in muted warm. */
.pdf-review-section-label {
  background: transparent;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  padding-top: 12px;
  padding-bottom: 9px;
  /* A divider beneath the caption gives it header structure without the
     heavy surface fill — reads as a header band, not a frozen row. */
  border-bottom: 1px solid var(--border-subtle);
}
.pdf-review-section-label-inner {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
/* Flag inherits the caption's dim color (a quiet outline ornament, not a
   warm block) — one signal, integral to the header. */
.pdf-review-flag-icon {
  flex: none;
  color: inherit;
  opacity: 0.8;
  margin-top: -1px;
}

/* ═══════════════════════════════════════════════════════════════════
   Details step — composed of up to three sections (vitals, bloodwork,
   lifestyle). Sections covered by an uploaded file collapse to a
   single tick row. Lifestyle rows reuse .calibrate-row; manual
   vitals/bloodwork rows share the same shape.
   ═══════════════════════════════════════════════════════════════════ */

.welcome-act-details {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 28px;
}

.welcome-act-footer-details {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  margin-top: 8px;
}

/* Sections (Vitals / Bloodwork / Lifestyle) — the breakpoint between
   sections is a thin horizontal rule; the label sits inline on the
   left as quiet context. The line carries the separation, not the
   label, so the page can lose most of its bold weight. */
.details-section {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.details-section-label {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0;
  text-align: left;
}
.details-section-label::after {
  content: '';
  flex: 1;
  height: 1px;
  background: rgba(140, 154, 172, 0.22);
}

.fh-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}


/* "Covered by your X" note — sits where the tuner rows would be when a
   file already provides the section's values. The section header
   (label + rule) stays identical to the other sections so the layout
   doesn't shift when a file flips the state. */
.details-section-covered-note {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-secondary);
}

.details-section-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: rgba(16, 185, 129, 0.14);
  color: var(--green);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
}

.details-section-source {
  color: var(--text-secondary);
}

/* ─── Tuner rows ─────────────────────────────────────────────────
   The vitals + bloodwork inputs on the details step. Each row is a
   continuous axis. Bands sit absolutely-positioned at the midpoint
   of their range on the axis, so labels land between (not on top of)
   the boundary ticks. The inner rail track is inset by the thumb's
   radius so the thumb never gets clipped at the edges.

     ┌──────────────────────────────────────────────┐
     │ Resting HR                          62 bpm   │  ← row label + readout
     │   ATHLETIC   HEALTHY    AVERAGE    HIGH      │  ← bands (axis-positioned)
     │    <55        55-70      70-85      85+      │
     │   ────|─────────|─────────●─────────|────    │  ← rail + ticks + thumb
     └──────────────────────────────────────────────┘

   Three input paths converge on the row's data-tuner-value:
   click a band → snap to that band's midpoint; drag the thumb or
   click the rail → continuous; type into the readout → exact. */

.tuner-rows {
  display: flex;
  flex-direction: column;
  /* Tuner rows are tall (label + bands + rail), so they carry more gap than
     the simple divider-separated field rows. The in-app reading tuner uses
     its own .reading-tuner-rows container, so this is data-setup only. */
  gap: 22px;
}

.tuner-row {
  padding: 0;
}

.tuner-row-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}

.tuner-row-label-group {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.tuner-row-label {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  letter-spacing: 0.2px;
}

/* Plain-language dek under the label on the phone-first manual steps — guides a
   user who doesn't know the marker by name but has a rough sense of the system
   it speaks for. Sits between the label and the bands; replaces the info icon. */
.tuner-row-dek {
  /* Hug the marker label above (tight top gap) and break away from the slider
     below (larger bottom gap) so the dek reads as part of the marker, not the
     input. Left-aligned to anchor to the label, against the card's centered
     default. Inter reading-face (like .sharpen-dek / .log-results-dek) — it's
     a description, so it sets itself apart from the mono labels around it. */
  margin: -2px 0 16px;
  text-align: left;
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: 0.1px;
  color: var(--text-dim);
}

/* Steps 3 & 4 keep the marker name neutral on touch — no green "answered" cue,
   matching the plain labels of steps 1–2. The readout still carries the touched
   state, so the answered signal isn't lost. */
.tuner-row-dekked.tuner-row-touched .tuner-row-label {
  color: var(--text-primary);
}

/* Info icon + tooltip — progressive disclosure for a biomarker the user
   recognizes but may not fully know. Hover or keyboard/tap focus reveals an
   authored one-liner (see tunerInfoText). Tooltip wraps + caps its width so a
   full sentence stays readable; positioned below the icon since these rows can
   sit near the card's top edge. */
.tuner-info {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  padding: 0;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  font-style: normal;
  line-height: 1;
  color: var(--text-dim);
  background: none;
  border: 1px solid rgba(140, 154, 172, 0.5);
  border-radius: 50%;
  cursor: help;
  transition: color 0.14s ease, border-color 0.14s ease;
}
.tuner-info:hover,
.tuner-info:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  outline: none;
}
.tuner-info::after {
  content: attr(data-info);
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 20;
  width: max-content;
  max-width: 240px;
  padding: 8px 10px;
  font-size: 11px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0.2px;
  text-transform: none;
  text-align: left;
  color: var(--text-secondary);
  background: rgba(255, 255, 255, 0.98);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 6px 18px rgba(10, 28, 44, 0.10);
  opacity: 0;
  pointer-events: none;
  transform: translateY(2px);
  transition: opacity 160ms ease, transform 160ms ease;
}
.tuner-info:hover::after,
.tuner-info:focus-visible::after {
  opacity: 1;
  transform: translateY(0);
}
@media (max-width: 720px) {
  .tuner-info::after { max-width: 70vw; }
}

/* Anonymous head (in-app single-field tuner) — the redundant name is gone,
   so the value readout becomes the card's hero figure, CENTERED over the
   bands + rail beneath it. Centering shares the scale's horizontal axis, so
   the card reads as a clean vertical column (value → scale → rail) instead
   of a small number floating top-left with dead space beside it. */
.tuner-row-head-anon {
  justify-content: center;
  margin-bottom: 10px;
}
.tuner-row-head-anon .tuner-readout-input {
  text-align: center;
  width: 3.2em;
  font-size: 22px;
}

/* Editable numeric readout — the answer the user is dialing in.
   Sized to dominate the row's typographic hierarchy without shouting:
   bigger than the row label, tabular for steady width. */
.tuner-readout {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font-variant-numeric: tabular-nums;
}
.tuner-readout-input {
  appearance: none;
  -moz-appearance: textfield;
  font-family: var(--font-mono);
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 2px 6px;
  width: 64px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.tuner-readout-input::-webkit-outer-spin-button,
.tuner-readout-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.tuner-readout-input:hover {
  border-color: rgba(140, 154, 172, 0.25);
}
.tuner-readout-input:focus {
  outline: none;
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.85);
}
/* Touched state — green appears only after the user interacts with the
   rail, a band, or the readout. The default-value start state stays
   neutral so it doesn't claim to be the user's answer. */
.tuner-row-touched .tuner-row-label {
  color: var(--green);
}
.tuner-row-touched .tuner-readout-input {
  border-color: rgba(16, 185, 129, 0.45);
}
.tuner-readout-unit {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.4px;
  color: var(--text-dim);
  text-transform: uppercase;
}

/* Bands — absolute-positioned over the rail width. Each band's `left`
   is set inline to its band-midpoint percentage on the axis, then the
   button is centered via translateX(-50%). Inset by the same amount
   as .tuner-rail-track so labels align with the rail edges. */
.tuner-bands {
  position: relative;
  height: 30px;
  margin: 0 9px 4px;
}
.tuner-band {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 2px 4px 3px;
  cursor: pointer;
  font-family: var(--font-mono);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  text-align: center;
  border-radius: 3px;
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  transition: background 0.14s ease;
}
.tuner-band:hover { background: rgba(140, 154, 172, 0.06); }
.tuner-band:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.tuner-band-label {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text-dim);
  white-space: nowrap;
  transition: color 0.14s ease, font-weight 0.14s ease;
}
.tuner-band-range {
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 0.2px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  transition: color 0.14s ease;
}
/* Active band's label — the value's home. Just darker + bold (neutral): the
   RAIL and the handle ring carry the tone, so coloring the label too would
   triple-encode the same signal and read loud. Color used once, with intent. */
.tuner-band.is-active .tuner-band-label {
  color: var(--text-primary);
  font-weight: 600;
}
.tuner-band.is-active .tuner-band-range { color: var(--text-secondary); }

/* Rail — outer is the pointer hit area; inner track is inset by the
   thumb's radius so positions at 0% and 100% sit cleanly inside the
   row rather than getting clipped by the card edge. */
.tuner-rail {
  position: relative;
  height: 22px;
  cursor: pointer;
  touch-action: none;
  user-select: none;
}
.tuner-rail-track {
  position: absolute;
  left: 9px;
  right: 9px;
  top: 0;
  bottom: 0;
}
/* Zone bar — the rail IS the spectrum. Each band is a soft tone-tinted block
   spanning its value range; the color change at every boundary replaces the
   old tick marks, and the zone the value lands in is brought forward. Rounded,
   slim, clipped by the container so the ends are clean. */
.tuner-zones {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 8px;
  transform: translateY(-50%);
  border-radius: 5px;
  overflow: hidden;
  background: rgba(140, 154, 172, 0.12);
}
.tuner-zone {
  position: absolute;
  top: 0;
  bottom: 0;
  box-sizing: border-box;
  /* Hairline boundary tick — the right edge of each zone is an interior cut
     point, so a 1px right border reads as a calibration mark (last zone, the
     bar's end, has none). Gives the bar a precise/instrument feel vs a chunky
     pill, without re-adding separate tick elements. */
  border-right: 1px solid rgba(15, 23, 42, 0.10);
  transition: background 0.14s ease;
}
.tuner-zone:last-child { border-right: none; }
/* Inactive = the faint spectrum MAP: a barely-there per-tone hint so the whole
   range (and ferritin's U-shape) previews without lighting a rainbow. This is
   also the unset resting state — the "unknown" read comes from the hollow
   handle + empty readout, not from graying the bar. */
.tuner-zone[data-tone="good"] { background: var(--green-bg); }
.tuner-zone[data-tone="warn"] { background: var(--amber-bg); }
.tuner-zone[data-tone="bad"]  { background: var(--red-bg); }
/* Active = the ONLY zone with real color — and a MUTED tone (tempered toward
   slate, not a pastel/candy fill), so it pops against the faint map while
   matching Aescle's clinical instrument voice. */
.tuner-zone.is-active[data-tone="good"] { background: color-mix(in srgb, color-mix(in srgb, var(--green) 70%, var(--text-secondary)) 42%, transparent); }
.tuner-zone.is-active[data-tone="warn"] { background: color-mix(in srgb, color-mix(in srgb, var(--amber) 70%, var(--text-secondary)) 42%, transparent); }
.tuner-zone.is-active[data-tone="bad"]  { background: color-mix(in srgb, color-mix(in srgb, var(--red) 70%, var(--text-secondary)) 42%, transparent); }

/* Handle — a clean white lozenge riding on the bar, with the active zone's
   tone as a ring (set live via the row's data-active-tone) and a soft lift.
   Sits above the zones (later in DOM + z-index) and is larger than the bar. */
.tuner-thumb {
  position: absolute;
  top: 50%;
  z-index: 2;
  width: 15px;
  height: 15px;
  margin: 0;
  padding: 0;
  transform: translate(-50%, -50%);
  background: #fff;
  border: 1.5px solid var(--accent);
  border-radius: 50%;
  cursor: grab;
  box-shadow: 0 1px 4px rgba(15, 23, 42, 0.22);
  transition: transform 0.08s ease, box-shadow 0.14s ease, border-color 0.14s ease;
}
/* Center pip — a small tone dot inside the white disc, so the handle reads as
   a precise target marker (ring · white gap · pip) rather than a plain knob.
   Same tone as the ring; hidden on the hollow unset handle. */
.tuner-thumb::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 4px;
  height: 4px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--accent);
}
.tuner-row[data-active-tone="good"] .tuner-thumb { border-color: var(--green); }
.tuner-row[data-active-tone="warn"] .tuner-thumb { border-color: var(--amber); }
.tuner-row[data-active-tone="bad"]  .tuner-thumb { border-color: var(--red); }
.tuner-row[data-active-tone="good"] .tuner-thumb::after { background: var(--green); }
.tuner-row[data-active-tone="warn"] .tuner-thumb::after { background: var(--amber); }
.tuner-row[data-active-tone="bad"]  .tuner-thumb::after { background: var(--red); }
.tuner-row-unset .tuner-thumb::after { display: none; }
.tuner-thumb:hover {
  box-shadow: 0 2px 7px rgba(15, 23, 42, 0.26);
}
.tuner-thumb:active {
  cursor: grabbing;
  transform: translate(-50%, -50%) scale(1.1);
}
.tuner-thumb:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── Unset row — the honest "we don't know this yet" resting state ──────
   A row the user hasn't touched (branch-ask key fields) renders blank so it
   can't pose as an answer: no band lit, the thumb hollow and parked at the
   rail center, the readout empty. The slider still drags and the bands still
   click — the first interaction removes .tuner-row-unset (see setTunerValue),
   at which point the normal lit/touched styling takes over. */
.tuner-row-unset .tuner-thumb {
  background: var(--bg-surface);
  border-color: rgba(140, 154, 172, 0.55);
  box-shadow: none;
  opacity: 0.7;
}
.tuner-row-unset .tuner-thumb:hover,
.tuner-row-unset .tuner-thumb:active {
  opacity: 1;
}
.tuner-row-unset .tuner-band-label,
.tuner-row-unset .tuner-band-range {
  opacity: 0.6;
}
.tuner-row-unset .tuner-readout-unit {
  opacity: 0.55;
}
/* Clear (×) — the undo for an accidental touch. Rendered on every
   unset-capable row but only revealed once the row is touched (lit), so it
   can't be pressed on an already-blank row. */
.tuner-clear {
  display: none;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-left: 2px;
  padding: 0;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1;
  color: var(--text-dim);
  background: none;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  transition: color 0.14s ease, background 0.14s ease;
}
.tuner-row-touched .tuner-clear {
  display: inline-flex;
}
.tuner-clear:hover,
.tuner-clear:focus-visible {
  color: var(--text-secondary);
  background: var(--bg-surface);
  outline: none;
}

/* ── Phone: bigger scrubber + legible scale ──────────────────────────
   The 14px thumb is a fine mouse target but a poor thumb-on-glass one, and
   the 9–10px scale text is tight on a small screen. Scale both up on phones.
   The track/band insets grow with the thumb radius so the thumb still can't
   clip at the rail extremes, and bands+rail stay aligned (equal inset).
   Band-label growth is kept modest (10→11px) — four uppercase bands sit at
   fixed midpoints, so a larger jump risks adjacent labels colliding on a
   narrow axis. Desktop is untouched. */
@media (max-width: 720px) {
  .tuner-thumb {
    width: 24px;
    height: 24px;
  }
  .tuner-rail {
    height: 32px;
  }
  .tuner-rail-track {
    left: 12px;
    right: 12px;
  }
  .tuner-bands {
    height: 34px;
    margin-left: 12px;
    margin-right: 12px;
  }
  .tuner-band-label { font-size: 11px; }
  .tuner-band-range { font-size: 10px; }
  .tuner-row-label { font-size: 15px; }
  .tuner-readout-input { font-size: 18px; }
  .tuner-readout-unit { font-size: 11px; }
}

/* iOS Safari auto-zooms focused form controls below 16px. Keep the setup
   flow locked at 16px+ on phones even when later component rules tune
   desktop typography smaller. The viewport meta also disables pinch zoom;
   this rule prevents focus-zoom in embedded/webview contexts that ignore it. */
@media (max-width: 720px) {
  .welcome input:not([type="hidden"]):not([type="file"]),
  .welcome select,
  .welcome textarea {
    font-size: 16px !important;
  }
}

/* ── In-app "log a reading" control (components/reading-tuner.js) ──────
   Reuses the tuner-row/band/rail vocabulary above; this just frames it as
   a self-contained card with a commit button, dropped into Risks lever
   expansions and Plan measurement cards. */
.reading-tuner {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px 12px 12px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.reading-tuner .tuner-rows {
  gap: 18px;
}
.reading-tuner-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.reading-tuner-foot .sr-btn-confirm {
  min-width: 104px;
}
.reading-tuner-foot .sr-btn-confirm:disabled {
  opacity: 0.7;
  cursor: default;
}
/* Compact variant — inside a Plan action card, where the surrounding card
   already supplies the frame, so the control drops its own border/fill. */
.reading-tuner-compact {
  border: 0;
  background: transparent;
  padding: 8px 0 0;
}

/* Log-results pop-out body (components/log-results.js) — the slide-over
   workspace that stacks one reading tuner per target an action moves. */
.log-results {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.log-results-dek {
  font-family: var(--font-sans);
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim);
}

/* Sharpen sheet — threat-scoped "make this estimate more accurate" slide-over.
   Reuses the reading-tuner + family-history chip vocabularies; only the wrapper,
   dek, live-number line, and group labels are bespoke. */
.sharpen-sheet {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.sharpen-dek {
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-dim);
  margin: 0;
}
/* Live lifetime-risk line — the payoff. The number re-sharpens as signals are
   added; once moved, the opening value shows struck-through before the arrow. */
.sharpen-prob-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-mono);
  padding: 0;
}
.sharpen-prob-label {
  font-size: 10px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
}
.sharpen-prob-was {
  font-size: 14px;
  color: var(--text-dim);
  text-decoration: line-through;
  font-variant-numeric: tabular-nums;
}
.sharpen-prob-arrow { color: var(--text-dim); }
.sharpen-prob {
  font-size: 18px;
  font-weight: 500;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
/* Flat list of peer inputs — family-history Yes/No rows + metric sliders share
   one container and one rhythm, no boxes or sub-headers (the user reads them as
   one set of inputs, not two kinds). The live tuner is compact (no card); pull
   its own padding and match its row gap to the family-history rows. */
.sharpen-rows {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding-top: 2px;
}
.sharpen-rows .reading-tuner-compact { padding: 0; }
.sharpen-rows .reading-tuner-rows,
.sharpen-rows .reading-tuner .tuner-rows { gap: 0; }

/* "+N more" disclosure — defers the low-impact tail so the default view stays
   concentrated on the highest-leverage signals. Quiet, left-aligned, reads as
   a control not a CTA. */
.sharpen-more-toggle {
  align-self: flex-start;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--accent);
  background: transparent;
  border: 0;
  padding: 2px 0;
  cursor: pointer;
}
.sharpen-more-toggle:hover { color: #034f63; }
/* The .sharpen-sheet flex gap already spaces the revealed tail from the toggle. */

/* Completeness affirmation — shown when no signal is missing: the sheet flips
   from "add" to "review/maintain". */
.sharpen-complete {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.6px;
  color: var(--green);
  margin: 0;
}

/* Family-history peer row — label left, Yes/No right, matching the metric rows'
   left-label / right-control shape. Buttons reuse the .sr-btn grammar. */
.fh-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 2px;
}
.fh-row-label {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.1px;
  line-height: 1.3;
  color: var(--text-secondary);
  max-width: 260px;
}
.fh-row-buttons {
  display: flex;
  gap: 6px;
  flex: none;
}
/* Yes / No / Done in the calibrate sheet use the data-setup pill grammar
   (.calibrate-pill); a shared min-width keeps the two-up pairs even. */
.fh-row-pill {
  min-width: 48px;
  text-align: center;
}

.sharpen-group {
  display: flex;
  flex-direction: column;
  gap: 0;
}
/* NB: `.profile-row` also names a row on the PROFILE page (see top of file);
   reset padding here so that rule's `padding: 6px 0` can't leak in. */
.profile-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-height: 34px;
  padding: 0;
}
.profile-row-main {
  display: contents;
}
.profile-row-name {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.1px;
  line-height: 1.3;
  color: var(--text-secondary);
  max-width: 260px;
}
/* Inline ⓘ glyph after a row name. Quiet until reached for, owns the
   definition tooltip ([data-tooltip] handles the reveal on hover/focus).
   focusable so the tooltip is tap-reachable on touch. */
.profile-info-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 13px;
  height: 13px;
  margin-left: 5px;
  vertical-align: 1px;
  border: 1px solid var(--border);
  border-radius: 50%;
  font-size: 9px;
  font-style: italic;
  font-weight: 600;
  line-height: 1;
  color: var(--text-dim);
  cursor: help;
  opacity: 0.7;
  transition: opacity 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.profile-info-dot:hover,
.profile-info-dot:focus-visible {
  opacity: 1;
  color: var(--text-secondary);
  border-color: var(--text-dim);
  outline: none;
}
.profile-row-buttons,
.profile-row-input-wrap {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
/* Inputs reuse the data-setup (demographics) grammar: quiet — transparent
   border until hover/focus, mono, tabular — so the calibrate sheet and
   onboarding read as one input system. */
.profile-row-input,
.profile-row-date-input {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: textfield;
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 4px 8px;
  font-variant-numeric: tabular-nums;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.profile-row-input::-webkit-outer-spin-button,
.profile-row-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.profile-row-input {
  width: 56px;
  text-align: right;
}
.profile-row-date-input {
  width: 148px;
  color: var(--text-secondary);
}
.profile-row-date-input::-webkit-calendar-picker-indicator {
  opacity: 0.45;
  cursor: pointer;
  padding: 0 0 0 4px;
  transition: opacity 0.14s ease;
}
.profile-row-date-input:hover::-webkit-calendar-picker-indicator,
.profile-row-date-input:focus::-webkit-calendar-picker-indicator {
  opacity: 0.85;
}
.profile-row-input:hover,
.profile-row-date-input:hover {
  border-color: rgba(140, 154, 172, 0.25);
}
.profile-row-input:focus,
.profile-row-date-input:focus {
  outline: none;
  border-color: var(--accent);
  background: rgba(255, 255, 255, 0.85);
}
.profile-row-unit {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Footer holds the lone, reversible escape hatch — quiet until there's
   something to undo (disabled = dimmed). Optimistic save means no submit. */
.sharpen-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 2px;
}
.sharpen-reset {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.4px;
  padding: 5px 12px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}
.sharpen-reset:hover:not(:disabled) {
  color: var(--text-primary);
  border-color: var(--text-secondary);
}
.sharpen-reset:disabled {
  opacity: 0.4;
  cursor: default;
}

@media (max-width: 720px) {
  .slide-over-compact .slide-over-header {
    padding: 18px 22px 15px;
  }

  .slide-over-compact .slide-over-body {
    padding: 22px;
  }

  .slide-over-compact .slide-over-body {
    overflow-x: hidden;
  }

  .slide-over-compact .sharpen-sheet,
  .slide-over-compact .sharpen-rows,
  .slide-over-compact .reading-tuner,
  .slide-over-compact .reading-tuner-rows,
  .slide-over-compact .tuner-row {
    min-width: 0;
    max-width: 100%;
  }

  .slide-over-compact .tuner-row-head {
    min-width: 0;
  }

  .slide-over-compact .tuner-row-label-group {
    min-width: 0;
  }

  .slide-over-compact .tuner-row-label {
    overflow-wrap: anywhere;
  }
}

/* The calibrate sheet is a dense review/edit surface, not the onboarding
   "big friendly scrubber" context. Keep the shared tuner mechanics but lower
   the visual temperature so the panel title and lifetime-risk readout lead. */
.slide-over-compact .tuner-row-head {
  margin-bottom: 8px;
}
.slide-over-compact .tuner-row-label {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.1px;
  color: var(--text-secondary);
}
.slide-over-compact .reading-tuner-rows,
.slide-over-compact .reading-tuner .tuner-rows {
  gap: 0;
}
.slide-over-compact .fh-row + .fh-row,
.slide-over-compact .fh-row + .sharpen-group,
.slide-over-compact .fh-row + .reading-tuner,
.slide-over-compact .profile-row + .profile-row,
.slide-over-compact .sharpen-group + .reading-tuner,
.slide-over-compact .tuner-row + .tuner-row {
  border-top: 1px solid var(--border-subtle);
}
.slide-over-compact .fh-row + .fh-row {
  padding-top: 22px;
}
/* Profile rows live in a gap-0 group, so they own their full rhythm: balanced
   top + bottom padding keeps the content centered between dividers instead of
   hugging the line below it. */
.slide-over-compact .profile-row + .profile-row {
  padding-top: 14px;
}
.slide-over-compact .profile-row:not(:last-child) {
  padding-bottom: 14px;
}
.slide-over-compact .fh-row + .sharpen-group,
.slide-over-compact .fh-row + .reading-tuner,
.slide-over-compact .sharpen-group + .reading-tuner {
  padding-top: 24px;
}
.slide-over-compact .tuner-row + .tuner-row {
  padding-top: 28px;
}
.slide-over-compact .tuner-row:not(:last-child) {
  padding-bottom: 28px;
}
.slide-over-compact .tuner-info {
  width: 13px;
  height: 13px;
  font-size: 8px;
  border-color: rgba(140, 154, 172, 0.42);
}
.slide-over-compact .tuner-readout-input {
  width: 54px;
  font-size: 15px;
  font-weight: 500;
  padding: 1px 4px;
}
.slide-over-compact .tuner-readout-unit {
  font-size: 9px;
  letter-spacing: 0.5px;
}
.slide-over-compact .tuner-bands {
  height: 29px;
  margin-bottom: 6px;
}
.slide-over-compact .tuner-band {
  padding-top: 1px;
  padding-bottom: 2px;
}
.slide-over-compact .tuner-band-label {
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.8px;
  color: var(--text-dim);
}
.slide-over-compact .tuner-band-range {
  font-size: 8px;
  color: var(--text-dim);
}
.slide-over-compact .tuner-band.is-active .tuner-band-label {
  font-weight: 500;
}
.slide-over-compact .tuner-rail {
  height: 20px;
}
.slide-over-compact .tuner-zones {
  height: 7px;
}
.slide-over-compact .tuner-thumb {
  width: 15px;
  height: 15px;
  box-shadow: 0 1px 4px rgba(15, 23, 42, 0.16);
}
.slide-over-compact .tuner-row-unset .tuner-thumb {
  background: #fff;
  border-color: color-mix(in srgb, var(--accent) 58%, #fff);
  opacity: 1;
  box-shadow: 0 1px 5px rgba(0, 109, 144, 0.14);
}
.slide-over-compact .tuner-row-unset .tuner-row-label,
.fh-row-missing .fh-row-label,
.profile-row-missing .profile-row-name {
  color: var(--text-secondary);
}
.slide-over-compact .tuner-row-unset .tuner-readout-input {
  border-color: rgba(140, 154, 172, 0.38);
  background: transparent;
}
.slide-over-compact .tuner-row-unset .tuner-band-label,
.slide-over-compact .tuner-row-unset .tuner-band-range,
.slide-over-compact .tuner-row-unset .tuner-readout-unit {
  opacity: 1;
}

@media (max-width: 720px) {
  .slide-over-compact .sharpen-sheet {
    gap: 16px;
  }

  .slide-over-compact .sharpen-dek {
    font-size: 13px;
    line-height: 1.45;
  }

  .slide-over-compact .fh-row-label,
  .slide-over-compact .profile-row-name,
  .slide-over-compact .tuner-row-label {
    font-size: 13px;
  }

  .slide-over-compact .fh-row,
  .slide-over-compact .profile-row {
    gap: 14px;
  }

  .slide-over-compact .fh-row-label,
  .slide-over-compact .profile-row-name {
    max-width: 420px;
  }

  .slide-over-compact .tuner-readout-input {
    font-size: 16px;
  }

  .slide-over-compact .tuner-thumb {
    width: 20px;
    height: 20px;
  }

  .slide-over-compact .tuner-rail {
    height: 28px;
  }

  .slide-over-compact .tuner-bands {
    margin-left: 10px;
    margin-right: 10px;
  }

  .slide-over-compact .tuner-rail-track {
    left: 10px;
    right: 10px;
  }

  .slide-over-compact .tuner-bands {
    height: 32px;
    margin-bottom: 7px;
  }

  .slide-over-compact .tuner-band-label {
    font-size: 10px;
  }

  .slide-over-compact .tuner-band-range {
    font-size: 9px;
  }

  .slide-over-compact .reading-tuner-rows,
  .slide-over-compact .reading-tuner .tuner-rows {
    gap: 0;
  }

  .slide-over-compact .fh-row + .fh-row {
    padding-top: 24px;
  }

  .slide-over-compact .profile-row + .profile-row {
    padding-top: 16px;
  }
  .slide-over-compact .profile-row:not(:last-child) {
    padding-bottom: 16px;
  }

  .slide-over-compact .fh-row + .sharpen-group,
  .slide-over-compact .fh-row + .reading-tuner,
  .slide-over-compact .sharpen-group + .reading-tuner {
    padding-top: 26px;
  }

  .slide-over-compact .tuner-row + .tuner-row {
    padding-top: 30px;
  }

  .slide-over-compact .tuner-row:not(:last-child) {
    padding-bottom: 30px;
  }
}

/* Lifestyle rows live in the same details container as the tuner
   rows; drop the inherited hairline dividers so the section reads as
   one consistent vocabulary instead of two visual languages. */
.details-section[data-section="lifestyle"] .calibrate-rows {
  /* Match the demographics act's 18px row rhythm so the two collection
     screens read as one flow (was 14px, which sat tighter than act 1). */
  gap: 18px;
}
.details-section[data-section="lifestyle"] .calibrate-row,
.details-section[data-section="lifestyle"] .calibrate-row:first-child {
  border: 0;
  padding: 0;
}
.details-section[data-section="lifestyle"] .calibrate-row-name {
  font-size: 13px;
  font-weight: 500;
}

/* ═══════════ WAITLIST ═══════════
   Email-capture modal shown by every marketing CTA in soft-launch
   mode (config.WAITLIST_MODE === true). Mounted on demand via
   js/waitlist.js — see openWaitlist().
*/

body.waitlist-modal-open {
  overflow: hidden;
}

.waitlist-modal {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.waitlist-modal.is-open {
  opacity: 1;
  pointer-events: auto;
}

.waitlist-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(15, 23, 42, 0.42);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.waitlist-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 24px 80px -16px rgba(15, 23, 42, 0.28),
              0 8px 24px -8px rgba(15, 23, 42, 0.12);
  padding: 32px 32px 28px;
  transform: translateY(8px);
  transition: transform 220ms ease;
}
.waitlist-modal.is-open .waitlist-card {
  transform: translateY(0);
}

.waitlist-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 28px;
  height: 28px;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font-size: 14px;
  cursor: pointer;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, color 0.15s;
}
.waitlist-close:hover {
  background: var(--bg-surface);
  color: var(--text-primary);
}


.waitlist-title {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.2px;
  color: var(--text-primary);
  margin: 0 0 10px;
}

.waitlist-sub {
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-secondary);
  margin: 0 0 22px;
}
.waitlist-sub strong {
  color: var(--text-primary);
  font-weight: 600;
}

.waitlist-bullets {
  list-style: none;
  padding: 0;
  margin: -10px 0 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.waitlist-bullets li {
  position: relative;
  padding-left: 18px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-secondary);
}
.waitlist-bullets li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.7;
}

.waitlist-kicker {
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-secondary);
  margin: -8px 0 22px;
}

.waitlist-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.waitlist-input {
  width: 100%;
  font: inherit;
  font-size: 14px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-surface);
  color: var(--text-primary);
  transition: border-color 0.15s, background 0.15s;
  box-sizing: border-box;
}
.waitlist-input:focus {
  outline: none;
  border-color: var(--accent);
  background: #fff;
}

.waitlist-submit {
  width: 100%;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: white;
  background: var(--accent);
  border: 0;
  border-radius: 8px;
  padding: 13px 18px;
  cursor: pointer;
  transition: background 0.15s, transform 0.12s, opacity 0.15s;
}
.waitlist-submit:hover { background: #0e7490; }
.waitlist-submit:active { transform: scale(0.98); }
.waitlist-submit:disabled {
  opacity: 0.6;
  cursor: progress;
}

.waitlist-error {
  font-size: 12px;
  color: var(--red);
  margin: 10px 0 0;
}

/* ═══════════════════════════════════════════
   MOBILE RISKS DRILL-DOWN (accordion detail)
   ═══════════════════════════════════════════
   The drill card (`risks/drill.js`) was authored for the ~760px desktop
   side pane and is reused verbatim inside the mobile accordion expansion.
   This block adapts it for the phone in two passes.

   (1) OVERFLOW GUARD. Several internal grids carry desktop min-widths —
   notably `.lever-row` at a ~464px floor (minmax(160px,…)+minmax(96px,…)×2
   +64px + gaps). With grid/flex `min-width: auto` that floor propagates up
   through the `.risks-layout` `1fr` track and forces the whole document
   wider than the viewport; every `width:100%`/`1fr` ancestor then stays
   viewport-wide while the card juts past, collapsing the `--space-lg`
   gutters to a sliver (the "snap to the corners"). Clamping the layout
   track to `minmax(0, 1fr)` lets it shrink below content min-width, so no
   descendant can blow the page wider than the viewport.

   (2) DEEP-READ LAYOUT. On mobile the selected accordion row above IS the
   header — it (plus the `.risks-rail-cols` labels) already carries the
   icon, name, RISK %, and MODIFIABLE years. So the detail body must not
   restate them: the card chrome is stripped (a bordered/shadowed card
   nested inside the accordion reads as a box-in-a-box) and the head
   collapses to lead with the description at full width. The one survivor
   is REVISED BY YOUR DATA — the only orienting fact not present in the
   row — which keeps its place just above the prose.

   Placed AFTER all base `.risk-detail*` / `.lever*` rules so these
   equal-specificity overrides win on source order — the same cascade
   discipline the mobile rail block documents. Breakpoint matches the
   880px accordion switch in `risks.js` + the rail block. */
@media (max-width: 880px) {
  /* (1) Overflow guard — see header note. */
  .risks-layout { grid-template-columns: minmax(0, 1fr); }
  .risks-rail-inline-detail { min-width: 0; }

  /* (2) Flush content, no card — but with an accordion spine. The open
     row's --accent-soft fill + squared bottom corners marks "this is
     unfolded," but it scrolls off the top of a long detail, leaving no
     persistent cue that you're still inside the expansion. A 2px accent
     spine flush at the rail's left edge (x=0) continues straight down
     from the open row's left edge and brackets the whole detail; its
     terminus — before the wrapper's bottom gap — is the unambiguous
     "you're back in the list" signal as the rows resume flush-left.
     Square the spine's ends (the base card's --radius-md rounds them
     into a curve) and give the content a small channel off the spine. */
  .risk-detail-card {
    padding: 12px 8px 0 16px;
    border: 0;
    border-left: 2px solid var(--accent);
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    gap: var(--space-md);
    min-width: 0;
  }
  .risk-mobile-section-kicker {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 1.5px;
    line-height: 1;
    text-transform: uppercase;
    color: var(--text-secondary);
  }
  .risk-section-icon {
    display: inline-block;
    flex: 0 0 auto;
    color: var(--text-dim);
    opacity: 0.95;
  }
  .risk-section-title {
    display: inline-flex;
    align-items: center;
    gap: 9px;
  }
  .risk-section-title .risk-section-icon {
    width: 17px;
    height: 17px;
  }
  .risk-detail-head .risk-mobile-section-kicker {
    margin-bottom: 10px;
  }
  .risk-detail-head + .risk-mobile-section-kicker {
    margin-top: 4px;
    margin-bottom: -4px;
  }
  /* Drop the chart's desktop left-indent on mobile. It's `width:100%`
     plus `margin-left: var(--space-md)`, so its right edge runs 16px
     past the card — desktop's wide padding absorbs that, the phone's
     8px right padding can't, and it overflows the page. The spine now
     supplies the "nested under the head" read this indent was for. */
  .mstream:not(.mstream-modal) .mstream-chart-wrap { margin-left: 0; }
  .mstream:not(.mstream-modal) .mstream-breakdown { padding-left: 0; }

  /* Head → lead with description. Drop the grid; hide the identity bits
     duplicated by the row; keep only the REVISED badge above the prose,
     which now spans the full width. */
  .risk-detail-head {
    display: block;
    padding: 0;
  }
  /* Identity (icon + name) AND the vitals strip all hide on mobile: the
     tapped rail row above the drill already shows the risk % and reachable
     years, and peak age lives in the chart caption below — so repeating them
     in a strip here was pure duplication. Badges + prose remain. */
  .risk-detail-icon,
  .risk-detail-name,
  .risk-detail-stats { display: none; }
  /* No paragraph indent on the phone — the narrow column is already a
     tight measure and a flush-left start reads cleaner. */
  .risk-detail-prose { text-indent: 0; }

  /* Chart head on mobile: title takes its own line, the peak-age annotation
     (hidden on desktop) drops beneath it with the ↗ — the two-line rhythm
     the old kicker + caption had, now correctly labeled. */
  .tdens-head { flex-wrap: wrap; }
  .tdens-title { flex-basis: 100%; }
  .tdens-peak { display: inline-block; }

  /* Meta is a one-line flex row holding the revised badge + sharpen CTA.
     The :has guard zeroes the bottom gap when NEITHER is present, so a plain
     un-revised threat adds no stray space before the prose. */
  .risk-detail-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin: 2px 0 var(--space-md);
  }
  .risk-detail-meta:not(:has(.risk-detail-revised)):not(:has(.risk-detail-sharpen)) {
    margin: 0;
  }
  .risk-detail-revised,
  .risk-detail-sharpen {
    margin: 0;
  }

  /* Factors viz — relax the desktop name/value mins so the decomposition
     rows fit a narrow phone (the empty 3fr bar track already shrinks). */
  .risk-baseline-row {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 2fr) minmax(0, auto);
    gap: 10px;
  }

  /* The data-explainer tooltip is rendered while hidden (opacity:0, not
     display:none), and its `width:max-content` (≤360px) box is centered
     with translateX(-50%) — on a ~330px row it pokes past the right edge,
     and every hidden row's tooltip pads the scroll pane's scrollWidth into
     a phantom horizontal scrollbar. Pin it to the row's own width so it can
     never exceed the row; it still fades in above on tap-focus, wrapping to
     row width. (Desktop keeps the centered floating tooltip.) */
  .risk-baseline-row[data-explainer]::after {
    left: 0;
    right: 0;
    width: auto;
    max-width: none;
    transform: translateY(2px);
  }
  .risk-baseline-row[data-explainer]:hover::after,
  .risk-baseline-row[data-explainer]:focus-visible::after {
    transform: translateY(0);
  }


  /* Lever table → single-line iOS-style rows. The desktop grid can't fit a
     phone, and a two-line restack made unmeasured rows read as broken.
     Mobile JTBD is scan-and-drill: name on the left, the row's "headline"
     standing on the right (measured value, severity-colored / binary state
     chip) + years badge. The gap gauge, goal text, and ghost track live in
     the row's tap-to-expand panel — an unmeasured continuous lever
     collapses to just name + years, as before.

     The "STATUS / IF HIT" header is dropped — at single-line shape those
     labels don't track to a coherent column, and the readout speaks for
     itself. */
  .lever-row {
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas: "name standing years";
    column-gap: var(--space-sm);
  }
  .lever-row-name  { grid-area: name; }
  /* Re-box the standing cell: desktop makes it display:contents (its children
     join the row grid), but the phone grid is areas-based, so it needs a real
     box again to occupy the "standing" area. */
  .lever-standing  { display: flex; align-items: center; grid-area: standing; gap: var(--space-sm); }
  .lever-row-years { grid-area: years; }
  .lever-row-header { display: none; }
  /* Drop the gauge and ghost track — they belong to the expansion at this
     width. The measured value and the binary state chip survive as the row's
     standing headline. */
  .lever-standing-bar,
  .lever-standing-track,
  .lever-standing-latent-label { display: none; }
  /* Severity coloring on the value itself — on desktop urgency reads off
     the range bar, but the bar isn't on the row at this width, so the
     number takes that job. Matches the .range-bar-marker palette. */
  .lever-row-current-sev-nominal    { color: var(--green); }
  .lever-row-current-sev-borderline { color: var(--yellow); }
  .lever-row-current-sev-high       { color: var(--amber); }
  .lever-row-current-sev-critical   { color: var(--red); }

  /* Action rows → inline-expand on tap, matching the Goals lever pattern
     above. Risks is the explore surface, Plan is the commit surface, so
     the tab jump is an explicit affordance inside the opened detail.

     Visual: drop the card chrome. The preceding Factors and Goals
     sections both render as open-bordered lists (rows on hairlines, no
     surrounding box); a stack of bordered/rounded action cards reads
     as a foreign visual mode at the bottom of the same view. Switch to
     the same hairline-row treatment so all three sections share one
     visual language. */
  .risk-actions-list,
  .risk-actions-more-list,
  .risk-banked-action-list,
  .risk-actions-dismissed-list { gap: 0; margin-top: 0; }
  .risk-action-row {
    background: transparent;
    border: 0;
    border-bottom: 1px solid var(--border-subtle);
    border-radius: 0;
    padding: 12px 2px;
    gap: var(--space-sm);
    grid-template-columns: auto 1fr auto;
  }
  /* Caret takes the new leading column; rotates on expand. Aligned to
     the top of the row so multi-line titles wrap beside it, not under. */
  .risk-action-row-caret {
    display: inline-block;
    align-self: start;
    padding-top: 1px;
  }
  .risk-action-row.expanded .risk-action-row-caret {
    transform: rotate(90deg);
    color: var(--accent);
  }
  .risk-banked-action-list > .risk-action-row:last-child,
  .risk-actions-dismissed-list > .risk-action-row:last-child { border-bottom: 0; }
  .risk-actions-more:not([open]) .risk-actions-more-list > .risk-action-row:last-child {
    border-bottom: 0;
  }
  /* Dismissed rows need a starting rule below their summary; the live
     "+N more" tail gets its boundary from the visible row above. */
  .risk-actions-dismissed-list > .risk-action-row:first-child {
    border-top: 1px solid var(--border-subtle);
  }
  /* Drop the 6px top margin the more/dismissed details carry on desktop
     — on mobile the lists are already separated by their internal
     hairlines, so the gap reads as stray padding instead of rhythm. */
  .risk-actions-more,
  .risk-actions-dismissed { margin-top: 0; }
  .risk-action-row:hover { background: transparent; }

  /* Meter compaction — drop the 1.5px bar (unscannable at phone width;
     rank is already encoded by sort order). Keep just the +Xy number. */
  .risk-action-row-meter { width: auto; padding-top: 1px; }
  .risk-action-row-meter-bar { display: none; }
  .risk-action-row-meter-num { font-size: 12px; }

  /* Collapsed: caret + title on the left, +Xy on the right. Title keeps
     its tag inline (Doctor/Rx is part of the at-a-glance readout) and
     wraps naturally — the row stops pretending to be a fixed card.
     Weight is dropped to 500 + text-secondary so the title sits at the
     same visual weight as the Goals row labels above; the desktop's
     bold-on-primary treatment reads as a card heading, but on an open
     list the same weight pops against the lighter Goals rows. */
  .risk-action-text {
    font-size: 12px;
    line-height: 1.4;
    font-weight: 500;
    color: var(--text-secondary);
  }
  .risk-action-row .risk-action-why,
  .risk-action-row .target-chips,
  .risk-action-row .risk-action-row-foot { display: none; }

  /* Expanded — full body returns with a subtle tint + accent stripe so
     the open row is findable when scrolling a long list. Each body
     block gets its own top margin so the prose, chips, and foot all
     have breathing room instead of stacking edge-to-edge. */
  .risk-action-row.expanded {
    background: var(--bg-surface);
    padding-bottom: 14px;
    border-bottom-color: var(--border-subtle);
  }
  .risk-action-row.expanded .risk-action-why {
    display: block;
    margin-top: 8px;
  }
  .risk-action-row.expanded .target-chips {
    display: flex;
    margin-top: 10px;
  }
  .risk-action-row.expanded .risk-action-row-foot {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-top: 12px;
  }
  /* Foot wrapper carries the spacing on mobile — reset the inner
     controls' own top margin so we don't double up. */
  .risk-action-row.expanded .risk-action-row-foot .action-controls {
    margin-top: 0;
    opacity: 1;
  }
  /* Plan-link inside the foot uses the same neutral button chassis as
     the self-report controls so the opened detail has one action row,
     not a detached CTA at the far edge. */
  .risk-action-row.expanded .risk-action-plan-link {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    align-self: center;
  }

  /* Banked footer → self-contained receipt. On desktop the bordered
     drill card closes this section on every side; mobile strips that
     chrome (see the .risk-detail-card override above), leaving the
     footer's lone top divider with a quiet line dangling beneath it and
     the next rail row hard underneath — orphaned. Re-close it as a
     subtle filled block (the same --bg-surface toggle language as the
     expanded action row above), bounded on all four sides, and drop the
     now-redundant divider; the fill carries the separation. */
  .risk-detail-footer {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }
  .risk-banked-section {
    background: var(--bg-surface);
    border-radius: var(--radius-md);
    padding: 12px 14px;
  }
  /* Summary spans the block so the whole receipt is one tap target;
     caret pins right as the expandable-row affordance. Drop the desktop
     hover-bleed margins — the fill already frames it. */
  .risk-banked-summary {
    display: flex;
    align-items: center;
    width: 100%;
    margin: 0;
    padding: 2px 0;
  }
  .risk-banked-lede { white-space: nowrap; }
  /* Collapsed line carries only the label + payoff years; the counts
     ("N actions · M goals") wrapped to a second line at phone width and
     are redundant with the expanded body's Actions/Goals groups, so
     drop them here. */
  .risk-banked-sep,
  .risk-banked-counts { display: none; }
  .risk-banked-caret { margin-left: auto; }
}

/* ═══════════════════════════════════════════════════════════════════
   READING TEXT → SANS (Inter)
   ───────────────────────────────────────────────────────────────────
   The dual-typeface split: mono is the instrument face (numbers,
   tabular columns, +Xy values, ALL-CAPS section titles, nav tabs,
   status chips) and stays the body default. The classes below carry
   *reading* text — prose paragraphs, one-line deks/subtitles,
   descriptions, empty-states, citation links, and readable name/label
   columns — so they opt into the sans face for word-shape legibility.

   This is one cross-cutting, same-specificity rule placed last in the
   file: it overrides only `font-family` (and zeroes mono-tuned
   tracking, which reads loose in a proportional face) while leaving
   every other property in each class's own rule untouched. Deliberately
   excluded: ALL-CAPS structural labels (.section-title, .*-group-label,
   eyebrows), nav tabs, chips/badges, and numeric cells — those are the
   instrument and stay mono. Scoped per-surface tweaks (size/contrast)
   live in each class's own rule; this block is font-family only.
   ═══════════════════════════════════════════════════════════════════ */
/* Overview + shared chart components */
.ovh-lever-text, .lhz-mark-station .lhz-mark-name,
.tt-dek, .tt-row-name,
/* Risks — drill, levers, expanded actions/goals (own .risk-action-* /
   .lever-exp-* classes, distinct from Plan's .action-* below) */
.risk-action-text, .risk-action-why, .risk-empty-line,
.risk-sheet-action-title, .risk-sheet-action-why,
.risk-detail-prose, .risk-section-dek, .risks-mobile-summary-sub,
.lever-exp-empty, .lever-exp-desc, .lever-exp-action-text,
.lever-secured-panel-empty, .lever-exp-also-item,
/* Plan (classes also shared into Risks Actions) */
.plan-card-title, .plan-row-title, .plan-sheet-action-title,
.action-text, .action-why, .plan-empty, .plan-deliver-title,
/* Product surfaces */
.perform-intro, .live-intro,
/* Shared building blocks — the self-report question + the action receipt's
   echoed action name (both things the user reads, not labels) */
.self-report-prompt, .action-receipt-text,
/* Trends explorer — insight prose, marker name columns, empty/CTA states */
.trends-empty, .trends-expand-cta, .trends-promo-title,
.trends-marker-name, .ti-marker-name, .tcb-name, .tcb-prose,
/* Framework — hero titles/ledes, trace name columns, methodology prose */
.model-hero-title, .model-hero-lede, .model-hero-caption,
.model-hero-caption-link, .method-hero-cta-sub,
.model-layer-def, .model-layer-examples,
.trace-sub, .trace-focal-name, .trace-focal-desc, .trace-focal-sub,
.trace-row-name, .trace-rail-label, .trace-band-empty,
.years-hero-title, .years-hero-lede, .years-tool-sub,
.years-inspector-title, .years-inspector-desc, .years-inspector-sub,
.years-threat-name, .years-threat-evidence, .years-rail-label,
.years-source-link, .framework-footer-link,
/* Data panel + source explorer (numeric columns stay mono) */
.dp-privacy, .dp-empty, .dp-stored, .dp-sr-action-text, .dp-prov-name,
.src-data-meta, .src-table-note, .src-empty, .src-td-desc, .src-td-cause,
.src-td-tier, .src-cod-detail-cell,
/* iOS waitlist modal — headline, subhead, feature bullets, kicker, errors */
.waitlist-title, .waitlist-sub, .waitlist-bullets li,
.waitlist-kicker, .waitlist-error,
/* Onboarding / data-setup — the per-step intro line, the consent +
   medical-disclaimer copy, the genome / Apple-Health / AI side-door
   instructions, and the waitlist-capture pitch. Field labels, units,
   numeric readouts, the ALL-CAPS step labels (.ai-step-label,
   .pdf-review-section-label), the loaded-card status header, and the
   copy-paste prompt block all stay mono — they're the instrument. */
.welcome-act-prompt,
.terms-gate-body, .terms-gate-ack-text,
.genome-howto-lede, .genome-howto-pipeline, .genome-howto-aside,
.genome-howto-steps li, .welcome-export-help-steps li,
.ai-modal-aside, .welcome-guide-list li,
.welcome-source-capture-sub, .welcome-source-capture-nudge,
.welcome-source-capture-done, .welcome-source-capture-error,
/* Pseudo-element tooltips: prose rendered via content:attr() can't be reached
   by a class selector on the host, so the ::after must be listed explicitly.
   The tuner-info + calibrate-pill-hint info bubbles are onboarding marker prose. */
.tuner-info::after, .calibrate-pill-hint[data-tooltip]::after,
.risk-baseline-row[data-explainer]::after {
  font-family: var(--font-sans);
  letter-spacing: 0;
}
