/* =====================================================================
   Hablexo attendee page — "Violet Bridge" skin + WCAG 2.2 AA.
   Re-skinned from the original attendee stylesheet: every colour now reads
   from a design token (dark default + light theme following the device, with
   a data-theme override), the brand equaliser lockup lives in the picker
   header, and the AA delta (control borders ≥3:1, focus ring, target sizes,
   tokenised status text) matches the operator console.
   Self-contained — no external design-system import, so it ships as-is.
   ===================================================================== */

/* Brand typefaces — Hanken Grotesk (UI/body), Newsreader (display headings).
   CHTR-243: SELF-HOSTED (fonts/skin.css) so the attendee page makes NO third-party (Google Fonts)
   request — it used to leak the attendee's IP/UA and the referring event URL (its key) to Google.
   Regenerate fonts/skin.css with scratchpad/fetch-skin-fonts.mjs if these families change. */
@import url('fonts/skin.css');

:root {
  color-scheme: dark;

  /* ---- Violet Bridge — true black (default control surface, OLED) ---- */
  --bg:        #000000;   /* pure black — page background */
  --surface:   #0c0c11;   /* raised panels / sheet */
  --surface-2: #141419;   /* inputs, cards, chat bubbles */
  --surface-3: #1d1d24;   /* hover / pressed */
  --line:      #2a2a33;   /* decorative dividers, borders */
  --line-soft: #1b1b22;
  --control-border: #6f6f82; /* WCAG 1.4.11 — interactive-control boundary ≥3:1 on black */

  --fg:    #f5f3fb;   /* primary text — ~19:1 on black */
  --muted: #b8b2c8;   /* secondary text / labels — ≥8:1 */
  --faint: #a49cb8;   /* tertiary meta — ≥6:1 */

  --primary:       #9384df;   /* brand violet — actions, links, accents */
  --primary-press: #4f3a9c;
  --magenta:       #d65a99;   /* secondary accent (brand mark bar) */
  --green:         #3fae86;   /* success / connected */
  --on-primary:    #160f2b;   /* text/icon on a --primary fill */

  /* Status (tokenised — replaces the old raw #34d399 / #f59e0b / #ef4444) */
  --status-ok:    var(--green);
  --status-warn:  #e3a857;    /* connecting / reconnecting — 8.3:1 on ink */
  --status-error: #e0596b;    /* operator stopped */

  /* App-specific surfaces derived from the brand tokens */
  --band:      var(--surface-2);           /* input / list-row / bubble fill */
  --lyric-bg:  #0d0d12;                    /* lower-third band — a hair above pure black */
  --lyric-fg:  #f2eefb;                    /* lyrics always sit on the dark band */

  --r-sm: 8px;  --r-md: 10px;  --r-lg: 14px;  --r-pill: 999px;
  --tap: 44px;  /* minimum comfortable touch target */
  /* Safe-area insets for notched / cutout phones. Default to the real device insets via env();
     the demo harness overrides these so the chrome clears the simulated camera cutout. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --accent: var(--primary); /* back-compat alias */

  /* CHTR-37: page font. Default is the brand UI face; the operator can still
     override it via ?font= (app.js rewrites this variable + loads the font). */
  --font-family:  "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
}

/* Light — Violet Bridge on warm paper. Follows the device unless data-theme forces it. */
@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;
    --bg:        #f5f3fb;
    --surface:   #ffffff;
    --surface-2: #efecf8;
    --surface-3: #e6e2f4;
    --line:      #ddd7ef;
    --line-soft: #ebe7f7;
    --control-border: #827ba0;
    --fg:    #1c1630;
    --muted: #5f5a74;
    --faint: #6a6480;
    --primary:       #4f3a9c;
    --primary-press: #3a2a78;
    --magenta:       #b02d6e;
    --green:         #1c7a59;
    --on-primary:    #ffffff;
    --status-warn:  #8a5a13;
    --status-error: #c0394b;
    --lyric-bg:  #241d3d;   /* the lower-third stays a dark band even on paper */
    --lyric-fg:  #f2eefb;
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg:        #f5f3fb;
  --surface:   #ffffff;
  --surface-2: #efecf8;
  --surface-3: #e6e2f4;
  --line:      #ddd7ef;
  --line-soft: #ebe7f7;
  --control-border: #827ba0;
  --fg:    #1c1630;
  --muted: #5f5a74;
  --faint: #6a6480;
  --primary:       #4f3a9c;
  --primary-press: #3a2a78;
  --magenta:       #b02d6e;
  --green:         #1c7a59;
  --on-primary:    #ffffff;
  --status-warn:  #8a5a13;
  --status-error: #c0394b;
  --lyric-bg:  #241d3d;
  --lyric-fg:  #f2eefb;
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:        #000000;
  --surface:   #0c0c11;
  --surface-2: #141419;
  --surface-3: #1d1d24;
  --line:      #2a2a33;
  --line-soft: #1b1b22;
  --control-border: #6f6f82;
  --fg:    #f5f3fb;
  --muted: #b8b2c8;
  --faint: #a49cb8;
  --primary:       #9384df;
  --primary-press: #6f61c4;
  --magenta:       #d65a99;
  --green:         #3fae86;
  --on-primary:    #160f2b;
  --status-warn:  #e3a857;
  --status-error: #e0596b;
  --lyric-bg:  #0d0d12;
  --lyric-fg:  #f2eefb;
}

* { box-sizing: border-box; }
/* Author display rules below would otherwise beat the UA [hidden] rule, so keep
   hidden authoritative for the picker / view / settings toggles. */
