/* ═══════════════════════════════════════════════════════════════════
   Iris — style.css  (v2)
   Single source of truth: tokens + reset + all component styles.
   No pixels except 1px hairlines. No color-mix(). No inline styles.
   ═══════════════════════════════════════════════════════════════════ */

/* ── 1. TOKENS ──────────────────────────────────────────────────── */
/* A5: self-hosted fonts (OFL), subset to Latin, served from our own origin —
   no third-party font host ever. Fallback stacks below keep text visible
   before the woff2 arrives (font-display: swap). */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/Inter-Regular.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("fonts/Inter-Medium.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("fonts/Inter-SemiBold.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("fonts/Inter-Bold.woff2") format("woff2"); }
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/JetBrainsMono-Regular.woff2") format("woff2"); }
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 700; font-display: swap; src: url("fonts/JetBrainsMono-Bold.woff2") format("woff2"); }

:root {
  /* Neutral ramp */
  --n-0:   #FFFFFF;
  --n-50:  #F7F7F9;
  --n-100: #EFEFF3;
  --n-200: #E0E0E8;
  --n-300: #C4C4D0;
  --n-400: #9494A8;
  --n-500: #64647A;
  --n-600: #44445A;
  --n-700: #2C2C3C;
  --n-800: #18182A;
  --n-900: #0C0C18;

  /* Accent — iris violet */
  --a-100: #EDE9FE;
  --a-200: #C4B5FD;
  --a-300: #8B5CF6;
  --a-400: #6D28D9;
  --a-500: #5B21B6;
  --a-600: #4C1D95;

  /* Semantic — light (default) */
  --bg-default:     var(--n-50);
  --bg-subtle:      var(--n-100);
  --bg-raised:      var(--n-0);
  --border-default: var(--n-200);
  --border-strong:  var(--n-300);
  /* Fainter than --border-default — for dividers INSIDE a surface (a hash
     line under a file card, say), so the surface's own outer edge still
     reads as the stronger line. */
  --border-subtle:  var(--n-100);
  --fg-default:     var(--n-800);
  --fg-muted:       var(--n-500);
  --fg-subtle:      var(--n-400);
  --accent:         var(--a-400);
  --accent-bg:      var(--a-100);
  --accent-fg:      var(--n-0);
  --accent-press:   var(--a-500);
  /* Edge for accent surfaces — without it an outgoing bubble/card has no
     border at all while its incoming counterpart does, so the two read as
     different classes of object instead of a matched pair. */
  --accent-border:  var(--a-200);

  /* Status */
  --status-ok:      #16A34A;
  --status-ok-bg:   #DCFCE7;
  --status-warn:    #C2740A;
  --status-warn-bg: #FEF3C7;
  --status-err:     #C42A2A;
  --status-err-bg:  #FEE2E2;

  /* Pre-computed focus shadow — no color-mix() */
  --sh-focus-color: rgba(109, 40, 217, 0.32);

  /* Font stacks */
  --font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont,
               "Segoe UI", "Segoe UI Variable", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code",
               "Fira Code", "Consolas", monospace;

  /* Type scale — fluid via clamp() */
  --t-display: clamp(2.75rem, 9vw, 4rem);
  --t-h1:      clamp(1.625rem, 3.5vw, 2.25rem);
  --t-h2:      clamp(1.25rem, 2.5vw, 1.625rem);
  --t-body:    clamp(0.9375rem, 1.5vw, 1rem);
  --t-small:   clamp(0.8125rem, 1.25vw, 0.875rem);
  --t-label:   0.75rem;
  --t-mono:    clamp(0.875rem, 1.5vw, 1rem);

  /* Spacing — 4px base */
  --sp-1:  0.25rem;
  --sp-2:  0.5rem;
  --sp-3:  0.75rem;
  --sp-4:  1rem;
  --sp-5:  1.25rem;
  --sp-6:  1.5rem;
  --sp-8:  2rem;
  --sp-10: 2.5rem;
  --sp-12: 3rem;
  --sp-16: 4rem;

  /* Radii */
  --r-sm:   0.25rem;
  --r-md:   0.5rem;
  --r-lg:   0.75rem;
  --r-xl:   1rem;
  --r-full: 9999px;

  /* Shadows — pre-computed, no color-mix() */
  --sh-sm:    0 1px 2px rgba(0, 0, 0, 0.05);
  --sh-md:    0 4px 16px rgba(0, 0, 0, 0.08),
              0 1px 3px  rgba(0, 0, 0, 0.04);
  --sh-lg:    0 16px 48px rgba(0, 0, 0, 0.14),
              0 4px 12px  rgba(0, 0, 0, 0.06);
  --sh-focus: 0 0 0 3px var(--sh-focus-color);
  /* Inset top highlight for a raised surface — written as a box-shadow so
     it can sit on the OLED canvas without becoming a drop shadow (which a
     black canvas would render invisible). A white surface has nothing to
     highlight, so it's off in light. */
  --sh-edge:  none;
  /* Drop shadow for a raised surface — lifts a white card off the near-white
     canvas in light, where a hairline alone reads flat. Mirror of --sh-edge:
     dark already reads as elevated via surface lightness plus --sh-edge, and
     a black shadow on the OLED canvas would be invisible, so it's off there. */
  --sh-raised: var(--sh-sm);

  /* Motion */
  --dur-fast: 120ms;
  --dur-base: 220ms;
  --dur-slow: 380ms;
  --ease:     cubic-bezier(0.16, 1, 0.3, 1);

  /* Layout */
  --max-content: 22rem;
  --max-chat:    40rem;
  --touch:       2.75rem;
}

