/* мост — base styles
 * Mobile-first. System fonts only (per README §9).
 * No framework. CSS class prefix is .bridge- (per naming convention).
 */

:root {
  --bridge-bg: #0f172a;
  --bridge-bg-elev: #1e293b;
  --bridge-fg: #e2e8f0;
  --bridge-muted: #94a3b8;
  --bridge-accent: #38bdf8;
  --bridge-accent-fg: #0c1828;
  --bridge-danger: #f87171;
  --bridge-warn: #fbbf24;
  --bridge-border: #334155;

  --bridge-radius: 0.5rem;
  --bridge-pad: 0.75rem;
  --bridge-tap: 2.75rem; /* min touch target */
}

* { box-sizing: border-box; }

/* The HTML5 `hidden` attribute must always win over any class-based
 * `display:` rule. Without this, `.bridge-panel { display: flex }` and
 * similar overrides keep elements visible even when JS sets `el.hidden = true`. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bridge-bg);
  color: var(--bridge-fg);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-text-size-adjust: 100%;
}

html { height: 100%; }

/* Body is a flex column: header at the top, main grows to fill, footer
 * sits at the bottom. This replaces the previous `min-height: calc(100dvh
 * - 4rem)` on .bridge-landing, which undercounted the actual header
 * height and didn't account for the footer at all — the result was a
 * page taller than the viewport with the footer pushed below the fold. */
body {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

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

/* --- landing ----------------------------------------------------------- */

.bridge-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--bridge-pad) 1rem;
  border-bottom: 1px solid var(--bridge-border);
}

.bridge-brand {
  font-size: 2.25rem;
  margin: 0;
  letter-spacing: -0.02em;
  /* Collapse the inherited 1.5 line-height so the h1's line-box equals
   * its font-size. Without this the glyphs sit slightly below center
   * within the line-box and the wordmark looks lower than the icon
   * beside it (which has no such padding). */
  line-height: 1;
  /* Optical centering: even with line-height: 1, the font's em-box
   * reserves space for descenders that the lowercase glyphs in "most"
   * / "мост" / "міст" don't use, so the visible letters sit
   * below their line-box center. A small upward translate compensates,
   * making the wordmark optically centered against the icon. Use
   * `transform` rather than `margin-top: -0.08em` so the shift is
   * purely visual and doesn't perturb the parent flex row's height. */
  transform: translateY(-0.08em);
}

/* Wordmark + bridge mark grouped on the left of the landing header.
 * The parent .bridge-header is `justify-content: space-between` with
 * three children: this row (left), tagline (center-ish), language
 * picker (right). Wrapping h1 + img in a flex group keeps them glued.
 *
 * Note: the h1 carries data-i18n="app_name" and i18n.js's applyDom()
 * uses .textContent on language switch — so the icon must be a
 * SIBLING of the h1, never a child, or it'd be wiped on every load. */
.bridge-brand-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* Bridge mark beside the wordmark — landing-page header only.
 * Selector is intentionally scoped to .bridge-header so this class
 * can't accidentally pick up styling elsewhere. Prose surfaces use
 * .bridge-inline-icon (per README §intro), the chat header uses
 * .bridge-brand-small + its own treatment. */
.bridge-header .bridge-logo {
  height: 2rem;
  width: auto;
  flex: none;
  user-select: none;
  -webkit-user-drag: none;
}

.bridge-brand-small {
  font-size: 1.25rem;
  font-weight: 600;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.bridge-brand-small:hover { opacity: 0.85; }
.bridge-brand-small:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.bridge-lang {
  background: var(--bridge-bg-elev);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0.4rem 0.6rem;
  min-height: var(--bridge-tap);
}

.bridge-landing {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  padding: 1.5rem;
  gap: 1rem;
  width: 100%;
  max-width: 28rem;
  margin: 0 auto; /* horizontal centering inside body's flex column */
}

/* Landing tagline — small muted line between the header and the action
 * panels. Five locale bundles each ship their own framing under the
 * `tagline` key (README §intro). Set in 0.875rem so it stays subordinate
 * to the 2.25rem h1 above and to the panels below; wrapping to 2–3 lines
 * on a narrow viewport is intentional and fine. */
.bridge-tagline {
  margin: 0;
  padding: 0 0.5rem;
  text-align: center;
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--bridge-muted);
}

.bridge-actions {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.bridge-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  background: var(--bridge-bg-elev);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 1rem;
}

.bridge-panel h2 {
  margin: 0;
  font-size: 1.25rem;
}

.bridge-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.bridge-field span {
  color: var(--bridge-muted);
  font-size: 0.9rem;
}

.bridge-field input {
  background: var(--bridge-bg);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0.6rem 0.75rem;
  min-height: var(--bridge-tap);
}

.bridge-field input:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

.bridge-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.bridge-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  min-height: var(--bridge-tap);
  padding: 0 1rem;
  background: var(--bridge-bg-elev);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  cursor: pointer;
}

.bridge-btn:hover { background: #273549; }
.bridge-btn:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

.bridge-btn-primary {
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  border-color: transparent;
  font-weight: 600;
}
.bridge-btn-primary:hover { filter: brightness(1.05); background: var(--bridge-accent); }

.bridge-btn-ghost {
  background: transparent;
  border-color: var(--bridge-border);
  color: var(--bridge-muted);
}

.bridge-error {
  color: var(--bridge-danger);
  margin: 0;
  font-size: 0.9rem;
}

/* --- chat -------------------------------------------------------------- */

.bridge-chat-body {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  overflow: hidden;
}

.bridge-chat-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: var(--bridge-pad) 1rem;
  border-bottom: 1px solid var(--bridge-border);
  background: var(--bridge-bg);
}

.bridge-pin {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.05rem;
  margin-left: auto;
  color: var(--bridge-muted);
  font-size: 0.95rem;
  line-height: 1.15;
}
.bridge-pin-row {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
}
.bridge-pin strong {
  color: var(--bridge-fg);
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
}

/* Share-room button in the chat header. Lives next to the PIN block.
 * Visually quieter than the action-bar buttons (transparent by default,
 * fills in on hover/focus) so the header stays a slim status bar rather
 * than a busy toolbar. The .bridge-icon-btn class gives us tap target
 * sizing; we override the default boxed look. */