[hidden] { display: none !important; }
html, body { margin: 0; height: 100%; }
body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-family);
  font-size: 18px;
  line-height: 1.45;
  -webkit-text-size-adjust: 100%;
  overflow: hidden;            /* the document never scrolls — only the inner panes do */
  overscroll-behavior: none;
}
button, input, textarea, select { font: inherit; }

/* WCAG 2.4.7 Focus visible — a single deliberate, theme-aware ring on every
   interactive element (the app previously relied on the UA ring, which several
   restyled controls suppressed). */
:where(a, button, input, textarea, select, [tabindex], [role="button"]):focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 6px;
}

/* WCAG 2.4.1 Bypass blocks — skip link to the main region. */
.skip-link {
  position: absolute;
  left: -9999px; top: 0; z-index: 40;
  padding: 10px 16px;
  background: var(--primary); color: var(--on-primary);
  font-weight: 700; border-radius: 0 0 var(--r-md) 0;
}
.skip-link:focus { left: 0; }

/* Bounded viewport-height column. Header/search stay put; only the inner list /
   caption pane scrolls. height (not min-height) is what keeps the header pinned. */
main { height: 100vh; height: 100dvh; display: flex; flex-direction: column; }
main:focus { outline: none; }
#picker, #view { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* #view is the positioning context for the lower-third lyrics overlay (CHTR-6);
   overflow:hidden clips the footer while it's parked below the bottom edge. */
#view { position: relative; overflow: hidden; }

/* ── Brand lockup (light branding) ─────────────────────────────────────
   Seven-bar audio-equaliser mark + "Hablexo" wordmark, per the Violet Bridge
   style guide. Bars use the three brand hues so the mark themes automatically.
   Two placements via [data-brand-variant]: A = compact/inline (default),
   B = a centred welcome moment. */
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit; }
.eq { display: inline-flex; align-items: center; gap: 3px; height: 33px; flex: none; }
.eq i { display: block; width: 5px; border-radius: 2.5px; background: var(--primary); }
.eq i:nth-child(1) { height: 13px; }
.eq i:nth-child(2) { height: 24px; }
.eq i:nth-child(3) { height: 33px; background: var(--magenta); }
.eq i:nth-child(4) { height: 18px; background: var(--green); }
.eq i:nth-child(5) { height: 28px; }
.eq i:nth-child(6) { height: 15px; background: var(--magenta); }
.eq i:nth-child(7) { height: 9px; }
.brand-word {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 24px;
  letter-spacing: -0.01em;
  color: var(--fg);
  line-height: 1;
}
.brand-tag {
  display: none;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--primary);
}

/* ── No event key (CHTR-24): the scan-a-QR prompt ──────────────────── */
.no-event { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: max(24px, var(--safe-top)) 24px max(24px, var(--safe-bottom)); }
.no-event-card { text-align: center; max-width: 22rem; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.no-event-card .brand { justify-content: center; margin-bottom: 20px; }
.no-event-icon {
  width: 64px; height: 64px; margin: 8px auto 20px;
  display: block; fill: var(--primary);
}
.no-event-card h1 { margin: 0; font-family: var(--font-display); font-size: 24px; font-weight: 500; line-height: 1.3; color: var(--fg); }

/* ── Language picker (first connect) ───────────────────────────────── */
.picker-head {
  flex: none;
  padding: 14px 16px 12px;
  padding-top: max(14px, var(--safe-top));
  border-bottom: 1px solid var(--line);
}
/* The brand lockup sits at the top of the picker header (light branding, CHTR ask). */
.picker-brand { margin: 2px 2px 14px; }
.event-brand-logo {
  display: block;
  max-width: min(16rem, 70vw);
  max-height: 4.5rem;
  object-fit: contain;
}
.live-brand {
  flex: none;
  max-width: 72px;
  min-width: 0;
  overflow: hidden;
  justify-content: flex-end;
}
.live-brand .brand-word { font-size: 13px; white-space: nowrap; }
.live-brand .event-brand-logo { max-width: 72px; max-height: 28px; }
.picker-title { margin: 0 0 12px; font-family: var(--font-display); font-size: 20px; font-weight: 500; }
.search {
  width: 100%;
  height: var(--tap);
  padding: 0 14px;
  border: 1px solid var(--control-border);
  border-radius: var(--r-md);
  background: var(--band);
  color: var(--fg);
  -webkit-appearance: none;
  appearance: none;
}
.search::placeholder { color: var(--muted); }
/* CHTR-754 — the doubled box-shadow is the cascade fallback, not a duplicate. `color-mix()` is
   Safari 16.2 / Chrome 111, above our attendee floor (Safari 15.6 / Chrome 90 — see
   tools/ci/browser-baseline.mjs for why an iPhone 7 defines it). An engine without it drops the
   second declaration ENTIRELY, taking the focus ring with it and leaving `outline: none` behind —
   a WCAG 2.4.7 failure on exactly the old phones we most need to serve. The plain declaration
   first gives them a solid brand ring; newer engines take the softer 35% one. Same pattern as
   `height: 100vh; height: 100dvh` above. Repeated on every :focus rule that mixes a colour. */
.search:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 35%, transparent); }