/* Dark theme */
@media (prefers-color-scheme: dark) {
  :root {
    --bg-default:     #000000;
    --bg-subtle:      #0D0D0D;
    --bg-raised:      #141414;
    --border-default: #222222;
    --border-strong:  #333333;
    --border-subtle:  #1C1C1C;
    --fg-default:     #F0F0F6;
    --fg-muted:       #8080A0;
    --fg-subtle:      #52526A;
    --accent:         #7C3AED;
    --accent-bg:      #1E1033;
    --accent-fg:      #FFFFFF;
    --accent-press:   #6D28D9;
    --accent-border:  var(--a-600);
    --status-ok:      #4ADE80;
    --status-ok-bg:   #052814;
    --status-warn:    #FBB650;
    --status-warn-bg: #2A1A02;
    --status-err:     #F87171;
    --status-err-bg:  #2A0808;
    --sh-focus-color: rgba(139, 92, 246, 0.32);
    --sh-edge:        inset 0 1px 0 rgba(255, 255, 255, 0.04);
    --sh-raised:      none;
  }
}

/* Reduced motion — collapse everything */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur-base: 0ms;
    --dur-slow: 0ms;
  }
  *, *::before, *::after {
    animation-duration: 0ms !important;
    transition-duration: 0ms !important;
  }
}

/* ── 2. RESET ────────────────────────────────────────────────────── */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  height: 100%;
}

body {
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--fg-default);
  background: var(--bg-default);
  min-height: 100dvh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

button, input, textarea, select {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

img, svg { display: block; max-width: 100%; }

/* ── 3. FOCUS ────────────────────────────────────────────────────── */
:focus { outline: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--sh-focus);
  border-radius: var(--r-sm);
}

/* ── 4. HIDDEN / VIEW SWITCHING ──────────────────────────────────── */
[hidden] { display: none !important; }

/* View enter animation — exits are instant (matches Signal/Linear) */
@keyframes bm-view-in {
  from { opacity: 0; transform: translateY(0.375rem); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── 5. VIEWS ────────────────────────────────────────────────────── */
.bm-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100svh;   /* svh: shrinks when keyboard opens */
  width: 100%;
  padding:
    max(var(--sp-8), env(safe-area-inset-top))
    max(var(--sp-5), env(safe-area-inset-right))
    max(var(--sp-8), env(safe-area-inset-bottom))
    max(var(--sp-5), env(safe-area-inset-left));
  animation: bm-view-in var(--dur-base) var(--ease) both;
}

.bm-view > * {
  width: 100%;
  max-width: var(--max-content);
}

/* ── 6. LANDING VIEW ─────────────────────────────────────────────── */
#view-landing { gap: var(--sp-12); position: relative; }

/* Landing glow (dark only): soft violet halo centred behind the action
   stack. Painted as a background — not a pseudo-element — so it can't
   create overflow, scrollbars, or layout shift on any screen size.
   Light theme deliberately excluded: the glow reads as ambience on
   true black and as a stain on white. */
@media (prefers-color-scheme: dark) {
  #view-landing {
    background: radial-gradient(circle 20rem at 50% 52%,
                rgba(124, 58, 237, 0.22) 0%,
                rgba(124, 58, 237, 0.08) 45%,
                transparent 70%);
  }
}

/* Footer — IN FLOW at the bottom of the landing flex column (was absolutely
   pinned, which let it overlap the tagline the moment the content column got
   taller than a short viewport).
   The margin-top:auto pair (here and on the wordmark) keeps the action stack
   visually centred while there is room, and gracefully turns into normal
   document flow + scroll when there isn't. Overlap is now impossible.
   max-width:none still beats the `.bm-view > *` 24rem clamp at ≥48rem so the
   nav row can be wider than the action stack. Do not remove it. */
#view-landing > .bm-wordmark { margin-top: auto; }
#view-landing .bm-landing-footwrap {
  margin-top: auto;
  width: auto;
  max-width: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-6) 0;
}

#view-landing .bm-landing-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-3) var(--sp-6);
  font-size: var(--t-body);
}

#view-landing .bm-landing-legal {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2) var(--sp-4);
  font-size: 0.78rem;
  color: var(--fg-muted);
  opacity: 0.7;
}

#view-landing .bm-landing-legal a {
  color: var(--fg-muted);
  text-decoration: none;
}

#view-landing .bm-landing-legal a:hover {
  color: var(--accent);
  text-decoration: underline;
}

#view-landing .bm-landing-foot a {
  color: var(--fg-muted);
  text-decoration: none;
  font-weight: 500;
}

#view-landing .bm-landing-foot a:hover {
  color: var(--accent);
  text-decoration: underline;
}

.bm-wordmark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  text-align: center;
}

.bm-wordmark-svg {
  width: 2rem;
  height: 2rem;
  color: var(--accent);
}

.bm-wordmark-text {
  font-family: var(--font-mono);
  font-size: 1.625rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-indent: 0.22em;
  color: var(--fg-default);
  line-height: 1;
}

.bm-wordmark-sub {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* One plain sentence between the wordmark and the buttons saying what this is.
   Sans, not mono — everything around it is mono, and the point of this line is
   that a first-time visitor reads it without effort. */
.bm-hero-desc {
  font-family: var(--font-sans);
  font-size: var(--t-body);
  color: var(--fg-muted);
  text-align: center;
  line-height: 1.5;
  text-wrap: balance;
  max-width: 26rem;
}

.bm-action-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.bm-tagline {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: 0.04em;
  color: var(--fg-subtle);
  text-align: center;
  line-height: 1.8;
  text-wrap: balance;
}

/* ── 7. BUTTONS ──────────────────────────────────────────────────── */
.bm-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  width: 100%;
  min-height: var(--touch);
  padding: 0 var(--sp-6);
  border-radius: var(--r-md);
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: -0.01em;
  cursor: pointer;
  border: 1px solid transparent;
  transition:
    background       var(--dur-fast) var(--ease),
    border-color     var(--dur-fast) var(--ease),
    box-shadow       var(--dur-fast) var(--ease),
    transform        var(--dur-fast) var(--ease),
    opacity          var(--dur-fast) var(--ease);
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  text-wrap: balance;
}