.bridge-share-btn {
  background: transparent;
  border-color: transparent;
  color: var(--bridge-muted);
}
.bridge-share-btn:hover {
  color: var(--bridge-fg);
  background: var(--bridge-bg-elev);
  border-color: var(--bridge-border);
}
.bridge-share-btn:focus-visible {
  color: var(--bridge-fg);
}

.bridge-roster {
  padding: 0.5rem 1rem;
  border-bottom: 1px solid var(--bridge-border);
  background: var(--bridge-bg);
}

.bridge-roster-count {
  color: var(--bridge-muted);
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

.bridge-roster-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.bridge-roster-item {
  background: var(--bridge-bg-elev);
  border: 1px solid var(--bridge-border);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  font-size: 0.9rem;
  transition: box-shadow 200ms ease, color 200ms ease, border-color 200ms ease;
}

/* Per-user soft color (chat polish). chat.js sets `--bridge-user-color`
 * inline on roster items and chat-line wrappers when there are 3+
 * people in the room. Saturation/lightness are baked into the HSL
 * value (50% / 78%) so colors stay readable on the dark background.
 * Falls back to the muted default when the variable is unset. */
.bridge-roster-item {
  color: var(--bridge-user-color, inherit);
}
.bridge-line-chat .bridge-line-meta {
  color: var(--bridge-user-color, var(--bridge-muted));
}

.bridge-roster-item.bridge-speaking {
  box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.4);
  color: var(--bridge-accent);
  border-color: var(--bridge-accent);
}

/* `.bridge-offline` was the strikethrough class for users who'd briefly
 * disconnected. README §7 *Tile grid* now says the roster (and tile grid)
 * shows only currently-present users — chat.js drops offline users from
 * renderRoster() entirely instead of restyling them. The class is no
 * longer applied; rule kept removed below for tidiness. */

.bridge-chat-panel {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  background: var(--bridge-bg);
}

.bridge-log {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0.75rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  background: var(--bridge-bg);
}

.bridge-line {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  word-wrap: break-word;
  overflow-wrap: anywhere;
}

/* Row wrapper: bubble + reply button side by side.
 * Others: [bubble][reply-btn] — button on right.
 * Self:   flex-direction: row-reverse → [reply-btn][bubble] — button on left. */
.bridge-line-row {
  display: flex;
  align-items: flex-end;
  gap: 0.3rem;
}
.bridge-line-row-self {
  align-self: flex-end;
  flex-direction: row-reverse;
}

.bridge-line-chat {
  max-width: min(80%, 28rem);
  background: var(--bridge-bg-elev);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0.5rem 0.75rem;
}

.bridge-line-self {
  background: #1e3a52;
  border-color: #2a4a6a;
}

.bridge-line-meta {
  font-size: 0.75rem;
  color: var(--bridge-muted);
}

.bridge-line-system {
  align-self: center;
  font-size: 0.85rem;
  color: var(--bridge-muted);
  font-style: italic;
}

/* §9 in-room donation hint — rendered as a system line, but with a
 * panel chrome so it doesn't disappear into the join/leave noise.
 * Slightly accented border + non-italic text + accent-coloured link.
 * Stays centered like other system lines.
 *
 * `display: block` overrides the flex-column from `.bridge-line` so the
 * three children (prose, <a>, trailing prose) flow inline as one
 * sentence instead of stacking as three flex rows. */
.bridge-line-donation {
  display: block;
  font-style: normal;
  background: rgba(56, 189, 248, 0.08);
  border: 1px solid rgba(56, 189, 248, 0.25);
  border-radius: var(--bridge-radius);
  padding: 0.5rem 0.75rem;
  color: var(--bridge-fg);
  text-align: center;
}

.bridge-donation-link {
  color: var(--bridge-accent);
  text-decoration: underline;
  font-weight: 500;
}
.bridge-donation-link:hover { filter: brightness(1.1); }
.bridge-donation-link:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Inline brand glyph used in chat sys-lines (donation hint) and any
 * other prose where the bridge mark wants to sit next to text. We
 * render `/icons/icon.svg` rather than the 🌉 codepoint because emoji
 * glyphs render inconsistently across platforms (Apple, Microsoft,
 * Twemoji, Noto) — inline SVG gives every user the same Fluent UI
 * "Bridge at Night" we shipped. Sized to text cap-height; the negative
 * vertical-align nudges the glyph onto the baseline rather than
 * sitting on top of it. alt="" keeps it decorative for screen readers
 * (the surrounding text already names the brand). */
.bridge-inline-icon {
  height: 1em;
  width: auto;
  vertical-align: -0.15em;
}

.bridge-composer {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-top: 1px solid var(--bridge-border);
  background: var(--bridge-bg);
}

.bridge-composer input {
  flex: 1;
  min-width: 0;
  min-height: var(--bridge-tap);
  background: var(--bridge-bg-elev);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0 0.75rem;
}
.bridge-composer input:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

.bridge-actionbar {
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem 1rem max(0.5rem, env(safe-area-inset-bottom)) 1rem;
  border-top: 1px solid var(--bridge-border);
  background: var(--bridge-bg);
  position: relative;
  z-index: 20;
}

.bridge-audio-sink {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  pointer-events: none;
}

.bridge-video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  grid-auto-rows: 120px;
  gap: 0.4rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--bridge-border);
  background: var(--bridge-bg);
  max-height: 40dvh;
  overflow-y: auto;
}

.bridge-video-grid:empty {
  display: none;
}

.bridge-chat-body[data-mode="video"] .bridge-video-grid {
  flex: 1 1 auto;
  max-height: none;
  grid-auto-rows: auto;
  min-height: 0;
  border-bottom: none;
  padding: 0.4rem;
  /* Override the base auto-fit template so count-aware templates
   * below have something stable to layer on top of. The actual
   * column/row layout is picked per-count via
   * .bridge-video-grid[data-tile-count="N"] selectors. */
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
}

/* --- Tile grid layout (README §7 *Tile grid*) ----------------------
 *
 * The (cols, rows) split is JS-driven (chat.js#updateGridLayout) based
 * on the GRID CONTAINER's actual aspect ratio, not the viewport's.
 * The previous viewport-orientation media query failed in the case of
 * a wider-than-tall window that still wasn't wide ENOUGH for 3 tiles
 * across to produce reasonable tile shapes — 3 columns × 1 row gave
 * each tile ~33% width × 95% height, very portrait. Now we evaluate
 * the per-tile aspect ratio for each candidate split and pick the one
 * closest to the source aspect (4:3, matching our capture pin).
 *
 * The CSS just reads the computed values; layouts for cols=1..6 and
 * rows=1..6 (max room size is 6, so neither axis ever exceeds 6).
 *
 * Single-orphan centering (e.g. 3 tiles in 2×2 leaves the 3rd alone
 * in the last row) is handled by [data-orphan-centered="1"]: the
 * last tile is pulled out of auto-flow, spans the full row, and sized
 * to one column's width. Only set when count % cols === 1 and rows > 1
 * — with two orphans the centering trick collapses, so we accept
 * grid auto-flow (left-aligned tiles in the last row) in that case. */