/* Variant A (default): compact inline lockup, title left-aligned. */
[data-brand-variant] .picker-title { text-align: start; }
/* Variant B: a centred welcome moment — bigger mark, tagline, centred title. */
[data-brand-variant="b"] .picker-brand { flex-direction: column; gap: 8px; margin: 6px 0 18px; }
[data-brand-variant="b"] .picker-brand .eq { height: 44px; gap: 4px; }
[data-brand-variant="b"] .picker-brand .eq i { width: 6px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(1) { height: 17px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(2) { height: 32px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(3) { height: 44px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(4) { height: 24px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(5) { height: 37px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(6) { height: 20px; }
[data-brand-variant="b"] .picker-brand .eq i:nth-child(7) { height: 12px; }
[data-brand-variant="b"] .picker-brand .brand-word { font-size: 30px; }
[data-brand-variant="b"] .picker-brand .brand-tag { display: block; }
[data-brand-variant="b"] .picker-title { text-align: center; }
[data-brand-variant="b"] .no-event-card .brand { flex-direction: column; gap: 8px; }
[data-brand-variant="b"] .no-event-card .eq { height: 44px; }
[data-brand-variant="b"] .no-event-card .brand-tag { display: block; }

.picker-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 6px 12px max(12px, var(--safe-bottom));
}
.section-head {
  padding: 14px 6px 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}
.empty { padding: 28px 12px; text-align: center; color: var(--muted); }

.lang-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: var(--tap);
  margin: 4px 0;
  padding: 9px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--band);
  color: var(--fg);
  cursor: pointer;
  text-align: start;
  -webkit-tap-highlight-color: transparent;
  transition: border-color 120ms ease, background 120ms ease;
}
.lang-btn:hover { border-color: var(--control-border); }
.lang-btn:active { border-color: var(--primary); background: var(--surface-3); }
/* native + English side by side to keep rows short */
.lang-btn .labels { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.lang-btn .native { font-size: 17px; font-weight: 600; white-space: nowrap; }
.lang-btn .en { color: var(--muted); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Flag chip — reserved slot so names stay aligned whether or not a flag exists */
.flag {
  flex: none;
  display: block;
  width: 31px;
  height: 21px;
  border-radius: 4px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}
.flag svg { display: block; width: 100%; height: 100%; }
.flag-empty { box-shadow: none; background: none; } /* keeps the slot width, no empty box */

/* ── Caption view ──────────────────────────────────────────────────── */
.bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  /* Sit BELOW the OS status-bar band + camera cutout: reserve the full top inset so the header
     clears the clock / signal / battery and the island. On a standalone/PWA device this is the
     real env(safe-area-inset-top); the demo harness supplies a matching value. */
  padding-top: max(10px, var(--safe-top));
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}
/* Right-hand action cluster — equal gaps between every icon, pushed right by the language block. */
.bar-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}
/* Trim the icon box tight to the 22px glyph (matching the 24px status chip) so the gaps read even;
   the 44px height keeps a comfortable 24×44 target (WCAG 2.5.8). */
.bar-actions .iconbtn { min-width: 24px; padding: 0; }
.iconbtn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* CHTR-104: size to the icon, not a 44px-wide box, so the header items pack tightly.
     Min width 32px + 44px height keeps a comfortable target (2.5.8). */
  min-width: 32px;
  height: var(--tap);
  padding: 0 5px;
  border: none;
  border-radius: var(--r-md);
  background: none;
  color: var(--muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.iconbtn:hover { color: var(--fg); }
.iconbtn:active { background: var(--surface-2); color: var(--fg); }
.iconbtn svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
/* CHTR-79: the spoken-audio toggle lights up brand green while audio is enabled, so "sound
   is on" reads at a glance and matches the page's green = active/live cue. */
#audio-toggle[aria-pressed="true"] { color: var(--green); }
/* Swap the muted/active glyphs from the button's pressed state. MUST be CSS, not JS: the icons
   are <svg> and SVGElement has no `hidden` IDL property, so setting el.hidden never reflects to
   the attribute and the icons would never swap (only the colour would change). */
#audio-toggle .icon-audio-on { display: none; }
#audio-toggle[aria-pressed="true"] .icon-audio-off { display: none; }
#audio-toggle[aria-pressed="true"] .icon-audio-on { display: block; }

/* CHTR-79 polish: the spoken-audio button isn't in the header until audio is actually available,
   so when it does appear give it a gentle pop, and a one-shot green ring each time it's switched
   on — so "audio is now available" and "audio is now on" both read at a glance. The ring is a
   pseudo-element that doesn't affect layout. All motion is opt-out via reduced-motion. */
#audio-toggle { position: relative; }
#audio-toggle::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: var(--r-md);
  border: 2px solid var(--green);
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  #audio-toggle:not([hidden]) { animation: audio-appear 0.34s cubic-bezier(0.2, 0.85, 0.3, 1.25) both; }
  #audio-toggle[aria-pressed="true"]::after { animation: audio-ping 0.55s ease-out; }
}
@keyframes audio-appear {
  from { opacity: 0; transform: scale(0.35); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes audio-ping {
  from { opacity: 0.75; transform: scale(0.75); }
  to   { opacity: 0; transform: scale(1.75); }
}

/* CHTR-104: the language label is tappable (changes language); dim it briefly on press. */
.lang-current { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
  cursor: pointer; -webkit-tap-highlight-color: transparent; border-radius: var(--r-sm); }
.lang-current:active { opacity: 0.6; }
.lang-current .flag { width: 27px; height: 18px; }
/* CHTR-104: stack the English name UNDER the native name to save horizontal space. */
.lang-text { display: flex; flex-direction: column; justify-content: center; min-width: 0; line-height: 1.15; }
.lang-native { font-weight: 600; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lang-en { color: var(--muted); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* CHTR-104: word-free status chip (Set B — transport glyphs). A small circular chip; the border +
   glyph share currentColor, so each state sets one colour. The shape is ALSO distinct per state, so
   the indicator survives red/green colour-blindness with the words removed. The word itself lives in
   #status-label (.sr-only) for screen readers + the title tooltip. Colours are tokenised. */
.status {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border: 1px solid currentColor;
  border-radius: 50%;
  color: var(--muted);
}
.status.live { color: var(--status-ok); }    /* connected (and, post-CHTR-105, operator running) */
.status.wait { color: var(--status-warn); }   /* connecting / reconnecting */
.status.idle { color: var(--status-error); }  /* operator stopped */

/* All three glyphs share the grid cell; the active class reveals exactly one. fill defaults to
   currentColor (the dot/square); the spinner is a stroked arc (stroke="currentColor" in markup). */
.status-ico { grid-area: 1 / 1; width: 14px; height: 14px; fill: currentColor; display: none; }
.status.live .ico-live { display: block; animation: status-pulse 1.6s ease-in-out infinite; }
.status.wait .ico-wait { display: block; animation: status-spin 0.9s linear infinite; transform-origin: 50% 50%; }
.status.idle .ico-idle { display: block; }
@keyframes status-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.45; transform: scale(0.82); } }
@keyframes status-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .status-ico { animation: none !important; opacity: 1; }
}

/* Visually-hidden but available to assistive tech (and Playwright's textContent). */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.captions {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 16px clamp(16px, 5vw, 48px);
  padding-bottom: max(16px, var(--safe-bottom));
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* The lower-third band overlays the bottom of the view, so when it slides in app.js
     grows this padding to reserve matching space — the band SHRINKS the caption area
     rather than hiding its last lines. Eased to match the band's 1s slide. */
  transition: padding-bottom 1s cubic-bezier(0.22, 1, 0.36, 1);
}
/* CHTR-144: caption colour + opacity are set inline per caption (per-speaker tint or, when the
   operator hasn't pinned ?fadeColor, left to inherit --fg so captions stay legible in BOTH themes;
   opacity dims with age). These rules are also the JS-off fallback. Captions stay NEUTRAL — the
   violet skin lives on the chrome, not the reading surface.

   Typography is UNIFORM across the roll: weight/size/style are set once on `.caption` and never
   vary by position. The newest caption used to carry `font-weight: 600` on its own, so the instant
   a new caption landed the one before it dropped to 400, changed metrics and RE-WRAPPED mid-read.
   Only the fade may differ between captions — matching the NDI feed renderer, which never bolds. */
/* CHTR-961: the caption sets its OWN leading. It used to inherit body's 1.45 — a chrome value sized
   for 18px labels and buttons, which at a 20–30px caption is 29–44px between wrapped lines, so a
   caption read as loose lines rather than one block. The listener retunes it (Settings → Line
   spacing) via --caption-leading; the literal fallback is what renders before app.js runs, with JS
   off, and in iOS Private Mode. Caption-to-caption spacing is the .captions `gap` above and is
   deliberately untouched, so tightening this does not run captions together. */
.caption { margin: 0; font-size: calc(clamp(20px, 5vw, 30px) * var(--text-scale, 1));
  line-height: var(--caption-leading, 1.25);
  font-weight: 600; color: var(--muted); }
.caption:last-child { color: var(--fg); }
/* CHTR-63: a provisional (in-flight progressive) caption is italic until its corrected final
   replaces it in place; its extra dim is applied inline (composed with the CHTR-144 age-fade). */
.caption.provisional { font-style: italic; }

/* CHTR-62/105: "translation incoming" ellipsis. */
/* CHTR-800: the spoken-audio handover note. Sits between the header and the captions, takes its own
   space (never overlays a caption), and is hidden whenever it has nothing true to say. */
.audio-note {
  margin: 0 0 6px;
  padding: 8px 12px;
  border-radius: var(--r-pill);
  background: var(--surface-2, rgba(127, 127, 127, 0.14));
  color: var(--muted);
  font-size: 13px;
  line-height: 1.35;
  text-align: center;
}
/* CHTR-971 — the carried-language, captions-off notice. It occupies the caption area on its own, so
   it is sized and coloured as guidance rather than as a caption: legible, calm, and clearly not
   something the speaker just said. */
.no-captions {
  margin: 0;
  padding: clamp(12px, 4vw, 24px) 0;
  font-size: clamp(0.95rem, 3.6vw, 1.15rem);
  line-height: 1.5;
  text-align: center;
  max-width: 34em;
  margin-inline: auto;
  opacity: 0.72;
}
/* CHTR-1087 — the case-2 disclosure at the top of the caption viewer. A quiet banner, clearly
   chrome rather than something the speaker said: smaller than a caption, dimmed, never fading. */
.ai-captions {
  margin: 0 auto;
  padding: 4px 0 8px;
  font-size: clamp(0.78rem, 2.8vw, 0.9rem);
  line-height: 1.4;
  text-align: center;
  max-width: 34em;
  opacity: 0.6;
}
.typing {
  display: flex;
  gap: 6px;
  align-items: center;
  min-height: clamp(20px, 5vw, 30px);
  pointer-events: none;
}
.typing-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--primary);
  opacity: 0.4;
  animation: typing-blink 1.2s infinite ease-in-out;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-blink {
  0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
  30% { opacity: 0.9; transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .typing-dot { animation: none; opacity: 0.6; }
}

/* "New" pill — shown only when the user has scrolled up and fresh text arrives */
.jump {
  position: fixed;
  left: 50%;
  bottom: max(18px, var(--safe-bottom));
  transform: translateX(-50%);
  padding: 10px 18px;
  border: none;
  border-radius: var(--r-pill);
  background: var(--primary);
  color: var(--on-primary);
  font-size: 14px;
  font-weight: 700;
  box-shadow: 0 6px 18px rgba(10, 6, 24, 0.5);
  cursor: pointer;
}

/* Lower-third lyrics footer (CHTR-6, reworked CHTR-26). A FIXED lower-third band pinned
   to the bottom, set apart from the captions by a darker violet background. */
.lyric {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 33%;                  /* the fixed lower third */
  overflow: hidden;
  background: var(--lyric-bg);
  border-top: 1px solid rgba(0, 0, 0, 0.5);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.5);
  visibility: hidden;
  transform: translateY(100%);  /* parked below the screen edge when there are no lyrics */
  transition: transform 1s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 1s;
  will-change: transform;
}
/* Slide up to reveal; flip to visible immediately on the way in. A smooth 1s slide. */
.lyric.shown {
  visibility: visible;
  transform: translateY(0);
  transition: transform 1s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s;
}
/* ── CHTR-615: the acquisition CTA ─────────────────────────────────────
   A flex sibling BELOW the captions, so it takes its own strip rather than overlaying the stream.
   Two presentations swapped by [data-state] (promo.js owns the decision). */
.promo {
  flex: none;
  border-top: 1px solid var(--line-soft);
  padding: 8px clamp(16px, 5vw, 48px) max(8px, var(--safe-bottom));
  text-align: center;
}
.promo[data-state="link"] .promo-panel,
.promo[data-state="panel"] .promo-line { display: none; }
/* The lower-third band overlays the foot of the view, so a visible slide would sit on top of the
   CTA. Slides win: app.js flags the band open and the CTA stands down until it clears. */
#view.lyric-open .promo { display: none; }
/* Deliberately quiet: tertiary colour, small, one line. It must read as a footnote next to the
   captions, never as a competing call to action. */
.promo-line {
  display: inline-block;
  color: var(--faint);
  font-size: 12px;
  line-height: 1.3;
  text-decoration: none;
}
.promo-line:hover { color: var(--muted); text-decoration: underline; }

/* The ended state earns more room: the event is over, so the captions have nothing left to lose. */
.promo-panel { position: relative; padding: 6px 32px 4px; }
.promo-title { margin: 0; font-family: var(--font-display); font-size: 17px; color: var(--fg); }
.promo-sub { margin: 2px 0 10px; font-size: 13px; color: var(--muted); }
.promo-cta {
  display: inline-block;
  min-height: var(--tap);
  padding: 11px 20px;
  border-radius: var(--r-pill);
  background: var(--primary);
  color: var(--on-primary);
  font-size: 15px;
  font-weight: 700;
  text-decoration: none;
}
.promo-cta:active { background: var(--primary-press); color: var(--fg); }
.promo-dismiss {
  position: absolute;
  top: -2px;
  inset-inline-end: -6px;   /* logical, so it follows an RTL caption language (CHTR-327) */
  width: var(--tap);
  height: var(--tap);
  display: grid;
  place-items: center;
  border: none;
  background: none;
  color: var(--faint);
  cursor: pointer;
}
.promo-dismiss svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

/* Two stacked layers; a slide change cross-fades the incoming over 1s (CHTR-26). */
.lyric-fit {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px clamp(12px, 4vw, 40px);
  padding-bottom: max(10px, var(--safe-bottom));
  opacity: 0;
  transition: opacity 1s ease;
  pointer-events: none;
}
.lyric-fit.on { opacity: 1; }
.lyric-text {
  display: flex;               /* one .lyric-line per row (CHTR-28) */
  flex-direction: column;
  align-items: center;
  text-align: center;
  color: var(--lyric-fg);      /* the band is always dark, so lyrics stay light in both themes */
  font-weight: 600;
  line-height: 1.2;
  font-size: 30px;             /* base; per-line size is relative (em); block scaled to fit */
  transform-origin: center center;
}
.lyric-line {
  white-space: pre;            /* keep a line intact; JS scales the block to fit the band */
  font-size: 1em;             /* overridden inline per line for relative sizing */
}
@media (prefers-reduced-motion: reduce) {
  .lyric { transition: visibility 0s linear 1s; }
  .lyric.shown { transition: none; }
  .lyric-fit { transition: none; }
  .captions { transition: none; }
}

/* ── Settings sheet (role=dialog, aria-modal) ──────────────────────── */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(10, 6, 24, 0.55);
}
.sheet-card {
  width: min(460px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-bottom: none;
  border-radius: 18px 18px 0 0;
  padding: 22px 22px max(22px, var(--safe-bottom));
  animation: rise 200ms ease;
}
@media (prefers-reduced-motion: reduce) { .sheet-card { animation: none; } }
.sheet-card h2 { margin: 0 0 6px; font-family: var(--font-display); font-weight: 500; font-size: 20px; }
.sheet-card p { margin: 0 0 18px; color: var(--muted); }
/* CHTR-120: settings rows (e.g. the chat text-size slider). */
.setting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; min-height: var(--tap); }
.setting-row label { color: var(--fg); font-size: 15px; }
.setting-range { flex: 1; max-width: 60%; accent-color: var(--primary); height: var(--tap); }
/* CHTR-806: the speaking-speed row stacks, because its scale has to line up with the FULL width of the
   slider — a 60%-wide control with labels underneath it cannot show where the centre is. */
.setting-row-stacked { flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 4px; }
.setting-row-stacked .setting-range { max-width: none; width: 100%; }
/* The normal-speed label is positioned from the VALUE, not from the box: 1× is not the midpoint of the
   0.9–1.2 range (1.05 is), and a label that sat in the middle would name a value the thumb does not
   land on. `--rate-normal-pos` is set by app.js from speech.js's own constants, so the two cannot
   drift; the 33.3% fallback here is only for a page whose JS has not run yet. */
.range-scale { position: relative; height: 22px; font-size: 12px; color: var(--muted); }
.range-scale-min { position: absolute; left: 0; }
.range-scale-max { position: absolute; right: 0; }
.range-scale-mid {
  position: absolute;
  left: var(--rate-normal-pos, 33.3%);
  transform: translateX(-50%);
  color: var(--fg);
  font-weight: 600;
  text-align: center;
}
/* The visible notch: a tick above the `1×` label, under the track. WebKit does not render datalist
   ticks for a range input, so this is what an iPhone actually shows. */
.range-scale-mid::before {
  content: ''; display: block; width: 2px; height: 5px;
  margin: 0 auto 2px; background: var(--muted); border-radius: 1px;
}
/* CHTR-161: the chat-name field, relocated from the composer into Settings. */
.setting-text {
  flex: 1; max-width: 60%; height: 40px; padding: 0 12px;
  border: 1px solid var(--control-border); border-radius: var(--r-md);
  background: var(--band); color: var(--fg);
  -webkit-appearance: none; appearance: none;
}
.setting-text::placeholder { color: var(--muted); }
.setting-text:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 35%, transparent); }
/* CHTR-282: the theme dropdown (Dark / Light / Auto). Native appearance keeps the platform
   arrow + picker; color-scheme on :root keeps the popup itself dark/light-correct. */