.bm-btn-icon {
  position: absolute;
  left: var(--sp-5);
  display: inline-flex;
  align-items: center;
}

.bm-btn--primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}

.bm-btn--secondary {
  background: var(--bg-raised);
  color: var(--fg-default);
  border-color: var(--border-default);
}

/* :active micro-motion — scale + darken */
.bm-btn--primary:active {
  transform: scale(0.97);
  background: var(--accent-press);
  border-color: var(--accent-press);
}

.bm-btn--secondary:active {
  transform: scale(0.97);
  background: var(--bg-subtle);
}

@media (hover: hover) {
  .bm-btn--primary:hover {
    background: var(--accent-press);
    border-color: var(--accent-press);
    box-shadow: var(--sh-sm);
  }
  .bm-btn--secondary:hover {
    background: var(--bg-subtle);
    border-color: var(--border-strong);
  }
}

.bm-btn:disabled {
  opacity: 0.38;
  cursor: not-allowed;
  pointer-events: none;
}

/* Icon-only button */
.bm-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--touch);
  min-height: var(--touch);
  padding: 0;
  border-radius: var(--r-md);
  background: transparent;
  border: 1px solid transparent;
  color: var(--fg-muted);
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease),
    color      var(--dur-fast) var(--ease),
    transform  var(--dur-fast) var(--ease);
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
}

.bm-icon-btn:active { transform: scale(0.92); }

.bm-icon-btn--primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}

.bm-icon-btn--primary:active {
  background: var(--accent-press);
  border-color: var(--accent-press);
}

@media (hover: hover) {
  .bm-icon-btn:not(.bm-icon-btn--primary):hover {
    background: var(--bg-subtle);
    color: var(--fg-default);
  }
  .bm-icon-btn--primary:hover {
    background: var(--accent-press);
    border-color: var(--accent-press);
  }
}

.bm-icon-btn:disabled {
  opacity: 0.38;
  cursor: not-allowed;
  pointer-events: none;
}

/* Copy button */
.bm-copy-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--touch);
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  background: var(--bg-subtle);
  border: 1px solid var(--border-default);
  color: var(--fg-muted);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  transition:
    background   var(--dur-fast) var(--ease),
    color        var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform    var(--dur-fast) var(--ease);
  -webkit-tap-highlight-color: transparent;
}

.bm-copy-btn:active { transform: scale(0.96); }

/* Copied state — JS adds this class, removes after 2 s */
.bm-copy-btn--copied {
  color: var(--status-ok);
  border-color: rgba(22, 163, 74, 0.35);
  background: var(--status-ok-bg);
}

@media (hover: hover) {
  .bm-copy-btn:not(.bm-copy-btn--copied):hover {
    background: var(--border-default);
    color: var(--fg-default);
  }
}

/* ── 8. SENDER VIEW ──────────────────────────────────────────────── */
#view-sender { gap: var(--sp-6); }

.bm-sender-label {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-muted);
  text-align: center;
}

/* Hero pairing code */
#big-code {
  font-family: var(--font-mono);
  font-size: clamp(1.5rem, 6.4vw, 3.25rem);
  /* 28.3: TV/4K browsers inflate text (text-size-adjust) and their wide mono
     fallbacks overflowed the card with 9 digits; pin adjustment and let the
     JS fit pass (fitCodeCard) shrink the font to the card, never wrap/clip. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  min-width: 0;
  white-space: nowrap;
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-bg);
  border-radius: var(--r-lg);
  padding: var(--sp-6) var(--sp-8);
  user-select: all;
  cursor: text;
  border: 1px solid rgba(79, 108, 240, 0.22);
  width: 100%;
  line-height: 1.1;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.18em;
}

#big-code > span {
  display: inline-block;
  text-align: center;
}

/* 3+3+3 grouping: visual gap only, copied text stays unspaced */
#big-code > span:nth-child(4), #big-code > span:nth-child(7) {
  margin-left: 0.55em;
}

@media (prefers-color-scheme: dark) {
  #big-code { border-color: rgba(143, 163, 248, 0.22); }
}

.bm-code-hint {
  font-size: 0.8125rem;
  color: var(--fg-muted);
  text-align: center;
  margin: 0;
  max-width: 30rem;
}

.bm-code-actions {
  display: flex;
  justify-content: center;
}

/* 23.6 QR pairing: white quiet-zone box regardless of theme (QRs need a
   light background to stay scannable in dark mode). Empty <div> when the
   vendored encoder didn't load or generate anything, so it collapses. */
.bm-qr {
  background: #fff;
  padding: var(--sp-3);
  border-radius: var(--r-sm);
  width: fit-content;
  margin: 0 auto;
}

.bm-qr:empty { display: none; }

.bm-qr svg {
  width: 10rem;
  height: 10rem;
  display: block;
}

/* ── 9. STATUS CHIP ──────────────────────────────────────────────── */
.bm-status-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-full);
  background: var(--bg-subtle);
  border: 1px solid var(--border-default);
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--fg-muted);
  transition:
    background   var(--dur-base) var(--ease),
    border-color var(--dur-base) var(--ease),
    color        var(--dur-base) var(--ease);
}

.bm-status-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

@keyframes bm-pulse {
  0%, 100% { opacity: 0.9; }
  50%       { opacity: 0.25; }
}

.bm-status-dot--waiting {
  background: var(--status-warn);
  animation: bm-pulse 1.8s ease-in-out infinite;
}

.bm-status-chip--ok {
  background: var(--status-ok-bg);
  border-color: rgba(22, 163, 74, 0.28);
  color: var(--status-ok);
}
@media (prefers-color-scheme: dark) {
  .bm-status-chip--ok { border-color: rgba(74, 222, 128, 0.28); }
}
.bm-status-chip--ok .bm-status-dot {
  background: var(--status-ok);
  animation: none;
}