.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-cols="1"] { grid-template-columns: 1fr; }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-cols="2"] { grid-template-columns: 1fr 1fr; }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-cols="3"] { grid-template-columns: 1fr 1fr 1fr; }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-cols="4"] { grid-template-columns: repeat(4, 1fr); }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-cols="5"] { grid-template-columns: repeat(5, 1fr); }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-cols="6"] { grid-template-columns: repeat(6, 1fr); }

.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-rows="1"] { grid-template-rows: 1fr; }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-rows="2"] { grid-template-rows: 1fr 1fr; }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-rows="3"] { grid-template-rows: 1fr 1fr 1fr; }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-rows="4"] { grid-template-rows: repeat(4, 1fr); }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-rows="5"] { grid-template-rows: repeat(5, 1fr); }
.bridge-chat-body[data-mode="video"] .bridge-video-grid[data-grid-rows="6"] { grid-template-rows: repeat(6, 1fr); }

.bridge-video-grid[data-orphan-centered="1"] > .bridge-video-tile:last-child {
  grid-column: 1 / -1;
  justify-self: center;
  /* --bridge-grid-cols is set inline by chat.js so we can compute the
   * orphan's width to match one regular cell. Fallback 2 keeps the rule
   * sensible if the variable is somehow unset. */
  width: calc(100% / var(--bridge-grid-cols, 2));
}

@media (prefers-reduced-motion: reduce) {
  .bridge-video-grid,
  .bridge-video-tile,
  .bridge-video-tile::after {
    transition: none;
  }
}
.bridge-chat-body[data-mode="video"] .bridge-roster {
  display: none;
}

.bridge-chat-body[data-mode="video"] .bridge-knocks {
  position: fixed;
  top: calc(env(safe-area-inset-top) + 0.75rem);
  left: 0.75rem;
  right: 0.75rem;
  z-index: 26;
  max-width: calc(100vw - 1.5rem);
  pointer-events: auto;
}
@media (min-width: 720px) {
  .bridge-chat-body[data-mode="video"] .bridge-knocks {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: calc(48rem - 1.5rem);
    max-width: calc(100vw - 1.5rem);
  }
}

.bridge-chat-body[data-mode="video"] .bridge-chat-panel {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--bridge-tap) + 1rem + max(0.5rem, env(safe-area-inset-bottom)));
  height: min(55dvh, 26rem);
  flex: 0 0 auto;
  z-index: 25;
  border-top: 1px solid var(--bridge-border);
  background: var(--bridge-bg);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.5);
  transform: translateY(110%);
  visibility: hidden;
  transition: transform 180ms ease-out, visibility 0s linear 180ms;
}
.bridge-chat-body[data-mode="video"][data-chat-open="true"] .bridge-chat-panel {
  transform: translateY(0);
  visibility: visible;
  transition: transform 180ms ease-out, visibility 0s linear 0s;
}

.bridge-chat-backdrop {
  position: fixed;
  inset: 0;
  z-index: 24;
  background: rgba(0, 0, 0, 0.5);
}
.bridge-chat-body:not([data-mode="video"]) .bridge-chat-backdrop,
.bridge-chat-body:not([data-chat-open="true"]) .bridge-chat-backdrop {
  display: none;
}

@media (min-width: 720px) {
  .bridge-chat-body[data-mode="video"] .bridge-chat-panel {
    left: 50%;
    right: auto;
    width: 100%;
    max-width: 48rem;
    transform: translate(-50%, 100%);
  }
  .bridge-chat-body[data-mode="video"][data-chat-open="true"] .bridge-chat-panel {
    transform: translate(-50%, 0);
  }
}

.bridge-chat-body[data-mode="video"] .bridge-chat-toggle {
  display: inline-flex;
}
.bridge-chat-body:not([data-mode="video"]) .bridge-chat-toggle {
  display: none;
}
.bridge-chat-toggle {
  position: relative;
}
.bridge-chat-toggle-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.3rem;
  border-radius: 0.55rem;
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1.1rem;
  text-align: center;
  pointer-events: none;
  box-shadow: 0 0 0 2px var(--bridge-bg);
}

.bridge-video-tile {
  position: relative;
  width: 100%;
  height: 100%;
  background: #000;
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  overflow: hidden;
  transition: box-shadow 200ms ease;
  /* Each tile is a grid-cell child; min-width/height: 0 stops the
   * default content-size minimum from blowing the grid out when the
   * <video>'s intrinsic size exceeds the cell. */
  min-width: 0;
  min-height: 0;
}

/* Speaking-indicator border overlay.
 *
 * `box-shadow: inset` on the tile itself paints during the tile's
 * background/decoration step, BEFORE child elements — so the <video>
 * (or the avatar div on audio-only tiles), which fills 100% of the
 * cell, renders on top and hides the inset border. Same goes for the
 * ±blue speaking glow border. Solution: move the inset border to an
 * ::after pseudo that overlays the tile's content. The pseudo paints
 * last in the tile's stacking context, so the colored edge always
 * shows above the video stream and the avatar.
 *
 * `pointer-events: none` keeps clicks passing through to the
 * underlying tile (flip-camera tap-to-flip still works).
 * `border-radius: inherit` keeps the inset border tracing the
 * rounded corners of the tile rather than rendering a square.
 * The outer glow stays on the tile element itself (rule below) so
 * it can bleed outside the border-radius into the grid gap. */
.bridge-video-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  box-shadow: inset 0 0 0 0 transparent;
  transition: box-shadow 200ms ease;
  /* Above in-flow children (the <video>, the avatar div) which have
   * default z-index. Below the flip-camera button (z-index: 2) and
   * the label (which paints later in DOM order anyway) — a 2px edge
   * border doesn't visually overlap either, but explicit z-index keeps
   * the stacking unambiguous. */
  z-index: 1;
}