.setting-select {
  flex: 1; max-width: 60%; height: 40px; padding: 0 12px;
  border: 1px solid var(--control-border); border-radius: var(--r-md);
  background: var(--band); color: var(--fg);
}
.setting-select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 35%, transparent); }
/* The Audio Output picker is CUSTOM (see the index.html comment): a button face showing the
   single word, and an inline listbox with compact rows and the leading word bold — the two
   things a native <select> cannot do on Android/iOS. */
/* font: inherit and NO size override — the native selects beside it inherit the body's 18px
   (the global `button, input, select { font: inherit }`), and the picker must match them. */
.mode-picker-face { font: inherit; text-align: left; cursor: pointer; }
.mode-picker-face::after { content: "▾"; float: right; color: var(--muted); }
.mode-picker-list {
  margin: -10px 0 18px; padding: 4px; list-style: none;
  background: var(--band); border: 1px solid var(--control-border); border-radius: var(--r-md);
}
.mode-picker-option {
  padding: 8px 10px; border-radius: calc(var(--r-md) - 4px);
  color: var(--fg); line-height: 1.35; cursor: pointer; /* size inherits — matches the face */
}
.mode-picker-option b { font-weight: 700; }
.mode-picker-option[aria-selected="true"] { background: var(--surface-3); }
.mode-picker-option[aria-disabled="true"] { color: var(--muted); opacity: 0.5; cursor: default; }
.sheet-close {
  width: 100%;
  min-height: var(--tap);
  border: 1px solid var(--control-border);
  border-radius: var(--r-md);
  background: var(--primary);
  color: var(--on-primary);
  font-weight: 700;
  cursor: pointer;
}
.sheet-close:hover { background: var(--primary-press); }
@keyframes rise { from { transform: translateY(100%); } to { transform: translateY(0); } }