.bm-status-chip--warn {
  background: var(--status-warn-bg);
  border-color: rgba(194, 116, 10, 0.28);
  color: var(--status-warn);
}
@media (prefers-color-scheme: dark) {
  .bm-status-chip--warn { border-color: rgba(251, 182, 80, 0.28); }
}
.bm-status-chip--warn .bm-status-dot { background: var(--status-warn); }

.bm-status-chip--err {
  background: var(--status-err-bg);
  border-color: rgba(196, 42, 42, 0.28);
  color: var(--status-err);
}
@media (prefers-color-scheme: dark) {
  .bm-status-chip--err { border-color: rgba(248, 113, 113, 0.28); }
}
.bm-status-chip--err .bm-status-dot {
  background: var(--status-err);
  animation: none;
}

/* ── 10. RECEIVER VIEW ───────────────────────────────────────────── */
#view-receiver { gap: var(--sp-4); }

.bm-receiver-label {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-muted);
  display: block;
}

#code-input {
  width: 100%;
  min-height: var(--touch);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border-default);
  background: var(--bg-raised);
  color: var(--fg-default);
  font-family: var(--font-mono);
  font-size: clamp(1.375rem, 5vw, 1.875rem);
  font-weight: 700;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  text-align: center;
  appearance: none;
  -webkit-appearance: none;
  transition:
    border-color var(--dur-fast) var(--ease),
    box-shadow   var(--dur-fast) var(--ease);
}

#code-input::placeholder {
  color: var(--fg-subtle);
  font-weight: 400;
  letter-spacing: 0.2em;
}

#code-input:focus {
  border-color: var(--accent);
  box-shadow: var(--sh-focus);
  outline: none;
}


@media (prefers-color-scheme: dark) {
}

/* Reserved-height error line — layout never shifts */
#receiver-error {
  min-height: 1.5em;
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--status-err);
  padding: 0 var(--sp-1);
  line-height: 1.5;
}

/* Rate-limit banners — sit below #receiver-error, above #btn-join */
.bm-rate-banner {
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--t-small);
  line-height: 1.6;
  width: 100%;
}

.bm-rate-banner--5min {
  background: var(--status-warn-bg);
  border: 1px solid rgba(194, 116, 10, 0.30);
  color: var(--status-warn);
}

.bm-rate-banner--30min {
  background: var(--status-err-bg);
  border: 1px solid rgba(196, 42, 42, 0.30);
  color: var(--status-err);
}

.bm-rate-banner--24h {
  background: var(--status-err-bg);
  border: 1px solid rgba(196, 42, 42, 0.50);
  color: var(--status-err);
  font-weight: 600;
}

@media (prefers-color-scheme: dark) {
  .bm-rate-banner--5min  { border-color: rgba(251, 182, 80, 0.30); }
  .bm-rate-banner--30min { border-color: rgba(248, 113, 113, 0.30); }
  .bm-rate-banner--24h   { border-color: rgba(248, 113, 113, 0.50); }
}

/* ── 11. CHAT VIEW ───────────────────────────────────────────────── */
/* Chat uses dvh so layout reflows dynamically when keyboard opens */
#view-chat {
  display: flex;
  flex-direction: column;
  height: 100dvh;        /* reflows on keyboard open/close */
  overflow: hidden;
  position: relative;
}

.bm-drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 10;
  background: var(--accent-bg);
  border: 1px dashed var(--accent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  font-size: var(--t-body);
  color: var(--accent);
  pointer-events: all;
  border-radius: 0;
}

@media (prefers-color-scheme: dark) {
  .bm-drop-overlay { color: var(--a-200); }
}

/* Chat header */
#chat-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  /* Vertical padding trimmed from sp-4 to sp-2: .bm-vanish-btn now sits at the
     touch minimum and is the tallest child, so the row's content height grew.
     Smaller padding keeps the header's rendered height where it was before —
     the button gained its target without pushing the log down. Safe-area still
     wins on notched devices, where it already dwarfed either token. */
  padding:
    max(var(--sp-2), env(safe-area-inset-top))
    max(var(--sp-5), env(safe-area-inset-right))
    var(--sp-2)
    max(var(--sp-5), env(safe-area-inset-left));
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border-default);
  flex-shrink: 0;
  min-height: 3.5rem;
}

.bm-chat-name {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-default);
}

/* Wordmark · hairline · status, grouped left */
.bm-chat-ident {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-width: 0;
}

.bm-chat-sep {
  width: 1px;
  height: 1rem;
  background: var(--border-default);
  flex: none;
}

/* The header status is a dot, not a pill — the tone lives in the dot, the label
   stays muted so it never competes with the conversation. Tone classes are still
   swapped at runtime (app.js:896-899, :957-960), so flatten all of them. */
#chat-header .bm-status-chip {
  padding: 0;
  border: none;
  background: none;
  border-radius: 0;
  font-family: var(--font-sans);
  color: var(--fg-muted);
}

#chat-header .bm-status-dot {
  width: 0.375rem;
  height: 0.375rem;
}

.bm-vanish-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  /* the app's single most destructive control — must clear the touch minimum.
     height comes from min-height, not padding, so the box grows no taller than
     it has to and stays quiet, not heavier. */
  min-height: var(--touch);
  padding: 0 var(--sp-3);
  border-radius: var(--r-md);
  border: 1px solid var(--border-default);
  background: transparent;
  color: var(--fg-muted);
  font-family: var(--font-sans);
  font-size: var(--t-small);
  font-weight: 500;
  cursor: pointer;
  flex-shrink: 0;
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s, color 0.15s, opacity 0.15s;
}
/* Focus-visible stays outside the hover guard — keyboard focus must show on every device. */
.bm-vanish-btn:focus-visible {
  background: var(--bg-subtle);
  border-color: var(--status-err);
  color: var(--status-err);
}
/* Quiet at rest, unmistakable at the moment of action. The colour appears when
   the pointer is on it — not before — so nothing red sits on screen all session.
   Hover-only: a tap has no pointer to leave, so this must not fire on touch or
   the red state sticks after the tap. */