/* Empty / placeholder tile (README §7 *Tile grid*). Shown for
 * in-room users without an active video stream while the room is
 * in video-mode — audio-only users, or cam-off users co-occupying
 * a video-mode call with peers who have video on. Same
 * .bridge-video-tile sizing/border so the count-aware grid layouts
 * count it identically to a real tile. The avatar shows the user's
 * first initial; the existing .bridge-video-label rule pins their
 * name to bottom-left. VAD glow (.bridge-speaking) reuses the
 * same selector path as real tiles. */
.bridge-video-tile-empty {
  background: var(--bridge-bg-elev);
  display: flex;
  align-items: center;
  justify-content: center;
}

.bridge-video-avatar {
  width: 30%;
  aspect-ratio: 1 / 1;
  max-width: 5rem;
  min-width: 2.25rem;
  border-radius: 50%;
  background: var(--bridge-bg);
  border: 1px solid var(--bridge-border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(1.25rem, 6vmin, 2.5rem);
  font-weight: 600;
  /* Picks up the per-user soft color when present (3+-user rooms);
   * falls back to muted otherwise. Same chat-polish HSL the roster
   * pills use, for visual continuity. */
  color: var(--bridge-user-color, var(--bridge-muted));
  user-select: none;
}

.bridge-video-tile.bridge-speaking {
  /* Outer glow only on the tile element — it needs to bleed outside
   * the border-radius into the grid gap to read as a halo. The
   * crisp 2px inset border is on ::after (see above) so it paints
   * above the video/avatar instead of being hidden by them. */
  box-shadow: 0 0 12px rgba(56, 189, 248, 0.25);
}
.bridge-video-tile.bridge-speaking::after {
  box-shadow: inset 0 0 0 2px var(--bridge-accent);
}
.bridge-video-tile.bridge-speaking .bridge-video-label {
  text-shadow: 0 0 8px rgba(56, 189, 248, 0.7);
  background: rgba(56, 189, 248, 0.55);
  color: #fff;
}

.bridge-video-tile video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.bridge-video-tile.bridge-video-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bridge-bg-elev);
  color: var(--bridge-muted);
}
.bridge-video-fallback-msg {
  font-size: 0.85rem;
  text-align: center;
  padding: 0.5rem;
  line-height: 1.3;
}

.bridge-video-self video {
  transform: scaleX(-1);
}

/* Back-facing camera (`environment`): un-mirror so the user sees the
 * world as it actually is. Selfie/front (`user`) keeps the default
 * mirrored transform above. data-facing is set by chat.js's
 * refreshSelfPreviewMirror() based on sender.getVideoFacingMode(). */
.bridge-video-self[data-facing="environment"] video {
  transform: none;
}

/* Flip-camera button overlaid on the self-preview tile (top-right).
 * Click anywhere on the tile also flips (chat.js wires both); this
 * button is the visual affordance signalling the gesture, especially
 * for users who'd otherwise not realize the tile is tappable. Hidden
 * on devices that report only one camera; chat.js sets `hidden` after
 * an enumerateDevices probe and keeps it hidden when a flip attempt
 * confirms NO_OTHER_CAMERA.
 *
 * Sized smaller than the action-bar buttons (--bridge-tap is too
 * large here) and positioned absolutely so it doesn't disrupt the
 * existing label layout. Translucent dark fill so it reads against
 * a bright camera image; lights up on hover. */
.bridge-video-flip-btn {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  width: 2rem;
  height: 2rem;
  min-height: 0;
  padding: 0;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.bridge-video-flip-btn:hover {
  background: rgba(0, 0, 0, 0.75);
  border-color: rgba(255, 255, 255, 0.35);
}
.bridge-video-flip-btn:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 2px;
}
.bridge-video-flip-btn .bridge-icon {
  width: 1.05rem;
  height: 1.05rem;
}

.bridge-video-label {
  position: absolute;
  left: 0.4rem;
  bottom: 0.4rem;
  padding: 0.1rem 0.4rem;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 0.75rem;
  border-radius: 4px;
  pointer-events: none;
  max-width: calc(100% - 0.8rem);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: text-shadow 200ms ease, background 200ms ease;
}

.bridge-mic.bridge-mic-active {
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  border-color: transparent;
  font-weight: 600;
}

.bridge-cam.bridge-cam-active {
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  border-color: transparent;
  font-weight: 600;
}

.bridge-icon-btn {
  width: var(--bridge-tap);
  min-height: var(--bridge-tap);
  padding: 0;
  flex: 0 0 auto;
}

.bridge-icon {
  width: 1.4rem;
  height: 1.4rem;
  display: block;
  pointer-events: none;
}

.bridge-icon-btn .bridge-icon-on { display: none; }
.bridge-mic-active .bridge-icon-off,
.bridge-cam-active .bridge-icon-off { display: none; }
.bridge-mic-active .bridge-icon-on,
.bridge-cam-active .bridge-icon-on { display: block; }

/* Popover — quality presets. Anchored above the action bar via fixed
 * positioning so it doesn't disturb the chat grid layout. */
.bridge-popover {
  position: fixed;
  left: 50%;
  bottom: calc(var(--bridge-tap) + 1.5rem + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 30;
  background: var(--bridge-bg-elev);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0.4rem;
  min-width: 14rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.bridge-popover-title {
  font-size: 0.8rem;
  color: var(--bridge-muted);
  padding: 0.3rem 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.bridge-popover-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: transparent;
  color: inherit;
  border: 0;
  border-radius: calc(var(--bridge-radius) - 2px);
  padding: 0.55rem 0.7rem;
  min-height: var(--bridge-tap);
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.bridge-popover-item:hover { background: #273549; }
.bridge-popover-item:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: -2px;
}

.bridge-popover-item[aria-checked="true"] {
  background: rgba(56, 189, 248, 0.12);
  color: var(--bridge-accent);
  font-weight: 600;
}
.bridge-popover-item[aria-checked="true"]::after {
  content: '\2713';
  margin-left: 0.6rem;
}

/* Compatibility indicator on the quality button (README §3 / v2
 * fallback). The "!" badge mirrors .bridge-chat-toggle-badge's shape
 * but uses the warn colour — it's an advisory ("the room runs the
 * compatibility pipeline"), not an error. chat.js toggles `hidden`. */
.bridge-quality {
  position: relative;
}
.bridge-quality-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.25rem;
  border-radius: 0.55rem;
  background: var(--bridge-warn);
  color: #1a1306;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.1rem;
  text-align: center;
  pointer-events: none;
  box-shadow: 0 0 0 2px var(--bridge-bg);
}

/* Explanatory note inside the quality popover, shown alongside the
 * badge. Warm-tinted like .bridge-notif-banner so it reads as advisory
 * info. Sits between the popover title and the preset list. */
.bridge-quality-note {
  margin: 0 0 0.15rem;
  padding: 0.4rem 0.6rem;
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--bridge-fg);
  background: color-mix(in srgb, var(--bridge-warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--bridge-warn) 40%, transparent);
  border-radius: calc(var(--bridge-radius) - 2px);
}

.bridge-roster-item.bridge-has-audio::before {
  content: '\1F3A4';
  margin-right: 0.25rem;
}

.bridge-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  background: var(--bridge-warn);
  color: #1a1306;
  text-align: center;
  padding: 0.5rem 1rem;
  font-weight: 600;
}