/* ── Chat forum (CHTR-112) ─────────────────────────────────────────── */
.chat {
  position: fixed;
  left: 0;
  width: 100%;
  top: var(--vv-top, 0px);
  height: var(--vv-height, 100%);
  display: none;
  flex-direction: column;
  background: var(--bg);
  z-index: 6;
}
#view.chat-open .chat { display: flex; }
/* Hide the live-caption stack while chat is open (it keeps updating underneath).
   CHTR-615: the CTA goes with it — the chat screen is a conversation, and its composer must not
   share the foot of the screen with a promo. */
#view.chat-open .promo,
#view.chat-open .captions,
#view.chat-open .jump,
#view.chat-open .lyric { display: none !important; }

.chat-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: max(8px, var(--safe-top)) clamp(12px, 4vw, 24px) 8px;
  border-bottom: 1px solid var(--line);
}
.chat-return {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 36px;
  padding: 6px 14px 6px 10px;
  border: 1px solid var(--control-border);
  border-radius: var(--r-pill);
  background: none;
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.chat-return svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chat-return:hover { color: var(--fg); }
.chat-return:active { color: var(--fg); background: var(--surface-2); }
.chat-title { flex: 1; min-width: 0; font-weight: 600; font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-forum {
  flex: 1;
  min-width: 0;
  height: 40px;
  padding: 0 32px 0 12px;
  border: 1px solid var(--control-border);
  border-radius: var(--r-md);
  background: var(--band);
  color: var(--fg);
  -webkit-appearance: none;
  appearance: none;
  /* a chevron so the combobox reads as a dropdown (native arrow is removed above) */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a39bbb' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 16px;
}

/* CHTR-845: the forum picker as an ANCHORED dropdown, not an Android bottom sheet.
   The rule above strips the closed-state arrow and paints its own, which is the whole of what a page
   has historically been allowed to touch — the OPTION LIST is UA chrome drawn outside the page, and
   Android Chrome parks it against the bottom edge of the viewport. So a tap on this control, which
   sits in the chat bar at the TOP of the screen, produced a menu at the very BOTTOM.
   `appearance: base-select` (Chrome 135+) opts the select into being rendered in-page: the list
   becomes a real popover in the top layer, anchored to the control, with light-dismiss and Esc still
   handled by the UA. It remains a genuine <select>, so app.js's `change` listener, keyboard
   navigation, type-ahead and the accessibility tree are all untouched.
   EVERYTHING here stays behind the @supports gate on purpose: a browser without customizable select
   (iOS Safari, today) keeps the native picker byte-identically, and a native picker at the bottom of
   the screen is a worse dropdown but a perfectly whole one. Do not hoist these rules out.
   Both the select AND its ::picker(select) must opt in — styling one alone is a no-op. */
@supports (appearance: base-select) {
  .chat-forum,
  .chat-forum::picker(select) { appearance: base-select; }

  .chat-forum {
    background-image: none;   /* the UA draws the chevron itself now (::picker-icon, below) */
    padding-inline: 12px 8px;
    gap: 6px;
    text-align: start;
    font: inherit;
    color: var(--fg);
    cursor: pointer;
    /* base-select turns the select into a flex container holding a UA-generated button. Without an
       explicit cross-axis alignment that button sits at the BLOCK-START, so the label rode ~5px high
       in the 40px control (7px above / 16px below) where the native control centres it. Reported on
       Android after CHTR-845 shipped. The UA button is not addressable from CSS or JS — there is no
       pseudo-element for it and no author <button> child here — so the alignment has to be set on
       the flex container, and it can only be verified by measuring pixels (see the e2e test). */
    align-items: center;
  }
  /* The arrow turns over while the list is open — the one affordance the painted-on chevron could
     never have, since a page cannot tell whether a native picker is showing.
     The UA's own picker-icon is a FILLED TRIANGLE, which is off-language for this page: every other
     glyph in the chat bar (the back chevron, the ⓘ, the cog) is a 2px stroked outline. So the glyph
     is suppressed with `color: transparent` and the page's own chevron — the same data URI the
     closed-state rule above uses — is painted underneath it. */
  .chat-forum::picker-icon {
    width: 16px;
    height: 16px;
    color: transparent;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a39bbb' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat center / 16px;
    transition: rotate 150ms ease;
  }
  .chat-forum:open::picker-icon { rotate: 180deg; }
  @media (prefers-reduced-motion: reduce) { .chat-forum::picker-icon { transition: none; } }

  .chat-forum::picker(select) {
    position-area: block-end span-inline-end;  /* directly below, aligned to the control's start edge */
    position-try-fallbacks: flip-block;        /* no room below (short landscape) → open upwards */
    margin-block-start: 6px;
    min-inline-size: anchor-size(inline);      /* never narrower than the control it came from */
    /* MAX_FORUMS is 30 (server/src/server.js) — an unbounded list would run off the screen, which is
       the original complaint in a different costume. Bound it and let it scroll. */
    max-block-size: 60vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 6px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
    box-shadow: 0 10px 30px rgb(0 0 0 / 0.45);
  }

  .chat-forum option {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: var(--tap);   /* WCAG 2.2 target size — the floor the rest of the page holds to */
    padding: 8px 10px;
    border-radius: var(--r-sm);
    color: var(--fg);
    background: none;
  }
  .chat-forum option:hover,
  .chat-forum option:focus { background: var(--surface-3); outline: none; }
  .chat-forum option:checked { font-weight: 600; }
  .chat-forum option::checkmark { color: var(--primary); }
}

.chat-messages {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px clamp(12px, 4vw, 28px);
  /* CHTR-120/144: chat renders SMALLER than captions, scaled by the shared text-size slider. */
  font-size: calc(16px * var(--text-scale, 1));
}
.chat-msg {
  max-width: 85%;
  align-self: flex-start;
  padding: 8px 12px;
  border-radius: 14px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--author, var(--primary)); /* per-author colour (CHTR-120); start edge flips in RTL (CHTR-327) */
  word-wrap: break-word;
  overflow-wrap: anywhere;
}
.chat-msg .chat-author { display: block; font-size: 0.72em; font-weight: 700; color: var(--author, var(--muted)); margin-bottom: 1px; }
.chat-msg .chat-body { display: block; color: var(--fg); }
/* CHTR-342: an optimistic local echo renders dim + italic (the CHTR-63 provisional treatment) until
   the published frame reconciles it; a timed-out / rejected echo stays dim with a subtle strike so it
   reads as "not delivered" rather than apparently-sent. */
.chat-msg.provisional { opacity: 0.55; font-style: italic; }
.chat-msg.provisional.undelivered .chat-body { text-decoration: line-through; }
.chat-empty { margin: auto; padding: 24px; color: var(--muted); text-align: center; font-size: 15px; }

.chat-composer {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px clamp(12px, 4vw, 24px) max(8px, var(--safe-bottom));
  border-top: 1px solid var(--line);
  background: var(--bg);
}
.chat-text::placeholder { color: var(--muted); }
/* CHTR-161: the send button sits INSIDE the message box at the bottom-right (iMessage-style). */
.chat-input-row { position: relative; display: flex; }
.chat-text {
  flex: 1;
  min-width: 0;
  resize: none;
  max-height: 120px;
  padding-block: 10px;
  padding-inline: 14px 46px; /* CHTR-327: inline-end reserves room for the send button, mirrors in RTL */
  border: 1px solid var(--control-border);
  border-radius: 14px;
  background: var(--band);
  color: var(--fg);
  line-height: 1.3;
}
.chat-text:focus, .chat-forum:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 35%, transparent); }
.chat-send {
  position: absolute;
  inset-inline-end: 6px; /* CHTR-327: sits at the inline-end — left in RTL, right in LTR */
  bottom: 6px;
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 50%;
  background: var(--primary);
  color: var(--on-primary);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}