@media (hover: hover) {
  .bm-vanish-btn:hover {
    background: var(--bg-subtle);
    border-color: var(--status-err);
    color: var(--status-err);
  }
}
.bm-vanish-btn:active { opacity: 0.7; }

/* Armed: one more press ends the session. */
.bm-vanish-btn--armed {
  background: var(--status-err-bg);
  border-color: var(--status-err);
  color: var(--status-err);
}
/* Keeps the armed colour under the pointer instead of falling back to the
   plain :hover above; guarded for the same reason — no sticky state on touch. */
@media (hover: hover) {
  .bm-vanish-btn--armed:hover {
    background: var(--status-err-bg);
    border-color: var(--status-err);
    color: var(--status-err);
  }
}

@keyframes bm-kill {
  0%   { transform: translateX(0)    scale(1);    background: transparent; }
  15%  { transform: translateX(-5px) scale(1.07); background: #ef4444; color: #fff; }
  35%  { transform: translateX(5px)  scale(1.07); background: #ef4444; color: #fff; }
  55%  { transform: translateX(-3px) scale(1.04); background: #dc2626; color: #fff; }
  75%  { transform: translateX(3px)  scale(1.02); background: #dc2626; color: #fff; }
  100% { transform: translateX(0)    scale(1);    background: #dc2626; color: #fff; }
}
.bm-vanish-btn--killing {
  animation: bm-kill 0.35s ease-out forwards;
  pointer-events: none;
}

/* Session strip — the session's standing facts, so the transport stops
   competing with the conversation for space in the log. */
.bm-session-strip {
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  padding:
    var(--sp-3)
    max(var(--sp-5), env(safe-area-inset-right))
    var(--sp-3)
    max(var(--sp-5), env(safe-area-inset-left));
  border-bottom: 1px solid var(--bg-raised);
}

.bm-session-strip-inner {
  width: 100%;
  max-width: var(--max-chat);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--fg-muted);
}

.bm-session-code {
  color: var(--fg-default);
  letter-spacing: 0.1em;
}

/* Path label changed underneath the user (e.g. relay fallback) — a quiet
   brighten-and-settle so the strip acknowledges the transition without
   alarming anyone. Colour only, no motion. */
@keyframes bm-path-change {
  0%   { color: var(--fg-muted); }
  30%  { color: var(--fg-default); }
  100% { color: var(--fg-muted); }
}
.bm-session-path--changed {
  animation: bm-path-change 1.2s var(--ease);
}

/* Narrow screens carry the short form: code · path · encrypted */
@media (max-width: 40rem) {
  .bm-session-long { display: none; }
}

/* Log area — wraps the log so the empty state can sit over it without living
   inside #chat-log, which app.js clears with innerHTML on every session start. */
.bm-log-area {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.bm-chat-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6);
  pointer-events: none;
}

/* Any real log content hides it — no JS, so it comes back on the next session.
   System messages do NOT count: the transport line fires the instant a session
   pairs, so counting them meant the panel was hidden before it was ever seen. */
.bm-log-area:has(#chat-log > *:not(.bm-sys-msg)) .bm-chat-empty { display: none; }

.bm-chat-empty-panel {
  width: 100%;
  max-width: var(--max-chat);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-10) var(--sp-6);
  border: 1px dashed var(--border-default);
  border-radius: var(--r-lg);
  color: var(--fg-muted);
  text-align: center;
}

.bm-chat-empty-title {
  font-size: var(--t-body);
  color: var(--fg-muted);
}

.bm-chat-empty-sub {
  font-size: var(--t-small);
  color: var(--fg-muted);
  text-wrap: pretty;
}

/* Chat log */
#chat-log {
  flex: 1;
  min-height: 0;          /* critical: allows flex child to shrink below content size */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  padding:
    var(--sp-5)
    max(var(--sp-5), env(safe-area-inset-right))
    var(--sp-5)
    max(var(--sp-5), env(safe-area-inset-left));
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  container-type: inline-size;
  container-name: chat-log;
  scroll-behavior: smooth;
}

/* Messenger anchoring: the log fills from the bottom, so the first message sits
   just above the composer instead of at the top of an empty screen. An auto
   margin (not justify-content) is used deliberately — it resolves to 0 once the
   content overflows, so the top of a long log stays scrollable. */
#chat-log > *:first-child { margin-top: auto; }

#chat-log::-webkit-scrollbar { width: 0.25rem; }
#chat-log::-webkit-scrollbar-track { background: transparent; }
#chat-log::-webkit-scrollbar-thumb {
  background: var(--border-default);
  border-radius: var(--r-full);
}

/* ── 12. CHAT MESSAGES ───────────────────────────────────────────── */
.bm-msg {
  position: relative;     /* anchors the hover-revealed copy button */
  display: flex;
  flex-direction: column;
  max-width: 82%;
  word-break: break-word;
  overflow-wrap: anywhere;
  animation: bm-view-in var(--dur-base) var(--ease) both;
}

.bm-msg--out { align-self: flex-end; align-items: flex-end; }
.bm-msg--in  { align-self: flex-start; align-items: flex-start; }

/* A run of messages from the same side is one group: tight inside, the log's
   full gap between groups. Negative margin because #chat-log uses `gap`, which
   cannot be varied per pair. */
.bm-msg--stacked { margin-top: calc(var(--sp-1) - var(--sp-3)); }

/* 23.1 — collapse long messages behind a fade + toggle */
.bm-msg-bubble--collapsed {
  max-height: 9em;
  overflow: hidden;
  mask-image: linear-gradient(to bottom, #000 80%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 80%, transparent 100%);
}

/* 23.2 — actions row holds the collapse toggle and copy button side by side */
.bm-msg-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-1);
  padding-inline: 2px;
}