@media (min-width: 720px) {
  .bridge-chat-body {
    max-width: 48rem;
    margin: 0 auto;
    border-left: 1px solid var(--bridge-border);
    border-right: 1px solid var(--bridge-border);
  }
  /* Video-mode lifts the desktop column cap so the tile grid uses
   * the available width — a 48rem-wide 3-column grid leaves each
   * tile only ~256px on a 27" monitor, which feels cramped vs.
   * Teams etc. The chat overlay panel keeps its own 48rem cap
   * (set in the chat-overlay block above) for readability when
   * the user opens it. */
  .bridge-chat-body[data-mode="video"] {
    max-width: none;
    border-left: none;
    border-right: none;
  }
}

/* --- *Ваши комнаты* (README §16) --------------------------------- */

.bridge-rooms-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--bridge-border);
  margin-bottom: 0.5rem;
}

.bridge-rooms-wrap h2 {
  margin: 0;
  font-size: 1rem;
  color: var(--bridge-muted);
  font-weight: 500;
  letter-spacing: 0.02em;
}

.bridge-rooms-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.bridge-rooms-item {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: 0.6rem;
  min-height: var(--bridge-tap);
  padding: 0.5rem 0.5rem 0.5rem 0.75rem;
  background: var(--bridge-bg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  cursor: pointer;
  user-select: none;
}
.bridge-rooms-item:hover { background: #273549; }
.bridge-rooms-item:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

.bridge-rooms-pin {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  font-weight: 600;
  align-self: center;
}
.bridge-rooms-info {
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}
.bridge-rooms-name {
  font-size: 0.85rem;
  color: var(--bridge-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bridge-rooms-members {
  font-size: 0.9rem;
  color: var(--bridge-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Status container on each *Ваши комнаты* row, holding the unread
 * badge and the ring bell side by side. Sits in the third grid slot
 * of .bridge-rooms-item; renders empty (zero width) by default and
 * grows when home.js shows the badge or the bell. The two children
 * are individually hidden via [hidden] until the home WS surfaces
 * activity / ring events for this row's PIN. */
.bridge-rooms-status {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  align-self: center;
  /* Reserve no width when both children are hidden so the grid's
   * `auto` slot collapses cleanly. */
  min-height: 1rem;
}

/* Ring bell. Shown when someone in the room rings while we're on
 * home; cleared when we resume the row. Plain emoji per design
 * decision — inconsistent rendering across platforms is acceptable
 * for a transient attention badge (unlike the brand mark, which
 * gets the SVG treatment via .bridge-inline-icon). The yellow tint
 * on hover is just visual hierarchy: the bell IS the point of
 * attention when present. */
.bridge-rooms-bell {
  font-size: 1rem;
  line-height: 1;
  user-select: none;
}

/* Unread badge — number-only pill in the row's status slot. The
 * localized "X new" string lives on aria-label (set by home.js) so
 * the visible width stays compact while screen readers still get
 * the meaning. min-width keeps single-digit badges round-ish so
 * the shape doesn't shift wildly between 1 and 99+. */
.bridge-rooms-badge {
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  font-size: 0.75rem;
  font-weight: 700;
  padding: 0.1rem 0.45rem;
  min-width: 1.25rem;
  border-radius: 999px;
  text-align: center;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.bridge-rooms-forget {
  background: transparent;
  border: 0;
  color: var(--bridge-muted);
  font-size: 1.4rem;
  line-height: 1;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  align-self: center;
}
.bridge-rooms-forget:hover {
  color: var(--bridge-danger);
  background: rgba(248, 113, 113, 0.12);
}
.bridge-rooms-forget:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

.bridge-actions-row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* Notification-permission banner. Built dynamically by chat.js when
 * the user's Notification permission is anything other than 'granted'
 * (or when iOS Safari outside a PWA makes prompting pointless). Sits
 * inside .bridge-chat-panel above the log so it stays visible in
 * text-mode and reappears with the chat overlay in video-mode.
 *
 * Warm-tinted (yellow accent) to read as info-not-error — the user
 * isn't broken, they just won't get OS notifications until they act.
 * Three children: message text, optional action button, dismiss ×. */
.bridge-notif-banner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin: 0.5rem 0.75rem;
  padding: 0.5rem 0.75rem;
  background: color-mix(in srgb, var(--bridge-warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--bridge-warn) 40%, transparent);
  border-radius: var(--bridge-radius);
  font-size: 0.85rem;
  color: var(--bridge-fg);
  /* Don't grow to fill the chat panel; sit at natural height. */
  flex: 0 0 auto;
}

.bridge-notif-banner-msg {
  flex: 1 1 14rem;
  line-height: 1.4;
}

/* Action button — reuses .bridge-btn-primary styling but compact so
 * it fits the banner's tight padding without dominating. */
.bridge-notif-banner-action {
  flex: 0 0 auto;
  min-height: 2rem;
  padding: 0.25rem 0.75rem;
  font-size: 0.85rem;
}

/* Dismiss ×. Same shape as the row's forget-X on the home page so the
 * gesture reads consistently. */
.bridge-notif-banner-dismiss {
  flex: 0 0 auto;
  background: transparent;
  border: 0;
  color: var(--bridge-muted);
  font-size: 1.2rem;
  line-height: 1;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.bridge-notif-banner-dismiss:hover {
  color: var(--bridge-fg);
  background: rgba(255, 255, 255, 0.06);
}
.bridge-notif-banner-dismiss:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

/* --- toast ------------------------------------------------------------- */

.bridge-toast {
  position: fixed;
  left: 50%;
  bottom: max(1rem, env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 40;
  background: var(--bridge-bg-elev);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0.6rem 1rem;
  max-width: calc(100vw - 2rem);
  text-align: center;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

/* --- knock cards (chat) ------------------------------------------------ */

.bridge-knocks {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0 0.75rem 0.5rem;
}

.bridge-knock-card {
  background: var(--bridge-bg-elev);
  border: 1px solid var(--bridge-accent);
  border-radius: var(--bridge-radius);
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.bridge-knock-card-title {
  font-weight: 600;
}

.bridge-knock-actions {
  display: flex;
  gap: 0.5rem;
}
.bridge-knock-actions .bridge-btn {
  flex: 1;
  min-height: var(--bridge-tap);
}

/* --- knock waiting panel (landing) ------------------------------------- */

.bridge-knock-panel {
  text-align: center;
}
.bridge-knock-msg {
  color: var(--bridge-muted);
  margin: 0.25rem 0 1rem;
}
.bridge-knock-spinner {
  width: 2rem;
  height: 2rem;
  margin: 0.5rem auto 1rem;
  border-radius: 50%;
  border: 3px solid var(--bridge-border);
  border-top-color: var(--bridge-accent);
  animation: bridge-spin 1s linear infinite;
}
@keyframes bridge-spin {
  to { transform: rotate(360deg); }
}

/* --- invite-from-link panel (landing, README §7) -----------------------
 *
 * Shown when arriving at /?pin=&from= without an existing membership.
 * Reuses .bridge-panel chrome so it visually matches the other landing
 * boxes; just adds a From: line and a read-only PIN display.
 */

.bridge-invite-from {
  margin: 0;
  color: var(--bridge-muted);
  font-size: 0.95rem;
  /* Avoid making the From: name look like a link or label — it's a
   * literal "From: <name>" attribution. */
  word-wrap: break-word;
  overflow-wrap: anywhere;
}

.bridge-invite-pin {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: var(--bridge-bg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  color: var(--bridge-muted);
  font-size: 0.95rem;
}
.bridge-invite-pin strong {
  color: var(--bridge-fg);
  font-size: 1.3rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.1em;
}

/* --- share modal (chat header, README §7) ------------------------------
 *
 * Modal (not popover) because mobile popovers crop against the viewport
 * edge. The outer .bridge-modal IS the backdrop — full-viewport fixed,
 * tap-outside-content closes. .bridge-modal-content is the centered card.
 *
 * z-index: 35 sits above the quality popover (30) so opening the share
 * modal while a popover is up wins. Toast is at 40 so feedback messages
 * (e.g. "Link copied") still appear on top.
 */

.bridge-modal {
  position: fixed;
  inset: 0;
  z-index: 35;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  /* Defensive: env() insets ensure the backdrop covers safe-area on
   * notched devices when body has its own safe-area padding. */
  padding-top: max(1rem, env(safe-area-inset-top));
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
}

.bridge-modal-content {
  width: 100%;
  max-width: 24rem;
  max-height: calc(100dvh - 2rem);
  overflow-y: auto;
  background: var(--bridge-bg-elev);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.6);
}

.bridge-modal-title {
  margin: 0;
  font-size: 1.15rem;
}

.bridge-share-link-row {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.bridge-share-link-label {
  color: var(--bridge-muted);
  font-size: 0.85rem;
}
.bridge-share-link-row input {
  width: 100%;
  background: var(--bridge-bg);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  padding: 0.5rem 0.6rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  /* The link is often longer than the input width; let the user scroll
   * horizontally rather than wrapping a URL across lines. */
  white-space: nowrap;
  overflow-x: auto;
}
.bridge-share-link-row input:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

.bridge-share-checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.95rem;
  cursor: pointer;
  /* Bumps the tap target so a thumb tap registers reliably on the
   * checkbox or its label. */
  min-height: 2rem;
}
.bridge-share-checkbox input {
  width: 1.1rem;
  height: 1.1rem;
  flex: 0 0 auto;
  accent-color: var(--bridge-accent);
}

.bridge-share-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}
.bridge-share-action {
  width: 100%;
}
/* The native-share button (when present) takes the full row above the
 * grid so it visually leads. We can't use grid-column on a button that
 * may be `hidden`, so we set a class-based span and rely on `hidden`
 * removing it from layout entirely. */
.bridge-share-native {
  grid-column: 1 / -1;
}

/* --- /donate (README §9) ----------------------------------------------
 *
 * Same panel vocabulary as the landing page (.bridge-panel) so the
 * donate page feels like the same app. The page is a vertical stack of
 * sections, each conditionally hidden by donate.js based on the
 * server-detected region bucket. RU/BY/IR see only the intro panel.
 */

.bridge-brand-link {
  color: inherit;
  text-decoration: none;
}
.bridge-brand-link:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.bridge-donate {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.5rem;
  max-width: 32rem;
  margin: 0 auto;
}

.bridge-donate-h1 {
  margin: 0;
  font-size: 1.5rem;
  letter-spacing: -0.01em;
}

.bridge-donate-intro p,
.bridge-donate-cta p,
.bridge-donate-honesty p {
  margin: 0;
  line-height: 1.5;
}
.bridge-donate-honesty {
  background: transparent;
  border-color: var(--bridge-border);
}
.bridge-donate-honesty p {
  font-size: 0.9rem;
  color: var(--bridge-muted);
}

.bridge-donate-status-success {
  border-color: var(--bridge-accent);
  background: rgba(56, 189, 248, 0.08);
}
.bridge-donate-status-cancel {
  border-color: var(--bridge-warn);
  background: rgba(251, 191, 36, 0.08);
}
.bridge-donate-status p { margin: 0; }

.bridge-donate-amounts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}
.bridge-donate-amounts-label {
  flex: 1 0 100%;
  font-size: 0.9rem;
  color: var(--bridge-muted);
}
.bridge-donate-amount {
  flex: 1 1 5rem;
  min-height: var(--bridge-tap);
  padding: 0.4rem 0.8rem;
  background: var(--bridge-bg);
  color: var(--bridge-fg);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.bridge-donate-amount:hover { background: #273549; }
.bridge-donate-amount:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}
.bridge-donate-amount-active {
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  border-color: transparent;
  font-weight: 600;
}
.bridge-donate-amount-active:hover {
  background: var(--bridge-accent);
  filter: brightness(1.05);
}

.bridge-donate-custom { gap: 0.3rem; }

.bridge-donate-crypto h2 {
  margin: 0;
  font-size: 1.1rem;
}

.bridge-donate-coin {
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
  background: var(--bridge-bg);
  overflow: hidden;
}
.bridge-donate-coin > summary {
  cursor: pointer;
  padding: 0.75rem;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-height: var(--bridge-tap);
}
.bridge-donate-coin > summary::-webkit-details-marker { display: none; }
.bridge-donate-coin > summary:hover { background: #273549; }
.bridge-donate-coin > summary:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: -2px;
}
.bridge-donate-coin-name {
  font-weight: 600;
}
.bridge-donate-coin-hint {
  color: var(--bridge-muted);
  font-size: 0.8rem;
}

.bridge-donate-coin-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.75rem;
  border-top: 1px solid var(--bridge-border);
}

.bridge-donate-qr {
  width: 100%;
  max-width: 14rem;
  align-self: center;
  background: var(--bridge-bg-elev);
  border-radius: var(--bridge-radius);
  padding: 0.5rem;
}
.bridge-donate-qr svg {
  display: block;
  width: 100%;
  height: auto;
}

.bridge-donate-addr {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  background: var(--bridge-bg-elev);
  border: 1px solid var(--bridge-border);
  border-radius: calc(var(--bridge-radius) - 2px);
  padding: 0.5rem 0.75rem;
  word-break: break-all;
  user-select: all;
}

.bridge-donate-copy { align-self: flex-start; }

.bridge-donate-footer {
  text-align: center;
  color: var(--bridge-muted);
  font-size: 0.85rem;
  padding: 1rem 0 2rem;
}
.bridge-donate-footer p { margin: 0.2rem 0; }
.bridge-donate-footer code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  color: var(--bridge-fg);
  background: var(--bridge-bg-elev);
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
}
.bridge-donate-checksum-help {
  font-size: 0.8rem;
  margin-top: 0.5rem !important;
}

/* --- landing footer (donate link) -------------------------------------- */

.bridge-landing-footer {
  text-align: center;
  padding: 1rem 1.5rem max(1rem, env(safe-area-inset-bottom));
  color: var(--bridge-muted);
}

.bridge-footer-link {
  color: var(--bridge-muted);
  text-decoration: none;
  font-size: 0.9rem;
  padding: 0.4rem 0.8rem;
  border-radius: var(--bridge-radius);
}
.bridge-footer-link:hover {
  color: var(--bridge-accent);
}
.bridge-footer-link:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 2px;
}

/* Ring-button highlight — accent treatment for ~900ms after the
 * sender taps the bell, since the only other feedback is the receiver's
 * chime which they don't hear. Animates in via the same cubic curve
 * the speaking indicator uses. */
.bridge-ring-pulse {
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  border-color: transparent;
  box-shadow: 0 0 12px rgba(56, 189, 248, 0.55);
  transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease;
}

/* --- M6.5: install affordances ----------------------------------------- */

/* Install button on the landing page. Subtle by default — the primary
 * actions are still Create / Join below. Hidden until install.js decides
 * the platform supports an install path. */
.bridge-install-btn {
  align-self: stretch;
  margin: 0 1rem;
  padding: 0.6rem 1rem;
  font-size: 0.95rem;
  background: transparent;
  color: var(--bridge-accent);
  border: 1px solid var(--bridge-accent);
  border-radius: var(--bridge-radius);
  cursor: pointer;
}
.bridge-install-btn:hover {
  background: color-mix(in srgb, var(--bridge-accent) 12%, transparent);
}
.bridge-install-btn:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 2px;
}

/* Install instruction modal. Reuses the .bridge-modal /
 * .bridge-modal-content shell from the share modal; adds layout for
 * stepwise instructions and the platform-specific panels. */
.bridge-install-modal .bridge-modal-content {
  max-width: 28rem;
}

/* Each panel corresponds to a data-mode value. CSS selectors below show
 * exactly one based on the outer data-mode attribute. Default state:
 * both hidden, so before init the modal renders nothing of substance. */
.bridge-install-panel { display: none; }
.bridge-install-modal[data-mode="ios-safari"]
  .bridge-install-panel[data-install-panel="ios-safari"] { display: block; }
.bridge-install-modal[data-mode="ios-other-browser"]
  .bridge-install-panel[data-install-panel="ios-other-browser"] { display: block; }

.bridge-install-steps {
  list-style: none;
  padding: 0;
  margin: 0 0 1rem 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.bridge-install-step {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.8rem;
  background: var(--bridge-bg-elev);
  border: 1px solid var(--bridge-border);
  border-radius: var(--bridge-radius);
}
.bridge-install-step-num {
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  background: var(--bridge-accent);
  color: var(--bridge-accent-fg);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.bridge-install-step-text { flex: 1 1 auto; }
.bridge-install-step-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--bridge-accent);
}
.bridge-install-step-icon .bridge-icon {
  width: 1.5rem;
  height: 1.5rem;
}

/* Pre-16.4 iOS warning. Hidden by default; revealed only when the outer
 * modal element carries data-warn-notifications="1". */
.bridge-install-warning {
  display: none;
  margin-top: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: color-mix(in srgb, var(--bridge-warn) 14%, transparent);
  border-left: 3px solid var(--bridge-warn);
  border-radius: calc(var(--bridge-radius) - 2px);
  color: var(--bridge-fg);
  font-size: 0.9rem;
}
.bridge-install-modal[data-warn-notifications="1"] .bridge-install-warning {
  display: block;
}

/* "Open in Safari" message panel. */
.bridge-install-msg {
  margin: 0 0 1rem 0;
  color: var(--bridge-fg);
  line-height: 1.5;
}

.bridge-install-close {
  align-self: flex-end;
}

/* --- kick UI ----------------------------------------------------------- */

/* Roster items are a flex row: name fills, kick button at the end. */
.bridge-roster-item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.bridge-roster-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Offline members: muted but still actionable (membership revoke). */
.bridge-roster-item-away {
  opacity: 0.45;
}

.bridge-kick-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  background: none;
  border: none;
  color: var(--bridge-muted);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  border-radius: var(--bridge-radius);
  transition: color 120ms, background 120ms;
}
.bridge-kick-btn:hover {
  color: var(--bridge-danger);
  background: color-mix(in srgb, var(--bridge-danger) 12%, transparent);
}
.bridge-kick-btn:focus-visible {
  outline: 2px solid var(--bridge-danger);
  outline-offset: 1px;
}

/* Danger button variant used for kick confirm and other destructive actions. */
.bridge-btn-danger {
  background: color-mix(in srgb, var(--bridge-danger) 14%, transparent);
  color: var(--bridge-danger);
  border: 1px solid var(--bridge-danger);
}
.bridge-btn-danger:hover {
  background: color-mix(in srgb, var(--bridge-danger) 26%, transparent);
}
.bridge-btn-danger:focus-visible {
  outline: 2px solid var(--bridge-danger);
  outline-offset: 2px;
}

/* Action row at the bottom of a modal (cancel + confirm side by side). */
.bridge-modal-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
}

/* Hint text beneath the revoke checkbox in the kick modal. */
.bridge-kick-hint {
  margin: 0;
  font-size: 0.85rem;
  color: var(--bridge-muted);
}

/* Single-emoji messages render at 2× size. */
.bridge-line-big-emoji {
  font-size: 2em;
  line-height: 1.25;
}

/* --- reply ------------------------------------------------------------ */

/* Reply button: always visible, sits outside the bubble in the row. */
.bridge-reply-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  background: none;
  border: none;
  color: var(--bridge-muted);
  font-size: 0.95rem;
  cursor: pointer;
  border-radius: var(--bridge-radius);
  transition: color 120ms, background 120ms;
  align-self: center;
}
.bridge-reply-btn:hover {
  color: var(--bridge-accent);
  background: color-mix(in srgb, var(--bridge-accent) 12%, transparent);
}
.bridge-reply-btn:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

/* Quote block inside a bubble — clicking scrolls to the original. */
.bridge-reply-quote {
  display: block;
  border: none;
  border-left: 2px solid var(--bridge-accent);
  padding: 0.1rem 0.45rem;
  margin-bottom: 0.3rem;
  background: none;
  cursor: pointer;
  text-align: left;
  width: 100%;
  max-width: 18rem;
  overflow: hidden;
  font: inherit;
  color: inherit;
  border-radius: 0;
}
.bridge-reply-quote:hover {
  background: rgba(255, 255, 255, 0.05);
}
.bridge-reply-quote-author {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--bridge-accent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bridge-reply-quote-text {
  display: block;
  font-size: 0.8rem;
  color: var(--bridge-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Composer reply preview bar — shown above the input when replying. */
.bridge-composer-reply {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 1rem;
  border-top: 1px solid var(--bridge-border);
  border-left: 3px solid var(--bridge-accent);
  background: color-mix(in srgb, var(--bridge-accent) 7%, var(--bridge-bg));
  font-size: 0.85rem;
}
.bridge-composer-reply-content {
  flex: 1;
  min-width: 0;
}
.bridge-composer-reply-author {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--bridge-accent);
}
.bridge-composer-reply-text {
  color: var(--bridge-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bridge-composer-reply-cancel {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--bridge-muted);
  font-size: 1.2rem;
  line-height: 1;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.bridge-composer-reply-cancel:hover {
  color: var(--bridge-fg);
  background: rgba(255, 255, 255, 0.06);
}
.bridge-composer-reply-cancel:focus-visible {
  outline: 2px solid var(--bridge-accent);
  outline-offset: 1px;
}

/* Flash highlight when jumping to a quoted message. */
@keyframes bridge-msg-highlight {
  0%   { box-shadow: 0 0 0 2px var(--bridge-accent), 0 0 8px rgba(56,189,248,0.4); }
  100% { box-shadow: none; }
}
.bridge-line-row.bridge-line-highlight .bridge-line-chat {
  animation: bridge-msg-highlight 1.2s ease-out forwards;
}

/* -------------------------------------------------------------------------
   Image attachments (§image-sharing)
   ------------------------------------------------------------------------- */

/* Attach (paperclip) button in the composer */
.bridge-attach-btn {
  flex-shrink: 0;
  color: var(--bridge-muted);
}
.bridge-attach-btn:hover,
.bridge-attach-btn:focus-visible {
  color: var(--bridge-fg);
  background: rgba(255, 255, 255, 0.06);
}

/* Bubble shape for image messages. Slightly wider than text-only.
   Caption (if any) is rendered below the image as normal .bridge-line-text. */
.bridge-line-image {
  max-width: min(320px, 88vw);
}

/* Wrapper inside the bubble holds the image (or thumb+overlay) */
.bridge-image-wrap {
  margin-top: 0.3rem;
  border-radius: 6px;
  overflow: hidden;
  display: inline-block;
  max-width: 100%;
}

/* Full-size image (live recipient or after fetch) */
.bridge-image {
  display: block;
  max-width: 100%;
  max-height: 300px;
  border-radius: 6px;
  cursor: zoom-in;
  object-fit: contain;
  background: rgba(0, 0, 0, 0.12);
}

/* Loading placeholder for late-joiner images (skeleton pulse) */
.bridge-image-placeholder {
  width: 100%;
  min-width: 120px;
  height: 160px;
  border-radius: 6px;
  background: linear-gradient(
    90deg,
    rgba(255,255,255,0.04) 25%,
    rgba(255,255,255,0.08) 50%,
    rgba(255,255,255,0.04) 75%
  );
  background-size: 200% 100%;
  animation: bridge-skeleton 1.4s ease-in-out infinite;
}
@keyframes bridge-skeleton {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
/* "Image no longer available" fallback */
.bridge-image-unavailable {
  font-size: 0.78rem;
  color: var(--bridge-muted);
  font-style: italic;
  margin: 0;
  padding: 0.25rem 0;
}

/* Caption below the image */
.bridge-line-image .bridge-line-text {
  margin-top: 0.35rem;
  display: block;
}

/* Full-screen image viewer modal */
.bridge-image-viewer {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0)
           env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
  cursor: zoom-out;
}
.bridge-image-viewer-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 4px;
  cursor: default;
  box-shadow: 0 4px 32px rgba(0, 0, 0, 0.6);
}
.bridge-image-viewer-close {
  position: absolute;
  top: max(1rem, env(safe-area-inset-top, 1rem));
  right: max(1rem, env(safe-area-inset-right, 1rem));
  background: rgba(255, 255, 255, 0.15);
  border: none;
  color: #fff;
  font-size: 1.4rem;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  padding: 0;
}
.bridge-image-viewer-close:hover,
.bridge-image-viewer-close:focus-visible {
  background: rgba(255, 255, 255, 0.28);
}