.chat-send svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chat-send:hover { background: var(--primary-press); }
.chat-send:active { filter: brightness(0.92); }
.chat-note { margin: 0; min-height: 1em; font-size: 12.5px; color: var(--status-warn); }

/* CHTR-401: the chat privacy notice. Reuses the Settings .sheet/.sheet-card chrome, so there is one
   dialog look and one focus mechanism (enhance.js). The lead sentence — the fact a DPO cares about —
   is emphasised; the body carries retention, erasure and the machine-translation caveat. */
/* The privacy-notice links (CHTR-401 follow-up): quiet, but clearly labelled and never buried. */
.picker-privacy { margin: 12px 0 4px; text-align: center; font-size: 12px; }
.picker-privacy a, .setting-link, .privacy-more a { color: var(--muted); text-decoration: underline; }
.picker-privacy a:hover, .setting-link:hover, .privacy-more a:hover { color: var(--fg); }
.setting-link { font-size: 14px; }
/* CHTR-799: the voice-setup route out of Settings is a BUTTON (it opens a dialog, it does not
   navigate), so strip the UA button chrome and let it read as the link beside it. */
button.setting-link { background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer; }
.privacy-more { margin: 0 0 16px; font-size: 13px; }

.privacy-lead { margin: 0 0 10px; font-weight: 600; color: var(--fg); line-height: 1.4; }
.privacy-body { margin: 0 0 16px; font-size: 14px; line-height: 1.5; color: var(--muted); }
.privacy-ack {
  display: block; width: 100%; padding: 12px 16px;
  border: 0; border-radius: 10px; cursor: pointer;
  background: var(--accent, #6ea8fe); color: #10121a;
  font: inherit; font-weight: 600;
}

/* ── CHTR-842 (CF-8): the feedback overlay + its entry points ──────────────────────────────────
   The overlay reuses .sheet/.sheet-card, so it inherits the dialog chrome, the rise animation (and
   its reduced-motion opt-out) and enhance.js's focus trap. Only the form needs new rules. */

/* Every entry uses the same speech bubble at var(--tap) — the platform touch-target floor. These
   sit next to other controls, so an under-sized one is not merely awkward, it is a miss. */
.fb-entry {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-width: var(--tap); min-height: var(--tap);
  background: none; border: 0; padding: 0 8px; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 13px;
}
.fb-entry:hover, .fb-entry:focus-visible { color: var(--fg); }
.fb-entry svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* In the collapsed footer the bubble sits beside the promo line, icon-only. */
.fb-entry-line { flex: 0 0 auto; padding: 0 6px; }
/* In the ended panel and the keyless card it is a labelled row, centred under the primary action. */
.fb-entry-row { width: 100%; margin-top: 10px; }

.feedback-card { max-height: 88vh; overflow-y: auto; }
.fb-types { border: 0; margin: 0 0 16px; padding: 0; display: grid; gap: 2px; }
.fb-type { display: flex; align-items: center; gap: 10px; min-height: var(--tap); cursor: pointer; color: var(--fg); font-size: 15px; }
.fb-type input { accent-color: var(--primary); width: 18px; height: 18px; }
.fb-field { display: block; margin: 0 0 14px; }
.fb-field > span { display: block; margin: 0 0 6px; font-size: 13px; color: var(--muted); }
.fb-field input, .fb-field textarea {
  width: 100%; box-sizing: border-box; min-height: var(--tap);
  padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface2, var(--bg)); color: var(--fg);
  font: inherit; font-size: 15px; /* 15px+: iOS Safari zooms the viewport on focus below 16px… */
}
.fb-field textarea { min-height: 88px; resize: vertical; }
.fb-field input:focus-visible, .fb-field textarea:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.fb-error { margin: -8px 0 12px; font-size: 13px; color: var(--status-warn); }
.fb-note { margin: -8px 0 14px; font-size: 12.5px; color: var(--muted); }
.fb-check { display: flex; align-items: flex-start; gap: 10px; min-height: var(--tap); margin: 0 0 6px; font-size: 14px; color: var(--fg); cursor: pointer; }
.fb-check input { accent-color: var(--primary); width: 18px; height: 18px; margin-top: 2px; flex: 0 0 auto; }
.fb-disclose { display: inline-block; min-height: var(--tap); }
.fb-diag-list { margin: 4px 0 14px; padding-left: 18px; font-size: 12.5px; color: var(--muted); line-height: 1.5; word-break: break-word; }
.fb-status { margin: 0 0 12px; font-size: 14px; line-height: 1.45; }
.fb-status.err { color: var(--status-warn); }
.fb-actions { display: grid; gap: 8px; margin: 0 0 14px; }
/* Send is the primary action; Close and Copy are not. `.sheet-close` is a filled button elsewhere,
   where it IS the only action — here it would render identically to Send, so the two compete for
   the same tap and the quieter one wins by accident. Demote them inside this form only. */
.fb-actions .sheet-close {
  background: none; color: var(--muted);
  border: 1px solid var(--line);
}
.fb-actions .sheet-close:hover, .fb-actions .sheet-close:focus-visible { background: none; color: var(--fg); border-color: var(--fg); }
/* The honeypot: out of sight AND out of the tab order. Not `display:none` — some bots skip those. */
.fb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