/* Time stamp. On a collapsed long message it is a real element on the actions
   row, where the fade cannot swallow it. Everywhere else it is generated content
   floated to the trailing edge of the bubble's last line — messenger placement,
   with none of the message's text content touched. */
.bm-msg-time {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--fg-muted);
}

.bm-msg-bubble[data-time]::after {
  content: attr(data-time);
  float: right;
  margin: 0.55em 0 0 var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--fg-muted);
  white-space: nowrap;
}

.bm-msg-toggle {
  padding: 0;
  border: none;
  background: none;
  font-size: var(--t-small);
  color: var(--fg-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease);
}

.bm-msg-toggle:hover { color: var(--fg-default); }

/* 23.9 — icon copy button: quiet by default, revealed on message hover (desktop),
   kept faintly visible + tappable on touch devices where there is no hover. */
.bm-msg-copy {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A definite box, not shrink-to-fit. Absolutely positioned with only an
     inline offset, this box sizes to its content — and the reset's
     `svg { max-width: 100% }` then sizes the icon against a width that depends
     on the icon. The browser resolves that circle by collapsing it: the button
     rendered 8px wide instead of 23, so the icon was a sliver. */
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: none;
  background: none;
  color: var(--fg-muted);
  cursor: pointer;
  border-radius: var(--r-sm);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

/* Outside the bubble, on the side away from the tail, so it never overlaps text. */
.bm-msg--out .bm-msg-copy { right: 100%; }
.bm-msg--in  .bm-msg-copy { left: 100%; }

.bm-msg-copy svg { display: block; }

.bm-msg-copy:focus-visible { opacity: 1; }

/* Reveal-on-hover, guarded: without hover:hover a tap leaves the message's
   :hover matched and the copy icon stuck visible until something else is tapped. */
@media (hover: hover) {
  .bm-msg:hover .bm-msg-copy { opacity: 1; }
  .bm-msg-copy:hover { color: var(--fg-default); }
}

.bm-msg-copy--copied { color: var(--status-ok); opacity: 1; }

/* Touch devices have no hover — keep the icon discoverable and tappable. */
@media (hover: none) {
  .bm-msg-copy { opacity: 0.5; }
}

.bm-msg-bubble {
  display: flow-root;     /* contains the floated time stamp in the bubble's height */
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-lg);
  font-size: var(--t-body);
  line-height: 1.55;
  white-space: pre-wrap;
}

.bm-msg--out .bm-msg-bubble {
  background: var(--accent-bg);
  color: var(--fg-default);
  /* The accent surface's own edge, kept at 1px like the incoming bubble's
     border so an outgoing one doesn't render 2px smaller for identical text. */
  border: 1px solid var(--accent-border);
  border-bottom-right-radius: var(--r-sm);
}

.bm-msg--in .bm-msg-bubble {
  background: var(--bg-raised);
  color: var(--fg-default);
  border: 1px solid var(--border-default);
  border-bottom-left-radius: var(--r-sm);
  /* Lit top edge — the only way a raised surface reads as raised on the
     true-black canvas, since a drop shadow would be invisible here. */
  box-shadow: var(--sh-edge);
}

@container chat-log (min-width: 36rem) {
  .bm-msg { max-width: 60%; }
}

/* System messages (peer disconnected, decrypt failure, etc.) */
.bm-sys-msg {
  align-self: center;
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--fg-muted);
  font-style: italic;
  padding: var(--sp-1) 0;
  word-break: break-all;
  max-width: 100%;
  text-align: center;
  animation: bm-view-in var(--dur-base) var(--ease) both;
}

/* ── 13. FILE ROW ────────────────────────────────────────────────── */
/* File card — mirrors the message bubbles: outgoing sits right on the accent
   surface, incoming sits left on the raised surface. */
.bm-file-row {
  width: 88%;
  max-width: 100%;
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  animation: bm-view-in var(--dur-base) var(--ease) both;
}

/* B3: an in-flight transfer card stays visible while the log scrolls under
   heavy texting — sticky within #chat-log, released at terminal state. */
.bm-file-row--active {
  position: sticky;
  top: 0.25rem;
  bottom: 0.25rem;
  z-index: 2;
}

.bm-file-row--out {
  align-self: flex-end;
  background: var(--accent-bg);
  border: 1px solid var(--accent-border);
  /* Lifts the card off the canvas in light; no-op in dark (see --sh-raised). */
  box-shadow: var(--sh-raised);
}

.bm-file-row--in {
  align-self: flex-start;
  background: var(--bg-raised);
  border: 1px solid var(--border-default);
  /* --sh-edge and --sh-raised are each `none` in the theme where the other
     applies, so they can't be composed with a comma (a `none` entry in a
     comma-separated shadow list is invalid and drops the whole property).
     This sets the light value directly; the dark override below replaces it
     wholesale with the lit top edge. */
  box-shadow: var(--sh-raised);
}

@media (prefers-color-scheme: dark) {
  .bm-file-row--in {
    /* Lit top edge — the only way a raised surface reads as raised on the
       true-black canvas, since a drop shadow would be invisible here. */
    box-shadow: var(--sh-edge);
  }
}

@container chat-log (min-width: 36rem) {
  .bm-file-row { width: 24rem; }
}

.bm-file-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.bm-file-icon {
  flex: none;
  display: grid;
  place-items: center;
  color: var(--fg-muted);
}

.bm-file-row--out .bm-file-icon { color: var(--a-200); }

.bm-file-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.bm-file-name {
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--fg-default);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bm-file-action {
  flex: none;
  display: flex;
  align-items: center;
}

.bm-file-btn {
  display: grid;
  place-items: center;
  width: 1.875rem;
  height: 1.875rem;
  background: none;
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  color: var(--fg-muted);
  cursor: pointer;
  flex: none;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.bm-file-btn svg { display: block; }

/* Guarded — a tap has no pointer to leave, so an unguarded hover here would
   stick the "hovered" look on a file card after a touch cancel/download tap. */
@media (hover: hover) {
  .bm-file-btn:hover {
    background: var(--bg-subtle);
    color: var(--fg-default);
  }
}

.bm-file-meta {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--fg-muted);
  /* One line, always. A wrapping meta pushed the progress bar around mid-transfer;
     the tail of this string is the speed, which is the part worth losing first. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The exception, set by app.js's setFileMeta: a one-off status message, not the
   per-second readout. It wraps because ellipsising it cuts the sentence that
   tells someone their transfer is coming back. Safe to wrap precisely because it
   is written once at a state change — the bar cannot jitter under a string that
   never changes width. */
.bm-file-meta--status {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Every byte left this machine but the peer hasn't confirmed assembly yet —
   the bar can't say "done" (it isn't, and might never be), so it goes inert
   grey instead of active purple: paused-on-confirmation, not still moving.
   .bm-file-row--done below still wins once delivery lands — app.js drops
   this class at that point, so a delivered card never keeps the grey fill. */
.bm-file-row--pending .bm-progress-fill { background: var(--fg-subtle); }

/* Terminal state. The bar STAYS and goes green — it is the completion signal
   people actually look for, and hiding it left a card whose only horizontal
   line was the hash divider, which read as a dead grey progress bar. The icon
   turns green alongside it; two signals agreeing is not redundancy here. */
.bm-file-row--done .bm-file-icon,
.bm-file-row--done.bm-file-row--out .bm-file-icon { color: var(--status-ok); }

/* SHA-256, on the card it belongs to. Truncated at rest; click to expand. */
.bm-file-verify {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding-top: var(--sp-1);
  /* Subtle, not default — this divider sits inside a card that already has
     its own outer edge; matching that edge's weight cut the card in half. */
  border-top: 1px solid var(--border-subtle);
}

/* The accent card's own edge, not a --border-default echo. */
.bm-file-row--out .bm-file-verify { border-top-color: var(--accent-border); }

.bm-file-hash {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--fg-muted);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--dur-fast) var(--ease);
}

/* Guarded — same sticky-hover-after-tap issue as the other file-card controls. */
@media (hover: hover) {
  .bm-file-hash:hover { color: var(--fg-default); }
}

.bm-file-verify--full .bm-file-hash {
  white-space: normal;
  word-break: break-all;
}

.bm-file-hash-copy {
  flex: none;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--fg-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease);
}

.bm-file-hash-copy svg { display: block; width: 13px; height: 13px; }
@media (hover: hover) {
  .bm-file-hash-copy:hover { color: var(--fg-default); }
}
.bm-file-hash-copy--copied { color: var(--status-ok); }

/* Verdict badge — takes the copy button's slot once both ends' digests are in
   and the app has compared them itself, so there's nothing left to copy-and-eyeball.
   Not a button: it reports a fact, it doesn't do anything on click. Allowed to
   shrink/wrap (unlike the hash button, which stays single-line and ellipsizes)
   since the mismatch wording is the longest thing on the card. */
.bm-file-verify-badge {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-1);
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--t-label);
  line-height: 1.3;
}
.bm-file-verify-badge svg { flex: none; display: block; width: 13px; height: 13px; margin-top: 1px; }
.bm-file-verify-badge span { white-space: normal; word-break: break-word; }
.bm-file-verify-badge--ok { color: var(--status-ok); }
.bm-file-verify-badge--err { color: var(--status-err); }

.bm-progress {
  height: 2px;
  background: var(--border-default);
  border-radius: var(--r-full);
  overflow: hidden;
}

.bm-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: var(--r-full);
  transition: width var(--dur-base) var(--ease);
}

.bm-progress-fill--complete { background: var(--status-ok); }

/* Same weight as the pending-grey rule above and declared after it, so a done
   card is green even if the pending class were somehow left on the row. Green
   must never depend on JS having cleaned up first. */
.bm-file-row--done .bm-progress-fill { background: var(--status-ok); }

/* 23.3 — inline thumbnail, display-only */
.bm-file-thumb {
  display: block;
  /* .bm-file-row is a column flex container, whose default align-items:stretch
     forces img children to the full row width and overrides width:auto — the
     aspect ratio collapses. flex-start keeps the intrinsic ratio. */
  align-self: flex-start;
  max-width: 100%;
  max-height: 12rem;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--r-sm);
  margin-top: var(--sp-2);
}

/* Staged image awaiting confirmation — sits directly above the input bar. */
.bm-img-stage {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) max(var(--sp-4), env(safe-area-inset-left));
  background: var(--bg-raised);
  border-top: 1px solid var(--border-default);
  flex-shrink: 0;
}

.bm-img-stage-thumb {
  display: block;
  width: auto;
  height: auto;
  max-width: 6rem;
  max-height: 4rem;
  object-fit: contain;
  border-radius: var(--r-sm);
  flex-shrink: 0;
}

.bm-img-stage-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.bm-img-stage-name {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bm-img-stage-size {
  font-size: var(--t-small);
  color: var(--fg-muted);
}

/* File status aria-live */
#file-status {
  min-height: 1.5em;
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--fg-muted);
  padding:
    0
    max(var(--sp-5), env(safe-area-inset-right))
    0
    max(var(--sp-5), env(safe-area-inset-left));
  flex-shrink: 0;
}

/* transfer progress now lives on the file cards, so this only ever holds an
   occasional error string — collapse the box when empty instead of always
   reserving a line above the composer; stays in the a11y tree so aria-live
   still announces it once .textContent fills it back in */
#file-status:empty {
  min-height: 0;
}

/* ── 14. INPUT BAR ───────────────────────────────────────────────── */
/* sticky to viewport bottom; safe-area covers home bar */
.bm-input-bar {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding:
    var(--sp-3)
    max(var(--sp-4), env(safe-area-inset-right))
    max(var(--sp-4), env(safe-area-inset-bottom))
    max(var(--sp-4), env(safe-area-inset-left));
  flex-shrink: 0;
  /* Hairline dock — without it the composer floats against the canvas with
     nothing separating it from the scrolling log above. */
  border-top: 1px solid var(--border-default);
}

/* Attach, input and send read as one surface; the focus ring belongs to the
   wrapper, not the textarea inside it. */
.bm-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-2);
  border-radius: var(--r-lg);
  border: 1px solid var(--border-default);
  background: var(--bg-raised);
  /* Lifts the composer off the canvas in light; no-op in dark (see
     --sh-raised). :focus-within below sets its own complete box-shadow, so
     it isn't clobbered by this. */
  box-shadow: var(--sh-raised);
  transition:
    border-color var(--dur-fast) var(--ease),
    box-shadow   var(--dur-fast) var(--ease);
}

.bm-composer:focus-within {
  border-color: var(--accent);
  box-shadow: var(--sh-focus);
}

.bm-composer-hint {
  font-size: var(--t-label);
  color: var(--fg-muted);
  padding-left: 2px;
}

/* The design omits the hint on phones — the shortcut isn't reachable there.
   The composer's dock hairline is no longer set here: it used --bg-raised,
   which left phones with a fainter line than every other width for no reason.
   The base .bm-input-bar rule now carries --border-default at every size. */
@media (max-width: 40rem) {
  .bm-composer-hint { display: none; }
}

/* Pointer devices don't need the full touch-target height on the text field;
   the icon buttons keep their own min-width: var(--touch) regardless. */
@media (min-width: 48rem) {
  #chat-input { min-height: 2.25rem; }
}

#chat-input {
  flex: 1;
  min-width: 0;
  min-height: var(--touch);
  max-height: calc(1rem * 1.5 * 5);
  padding: var(--sp-2) 0;
  border: none;
  background: none;
  color: var(--fg-default);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: 1.5;
  resize: none;
  appearance: none;
  -webkit-appearance: none;
  field-sizing: content; /* progressive: auto-height in Chrome 123+ */
}

#chat-input::placeholder { color: var(--fg-muted); }

#chat-input:focus { outline: none; }

/* Send stays quiet until there is something to send. */
.bm-composer .bm-icon-btn--primary {
  background: var(--bg-subtle);
  border: 1px solid var(--border-default);
  color: var(--fg-muted);
}

/* "Something to send" is typed text OR a staged image — a pasted image is just
   as sendable as a typed line, and leaving send grey for it said otherwise.
   #img-stage is a sibling of the composer, so that half anchors on the view. */
.bm-composer:has(#chat-input:not(:placeholder-shown)) .bm-icon-btn--primary,
#view-chat:has(#img-stage:not([hidden])) .bm-composer .bm-icon-btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}

/* These flat overrides sit after the base rules, so the press and hover states
   have to be restated at this specificity or they never paint. */
.bm-composer .bm-icon-btn--primary:active {
  background: var(--bg-subtle);
}

.bm-composer:has(#chat-input:not(:placeholder-shown)) .bm-icon-btn--primary:active,
#view-chat:has(#img-stage:not([hidden])) .bm-composer .bm-icon-btn--primary:active {
  background: var(--accent-press);
  border-color: var(--accent-press);
}

@media (hover: hover) {
  .bm-composer .bm-icon-btn--primary:hover {
    background: var(--bg-subtle);
    color: var(--fg-default);
  }
  .bm-composer:has(#chat-input:not(:placeholder-shown)) .bm-icon-btn--primary:hover,
  #view-chat:has(#img-stage:not([hidden])) .bm-composer .bm-icon-btn--primary:hover {
    background: var(--accent-press);
    border-color: var(--accent-press);
    color: var(--accent-fg);
  }
}

/* ── 15. WIDER LAYOUTS ───────────────────────────────────────────── */
@media (min-width: 40rem) {
  .bm-view {
    padding:
      max(var(--sp-16), env(safe-area-inset-top))
      max(var(--sp-8), env(safe-area-inset-right))
      max(var(--sp-16), env(safe-area-inset-bottom))
      max(var(--sp-8), env(safe-area-inset-left));
  }
  .bm-view > * { max-width: 24rem; }
}

/* Chat column constrained to readable width on desktop.
   This was a @container chat-log query, which never matched anything: a container
   query styles the container's DESCENDANTS, but #chat-log is the container itself
   and .bm-input-bar is its sibling. A plain media query does what was intended. */
@media (min-width: 48rem) {
  #chat-log,
  .bm-input-bar {
    padding-inline: max(var(--sp-8), calc((100% - var(--max-chat)) / 2));
  }
}

/* ── 16. FATAL VIEW ──────────────────────────────────────────────── */
/* Full-viewport replacement — not an overlay */
.bm-fatal-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  padding:
    max(var(--sp-8), env(safe-area-inset-top))
    max(var(--sp-6), env(safe-area-inset-right))
    max(var(--sp-8), env(safe-area-inset-bottom))
    max(var(--sp-6), env(safe-area-inset-left));
  text-align: center;
  gap: var(--sp-5);
  animation: bm-view-in var(--dur-base) var(--ease) both;
}

.bm-fatal-title {
  font-size: var(--t-h2);
  font-weight: 700;
  color: var(--fg-default);
  text-wrap: balance;
}

.bm-fatal-body {
  font-size: var(--t-body);
  color: var(--fg-muted);
  max-width: 30ch;
  text-wrap: pretty;
}


.bm-fatal-retry {
  font-family: var(--font-mono);
  font-size: var(--t-small);
  color: var(--fg-default);
  background: var(--bg-subtle);
  padding: var(--sp-3) var(--sp-5);
  border-radius: var(--r-md);
  border: 1px solid var(--border-default);
  cursor: pointer;
  margin-top: var(--sp-2);
}
.bm-fatal-retry:hover { background: var(--bg-raised); }


.bm-divider {
  height: 1px;
  background: var(--border-default);
  width: 100%;
  flex-shrink: 0;
  border: none;
}

