@charset "UTF-8";
/* ===== Velta — Telegram-style theme ===== */
:root {
  /* Viewport units corrected for the interface scale ui-scale.js
     zooms <html>, and under CSS zoom 100vw/100dvh are multiplied by the zoom
     (414px on a 360px phone at "Large"), so full-width sheets overflowed.
     Always size against --vw/--vh/--dvh, never raw vw/vh/dvh. */
  --ui-zoom: 1;
  --vw: calc(100vw / var(--ui-zoom));
  --vh: calc(100vh / var(--ui-zoom));
  --dvh: calc(100dvh / var(--ui-zoom));
  /* Cap for fixed min-widths inside bubbles (file/app/voice cards): a bubble
     on a 320px phone (avatar column, list padding, 90% max) is ~170px wide
     inside, so a bare min-width: 220px pushed the card past the screen. */
  --bubble-inner-max: calc(var(--vw) - 120px);
  --bg: #0f0f14;
  --bg-sidebar: #181820;
  --bg-chat: #0b0b10;
  --bg-hover: rgba(244,244,244,.055);
  --bg-active: #2b5278;
  /* Selected-chat fill (#182): its own token, not --bg-active — selection and
     pressed/active are different states, and the outgoing bubble must be free
     to change without dragging the chat-list selection along. */
  --selection-bg: #2b5278;
  --bg-bubble-in: #1c1c26;
  --bg-bubble-out: #2b5278;
  --bg-reply: rgba(244,244,244,.06);
  --accent: #5aa2e6;
  --accent-2: #8774e1;
  --text: #f2f2f5;
  --text-dim: #8b8b96;
  --text-meta-in: #8f8f9c;
  --text-meta-out: #a8c6e2;
  --green: #4fae4f;
  --danger: #e05a5a;
  --border: rgba(244,244,244,.07);
  --shadow: 0 4px 18px rgba(16,16,16,.45);
  --radius-bubble: 14px;
  /* Chat appearance (drawer, app/js/chat-appearance.js) overrides these on
     <html> inline: message text size and bubble corners. The derived radii
     keep today's look at the defaults (tail 6, quote 6, media 10 = 14 - 4)
     and all reach 0 together at "square". */
  --msg-font-size: 15.5px;
  --radius-tail: 6px;
  --radius-quote: 6px;
  --radius-media: 10px;
  /* Accent-driven tokens. Defaults reproduce the hand-measured legacy
     colors; a custom accent replaces them per theme with derived values
     that keep every text pair at WCAG AA (chat-appearance.js deriveAccent). */
  --accent-tint: #5aa2e6; /* translucent selection/pressed fills */
  --focus-ring: var(--accent); /* #182: one ring color per theme; light overrides below (raw light --accent is ~3:1, below the focus-indicator bar) */
  --accent-hover: color-mix(in srgb, var(--accent) 90%, #f4f4f4);
  --accent-active: color-mix(in srgb, var(--accent) 88%, #0b0b10);
  --on-accent: #10131a; /* ink on accent fills: #f4f4f4 on #5aa2e6 was 2.6:1, on #3390ec 3.1:1 */
  --accent-text: var(--accent); /* accent-colored UI text (buttons, links, status) on app surfaces */
  --on-badge: #10131a; /* unread badges on --accent-2 */
  --on-badge-muted: #10131a; /* muted badge on --text-dim */
  --out-link: #dfeaf6;
  --ticks-read: #6ec2ff;
  /* 1:1 call screen (#call-overlay). Dark full-screen surface in the soft
     blacks; light and brutal re-map below. */
  --call-bg: var(--bg-chat);
  --call-fg: var(--text);
  --call-fg-dim: var(--text-dim);
  --call-video-fg: #f2f2f5; /* over the remote video scrim, every theme */
  --call-scrim: color-mix(in srgb, #0b0b10 55%, transparent);
  --call-btn: color-mix(in srgb, var(--text) 22%, var(--bg-chat));
  --call-btn-on: var(--text);
  --call-btn-on-fg: var(--bg-chat);
  --call-danger: var(--danger);
  --call-danger-fg: #f4f4f4;
  --call-accept: color-mix(in srgb, var(--green) 80%, #0b0b10);
  --call-accept-fg: #f4f4f4;
  --call-avatar-bg: #5a6270; /* no peer color yet */
  --call-avatar-fg: #f4f4f4;
  --call-pip-bg: var(--bg-bubble-in);
  --call-btn-size: 56px; /* display size: 2 x --ico-5 + padding */
  /* Shared non-color tokens */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Theme default accent, kept separately so the Chat appearance "default"
     swatch shows it even while a custom --accent is active. */
  --accent-default: #5aa2e6;
  /* Accent tokens a custom accent overrides (chat-appearance.js VAR_NAMES). */
  --on-accent-chip: #10131a;
  --accent-text-in: var(--accent); /* accent-colored text on incoming bubbles / sidebar */
  --quote-name: var(--accent);
  --quote-name-out: #d9e9fa;
  --bg-elev: #1c1c26; /* raised placeholder tiles (media grid, splash card) */
  /* icon ladder: --ico-1 inline/meta · --ico-2 small ui · --ico-3 rows/buttons · --ico-4 bars · --ico-5 composer.
     The global svg fallback stays 24px; display graphics (30/56/168/280) are deliberately not tokens. */
  --ico-1: 14px; --ico-2: 16px; --ico-3: 20px; --ico-4: 22px; --ico-5: 26px;
  --head-h: 56px; /* shared height of the sidebar and chat header bars */
  --list-bar-h: calc(64px + env(safe-area-inset-bottom)); /* 46px buttons + 18px padding + safe area */
  color-scheme: dark;
}
/* Pure black (AMOLED, #277): an opt-in exception to the "no pure black"
   rule (AGENTS 6.4), dark theme only. Page, chat list and history go #000;
   elevated surfaces (headers, menus, dialogs, composer: --bg-sidebar;
   bubbles) keep their soft greys so layering stays visible. Text only
   gains contrast on the darker ground. */
html[data-theme="dark"][data-amoled] { --bg: #000000; --bg-chat: #000000; }
html[data-theme="dark"][data-amoled] .sidebar { background: var(--bg); }
html[data-theme="dark"][data-amoled] .history-scroll { background: var(--bg-chat); }

html[data-theme="light"] {
  --bg: #f4f4f4;
  --bg-sidebar: #f4f4f4;
  --bg-chat: #e7ebf0;
  --bg-hover: rgba(16,16,16,.05);
  --bg-active: #5aa2e6;
  --selection-bg: #5aa2e6;
  --bg-bubble-in: #f4f4f4;
  --bg-bubble-out: #e1f0fb;
  --bg-reply: rgba(16,16,16,.05);
  --accent: #3390ec;
  --accent-tint: #3390ec; /* #182: derived per theme — was inherited dark blue */
  --focus-ring: #1a68b8; /* focus indicator at text-accent strength: raw --accent is ~3:1 on light */
  --text: #16181d;
  --text-dim: #5d626d; /* 5.0:1 on #e9e9e9 chips (was #656a76) */
  --text-meta-in: #676d7a; /* 4.72:1 on #f4f4f4 (was #9aa0ab, 2.39) */
  --text-meta-out: #2a6ea6;
  --out-link: #14507e;
  --ticks-read: #1d8bf1;
  --call-btn: color-mix(in srgb, var(--text) 12%, var(--bg-chat));
  --call-accept: var(--green); /* #2a7032: 5.9:1 under the white icon */
  --call-pip-bg: var(--bg-sidebar);
  --accent-default: #3390ec;
  --accent-text: #1a68b8; /* 4.65:1 on #e9e9e9, 5.1 on #f4f4f4 */
  --on-badge-muted: #f4f4f4; /* 5.9:1 on #5d626d */
  --accent-text-in: #1a68b8; /* raw #3390ec is 3.1:1 on #f4f4f4 */
  --quote-name: #14507e;
  --quote-name-out: #14507e;
  --bg-elev: #e7ebf0;
  --green: #2a7032; /* text on #f4f4f4 / the green note tint (was #3d9146, 3.6:1) */
  --danger: #bf2a2a; /* was inherited #e05a5a, 3.5:1 on #f4f4f4 */
  --border: rgba(16,16,16,.08);
  --shadow: 0 4px 18px rgba(16,16,16,.18);
  color-scheme: light;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
/* Neutralize browser font-boosting / text inflation: Velta owns its scaling
   end to end (drawer → Interface scale). The Android shell additionally pins
   WebView textZoom to 100 (MainActivity.kt). */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
html, body { height: 100%; overscroll-behavior: none; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
/* #182: the shared focus contract — every button and link gets the 2px
   --focus-ring outline even where no per-component rule exists (modal feet,
   icon buttons, ctx items without their own). Per-component focus rules with
   custom offsets keep winning over this base. */
button:focus-visible, a:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
input, textarea { font: inherit; color: inherit; }
svg { width: 24px; height: 24px; display: block; }

.app { display: flex; height: 100%; }

/* ---------- Sidebar ---------- */
.sidebar {
  width: 33%; min-width: 300px; max-width: 420px;
  display: flex; flex-direction: column;
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  position: relative; z-index: 5;
}
.sidebar-head {
  position: relative; /* anchors the centered brand mark */
  display: flex; align-items: center; gap: 6px;
  /* Top padding carries the relay-zone's 10px gap below: the logo/search
     cluster reads its bottom whitespace through to the strip, so the top
     pads equally (8 + 10) or the header looks top-glued. */
  padding: calc(18px + env(safe-area-inset-top)) 12px 8px;
  box-sizing: border-box;
  height: calc(var(--head-h) + env(safe-area-inset-top));
}
/* Brand mark centered between the menu and search buttons. The V glyph
   covers only ~57% of the SVG viewBox, so the img is oversized to make the
   visible mark ~22px, matching the 40px icon buttons beside it. */
.sidebar-logo {
  position: absolute; left: 50%; top: calc(50% + 5px + env(safe-area-inset-top) / 2);
  transform: translate(-50%, -50%);
  height: 38px; width: auto; opacity: .9;
  pointer-events: none;
}
#btn-search { margin-left: auto; }

.icon-btn {
  width: 40px; height: 40px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  color: var(--text-dim);
  transition: background .15s;
}
@media (hover: hover) and (pointer: fine) {
  .icon-btn:hover { background: var(--bg-hover); }
}

.chat-list { flex: 1; overflow-y: auto; padding-bottom: 10px; scrollbar-gutter: stable both-edges; }
/* Category chips above the chat list (#57): equal-width buttons, one row. */
.chat-cats { display: flex; gap: 4px; padding: 2px 8px 6px; flex: none; }
.chat-cats button {
  flex: 1 1 0; min-width: 0; padding: 5px 2px; font-size: 11.5px; line-height: 1;
  color: var(--text-dim); background: var(--bg-hover); border: none; border-radius: 999px;
  cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chat-cats button.active { background: var(--accent); color: var(--on-accent-chip); }
@media (hover: hover) and (pointer: fine) {
  .chat-cats button:hover:not(.active) { color: var(--text); }
}
/* Relay status line — thin strip just below the sidebar header. The color
   lives on `color` so the sending-dash gradient can reuse it as currentColor. */
/* Relay status strip: one equal-width segment per configured relay
   (up to MAX_RELAYS=5 in the core), each colored by that relay's own
   state; a single relay fills the line. Lives on per-segment `color` so
   the sending-dash gradient can reuse it as currentColor. */
/* Scoped to the strip's ID: the relays modal's card rows reuse the
   .relay-line class name and must not inherit the 6px meter height — nor
   may their align-items:center reach here: an empty segment has no height,
   and centering collapses it to 0px (the strip "died" exactly that way). */
#relay-line { display: flex; gap: 4px; height: 6px; flex: 1 1 auto; align-items: stretch; }
#relay-line .relay-seg { flex: 1 1 0; min-width: 0; background: currentColor; color: #e6a23c; transition: color .25s; }
#relay-line .relay-seg[data-state="ok"]         { color: #2ecc71; }
#relay-line .relay-seg[data-state="connecting"] { color: #e6a23c; }
#relay-line .relay-seg[data-state="unreachable"] { color: #e6a23c; }
/* #108 sustained offline (probe failing > 10 min): calm grey, not amber —
   a relay down for days is a fact, not an active problem. */
#relay-line .relay-seg[data-state="offline"]     { color: #9aa0a6; }
/* #102 sending delayed: relay up, SMTP retrying — amber like connecting;
   the difference lives in the line title and the detail chip text. */
#relay-line .relay-seg[data-state="delayed"]    { color: #e6a23c; }
#relay-line .relay-seg[data-state="down"]       { color: #e05252; }
#relay-line .relay-seg[data-state="local"]      { color: #4c8dff; }
/* Sending dashes on the sending relay's segment only (marked per-segment
   in renderRelayLine — the primary transport, i.e. state.account.addr). */
#relay-line .relay-seg[data-sending] {
  background: repeating-linear-gradient(90deg, currentColor 0 7px, transparent 7px 12px);
  animation: relay-dashes .45s linear infinite;
}
@keyframes relay-dashes { to { background-position: 12px 0; } }
@media (prefers-reduced-motion: reduce) { #relay-line .relay-seg[data-sending] { animation: none; } }

/* Relay detail chips — per-relay info, fed by renderRelayLine. One chip per
   relay-line segment (same flex template, so each chip sits on its own
   segment), showing only the masked domain + quota percent, e.g.
   "cha*.uk · 55% used". Rendered INSIDE the relay zone (inset:0, covering
   only the 6px line — the zone owns the chips' height) so the sidebar
   header / search button above is never covered or click-blocked.
   Desktop: revealed by hovering the relay line. Mobile: pull down at the
   top of the chat list (pull-open class set by the touch handler in
   app.js); it hides itself again after a few seconds. */
.relay-zone {
  position: relative; flex: none; z-index: 6;
  height: 26px; display: flex;
  /* Equal 10px gaps above and below the 6px line (measured: the header
     contributes no extra gap, so symmetric padding is symmetric). The zone
     height is fixed at 26px — 10 + 6 + 10 fills it exactly. */
  padding: 10px 0;
}
.relay-detail {
  position: absolute; inset: 0; z-index: 7;
  display: none; gap: 4px;
  cursor: default;
  animation: relay-detail-in .18s ease-out;
}
@keyframes relay-detail-in { from { opacity: 0; transform: translateY(5px); } }
.relay-detail.pull-open { display: flex; }
@media (hover: hover) and (pointer: fine) {
  .relay-zone:hover .relay-detail,
  .relay-detail:hover { display: flex; }
}
.relay-detail-chip {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: 4px;
  padding: 4px 8px; font-size: 11px; line-height: 1.25;
  color: var(--text); background: var(--bg-sidebar);
  border: 1px solid var(--border); border-radius: 999px;
  box-shadow: var(--shadow);
}
.relay-detail-chip .relay-detail-label {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.relay-detail-chip[data-state="down"] { border-color: #e05252; }
.relay-detail-chip[data-state="unreachable"] { border-color: #e6a23c; }
.relay-detail-chip[data-state="offline"] { border-color: #9aa0a6; }
/* Frosted variant where supported: the relay line's state colors ghost
   through each chip (62% bg keeps the 11px labels readable). Without
   backdrop-filter the solid var(--bg-sidebar) rule above stands. */
@supports (backdrop-filter: blur(7px)) or (-webkit-backdrop-filter: blur(7px)) {
  .relay-detail-chip {
    background: color-mix(in srgb, var(--bg-sidebar) 62%, transparent);
    backdrop-filter: blur(7px) saturate(1.6);
    -webkit-backdrop-filter: blur(7px) saturate(1.6);
  }
}
/* The relay selected for sending: static envelope, always visible, colored
   by SMTP-loop health (app.js sets data-smtp). */
.relay-detail-chip .relay-detail-send {
  display: inline-grid; place-items: center; flex: none;
  width: var(--ico-1); height: var(--ico-1); color: var(--accent);
}
.relay-detail-send[data-smtp="ok"]      { color: #2ecc71; }
.relay-detail-send[data-smtp="retrying"] { color: #e6a23c; }
.relay-detail-send[data-smtp="down"]    { color: #e05252; }
.relay-detail-send[data-smtp="unknown"] { color: #9aa0a6; }
/* Quota % text carries the warning color the old <meter> bar had. */
.relay-pct[data-level="ok"]   { color: #2ecc71; }
.relay-pct[data-level="high"] { color: #e6a23c; }
.relay-pct[data-level="full"] { color: #e05252; }
.relay-detail-chip .relay-detail-send svg { width: var(--ico-1); height: var(--ico-1); }
.list-bar {
  display: flex; align-items: stretch; gap: 6px;
  padding: 8px 10px calc(10px + env(safe-area-inset-bottom));
  background: var(--bg-sidebar);
  border-top: 1px solid var(--border);
}
.bar-btn {
  flex: 1; height: 46px;
  display: grid; place-items: center;
  background: none; border: none; border-radius: 8px;
  color: var(--text); cursor: pointer;
  transition: transform .1s, background .15s, box-shadow .1s;
}
.bar-btn svg { width: var(--ico-4); height: var(--ico-4); }
@media (hover: hover) and (pointer: fine) {
  .bar-btn:hover { background: var(--bg-hover); }
}
.bar-btn.active { color: var(--accent); box-shadow: inset 0 -2px 0 var(--accent); border-radius: 8px 8px 0 0; }
.bar-btn.bar-accent { background: var(--accent); color: var(--on-accent); }
/* #109: .bar-btn.active squares the bottom corners for the view tabs. The + is not a tab. */
.bar-btn.bar-accent.active { border-radius: 8px; }
@media (hover: hover) and (pointer: fine) {
  .bar-btn.bar-accent:hover { transform: scale(1.05); }
}
/* Every view button hidden: the bar melts into the sidebar — only the
   always-visible Menu and the + remain, floating. */
.list-bar.bar-bare { background: transparent; border-top-color: transparent; }

/* Side views rendered in place of the chats list */
.side-view { padding: 8px 8px 10px; }
.side-view-title { font-size: 15.5px; font-weight: 600; color: var(--text); padding: 4px 6px 2px; }
.side-view-sub { font-size: 12.5px; color: var(--text-dim); padding: 0 6px 10px; }
.side-view-empty { font-size: 13.5px; color: var(--text-dim); padding: 18px 6px; text-align: center; }
.side-view-rows { display: flex; flex-direction: column; gap: 4px; }
.side-view .chat-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; text-align: left; background: none; border: none; cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .side-view .chat-item:hover { background: var(--bg-hover); }
}
.side-view .velta-avatar, .side-view .call-row-ico { flex: none; }
.call-row-ico {
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--bg-bubble-in); color: var(--accent);
}
.call-row-ico svg { width: var(--ico-3); height: var(--ico-3); }
.bar-opts-hint { font-size: 12px; color: var(--text-dim); padding: 2px 4px 4px; }
/* Hint under a drawer ctx-item row (multi-device): aligned with the row's
   label text (row padding + icon + gap). */
.ctx-hint { padding: 0 12px 6px calc(12px + var(--ico-3) + 12px); margin-top: -4px; }
.ctx-stack { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.ctx-sub { font-size: 12px; color: var(--text-dim); line-height: 1.3; }
.md-note { color: var(--text); }
.md-note[hidden] { display: none; }
.md-note-acts { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; margin-left: -8px; }
.side-search-input { margin-bottom: 10px; }
.side-option { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.side-option-label { font-size: 14.5px; font-weight: 500; color: var(--text); }
.side-option-arrow { width: var(--ico-3); height: var(--ico-3); color: var(--text-dim); flex: none; }
.side-view .qr-view { display: flex; flex-direction: column; gap: 12px; padding: 4px 6px; }

/* ---------- Main ---------- */
.main { flex: 1; min-width: 0; display: flex; background: var(--bg-chat); position: relative; }
.no-chat {
  margin: auto; text-align: center;
}
.no-chat-badge {
  background: var(--bg-hover);
  padding: 10px 18px; border-radius: 16px;
  font-size: 14.5px; color: var(--text-dim);
  display: inline-block;
}
.no-chat-sub { margin-top: 14px; font-size: 13px; color: var(--text-dim); }

.chat-view { display: flex; flex-direction: column; flex: 1; min-width: 0; position: relative; }
/* History loading strip: thin blue gradient sweeping right, pinned just
   under the chat header while the open chat's history loads. Blue on a
   faint tint of itself so the track stays visible on every theme. */
.chat-load-bar {
  position: absolute;
  top: calc(var(--head-h) + env(safe-area-inset-top));
  left: 0; right: 0; height: 6px; /* same as #relay-line */
  overflow: hidden; z-index: 4; pointer-events: none;
  background: rgba(76, 141, 255, .18);
  opacity: 0; transition: opacity .25s;
}
.chat-load-bar[data-on] { opacity: 1; }
/* progress is a replaced element: hide the native bar so the sweep (or the tint) shows */
progress.chat-load-bar {
  appearance: none; -webkit-appearance: none;
  display: block; width: 100%; border: 0;
}
progress.chat-load-bar::-webkit-progress-bar,
progress.chat-load-bar::-webkit-progress-value { background: transparent; }
progress.chat-load-bar::-moz-progress-bar { background: transparent; }

/* Pinned-message strip between chat head and history (chat-view _refreshPinnedBar) */
.pinned-bar {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 14px; border: 0; cursor: pointer; text-align: left;
  background: var(--bg-elev); color: var(--text-dim);
  border-bottom: 1px solid rgba(128, 128, 128, .18);
  font: inherit; font-size: 13px;
}
.pinned-bar svg { width: var(--ico-1); height: var(--ico-1); color: var(--accent); flex: none; }
.pinned-bar .pb-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pinned-bar .pb-text b { color: var(--text); font-weight: 600; }

/* ---------- Pin tray (details element, issue: multi-pin + unpin) ---------- */
/* Same surface as the chat header. The old #14141c fallback hid light-theme names. */
.pin-tray {
  width: 100%; background: var(--bg-sidebar);
  border-bottom: 1px solid var(--border);
  color: var(--text-dim); font: inherit; font-size: 13px;
}
.pin-tray > summary {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 14px; cursor: pointer; list-style: none; text-align: left;
}
.pin-tray > summary::-webkit-details-marker { display: none; }
.pin-tray > summary svg { width: var(--ico-1); height: var(--ico-1); color: var(--accent); flex: none; }
.pin-tray > summary .pb-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pin-tray > summary .pb-text b { color: var(--text); font-weight: 600; }
.pin-tray-count {
  flex: none; min-width: 18px; height: 18px; border-radius: 9px;
  display: grid; place-items: center; padding: 0 4px;
  background: var(--accent); color: var(--on-accent); font-size: 11px; font-weight: 600;
}
.pin-tray-chevron { display: grid; place-items: center; color: var(--text-dim); }
.pin-tray-chevron svg { width: var(--ico-2); height: var(--ico-2); }
.pin-tray[open] .pin-tray-chevron svg { transform: rotate(180deg); }
.pin-tray-list {
  display: flex; flex-direction: column;
  max-height: calc(var(--vh) * .40); overflow-y: auto;
  border-top: 1px solid var(--border);
}
.pin-tray-item {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; cursor: pointer;
}
.pin-tray-item:hover { background: var(--bg-hover); }
.pin-tray-item .pb-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pin-tray-item .pb-text b { color: var(--text); font-weight: 600; }
.pin-unpin {
  flex: none; width: 24px; height: 24px; border: 0; border-radius: 6px;
  background: transparent; color: var(--text-dim); font: inherit; font-size: 13px;
  cursor: pointer; display: grid; place-items: center;
}
.pin-unpin:hover { color: var(--danger); background: var(--bg-hover); }

.chat-load-bar::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 45%;
  background: linear-gradient(90deg, transparent, #4c8dff 30%, #8fbcff 50%, #4c8dff 70%, transparent);
  animation: chat-load-sweep 1.1s ease-in-out infinite;
}
@keyframes chat-load-sweep { from { transform: translateX(-100%); } to { transform: translateX(350%); } }
@media (prefers-reduced-motion: reduce) { .chat-load-bar::before { animation: none; } }

/* #233: first-sync strip on an empty chat list — "Fetching messages…" while
   the core brings the profile up, "Updating…" while it actively works
   (3000-3999). Gone once the list has chats or the core reaches 4000. */
#sync-bar {
  display: flex; align-items: center; gap: 8px; flex: none;
  padding: 5px 12px; font-size: 13px; color: var(--text-dim);
}
#sync-bar[hidden] { display: none; }

/* #238: thin sweep strip in the in-chat search modal — the FTS query can
   take seconds on big accounts; same activity language as .chat-load-bar. */
.search-load-bar {
  appearance: none; -webkit-appearance: none;
  display: block; width: 100%; height: 3px; margin-top: 6px;
  border: 0; border-radius: 2px; overflow: hidden; position: relative;
  background: rgba(76, 141, 255, .18);
  opacity: 0; transition: opacity .2s;
}
.search-load-bar[data-on] { opacity: 1; }
/* progress is a replaced element: hide the native bar so the sweep shows */
progress.search-load-bar::-webkit-progress-bar,
progress.search-load-bar::-webkit-progress-value { background: transparent; }
progress.search-load-bar::-moz-progress-bar { background: transparent; }
.search-load-bar::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 45%;
  background: linear-gradient(90deg, transparent, #4c8dff 30%, #8fbcff 50%, #4c8dff 70%, transparent);
  animation: chat-load-sweep 1.1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .search-load-bar::before { animation: none; } }
.chat-head {
  display: flex; align-items: center; gap: 4px;
  background: var(--bg-sidebar);
  border-bottom: 1px solid var(--border);
  padding: calc(7px + env(safe-area-inset-top)) 10px 7px 12px;
  box-sizing: border-box;
  height: calc(var(--head-h) + env(safe-area-inset-top));
  position: relative; z-index: 3;
}
.chat-head-info { flex: 1; min-width: 0; }
.chat-head-actions { display: flex; gap: 2px; }

/* selection bar overlays head actions */
.selection-bar {
  position: absolute; inset: 0;
  background: var(--bg-sidebar);
  display: flex; align-items: center; gap: 6px;
  padding: calc(5px + env(safe-area-inset-top)) 10px 5px 6px;
  z-index: 4;
}
.sel-count { font-size: 17px; font-weight: 600; margin: 0 6px; }
.sel-actions { margin-left: auto; display: flex; gap: 2px; }

/* ---------- History ---------- */
.history-scroll {
  flex: 1; position: relative; overflow-y: auto;
  scrollbar-width: thin;
  overscroll-behavior: contain;
  /* The virtual scroller positions items with padding adjustments and manages
     scroll offsets itself; the browser's scroll anchoring fights that and can
     yank the view to stale positions (broken scroll after reopening a chat). */
  overflow-anchor: none;
  background:
    radial-gradient(1200px 600px at 20% -10%, color-mix(in srgb, var(--accent-tint) 6%, transparent), transparent 60%),
    radial-gradient(900px 500px at 90% 110%, rgba(135,116,225,.05), transparent 60%),
    var(--bg-chat);
}
.history {
  max-width: 728px; margin: 0 auto;
  padding: 14px 12px 18px;
  display: flex; flex-direction: column;
}
html[data-theme="light"] .history-scroll {
  background:
    radial-gradient(1200px 600px at 20% -10%, rgba(51,144,236,.10), transparent 60%),
    radial-gradient(900px 500px at 90% 110%, rgba(135,116,225,.10), transparent 60%),
    var(--bg-chat);
}

/* Day chip: rendered INSIDE the first msg-row of each day (dayFirst flag) —
   day chips must not be separate virtual-list items (see chat-view.js
   _annotateMessages). width:100% + flex-wrap on .msg-row gives the chip its
   own line inside the row. */
.day-chip {
  width: 100%; display: flex; justify-content: center;
  /* Solid fill, no backdrop blur. A blur on a sticky element inside
     the scroller is recomputed every scroll frame (costly on Android GPUs).
     Opaque, so text behind never bleeds through. Contrast: dark #d7d7de on
     #1c1c22 11.8:1; light #5d636e on #f4f4f4 5.5:1;
     brutal #d7d7de on its opaque --bg-sidebar #2a2a35 9.9:1 (rule below). */
  background: #1c1c22;
  color: #d7d7de;
  font-size: 13px; padding: 4px 12px; border-radius: 14px;
  margin: 12px 0 6px;
  position: sticky; top: 8px; z-index: 2;
}
html[data-theme="light"] .day-chip { background: #f4f4f4; color: #5d636e; box-shadow: 0 1px 4px rgba(16,16,16,.12); }
/* service messages (diagnosticRow) can start a day too — block wrap */
.msg-row.day-first { display: block; }

/* "Unread messages" line and the manual read marker: both ride INSIDE
   their message row (unreadFirst / readMarker flags, see chat-view.js
   _annotateMessages) and take their own line via the row's flex-wrap.
   Text colors measured against --bg-chat plus the history gradients:
   dark accent 7.2, brutal accent 5.3, light #155a9e >= 5.2 : 1 (raw light
   accent is 2.8 : 1). */
.unread-sep {
  flex: none; width: calc(100% - 48px);
  text-align: center;
  color: var(--accent); font-size: 13px;
  border-top: 1px solid var(--accent);
  margin: 10px 24px; padding-top: 4px;
}
.read-marker {
  flex: none; width: calc(100% - 48px);
  display: flex; align-items: center; gap: 8px;
  margin: 8px 24px 6px;
  color: var(--accent); font-size: 12.5px; font-weight: 600;
}
.read-marker::before, .read-marker::after {
  content: ""; flex: 1; border-top: 2px dashed currentColor;
}
.read-marker-x {
  display: grid; place-items: center;
  width: var(--ico-4); height: var(--ico-4); margin-left: -4px; border-radius: 50%;
  color: inherit;
}
.read-marker-x svg { width: var(--ico-1); height: var(--ico-1); }
.read-marker-x:active { background: color-mix(in srgb, var(--accent-tint) 20%, transparent); }
@media (hover: hover) and (pointer: fine) {
  .read-marker-x:hover { background: color-mix(in srgb, var(--accent-tint) 16%, transparent); }
}
html[data-theme="light"] .unread-sep,
html[data-theme="light"] .read-marker { color: #155a9e; border-color: #155a9e; }

/* ---------- Message bubbles ---------- */
/* flex-wrap lets the 100%-width day chip take its own line inside the row */
.msg-row { display: flex; flex-wrap: wrap; padding: 2px 6px; position: relative; }
.msg-row.out { justify-content: flex-end; }
.msg-row.service { justify-content: flex-start; padding-right: 34px; }
.msg-row.selectable { cursor: pointer; border-radius: 8px; }
.msg-row.selected { background: color-mix(in srgb, var(--accent-tint) 16%, transparent); }

.bubble {
  position: relative;
  max-width: min(480px, 90%);
  background: var(--bg-bubble-in);
  border-radius: var(--radius-bubble);
  border-bottom-left-radius: var(--radius-tail);
  padding: 7px 10px 6px;
  box-shadow: 0 1px 2px rgba(16,16,16,.25);
  /* anywhere (not break-word): it also lowers the min-content width, so a
     long URL or token shrinks the bubble instead of widening it. */
  overflow-wrap: anywhere;
  animation: msgIn .12s ease-out;
  /* No paint containment: the reply track hangs 4px past the right edge
     and would be clipped. Rows must also never get content-visibility:
     the virtual scroller measures mounted rows itself, and a row collapsing
     to its intrinsic placeholder when scrolled out of view desyncs the
     scroller's height cache (scroll jumps on remount). */
  contain: layout style;
}
@keyframes msgIn { from { opacity: 0; transform: translateY(4px); } }
/* Touch: bubble text is NOT natively selectable — long-press belongs to the
   context menu, and a native word selection stole half the gesture (both
   fired at once). Select text in the message menu enters text-selection
   mode (chat-view _enterBubbleTextSelection flips .text-selecting on the
   .msg-text and inserts .msg-select-bar). Desktop keeps native mouse
   selection — hence the media gate. */
@media (hover: none) and (pointer: coarse) {
  .bubble, .bubble .msg-text { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
  .bubble .msg-text.text-selecting { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
}
.msg-select-bar {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 0 0 6px;
  -webkit-user-select: none; user-select: none;
}
.msg-select-btn {
  border: none; border-radius: 14px;
  min-height: 32px; padding: 4px 12px;
  background: rgba(244, 244, 244, .14); color: inherit;
  font: inherit; font-size: 13px; font-weight: 600;
  cursor: pointer;
}
.msg-row.selectable .msg-select-bar { display: none; }
html[data-theme="light"] .msg-select-btn { background: rgba(16, 16, 16, .08); }
/* Delete hold: a compact tombstone (label + Undo on one line), whatever the
   size of the bubble it replaces. */
.bubble.msg-deleted {
  display: flex; flex-flow: row wrap; align-items: center;
  gap: 4px 10px; box-sizing: border-box;
  /* Secondary text token (dim 80 / text 20): bare --text-dim measured
     2.41:1 on the dark outgoing bubble and 1.32:1 on brutal yellow. */
  color: color-mix(in srgb, var(--text-dim) 80%, var(--text));
  min-height: 0; max-width: min(100%, 320px); font-style: italic;
}
.msg-row.out .bubble.msg-deleted { color: color-mix(in srgb, var(--text-meta-out) 80%, var(--text)); }
html[data-theme="brutal"] .msg-row.out .bubble.msg-deleted { color: var(--nb-main-ink); }
.msg-deleted-label { font-size: 14px; line-height: 1.35; }
/* After the Undo window: one-line row (chat-view _collapseTombstones). */
.bubble.msg-deleted.is-compact { padding-top: 4px; padding-bottom: 4px; }
.bubble.msg-deleted.is-compact .msg-deleted-label { font-size: 13px; }
.msg-undo {
  position: relative; overflow: hidden;
  border: 0; border-radius: 14px;
  min-height: 32px; padding: 6px 16px 8px;
  background: rgba(244, 244, 244, .14); color: var(--text);
  font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
}
.msg-undo-bar {
  position: absolute; left: 0; bottom: 0; height: 2px; width: 100%;
  background: var(--accent); transform-origin: left center;
  animation: msg-undo-shrink linear forwards;
}
@keyframes msg-undo-shrink { from { transform: scaleX(1); } to { transform: scaleX(0); } }
html[data-theme="light"] .msg-undo { background: rgba(16, 16, 16, .08); }
html[data-theme="brutal"] .msg-row.out .msg-undo { background: rgba(18, 16, 22, .1); color: var(--nb-main-ink); }
.msg-row.out .bubble {
  background: var(--bg-bubble-out);
  border-bottom-left-radius: var(--radius-bubble);
  border-bottom-right-radius: var(--radius-tail);
}
.msg-row.with-avatar { align-items: flex-end; gap: 8px; }
/* reserve 42px avatar + 8px gap */
.msg-row.with-avatar .bubble { max-width: min(480px, calc(90% - 50px)); }

/* Desktop hover: a custom photo (the padded square on the color matrix)
   grows to fill its tile. Same rule on a message row, the chat header,
   and the profile sheet. hover:hover + pointer:fine keeps it off touch,
   where hover states stick. Transition lives in this block so touch
   never pays for it. Initials and fingerprint tiles have no photo. */
@media (hover: hover) and (pointer: fine) {
  .msg-row velta-avatar .velta-avatar-photo,
  .chat-head-avatar .velta-avatar-photo,
  velta-avatar.chat-info-avatar .velta-avatar-photo { transition: inset .12s ease, padding .12s ease; }
  .msg-row velta-avatar:hover .velta-avatar-photo,
  .chat-head-avatar:hover .velta-avatar-photo,
  velta-avatar.chat-info-avatar:hover .velta-avatar-photo { inset: 0; padding: 0; }
}

/* Saved copy: round chevron back to the original message (#19), the same
   arrow the desktop shortcut menu uses. Always visible — touch has no
   hover. Sits just outside the bubble; .has-original shrinks the bubble
   so the button stays inside the row. Hidden while selecting messages. */
.msg-show-in-chat {
  position: absolute; top: 0; z-index: 2;
  width: 36px; height: 36px; padding: 0;
  border: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: #262b38; color: var(--text); cursor: pointer;
}
.msg-row.out .msg-show-in-chat { left: -44px; }
.msg-row:not(.out) .msg-show-in-chat { right: -44px; }
.msg-show-in-chat svg { width: var(--ico-3); height: var(--ico-3); }
.msg-row.has-original .bubble { max-width: min(480px, calc(90% - 44px)); }
.msg-row.has-original.with-avatar .bubble { max-width: min(480px, calc(90% - 94px)); }
.msg-row.selectable .msg-show-in-chat { display: none; }
html[data-theme="light"] .msg-show-in-chat {
  background: #fbfbfc;
  box-shadow: 0 1px 4px rgba(16, 16, 16, .16);
}

/* One-click "Reply" pill (desktop only, issue #26). The track covers the
   bubble without taking layout space, and the button sticks inside
   .history-scroll for that whole height: it sits on the top-right until
   that corner scrolls away, then rides the top of the history area and
   leaves with the bubble. The track stays pointer-events: none so text
   selection still hits the bubble; the button opts back in on row hover.
   Hidden on touch, in selection mode, and in read-only chats. The 4px
   right overhang matches the old pill and stays inside the row padding. */
.msg-hover-reply-track {
  position: absolute; top: 0; bottom: 0; right: -4px;
  width: max-content; z-index: 3;
  pointer-events: none;
  display: flex; align-items: flex-start;
}
.msg-hover-reply {
  position: sticky; top: 8px; height: 24px;
  display: flex; width: max-content; align-items: center; gap: 4px;
  padding: 3px 10px 3px 8px; border-radius: 999px;
  background: #262b38; color: var(--text);
  border: 1px solid rgba(244, 244, 244, .18);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .35);
  font: inherit; font-size: 12.5px; line-height: 1.25; font-weight: 500;
  cursor: pointer;
  opacity: 0; pointer-events: none; transform: translateY(3px);
  transition: opacity .12s ease, transform .12s ease, background .12s ease;
}
.msg-hover-reply svg { width: var(--ico-1); height: var(--ico-1); flex: none; }
@media (hover: hover) and (pointer: fine) {
  .msg-row:hover .msg-hover-reply {
    opacity: 1; pointer-events: auto; transform: translateY(0);
  }
}
@media (hover: hover) and (pointer: fine) {
  .msg-hover-reply:hover { background: #303648; }
}
.msg-row.selectable .msg-hover-reply { display: none; }
/* The bubble action bar anchors above the bubble's top edge — right where
   the sticky chips ride. Hide the row's track while ITS bar is open, or the
   bar covers the ⋮ and clicks land on the bar instead. */
.msg-row.bar-open .msg-hover-reply-track { display: none; }
/* Read-only chats (device chats, channels without posting rights): no
   composer exists to reply into — hide the hover pill, the selection-quote
   chip and the selection bar's Reply button (flag set by chat-view.js). */
body.chat-read-only .msg-hover-reply,
body.chat-read-only #sel-quote-chip,
body.chat-read-only [data-sel="reply"] { display: none; }
/* Read-only chats (channels without posting rights, device chats) hide the
   composer with [hidden] - without extra bottom space the last message row
   sits flush against the viewport edge. Kept on the SCROLL container: the
   virtual scroller inline-writes padding-bottom onto .history itself
   (before/afterItemsHeight spacers), which would clobber a rule there. */
body.chat-read-only .history-scroll { padding-bottom: 34px; }
@media (hover: none) {
  .msg-hover-reply { display: none; }
}
html[data-theme="light"] .msg-hover-reply {
  background: #fbfbfc; border-color: rgba(16, 16, 16, .14);
  box-shadow: 0 2px 8px rgba(16, 16, 16, .16);
}
@media (hover: hover) and (pointer: fine) {
  html[data-theme="light"] .msg-hover-reply:hover { background: #f0f2f6; }
}
/* The ⋮ chip rides the reply track in read-only chats too (message actions
   still apply there) — un-hide it from the read-only rule above. */
body.chat-read-only .msg-hover-more { display: flex; }

/* Message action bar (desktop message menu as chips): opens on right-click
   or the ⋮ hover chip, right-aligned above the bubble's top edge; ui.js
   flips it below the bubble when the top would clip. Action chips keep the
   ctx-item icon+span shape so existing menu tooling and tests see labels. */
.msg-action-bar {
  display: flex; flex-direction: column; gap: 6px; min-width: 0;
  max-width: min(720px, calc(var(--vw) - 16px));
}
.msg-bar-chips, .msg-bar-reacts { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.msg-bar-chip {
  display: flex; align-items: center; gap: 7px;
  padding: 7px 10px; border-radius: 8px;
  font-size: 13.5px; text-align: left;
}
.msg-bar-chip svg { width: var(--ico-3); height: var(--ico-3); color: var(--text-dim); }
.msg-bar-chip.danger, .msg-bar-chip.danger svg { color: var(--danger); }
.msg-bar-react {
  display: grid; place-items: center; padding: 0;
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--bg-hover); color: var(--text);
  font-size: 19px; line-height: 1;
}
.msg-bar-react svg { width: 20px; height: 20px; color: var(--text-dim); }
.msg-bar-react.mine { background: color-mix(in srgb, var(--accent-tint) 18%, transparent); box-shadow: inset 0 0 0 2px var(--accent); }
@media (hover: hover) and (pointer: fine) {
  .msg-bar-chip:hover, .msg-bar-react:hover { background: var(--bg-hover); }
}
.msg-action-bar:has(.msg-emoji-tray:not([hidden])) { width: 352px; }
.msg-action-bar .msg-emoji-tray { margin: 0; }
.msg-row.selectable .msg-action-bar { display: none; }

/* Diagnostics entries render as console lines (Chrome DevTools look):
   monospace, left-aligned, in a soft pill, with a hover copy button
   in the row's top-right corner. */
.service-msg {
  font-family: Menlo, Consolas, "DejaVu Sans Mono", monospace;
  font-size: 12.5px; line-height: 1.5;
  color: #d7d7de;
  background: rgba(16,16,16,.28);
  padding: 4px 12px; margin: 3px 0; border-radius: 10px;
  max-width: 100%;
  white-space: pre-wrap; word-break: break-word;
}
html[data-theme="light"] .service-msg { background: rgba(244,244,244,.8); color: #464b55; }
/* Join/leave notices: involved contacts as profile chips (serviceChipsHtml). */
.service-msg .svc-chip { font: inherit; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); border: 0; border-radius: 999px; padding: 0 .45em; margin: 0 .05em; cursor: pointer; }
.service-msg .svc-chip:hover { background: color-mix(in srgb, var(--accent) 24%, transparent); }
.service-msg .svc-chip:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.service-msg .svc-chip.is-self { cursor: inherit; }
.service-msg .svc-chip.is-self:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.service-msg.webxdc-info { cursor: pointer; }
.service-msg.webxdc-info:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.service-msg .webxdc-info-icon { width: 1.25em; height: 1.25em; border-radius: 4px; object-fit: cover; vertical-align: -0.25em; margin-right: .35em; }
.diag-copy {
  position: absolute; top: 5px; right: 7px;
  width: var(--ico-4); height: var(--ico-4); padding: 0; border: none; border-radius: 6px;
  background: transparent; color: var(--text-dim);
  display: none; align-items: center; justify-content: center;
  cursor: pointer; font-size: 12px;
}
.diag-copy svg { width: var(--ico-1); height: var(--ico-1); }
@media (hover: hover) and (pointer: fine) {
  .msg-row.service:hover .diag-copy { display: flex; }
}
@media (hover: hover) and (pointer: fine) {
  .diag-copy:hover { background: var(--bg-hover); color: var(--text); }
}

.msg-sender {
  font-size: calc(var(--msg-font-size) * .871); font-weight: 600; margin-bottom: 2px;
}
.msg-sender { color: var(--sender, var(--accent-text)); }
html[data-theme="light"] .msg-sender { color: var(--sender-light, var(--sender, var(--accent-text))); }
html[data-theme="brutal"] .msg-sender { color: var(--sender-brutal, var(--sender, var(--accent-text))); }
.msg-fwd {
  /* Meta-dim colors, not --accent: accent blue is unreadable on the
     outgoing bubble's blue background. */
  font-size: calc(var(--msg-font-size) * .839); color: var(--text-meta-in); /* 13px at 15.5 */
  font-style: italic; margin-bottom: 5px;
}
.msg-row.out .msg-fwd { color: var(--text-meta-out); }
/* Keep a fwd/quote label tightly coupled to the media it labels. */
.msg-fwd + .msg-video, .msg-fwd + .msg-image, .msg-fwd + .msg-audio { margin-top: 0; }
.msg-quote + .msg-video, .msg-quote + .msg-image, .msg-quote + .msg-audio { margin-top: 0; }
.msg-quote {
  border-left: 2.5px solid var(--accent);
  background: var(--bg-reply);
  border-radius: var(--radius-quote);
  padding: 4px 8px; margin-bottom: 5px;
  font-size: calc(var(--msg-font-size) * .871); cursor: pointer; /* 13.5px at 15.5 */
}
.msg-quote .q-name { font-weight: 600; color: var(--quote-name); display: block; }
/* Quote text: measured WCAG ratios vs each bubble's quote background
   (accent-on-blue was 2.58:1, text-dim-on-blue 2.08:1 — below AA 4.5).
   Outgoing bubbles get a dedicated light-on-blue palette. */
.msg-quote .q-text { color: #9aa3b5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 320px; display: block; }
.msg-row.out .msg-quote .q-name { color: var(--quote-name-out); }
.msg-row.out .msg-quote .q-text { color: #cfe0f2; }
html[data-theme="light"] .msg-quote .q-text { color: #4d5a68; }
/* same specificity as the dark outgoing rules, declared later — wins in light */
html[data-theme="light"] .msg-row.out .msg-quote .q-text { color: #4d5a68; }

.msg-text {
  font-size: var(--msg-font-size); line-height: 1.36; white-space: pre-wrap;
  /* Hyphenation (EN/RU): chat-view sets lang per message (text-lang.js), the
     engine picks the dictionary. overflow-wrap: anywhere (on .bubble) still
     breaks strings no dictionary knows. html.no-hyphens = drawer switch off. */
  -webkit-hyphens: auto; hyphens: auto;
}
html.no-hyphens .msg-text { -webkit-hyphens: manual; hyphens: manual; }
/* Never hyphenate machine text: a hyphen inside a URL, address, command or
   code changes what gets copied. */
.msg-text a, .msg-text code, .msg-text pre, .msg-text kbd, .msg-text samp,
.msg-text .hashtag, .msg-text .msg-nohyph, .msg-text .msg-code, .msg-cmd { -webkit-hyphens: manual; hyphens: manual; }
.msg-text a {
  color: var(--accent-text-in);
  /* A swipe may start on an inline link (#35): no native link drag. Text
     selection / long-press are untouched (user-select is not changed). */
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
/* Outgoing bubble is blue — the accent link vanishes on it (blue on blue,
   ~3:1 dark / ~2.9:1 light, both under the 4.5:1 AA text bar). Use the
   established light-on-blue bubble palette (same tones as .msg-row.out
   blockquote/quote text): 6.7:1 dark, 7.3:1 light. */
.msg-row.out .msg-text a, .msg-row.out .msg-text .hashtag { color: var(--out-link); }
/* #134: #hashtags are native <button>s (focusable, Enter/Space) styled as
   inline links; tap = search the tag in this chat. */
.msg-text .hashtag {
  color: var(--accent); -webkit-touch-callout: none;
  font: inherit; background: none; border: 0; padding: 0; margin: 0;
  display: inline; cursor: pointer; text-decoration: underline;
  text-underline-offset: 2px; white-space: inherit;
}
.msg-text .hashtag:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; border-radius: 2px; }
/* #156: `code` spans are native <button>s (focusable, Enter/Space) in a mono
   pill; tap = copy the text. */
.msg-text .msg-code {
  font-family: var(--font-mono); font-size: .92em;
  color: var(--text); background: var(--bg-hover);
  border: 0; border-radius: 4px; padding: 0 4px; margin: 0;
  display: inline; cursor: copy; white-space: inherit;
  -webkit-touch-callout: none;
}
.msg-text .msg-code:focus-visible { outline: 2px solid currentColor; outline-offset: 1px; border-radius: 4px; }
/* Markdown lists (markdown.js emits <ul>/<ol> as block units; the surrounding
   msg-text keeps pre-wrap for plain paragraphs) */
.msg-text ul, .msg-text ol { margin: 2px 0; padding-left: 20px; }
.msg-text blockquote {
  margin: 2px 0; padding: 2px 0 2px 10px;
  border-left: 3px solid var(--accent);
  color: var(--text-dim);
}
/* Outgoing bubbles sit on the blue bg — silver text-dim and the accent border
   don't read there (same treatment as .msg-row.out .msg-quote). */
.msg-row.out .msg-text blockquote {
  border-left-color: rgba(244,244,244,.55);
  color: #dfeaf6;
}
html[data-theme="light"] .msg-row.out .msg-text blockquote {
  border-left-color: #9ec3e8;
  color: #4d5a68;
}

/* Floating "Reply" chip when text is selected inside a bubble (fragment quote) */
#sel-quote-chip {
  position: fixed; z-index: 860;
  border: 0; border-radius: 16px; padding: 6px 14px;
  background: var(--accent); color: var(--on-accent);
  font-size: 13px; font-weight: 600;
  box-shadow: var(--shadow); cursor: pointer;
}
.msg-text li { margin: 4px 0; display: list-item; }
.msg-text li:first-child { margin-top: 0; }
.msg-text li:last-child { margin-bottom: 0; }
.msg-text u { text-underline-offset: 2px; }

/* Invite card: "Pavel invited you to a Chat RU group" / "Chat with Pavel",
   copy/share icons on the right. Rendered instead of the raw invite link.
   Neutral --bg-reply surface over an opaque --bg-bubble-in (same on incoming
   and outgoing bubbles). The whole text area joins, but only its chevron uses
   accent so an invite does not outrank the conversation. Brutal keeps the ink
   border/hard shadow and a small yellow cue. AA pinned by invite-cards tests. */
.invite-card {
  display: flex; align-items: stretch; gap: 0;
  margin: 6px 0; max-width: 100%;
  background: linear-gradient(var(--bg-reply), var(--bg-reply)), var(--bg-bubble-in);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px; overflow: hidden;
  vertical-align: middle;
}
.invite-main {
  flex: 1; min-width: 0; position: relative;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 8px 32px 8px 10px; text-align: left;
  background: transparent; border: none; cursor: pointer;
  color: var(--text); font: inherit; font-size: 15px; line-height: 1.35;
  user-select: none; -webkit-user-select: none;
}
html .invite-card .invite-main:active { background: var(--bg-hover); }
@media (hover: hover) and (pointer: fine) {
  html .invite-card button.invite-main:hover { background: var(--bg-hover); }
}
.invite-main:focus-visible, .invite-copy:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -3px; }
html .invite-card .invite-line, html .invite-card .invite-line b { color: var(--text); }
.invite-line b { font-weight: 700; }
.invite-sub {
  font-size: 12.5px; color: var(--text-dim);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.invite-action {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  display: flex; color: var(--accent-text-in); pointer-events: none;
}
.invite-action svg { width: var(--ico-2); height: var(--ico-2); }
.invite-copy {
  display: flex; align-items: center;
  padding: 0 11px; border: none; border-left: 1px solid var(--border);
  background: none; cursor: pointer;
  color: var(--text);
}
html .invite-card .invite-copy:active { color: var(--text); background: var(--bg-hover); }
@media (hover: hover) and (pointer: fine) {
  html .invite-card .invite-copy:hover { color: var(--text); background: var(--bg-hover); }
}
.invite-copy svg { width: var(--ico-2); height: var(--ico-2); }
html[data-theme="brutal"] .invite-card { border: 2px solid var(--nb-ink); box-shadow: 3px 3px 0 var(--nb-ink); border-radius: var(--radius-bubble); }
html[data-theme="brutal"] .invite-card .invite-main { background: transparent; border-right: 2px solid var(--nb-ink); }
html[data-theme="brutal"] .invite-card .invite-main:active { background: var(--bg-hover); box-shadow: inset 2px 2px 0 var(--nb-ink); }
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .invite-card button.invite-main:hover { background: var(--bg-hover); }
}
html[data-theme="brutal"] .invite-card .invite-line, html[data-theme="brutal"] .invite-card .invite-line b { color: var(--text); }
html[data-theme="brutal"] .invite-card .invite-sub { color: var(--text-dim); }
html[data-theme="brutal"] .invite-card .invite-action { color: var(--nb-main); }
html[data-theme="brutal"] .invite-card .invite-copy { border-left: 0; }
html[data-theme="brutal"] .invite-card .invite-copy + .invite-copy { border-left: 2px solid var(--nb-ink); }
/* Velta invite cards (invite-cards.js, docs/invite-cards.md) and the Cryptex
   download card (chat-view.js cryptexCardHtml): theme tokens, like .lang-card
   (velta.cc/design). Surface = --bg-reply over an opaque --bg-bubble-in, so the
   colors are the same on incoming and outgoing bubbles (brutal yellow too).
   Text --text, secondary --text-dim, primary button = .btn-primary look
   (--accent / --on-accent, radius 8, 15px), others outlined. Expired cards stay
   readable (no opacity); only icons/title switch to --text-dim.
   Contrast pinned by tests/invite-cards.test.mjs (AA in light/dark/brutal). */
.ivc {
  display: flex; flex-direction: column; gap: 6px;
  margin: 8px 0 2px; padding: 10px 12px; max-width: 360px;
  white-space: normal; text-align: left;
  background: linear-gradient(var(--bg-reply), var(--bg-reply)), var(--bg-bubble-in);
  color: var(--text);
  /* secondary text: --text-dim nudged toward --text (dark --text-dim is 4.3:1 here) */
  --ivc-dim: color-mix(in srgb, var(--text-dim) 80%, var(--text));
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 14px; line-height: 1.35;
}
.ivc svg { width: 18px; height: 18px; flex: none; }
.ivc-head { display: flex; align-items: center; gap: 8px; }
.ivc-head svg { width: 22px; height: 22px; color: var(--accent-text-in); }
.ivc-title { flex: 1; min-width: 0; font-weight: 700; overflow-wrap: anywhere; color: var(--text); }
.ivc-badge {
  flex: none; padding: 1px 8px; border-radius: 999px;
  font-size: 12px; font-weight: 700; color: var(--on-badge-muted); background: var(--text-dim);
}
.ivc-from { margin: 0; font-size: 12.5px; color: var(--ivc-dim); }
.ivc .ivc-meta { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.ivc .ivc-meta li { display: flex; margin: 0; align-items: center; gap: 7px; color: var(--text); overflow-wrap: anywhere; }
.ivc-meta li svg { color: var(--text-dim); }
.ivc-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
html .ivc .ivc-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 36px; min-width: 36px; padding: 6px 12px;
  border-radius: 8px; border: 1px solid var(--text-dim);
  background: transparent; color: var(--text); font: inherit; font-weight: 600; cursor: pointer;
}
.ivc-btn span:empty { display: none; }
html .ivc .ivc-btn.ivc-primary:not([disabled]) {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
  font-size: 15px; padding: 6px 16px;
}
html .ivc .ivc-btn:not([disabled]):active { background: var(--bg-hover); }
html .ivc .ivc-btn.ivc-primary:not([disabled]):active { background: var(--accent-active); }
@media (hover: hover) and (pointer: fine) {
  html .ivc .ivc-btn:not([disabled]):hover { background: var(--bg-hover); }
  html .ivc .ivc-btn.ivc-primary:not([disabled]):hover { background: var(--accent-hover); }
}
.ivc-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
html .ivc .ivc-btn[disabled] { cursor: not-allowed; background: transparent; border-color: var(--border); color: var(--ivc-dim); }
.ivc-link {
  display: block; padding: 6px 8px; border-radius: 6px;
  background: var(--bg-reply); color: var(--text);
  font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; user-select: all;
}
.ivc-link[hidden] { display: none; }
.ivc-expired .ivc-head svg, .ivc-expired .ivc-meta li svg { color: var(--text-dim); }
html[data-theme="brutal"] .ivc { border: 2px solid var(--nb-ink); box-shadow: 3px 3px 0 var(--nb-ink); border-radius: var(--radius-bubble); }
html[data-theme="brutal"] .ivc .ivc-btn { border-width: 2px; }
html[data-theme="brutal"] .ivc .ivc-btn.ivc-primary:not([disabled]) {
  border: 2px solid var(--nb-ink); background: var(--nb-main); color: var(--nb-main-ink); box-shadow: 3px 3px 0 var(--nb-ink);
}
html[data-theme="brutal"] .ivc .ivc-btn.ivc-primary:not([disabled]):active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--nb-ink); background: var(--nb-main); }
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .ivc .ivc-btn.ivc-primary:not([disabled]):hover { background: color-mix(in srgb, var(--nb-main) 88%, #fff); }
}
/* Token-free stand-in for an invite URL inside the message text. */
.ivc-chip {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: middle;
  padding: 0 6px; border-radius: 6px; font-size: 13px;
  background: rgba(127, 127, 127, .18);
}
.ivc-chip svg { width: 14px; height: 14px; }
/* Short-link placeholder while the full invite URL is being fetched. */
/* Pending: not clickable yet, so the card surface instead of the accent fill. */
html .invite-card.invite-pending .invite-main { cursor: progress; background: transparent; }
html .invite-card.invite-pending .invite-line { color: var(--text); font-style: italic; }
html .invite-card.invite-pending .invite-sub { color: color-mix(in srgb, var(--text-dim) 80%, var(--text)); }
/* Shared-contact card avatar: compact fixed-size tile, centered next to the
   text — never stretches the card (fixed dimensions, no aspect/intrinsic
   sizing from the decoded image). */
.vcard-avatar {
  flex: none; align-self: center;
  width: 36px; height: 36px; margin-left: 8px;
  display: flex; align-items: center; justify-content: center;
  background: #777; overflow: hidden; border-radius: 8px;
  font-size: 20px; font-weight: 600; color: #f4f4f4;
  user-select: none; -webkit-user-select: none;
}
.vcard-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.msg-meta {
  float: right; display: inline-flex; align-items: center; gap: 3px;
  font-size: 11.5px; color: var(--text-meta-in);
  margin: 6px 0 0 10px; transform: translateY(2px);
  user-select: none;
}
.msg-row.out .msg-meta { color: var(--text-meta-out); }
.msg-meta .edited { font-style: normal; }
/* Reply threads (Velta #14). Passive "N replies" count next to the
   timestamp: plain meta text (inherits --text-meta-in/-out, same contrast as
   the time), not a tap target. Shown after the time on every screen size; the round
   side button below opens the thread. */
.msg-replies-count { margin: 0 0 0 4px; white-space: nowrap; }
/* Round side button beside a bubble with replies (THREAD_BUTTON_MIN_REPLIES
   in chat-view.js): 32px visual disc inside a 44px hit box, on the open
   side (right of incoming, left of outgoing), bottom-aligned with the bubble.
   .msg-row wraps, and that wrap is chosen from the bubble's full width, so
   min-width:0 never stopped the button falling under the bubble on a phone.
   The pair is its own nowrap line; flex-basis 0 lets that line share the
   row with the avatar instead of wrapping under it.
   min-width 50% (not 0): with a 0 basis AND 0 min the line's hypothetical
   size is 0, so a full-width .unread-sep / .read-marker in the same row
   (100% outer) still "fits" beside it on ONE flex line — the line then got
   0px, and the bubble shrank to its overflow-wrap:anywhere min-content, one
   letter per line. A non-zero floor pushes the separator onto its own line;
   50% still leaves room for the avatar. */
.msg-row.has-thread { align-items: flex-end; }
.msg-thread-line {
  display: flex; flex-wrap: nowrap; align-items: flex-end;
  min-width: 50%; max-width: 100%; flex: 1 1 0;
}
.msg-row.out .msg-thread-line { justify-content: flex-end; }
.msg-row.has-thread .bubble { max-width: min(480px, calc(100% - 48px)); min-width: 0; }
.msg-thread-btn {
  flex: none; align-self: flex-end;
  width: 44px; height: 44px; margin: 0 2px; padding: 0;
  border: 0; background: none; cursor: pointer;
  display: grid; place-items: center;
  color: var(--text); font: inherit;
  -webkit-tap-highlight-color: transparent;
}
.msg-thread-btn::before {
  align-self: end; content: ""; grid-area: 1 / 1; width: 32px; height: 32px; border-radius: 16px;
  /* Attached look: the corner touching the bubble's bottom is sharp.
     Logical corners, so RTL (row reversed) mirrors it automatically. */
  border-end-start-radius: 0;
  background: var(--bg-bubble-in); box-shadow: var(--shadow);
  border: 1px solid var(--border);
}
.msg-row.out .msg-thread-btn::before { border-end-start-radius: 16px; border-end-end-radius: 0; }
.msg-thread-btn { place-items: end center; }
.msg-thread-btn svg { grid-area: 1 / 1; position: relative; width: 16px; height: 16px; margin-bottom: 8px; }
.msg-thread-btn:focus-visible { outline: none; }
.msg-thread-btn:focus-visible::before { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .msg-thread-btn:hover::before { background: color-mix(in srgb, var(--text) 8%, var(--bg-bubble-in)); }
}
/* Thread panel: non-modal bottom sheet between the history and the
   composer, 70% of the chat view, own scroll; the composer stays below. */
.thread-panel {
  flex: 0 0 70%; min-height: 0; max-height: 70%;
  display: flex; flex-direction: column;
  margin-top: -18px; position: relative; z-index: 4;
  background: var(--bg-chat);
  border: 1px solid var(--border); border-bottom: 0;
  border-radius: 18px 18px 0 0;
  /* Top-cast shadow: the sheet reads as a layer floating OVER the chat. */
  box-shadow: 0 -14px 28px rgba(16,16,16,.4), 0 2px 8px rgba(16,16,16,.2);
  animation: sheetIn .16s ease-out;
}
.thread-panel[hidden] { display: none; }
/* Scrim over the covered history: a 0-height sticky pseudo-element pinned
   to the scrollport bottom (its normal position is the content end, so it
   travels the whole scroll range) whose outward shadow fills the visible
   history. It is the scroller's LAST child, so it paints above the bubbles;
   an inset shadow would paint beneath them. */
#chat-view:has(#thread-panel:not([hidden])) #history-scroll::after {
  content: ""; position: sticky; bottom: 0; display: block; height: 0;
  box-shadow: 0 0 0 100vmax rgba(16,16,16,.22);
  pointer-events: none;
}
html[data-theme="light"] #chat-view:has(#thread-panel:not([hidden])) #history-scroll::after {
  box-shadow: 0 0 0 100vmax rgba(16,16,16,.12);
}
/* The go-down pill would float over the sheet header. */
#chat-view:has(#thread-panel:not([hidden])) #btn-go-down { display: none; }
.thread-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 8px 10px 16px;
  background: var(--bg-sidebar);
  border-bottom: 1px solid var(--border);
  border-radius: 18px 18px 0 0;
}
.thread-titles { flex: 1; min-width: 0; }
.thread-title { margin: 0; font-size: 16px; font-weight: 600; color: var(--text); }
.thread-count { font-weight: 400; color: var(--text-dim); }
.thread-orig {
  font-size: 13px; color: var(--text-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.thread-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 6px 0; }
.thread-sep { border-top: 1px solid var(--border); margin: 8px 12px; }
/* Tiny "bot" badge next to the timestamp: bordered uppercase tag so it reads
   as a label, not text. Inherits the meta color — visible on every theme's
   incoming bubble (contrast held by the meta color, ≥4.5:1 like the time). */
/* Tiny "bot" badge next to the timestamp: bordered uppercase tag so it reads
   as a label, not text. No letter-spacing — CSS tracking applies after the
   LAST glyph too, which left the tag visibly lopsided at this size. Inherits
   the meta color — visible on every theme's incoming bubble (contrast held
   by the meta color, ≥4.5:1 like the time). */
/* Issue #13: the archived-folder header button hides entirely when the
   account has no archived chats (display beats [hidden] — §6.2). */
.sidebar-head .icon-btn[hidden] { display: none; }
.sidebar-head .icon-btn.active { color: var(--accent); }
/* Relays modal: per-relay status hint under the address (issue #11). */
.relay-row-status {
  display: block; font-size: 11.5px; font-weight: 400;
  color: var(--text-dim); margin-top: 1px;
}
.archived-sub {
  font-size: 12px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Call message card (issue #8): the core's stock "Missed call" strings
   render as a labeled card with direction/state/duration. Red tint for
   missed/declined/canceled via .missed. */
.call-card {
  display: flex; align-items: center; gap: 10px;
  min-width: min(190px, var(--bubble-inner-max)); padding: 10px 12px;
}
.call-card-ico {
  width: 34px; height: 34px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-hover); color: var(--accent);
}
.call-card-ico svg { width: var(--ico-2); height: var(--ico-2); }
.call-card-main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.call-card-label { font-weight: 600; font-size: 13.5px; }
.call-card-sub { font-size: 12px; color: var(--text-dim); }
.call-card.missed .call-card-ico { color: var(--danger); }
.call-card.missed .call-card-label { color: var(--danger); }
.msg-meta .bot-chip {
  font-size: 8.5px; font-weight: 700;
  text-transform: uppercase; line-height: 1;
  padding: 1.5px 4px; border: 1px solid currentColor; border-radius: 4px;
  opacity: .85; cursor: default;
}
.msg-meta .admin-chip {
  display: inline-flex; align-items: center; gap: 2px;
  font-size: 8.5px; font-weight: 700;
  text-transform: uppercase; line-height: 1;
  padding: 1.5px 4px; border: 1px solid currentColor; border-radius: 4px;
  cursor: default; /* no .85 fade like .bot-chip: it drops below 4.5:1 in dark (WCAG AA) */
}
.msg-meta .admin-chip svg { width: 9px; height: 9px; }
.msg-meta svg { width: var(--ico-2); height: var(--ico-2); }
/* The ticks are a 3:2 glyph letterboxed in a 16px square (drawn height
   ~10.7px), while the timestamp digits' optical center sits ~1.5px above
   their line-box center (no descenders) — without the nudge the ticks read
   visibly low vs the time. */
.msg-meta .ticks-slot { display: inline-flex; transform: translateY(-1.5px); }
.msg-meta .ticks.read { color: var(--ticks-read); }
/* Failed send: the red exclamation in the tick slot (the bubble additionally
   carries the reason badge + retry/remove). Also used in the chat list. */
.ticks.failed, .ci-ticks.failed { color: var(--danger); }
.msg-meta .star-ico { width: var(--ico-1); height: var(--ico-1); }

/* In-flight send: dotted ring from ticksSvg("pending") spins in place. */
.msg-meta .ticks-spin { animation: ticks-spin .9s linear infinite; }
@keyframes ticks-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .msg-meta .ticks-spin { animation: none; } }

/* Failed send: small red reason badge on its own line below the message
   content (clears the right-floating timestamp) + retry/remove buttons
   in a column left of the bubble (out rows right-justify; the actions div
   precedes the bubble so it lands on the bubble's left side). */
.msg-fail-badge {
  display: block; clear: both; float: none;
  width: fit-content; max-width: 100%;
  margin: 6px 0 0; padding: 1px 8px;
  font-size: 11.5px; line-height: 16px; border-radius: 8px;
  background: rgba(229, 72, 77, .16); color: #f2b8bb;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
html[data-theme="light"] .msg-fail-badge { color: #a4262b; }
html[data-theme="brutal"] .msg-fail-badge { color: #3d3405; background: rgba(229, 72, 77, .24); }
.msg-fail-actions {
  display: flex; flex-direction: column; gap: 6px;
  align-self: center; margin-right: 6px;
}
.msg-fail-actions button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 5px;
  border: none; background: none; cursor: pointer; border-radius: 50%;
  color: var(--danger); user-select: none;
}
.msg-fail-actions svg { width: var(--ico-2); height: var(--ico-2); display: block; }
@media (hover: hover) and (pointer: fine) {
  .msg-fail-actions button:hover { background: rgba(229, 72, 77, .14); }
}

/* Media blocks (image/video/audio) BLEED 5px left/right past the bubble
   padding (bubble: 7px 10px 6px) - edge-to-edge like the official clients.
   The bubble's contain: layout style clips nothing (no paint containment),
   so the bleed renders; keep the pair in sync if bubble padding changes. */
.msg-image {
  max-width: 100%;
  border-radius: var(--radius-media); margin: 2px -5px 5px;
  display: grid; place-items: center; color: #f4f4f4;
  font-size: 13px; text-shadow: 0 1px 3px rgba(16,16,16,.5);
  background: var(--bg-hover); background-size: cover; position: relative; overflow: hidden;
}
/* Stickers float on the chat background like the official client: no bubble
   fill, no chrome. The time/ticks meta keeps its theme colors — readable on
   both themes since it sits where the chat background shows through. Two
   rules out-specificity `.msg-row.out .bubble` (341) and the brutal theme
   override (1619); the transparent background must beat both. */
.msg-row .bubble.sticker { background: transparent; box-shadow: none; padding: 0; }
.msg-row.out .bubble.sticker { background: transparent; }
.bubble.sticker .msg-image { margin: 0; background: none; overflow: visible; }
.bubble.sticker .img-wrap { border-radius: 0; background: none; }
/* Transparent sticker PNGs must letterbox, never crop (photo img uses cover
   to fill the reserved box). */
.bubble.sticker .img-wrap img { object-fit: contain; }
/* Animated image loading: the wrap reserves the final box (aspect-ratio from
   the message's dimensions, else a 4/3 guess until decode) so the row height
   is stable while the file loads; the image then fades in over the shimmer.
   The reserved geometry reproduces the old natural-size-capped img box:
   min(natural px, bubble width, max-height * ratio). Width stays px-only
   (#27): a % term inside min() collapses in Safari's shrink-to-fit bubble —
   max-width clamps the narrow-bubble case instead. */
.msg-image .img-wrap {
  position: relative; overflow: hidden; border-radius: var(--radius-media);
  width: 320px; max-width: 100%; aspect-ratio: 4 / 3;
  background: var(--bg-hover);
}
.msg-image .img-wrap img {
  width: 100%; height: 100%; display: block; object-fit: cover;
  opacity: 0; transform: scale(1.03);
  transition: opacity .35s ease, transform .45s cubic-bezier(.2, .7, .3, 1);
}
/* Landscape photos show the whole frame. Cover would clip the sides when
   the reserved box is taller than the picture. */
.msg-image .img-wrap.is-land img { object-fit: contain; }
/* Unknown size (core had no dimensions): the 4/3 placeholder IS the final
   box — show the whole picture letterboxed instead of resizing the row. */
.msg-image .img-wrap.no-dims img { object-fit: contain; }
/* Chromium only. Safari keeps the px-only inline width: a percentage inside
   min() collapses its shrink-to-fit bubble (#27). The bubble width is set
   first so 100% is a real length. For a 960×376 photo this is
   min(960px, 100%, 2.5532 * min(450px, 45vh)). */
html[data-engine="chromium"] .bubble:has(.img-wrap.is-land) { width: min(480px, 100%); }
html[data-engine="chromium"] .img-wrap.is-land.ready {
  width: min(var(--img-w, 480px), 100%, calc(min(var(--img-cap, 450px), calc(var(--vh) * .45)) * var(--img-r, 1))) !important;
}
.msg-image .img-wrap.ready img { opacity: 1; transform: none; }
.img-ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: #8b93a1; background: var(--bg-hover); overflow: hidden;
}
.img-ph::after {
  content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(100deg, transparent 25%, rgba(255,255,255,.08) 45%, rgba(255,255,255,.14) 50%, rgba(255,255,255,.08) 55%, transparent 75%);
  animation: img-shimmer 1.5s ease-in-out infinite;
}
@keyframes img-shimmer { to { transform: translateX(100%); } }
/* Native QR scanning (Android barcode-scanner plugin): the plugin turns the
   webview background transparent so the camera feed shows through — hide the
   opaque app chrome while a scan is active. */
body.native-scanning,
body.native-scanning #app {
  background: transparent !important;
}

.img-ph-ico { width: 34px; height: 34px; opacity: .5; }
.img-ph-ico svg { width: 100%; height: 100%; }
.img-wrap.ready .img-ph { opacity: 0; transition: opacity .3s ease; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .img-ph::after { animation: none; }
  .msg-image .img-wrap img { transition: none; transform: none; }
}
.msg-video {
  /* Positive top margin: a negative one let the box overlap the sender/
     quote line above. */
  width: auto; max-width: 100%; margin: 2px -5px 5px;
  border-radius: var(--radius-media); overflow: hidden; background: #101010;
}
/* Fingerprint block reused inside the chat-info profile rows. */
.avatar-profile-fpr { font-size: 12.5px; color: var(--text-dim); text-align: center; line-height: 1.55; white-space: pre-line; }
.msg-video video {
  width: 100%; min-width: min(200px, var(--bubble-inner-max)); height: 260px; display: block;
  /* JS overrides .msg-video to a true aspect box when the message carries
     dimensions (style="width:min(Wpx,480px,calc(min(var(--vh)*.45,260px)*W/H));
     aspect-ratio:W/H; max-width:100%") - then height:100% fills the box.
     Without it: the fixed 260px band. Either way the row height is
     deterministic (virtual-scroller safe). */
  object-fit: contain; background: #101010;
}
.msg-video[style] video { height: 100%; }
.msg-video:not([style]) { min-width: min(200px, var(--bubble-inner-max)); height: 260px; } /* fixed band until metadata shapes it */
velta-video { display: block; height: 100%; }
/* Native-preload video card: the <video> IS the poster (first frame via
   preload=metadata + #t fragment). Badges overlay the playing frame. */
.velta-video-card { position: relative; width: 100%; height: 100%; }
.velta-video-card video {
  display: block; width: 100%; height: 100%;
  background: #101010;
  /* A swipe may start on the poster (#139): no native video drag. */
  -webkit-user-drag: none;
  /* min-width: 200px from the band rules must NOT reach inside a styled
     box: on portrait videos it inflates the element past the host
     (200×355 under a 146×260 clipped box) and the play button centers on
     the clipped-away region — below center. The band's floor lives on
     .msg-video:not([style]). */
  min-width: 0;
}
/* Centered play affordance while paused: icon markup from the component,
   hidden once the video plays, back on pause. pointer-events: none — taps
   fall through to the component's own lightbox routing. */
.velta-video-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1;
  width: 54px; height: 54px; border-radius: 50%; pointer-events: none;
  background: rgba(11, 11, 16, 0.55); display: grid; place-items: center;
  padding: 15px; color: #f4f4f4;
}
.velta-video-play svg { width: 100%; height: 100%; display: block; }
.velta-video-card.playing .velta-video-play { display: none; }
.velta-video-size {
  position: absolute; top: 8px; left: 8px; z-index: 1;
  background: rgba(11, 11, 16, 0.86); border-radius: 6px;
  font-size: 12px; line-height: 1; padding: 4px 6px; color: #f2f2f5;
}
.velta-video-dur {
  position: absolute; right: 8px; bottom: 8px; z-index: 1;
  background: rgba(11, 11, 16, 0.86); border-radius: 6px;
  font-size: 12px; line-height: 1; padding: 4px 6px; color: #f2f2f5;
}
/* Fixed width: the player never collapses (outgoing bubbles shrink-wrap). */
.msg-audio {
  width: 100%; max-width: 320px; min-width: min(240px, var(--bubble-inner-max)); margin: 2px -5px 5px; box-sizing: border-box;
}
.msg-audio audio { display: none; }
.vp { display: flex; align-items: center; gap: 8px; width: 100%; height: 40px; padding: 0 10px 0 4px; box-sizing: border-box; border-radius: 20px; background: rgba(0,0,0,.18); }
.vp-play { flex: none; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--accent); color: var(--on-accent); }
.vp-play svg { width: 20px; height: 20px; }
.vp-seek { flex: 1; min-width: 0; margin: 0; accent-color: var(--accent); }
.vp-time { flex: none; font-size: 12px; font-variant-numeric: tabular-nums; opacity: .85; white-space: nowrap; }
.msg-file {
  display: flex; align-items: center; gap: 10px; margin-bottom: 4px; min-width: min(210px, var(--bubble-inner-max));
}
.msg-file.download-btn { cursor: pointer; }
.msg-file .file-ico {
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--accent); color: var(--on-accent);
  display: grid; place-items: center; flex: none;
}
.msg-file .file-ico svg { width: var(--ico-4); height: var(--ico-4); }
.msg-file .file-name { font-size: 14.5px; font-weight: 500; }
/* The name/size column is a flex child: without min-width: 0 a long file or
   app name (no spaces) sets the card's min-content and overflows the bubble. */
.msg-file > div:not(.file-ico), .msg-webxdc > div:not(.webxdc-ico) { min-width: 0; overflow-wrap: anywhere; }
.msg-file .file-size { font-size: 12.5px; color: var(--text-dim); }
.msg-row.out .msg-file .file-size { color: var(--text-meta-out); }

.msg-voice { display: flex; align-items: center; gap: 10px; min-width: min(220px, var(--bubble-inner-max)); margin-bottom: 2px; }
.msg-voice .play {
  width: 42px; height: 42px; border-radius: 50%; background: var(--accent);
  color: #f4f4f4; display: grid; place-items: center; flex: none;
}
.msg-voice .wave { display: flex; align-items: center; gap: 2px; height: 26px; }
.msg-voice .wave i { width: 3px; background: currentColor; opacity: .55; border-radius: 2px; }
.msg-voice .vtime { font-size: 12px; color: var(--text-dim); align-self: flex-end; }

/* Reactions follow the message text size (Chat appearance): the chips
   inherit 16px at the 15.5px default, so they keep that 16 : 15.5 ratio and
   their spacing is in em (= the old 4/5/2/8/12px at the default size). */
.msg-reactions {
  display: flex; flex-wrap: wrap; gap: .25em; margin-top: .3125em;
  font-size: calc(var(--msg-font-size) * 16 / 15.5); max-width: 100%;
}
.reaction-chip {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--bg-reply); border: 1px solid var(--border);
  border-radius: 12px; padding: 2px 8px; font-size: 13px;
  cursor: pointer; user-select: none;
}
button.reaction-chip { font: inherit; color: inherit; }
.reaction-chip.mine { border-color: var(--accent); background: color-mix(in srgb, var(--accent-tint) 18%, transparent); }
.msg-reactions .reaction-chip { gap: .25em; padding: .125em .5em; border-radius: .75em; max-width: 100%; min-width: 0; }

.msg-checkbox {
  appearance: none; -webkit-appearance: none;
  align-self: center; margin: 0 8px 0 0; padding: 0;
  width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--text-dim); flex: none;
  display: grid; place-items: center; color: #f4f4f4;
  background: transparent;
}
.msg-row.selected .msg-checkbox,
.msg-checkbox:checked { background: var(--accent); border-color: var(--accent); }
.msg-checkbox:checked::after {
  content: ""; width: 5px; height: 9px;
  border: solid #f4f4f4; border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}

/* ---------- Go-down button ---------- */
/* Sticky, not absolute: the button lives inside the scroll container, where
   an absolute box scrolls away with the content. The -52px top margin keeps
   it out of the content height (no scrollHeight change when it toggles). */
.go-down {
  position: sticky; bottom: calc(18px + env(safe-area-inset-bottom));
  margin: -52px 22px 0 auto;
  width: 52px; height: 52px; border-radius: 50%;
  background: var(--bg-sidebar); color: var(--text-dim);
  box-shadow: var(--shadow); display: grid; place-items: center;
  z-index: 3;
}
.go-down-badge {
  position: absolute; top: -5px; right: -5px;
  background: var(--accent); color: var(--on-accent);
  font-size: 12px; font-weight: 600;
  min-width: 22px; height: 22px; border-radius: 12px;
  display: grid; place-items: center; padding: 0 4px;
}
.go-down[hidden], .go-down-badge[hidden] { display: none; }

/* ---------- Reply preview ---------- */
.reply-preview {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-sidebar);
  padding: 7px 14px; border-top: 1px solid var(--border);
  max-width: 728px; margin: 0 auto; width: 100%;
}
.reply-bar { width: 3px; align-self: stretch; background: var(--accent); border-radius: 2px; }
.reply-body { flex: 1; min-width: 0; }
.reply-name { color: var(--accent-text); font-weight: 600; font-size: 13.5px; }
.reply-text { color: var(--text-dim); font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- Composer ---------- */
.composer {
  display: flex; align-items: flex-end; gap: 4px;
  padding: 6px 14px calc(10px + env(safe-area-inset-bottom));
  width: 100%;
}
/* Pending attachment strip (official-client pattern): sits above the
   composer row, caption = the composer input itself. */
.media-preview {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px;
  background: var(--bg-sidebar);
  border-top: 1px solid var(--border);
}
.media-preview[hidden] { display: none; }
/* 96x64 box, object-fit: contain — the real aspect ratio shows letterboxed
   instead of being cropped to a square. */
#media-preview-thumb {
  width: 96px; height: 64px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px; background: var(--bg-hover); overflow: hidden;
}
#media-preview-thumb img,
#media-preview-thumb video {
  max-width: 100%; max-height: 100%; object-fit: contain;
  display: block;
}
.media-preview-side { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
.media-preview-name {
  font-size: 13.5px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.media-preview .icon-btn { margin-left: auto; }
.voice-rec-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--danger); flex: none; }
#voice-rec-label { flex: 1; font-size: 14px; }
.lp-draft-body { flex: 1; min-width: 0; }
.composer-input-wrap {
  flex: 1; display: flex; align-items: flex-end;
  background: var(--bg-sidebar);
  border-radius: 16px;
  border: 1px solid var(--border);
  min-height: 48px;
  min-width: 0; /* long unbreakable links must not inflate the flex item */
}
#composer-input {
  flex: 1; border: 0; outline: 0; resize: none;
  background: none; padding: 13px 6px 13px 16px;
  max-height: calc(var(--dvh) * .40); font-size: 15.5px; line-height: 1.3;
  min-width: 0; overflow-wrap: anywhere; /* break long URLs instead of growing (field-sizing: content) */
}
#composer-input::placeholder { color: var(--text-dim); }
/* Chromium 123+. Android 7 and Safari keep the scrollHeight grow in chat-view. */
@supports (field-sizing: content) {
  #composer-input { field-sizing: content; }
}
/* Sticker picker trigger: tucked inside the input wrap's right edge like the
   official clients' emoji/sticker button — not a separate composer column. */
#btn-sticker {
  flex: none; align-self: center; width: 34px; height: 34px;
  margin-right: 5px; padding: 0; border: 0; border-radius: 50%;
  background: none; color: var(--text-dim); cursor: pointer;
}
#btn-sticker svg { width: var(--ico-4); height: var(--ico-4); }
#btn-sticker:active { color: var(--accent); }
@media (hover: hover) and (pointer: fine) {
  #btn-sticker:hover { color: var(--text); }
}
.sticker-pop {
  position: fixed; z-index: 930;
  right: 10px; bottom: calc(78px + env(safe-area-inset-bottom));
  width: min(320px, calc(var(--vw) - 20px)); max-height: min(420px, calc(var(--dvh) * .50));
  overflow: auto;
  background: var(--bg-sidebar); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: var(--shadow); padding: 10px;
}
.sticker-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.sticker-coll { grid-column: 1 / -1; font-size: 12px; color: var(--text-dim); margin: 6px 2px 0; }
.sticker-coll:first-child { margin-top: 0; }
.sticker-tile {
  border: 0; background: none; border-radius: 10px; padding: 6px;
  aspect-ratio: 1; display: grid; place-items: center;
  font-size: 30px; cursor: pointer;
}
.sticker-tile img { width: 100%; height: 100%; object-fit: contain; display: block; }
.sticker-tile:active { background: var(--bg-hover); }
@media (hover: hover) and (pointer: fine) {
  .sticker-tile:hover { background: var(--bg-hover); }
}
.sticker-note { grid-column: 1 / -1; color: var(--text-dim); font-size: 13px; padding: 8px; line-height: 1.4; }
/* Demo (mock-core) stickers are emoji text, not files. */
.sticker-emoji { font-size: 96px; line-height: 1; padding: 6px 10px; }
.send-btn { color: var(--accent-text); margin-bottom: 4px; }
.send-btn:disabled { color: var(--text-dim); cursor: default; }
/* Composer buttons sized to the input line (bottom-aligned like the input,
   no extra offset — a 48px button centers its icon on the text line), with
   icons a notch larger than the generic 24px. */
#main-composer .icon-btn { width: 48px; height: 48px; margin-bottom: 0; }
#main-composer .icon-btn svg { width: var(--ico-5); height: var(--ico-5); }

/* ---------- Elena components ---------- */
velta-avatar { display: block; flex: none; flex-shrink: 0; }
/* Reserve the avatar slot from the size attribute, so a row never collapses
   to zero width even while the tile is (re)rendering. */
velta-avatar[size="36"] { width: 36px; height: 36px; min-width: 36px; }
velta-avatar[size="42"] { width: 42px; height: 42px; min-width: 42px; }
velta-avatar[size="48"] { width: 48px; height: 48px; min-width: 48px; }
velta-avatar[size="56"] { width: 56px; height: 56px; min-width: 56px; }
velta-avatar[size="84"] { width: 84px; height: 84px; min-width: 84px; }
velta-avatar[size="168"] { width: 168px; height: 168px; min-width: 168px; }
.velta-avatar-tile {
  border-radius: 22%; display: grid; place-items: center;
  color: #f4f4f4; font-weight: 600; user-select: none;
  position: relative; overflow: hidden;
}
/* The global `svg { width: 24px }` icon rule must not shrink the identity
   matrix — it has to fill the tile edge to edge. (Saved/device icons keep
   their size via their inline width/height style.) */
.velta-avatar-tile > svg { width: 100%; height: 100%; }
.velta-avatar-tile.saved { background: linear-gradient(135deg, #5aa2e6, #8774e1); }
.velta-avatar-tile img.velta-avatar-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: inherit; /* match the tile mask, not a circle — a 50% clip left the tile's corners visible */
}
/* Single contacts with a photo: the picture is a padded rounded square sitting
   on the color matrix, separated from it by a soft-black ring. Percentages
   keep every part proportional at any tile size (120-unit viewBox reference:
   inset 23, box 74, ring 3, photo 68, radius 15). */
.velta-avatar-tile .velta-avatar-photo {
  position: absolute; inset: 19.17%; display: block;
  background: #1c1c1c; padding: 2.5%; border-radius: 24.3%;
}
.velta-avatar-tile .velta-avatar-photo img.velta-avatar-img {
  position: static; inset: auto; display: block;
  width: 100%; height: 100%; object-fit: cover; border-radius: 22%;
}
/* Photo shimmer: while the photo bytes are in flight the img paints a soft
   sweep itself; decode covers it and .loaded (set in updated()) drops the
   background entirely — stops the animation, and keeps alpha-PNG avatars
   from shimmering through transparent pixels forever. Soft tones only. */
.velta-avatar-tile img.velta-avatar-img:not(.loaded) {
  background-color: rgba(122, 132, 153, .16);
  background-image: linear-gradient(100deg, transparent 25%, rgba(244, 244, 244, .22) 50%, transparent 75%);
  background-repeat: no-repeat;
  background-size: 220% 100%;
  animation: velta-avatar-shimmer 1.4s linear infinite;
}
@keyframes velta-avatar-shimmer {
  from { background-position: 120% 0; }
  to { background-position: -20% 0; }
}
.velta-avatar-tile img.velta-avatar-img.loaded { background: none; }
@media (prefers-reduced-motion: reduce) {
  .velta-avatar-tile img.velta-avatar-img:not(.loaded) { animation: none; }
}

velta-chat-item { display: block; }
.chat-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; cursor: pointer;
  border-radius: 10px; margin: 0 6px;
  position: relative; user-select: none;
  /* Cards have no overflow children — contain keeps a badge/draft/hover
     change from invalidating layout or repainting sibling rows. */
  contain: layout paint style;
}
@media (hover: hover) and (pointer: fine) {
  .chat-item:hover { background: var(--bg-hover); }
}
.chat-item.active { background: var(--selection-bg); }
.chat-item.active .ci-time, .chat-item.active .ci-last, .chat-item.active .ci-mute { color: rgba(244,244,244,.75); }
/* Off-screen stand-in for a not-yet-mounted chat row (#44): one real row's
   height (48px avatar + 2×10px padding); the IntersectionObserver swaps in
   the real element before it scrolls into view. */
.chat-item-ghost { height: 68px; margin: 0 6px; }
/* "Archived chats (N)" row at the bottom of the chat list. */
.archived-link {
  display: flex; align-items: center; gap: 10px; width: calc(100% - 12px);
  margin: 4px 6px 8px; padding: 10px 12px; border: none; border-radius: 10px;
  background: none; color: var(--text-dim); font: inherit; font-size: 14px;
  text-align: left; cursor: pointer;
}
.archived-link-icon { display: inline-flex; width: 48px; justify-content: center; }
.archived-link-icon svg { width: 22px; height: 22px; }
@media (hover: hover) and (pointer: fine) { .archived-link:hover { background: var(--bg-hover); color: var(--text); } }
.archived-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.ci-main { flex: 1; min-width: 0; }
.ci-top { display: flex; align-items: baseline; gap: 6px; }
.ci-name { flex: 1; min-width: 0; font-size: 15.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; gap: 4px; }
.ci-lock { width: var(--ico-1); height: var(--ico-1); color: var(--green); flex: none; }
.ci-lock.open { color: var(--text-dim); }
.ci-wifi { width: var(--ico-1); height: var(--ico-1); color: var(--green); flex: none; }
.ci-time { font-size: 12px; color: var(--text-dim); flex: none; }
.ci-bottom { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
.ci-last { flex: 1; min-width: 0; font-size: 14px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ci-last .draft { color: var(--danger); }
.ci-last .typing { color: var(--accent); }
.ci-badge {
  background: var(--accent-2); color: var(--on-badge);
  font-size: 12.5px; font-weight: 600;
  min-width: 23px; height: 23px; border-radius: 12px;
  display: grid; place-items: center; padding: 0 6px; flex: none;
}
.ci-badge.muted { background: var(--text-dim); color: var(--on-badge-muted); }
.ci-pin { width: var(--ico-2); height: var(--ico-2); color: var(--text-dim); flex: none; }
.ci-mute { width: var(--ico-2); height: var(--ico-2); color: var(--text-dim); flex: none; }
.ci-ticks { width: var(--ico-2); height: var(--ico-2); color: var(--text-dim); flex: none; }
/* The global `svg { display: block }` rule turns the ticks into their own
   block line inside .ci-last, adding a whole text line of height to chat
   items that have one. Keep them inline with the preview text instead. */
.ci-last .ci-ticks { width: 17px; height: 12px; display: inline-block; vertical-align: middle; }
.ci-ticks.read { color: var(--accent); }

.chat-head-avatar { cursor: pointer; flex: none; }
/* velta-chat-head is a custom element (inline by default) — without flex the
   avatar and the name column don't share a row. */
velta-chat-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
.chat-head-text { flex: 1; min-width: 0; cursor: pointer; }
.cht-name { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 6px; min-width: 0; }
.cht-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cht-status { font-size: 13px; color: var(--text-dim); }
.cht-status.online { color: var(--accent-text); }

/* ---------- Popups / menus / modals ---------- */
.pop-overlay {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(16,16,16,.45);
  display: grid; place-items: center;
  animation: fadeIn .15s;
}
.pop-overlay.transparent { background: transparent; }
/* Drawer's overlay only: stop above the action bar like .drawer does, so the
   bar-menu toggle gets the whole tap (down, up and click on one element) —
   an overlay edge under the finger made mobile WebViews retarget the click
   and re-toggle the drawer right after closing it. */
.drawer-overlay { bottom: var(--list-bar-h); }
@keyframes fadeIn { from { opacity: 0; } }

.ctx-menu {
  position: fixed; z-index: 60;
  background: var(--bg-sidebar);
  border: 1px solid var(--border);
  border-radius: 12px; padding: 6px;
  min-width: 210px; box-shadow: var(--shadow);
  animation: menuIn .12s ease-out;
}
@keyframes menuIn { from { opacity: 0; transform: scale(.94); } }
.ctx-item {
  display: flex; align-items: center; gap: 12px;
  width: 100%; padding: 10px 12px; border-radius: 8px;
  font-size: 14.5px; text-align: left;
}
@media (hover: hover) and (pointer: fine) {
  .ctx-item:hover { background: var(--bg-hover); }
}
.ctx-item.danger { color: var(--danger); }
.ctx-item.pressed { background: color-mix(in srgb, var(--accent-tint) 18%, transparent); box-shadow: inset 0 0 0 1px var(--accent); }
.ctx-item svg { width: var(--ico-3); height: var(--ico-3); color: var(--text-dim); }
.ctx-item.danger svg { color: var(--danger); }
/* Setting toggles rendered as checkbox rows: the box is the state indicator
   and sits at the row's far edge; the label text stays on the left. */
.ctx-item input[type="checkbox"] {
  margin-left: auto; flex: none;
  width: 16px; height: 16px;
  accent-color: var(--accent);
}

/* Drawer spoiler with interface-scale radios */
.drawer-details summary {
  display: flex; align-items: center; gap: 12px;
  width: 100%; padding: 10px 12px; border-radius: 8px;
  font-size: 14.5px; text-align: left; cursor: pointer; user-select: none;
  list-style: none; color: var(--text);
}
.drawer-details summary::-webkit-details-marker { display: none; }
.drawer-details summary::after {
  content: ""; width: 8px; height: 8px; margin-left: auto; flex: none;
  border-right: 2px solid var(--text-dim); border-bottom: 2px solid var(--text-dim);
  transform: rotate(-45deg); transition: transform .15s;
}
.drawer-details[open] summary::after { transform: rotate(45deg); }
@media (hover: hover) and (pointer: fine) {
  .drawer-details summary:hover { background: var(--bg-hover); }
}
.drawer-details summary svg { width: var(--ico-3); height: var(--ico-3); color: var(--text-dim); flex: none; }
.scale-opts { display: flex; flex-direction: column; gap: 2px; padding: 2px 12px 8px 14px; }
/* Drawer → Chat appearance (ui.js chatAppearanceHtml). The preview reuses
   the real .msg-row/.bubble rules, so it shows exactly what the chat will. */
.ca-panel { display: flex; flex-direction: column; gap: 4px; padding: 2px 12px 10px 14px; min-width: 0; }
.ca-preview {
  background: var(--bg-chat); border-radius: 10px; padding: 8px 0 6px;
  display: flex; flex-direction: column; gap: 2px; margin-bottom: 6px;
  pointer-events: none; overflow: hidden;
}
.ca-preview .bubble { animation: none; max-width: 86%; }
.ca-preview .msg-quote .q-text { max-width: 100%; }
.ca-field { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 6px 4px 0; font-size: 14px; }
.ca-field output { color: var(--text-dim); font-variant-numeric: tabular-nums; font-size: 13px; white-space: nowrap; }
.ca-range { width: 100%; margin: 4px 0; accent-color: var(--accent); min-height: 32px; cursor: pointer; }
.ca-accent { border: 0; padding: 6px 4px 0; min-width: 0; }
.ca-accent legend { font-size: 14px; padding: 0 0 8px; }
.ca-swatches { display: flex; flex-wrap: wrap; gap: 8px; }
.ca-swatch {
  --sw: var(--accent-default); position: relative; width: 32px; height: 32px; flex: none;
  border-radius: 50%; background: var(--sw); cursor: pointer;
}
.ca-swatch input[type="radio"] {
  appearance: none; -webkit-appearance: none; position: absolute; inset: 0;
  width: 100%; height: 100%; margin: 0; border-radius: 50%; cursor: pointer;
}
.ca-swatch input:checked, .ca-custom input[data-active] {
  box-shadow: 0 0 0 2px var(--bg-sidebar), 0 0 0 4px var(--text);
}
.ca-swatch input:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 5px; }
.ca-custom { background: var(--ca-rainbow); } /* set from PRESETS in ui.js */
.ca-custom input[type="color"] {
  -webkit-appearance: none; appearance: none; position: absolute; inset: 4px;
  width: calc(100% - 8px); height: calc(100% - 8px); padding: 0; margin: 0;
  border: 2px solid var(--bg-sidebar); border-radius: 50%; background: none; cursor: pointer;
}
.ca-custom input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.ca-custom input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 50%; }
.ca-custom input[type="color"]::-moz-color-swatch { border: 0; border-radius: 50%; }
.ca-custom input[type="color"][data-active] { inset: 0; width: 100%; height: 100%; }
.ca-actions { display: flex; justify-content: flex-end; padding-top: 2px; }
.scale-opt {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; border-radius: 8px; cursor: pointer;
  font-size: 14px; color: var(--text);
}
@media (hover: hover) and (pointer: fine) {
  .scale-opt:hover { background: var(--bg-hover); }
}
.scale-opt input { accent-color: var(--accent); flex: none; }
.ctx-sep { height: 1px; background: var(--border); margin: 5px 8px; }

/* Phone message menu: bottom sheet, reactions, then two columns of pairs. */
.ctx-menu.msg-sheet {
  left: 0; right: 0; top: auto; bottom: 0;
  width: auto; min-width: 0; max-height: min(calc(var(--vh) * .80), 640px);
  overflow: auto;
  border-radius: 18px 18px 0 0;
  border-bottom: 0;
  padding: 6px 12px calc(12px + env(safe-area-inset-bottom));
  animation: sheetIn .16s ease-out;
}
@keyframes sheetIn { from { transform: translateY(12px); } }
/* Drag handle: a 44px-tall button (tap or drag down closes); the visible bar
   is --text-dim, ≥5:1 on the sheet in light, dark and brutal. */
.msg-sheet-grab {
  display: block; width: 72px; height: 28px; margin: 0 auto 6px; padding: 0;
  background: none; border: 0; border-radius: 8px; cursor: grab; touch-action: none;
}
.msg-sheet-grab::before {
  content: ""; display: block; width: 44px; height: 5px; margin: 0 auto;
  border-radius: 5px; background: var(--text-dim);
}
/* Narrow phones: two tiles per row sit icon-beside-label (Pavel: two per
   row only with icon and caption on one line); three per row keep icon on top. */
@media (max-width: 339.98px) {
  .msg-actions .msg-grid .ctx-item.msg-tile {
    flex-direction: row; justify-content: flex-start; text-align: left;
    min-height: 48px; padding: 10px 12px; gap: 10px;
  }
  .msg-grid .msg-tile span { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .msg-grid .msg-tile svg { width: 20px; height: 20px; }
}
/* Sheet emoji picker: replaces the body at the sheet's locked height. */
.ctx-menu.msg-sheet.picking { display: flex; flex-direction: column; overflow: hidden; }
.ctx-menu.msg-sheet.picking > :not(.msg-sheet-grab):not(.msg-sheet-picker) { display: none; }
.msg-sheet-picker:not([hidden]) { display: flex; flex-direction: column; flex: 1; min-height: 0; margin: 0; }
.msg-sheet-picker .msg-emoji-grid {
  flex: 1; min-height: 0; max-height: none; overflow: auto;
  overscroll-behavior: contain; align-content: start;
}
.msg-emoji-back { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; }
.msg-emoji-back svg { width: 18px; height: 18px; }
.msg-emoji-back:focus-visible, .msg-emoji:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
/* Inline delete confirm: Cancel + one or two red choices in the Delete row.
   Labels wrap at 320px rather than truncate. */
.msg-del-confirm { display: flex; gap: 8px; animation: delConfirmIn .14s ease-out; }
.msg-del-confirm .ctx-item {
  flex: 1 1 0; min-width: 0; justify-content: center; text-align: center;
  min-height: 48px; padding: 8px 6px; font-size: 14px; line-height: 1.2;
  white-space: normal; overflow-wrap: anywhere;
}
.msg-del-confirm[data-count="3"] .ctx-item { flex-direction: column; gap: 4px; }
.msg-del-confirm[data-count="2"] .ctx-item { flex-direction: row; justify-content: flex-start; text-align: left; gap: 10px; padding: 8px 12px; }
.msg-del-confirm .ctx-item svg { width: 20px; height: 20px; flex: none; }
.msg-del-confirm .ctx-item:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
@keyframes delConfirmIn { from { opacity: 0; transform: scale(.97); } }
@media (prefers-reduced-motion: reduce) { .msg-del-confirm { animation: none; } }
.msg-sheet-grab:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.ctx-menu.msg-sheet .msg-reacts { touch-action: none; } /* top strip drags the sheet */
@media (prefers-reduced-motion: reduce) { .ctx-menu.msg-sheet { animation: none; } }
.msg-reacts {
  display: grid; grid-template-columns: repeat(6, 1fr);
  gap: 8px; margin-bottom: 12px;
}
.msg-react {
  aspect-ratio: 1; border-radius: 50%;
  background: var(--bg-hover); color: var(--text);
  font-size: 22px; display: grid; place-items: center; padding: 0;
}
.msg-react.mine { background: color-mix(in srgb, var(--accent-tint) 18%, transparent); box-shadow: inset 0 0 0 2px var(--accent); }
.msg-react svg { width: 22px; height: 22px; }
.msg-react:active, .msg-emoji:active, .msg-actions .ctx-item:active { background: var(--bg-hover); }
.msg-actions { display: flex; flex-direction: column; gap: 8px; }
/* Firefox-style action grid: three tiles per row, two on narrow phones. */
.msg-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
@media (max-width: 339.98px) { .msg-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.msg-actions .ctx-item.msg-tile {
  flex-direction: column; justify-content: center; gap: 6px;
  min-height: 72px; padding: 10px 6px; text-align: center;
}
.msg-tile span { font-size: 13px; line-height: 1.2; overflow-wrap: anywhere; }
.msg-tile svg { width: 24px; height: 24px; flex: none; }
.msg-actions .ctx-item:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Light: the sheet and --bg are both #f4f4f4 — lift the tiles so their cells read. */
html[data-theme="light"] .msg-actions .ctx-item { background: #fff; box-shadow: inset 0 0 0 1px rgba(16,16,16,.08); }
.msg-actions .ctx-item {
  background: var(--bg); border-radius: 12px; min-height: 48px;
}
.msg-emoji-tray { margin-bottom: 12px; }
.msg-emoji-back {
  margin: 0 0 8px; padding: 4px 12px; border-radius: 999px;
  background: var(--bg-hover); color: var(--text); font: inherit; font-size: 14px;
}
.msg-emoji-tray .text-field { width: 100%; margin: 0 0 8px; }
.msg-emoji-grid {
  display: grid; grid-template-columns: repeat(8, 1fr);
  gap: 2px; max-height: 168px; overflow: auto;
}
.msg-emoji { font-size: 24px; line-height: 1.2; padding: 4px; border-radius: 8px; }
.msg-emoji-empty { grid-column: 1 / -1; color: var(--text-dim); font-size: 14px; padding: 8px; }
/* "Show links" (#161 mobile): real anchors — the WebView's own long-press
   menu (open / copy address / share) works on each. No user-select override:
   link long-press is browser UI, not text selection. */
.msg-links { margin-top: 10px; }
.msg-links summary {
  cursor: pointer; list-style: none; padding: 8px 12px; border-radius: 8px;
  color: var(--text-dim); font-size: 14px;
}
.msg-links summary::-webkit-details-marker { display: none; }
.msg-links summary::after { content: "\203A"; float: right; transition: transform .15s ease-out; }
.msg-links[open] summary::after { transform: rotate(90deg); }
.msg-links a {
  display: block; padding: 9px 12px; border-radius: 8px;
  color: var(--accent-text-in); font-size: 14px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Popover UA sheet (inset:0, margin:auto, Canvas, border) leaks on unset
   properties. Do not set left/right here: the 821px toast query and the
   sticker/queue anchors own those edges. display:flex beats the UA closed
   state, so those widgets hide explicitly. */
.ctx-menu[popover] {
  margin: 0; position: fixed;
  top: auto; left: auto; right: auto; bottom: auto;
  width: max-content; height: auto;
  background: var(--bg-sidebar); color: var(--text);
  border: 1px solid var(--border); overflow: visible;
}
.sticker-pop[popover],
#lc-queue-pop[popover] {
  margin: 0; top: auto; left: auto; height: auto;
  background: var(--bg-sidebar); color: var(--text);
}
.toasts[popover] {
  margin: 0; border: none; background: transparent; color: inherit;
  overflow: visible; width: auto; height: auto; top: auto;
}
.toasts[popover]:not(:popover-open) { display: none; }
.toasts[popover]:popover-open { display: flex; }

.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
fieldset.scale-opts { border: 0; margin: 0; min-inline-size: 0; }
fieldset.eph-set { border: 0; margin: 0; padding: 0; min-inline-size: 0; }

.modal > form { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.modal > form .modal-body { flex: 1; min-height: 0; }
.modal {
  background: var(--bg-sidebar);
  border: 1px solid var(--border);
  border-radius: 16px; box-shadow: var(--shadow);
  width: min(420px, calc(var(--vw) - 40px));
  max-height: min(600px, calc(var(--dvh) - 60px));
  display: flex; flex-direction: column;
  animation: modalIn .16s ease-out;
  overflow: hidden;
}
@keyframes modalIn { from { opacity: 0; transform: translateY(14px) scale(.97); } }
/* Phones: task modals (relays, search, profile flows) take the whole screen
   like a dedicated page; compact confirmations keep the floating card. */
@media (max-width: 600px) {
  .modal:not(.modal-compact) {
    width: var(--vw); height: var(--dvh); max-height: none;
    border-radius: 0; border-left: none; border-right: none; border-top: none;
    animation: sheetIn .18s ease-out;
  }
  /* Edge-to-edge: the page draws under the status bar, so the modal head
     clears it the same way the sidebar head does (env safe-area top). */
  .modal:not(.modal-compact) .modal-head {
    padding-top: calc(16px + env(safe-area-inset-top));
  }
  .modal:not(.modal-compact) .modal-body { flex: 1; }
  .modal:not(.modal-compact) .modal-foot { padding-bottom: max(14px, env(safe-area-inset-bottom)); }
  /* Chat-list context: stop above the bottom action bar (same offset as the
     drawer overlay) — the bar stays visible, dimmed under the scrim, and a
     tap there dismisses the modal. In a chat (search modal) stay full-screen. */
  body:not(:has(.app.chat-open)) .modal:not(.modal-compact) {
    height: calc(var(--dvh) - var(--list-bar-h));
    align-self: start;
  }
}
@keyframes sheetIn { from { opacity: 0; transform: translateY(28px); } }
/* Desktop: task modals fill the chat-list pane instead of floating a small
   card (Telegram/WhatsApp-style) — conversations stay visible and dimmed
   under the scrim, and a click outside dismisses. Width mirrors .sidebar
   (33%, clamped 300–420px); height stops above the list bottom bar, same
   offset as the drawer overlay. Compact confirmations and media lightboxes
   keep their own presentations. */
@media (min-width: 601px) {
  .modal:not(.modal-compact) {
    position: fixed; left: 0; top: 0;
    width: clamp(300px, calc(var(--vw) * .33), 420px);
    height: calc(var(--dvh) - var(--list-bar-h));
    max-height: none;
    border-radius: 0;
    border-top: none; border-bottom: none; border-left: none;
  }
}
.modal-head {
  display: flex; align-items: center; gap: 10px;
  padding: 16px 18px 10px;
}
.modal-title { flex: 1; font-size: 18px; font-weight: 600; }
.modal-body { padding: 6px 18px 18px; overflow-y: auto; }
.modal-list { display: flex; flex-direction: column; }
/* #97 "Share to…" picker: search, profile chips, chat rows. */
.share-picker { display: flex; flex-direction: column; gap: 10px; }
.share-search { width: 100%; box-sizing: border-box; }
.share-accounts { display: flex; flex-wrap: wrap; gap: 6px; }
.share-account {
  border: 1px solid var(--border); background: transparent; color: var(--text);
  border-radius: 999px; padding: 5px 12px; font: inherit; font-size: 13.5px; cursor: pointer;
}
.share-account.current { background: var(--accent); border-color: var(--accent); color: var(--on-accent); cursor: default; }
/* Profile/chat info header: avatar, name under it, description block under
   the name. */
.profile-name {
  text-align: center; font-size: 17px; font-weight: 600;
  padding: 8px 14px 2px;
}
.profile-description {
  text-align: center; font-size: 14px; line-height: 1.45;
  color: var(--text-dim);
  padding: 2px 18px 6px; white-space: pre-line;
}
/* Profile management tabs (Add profile / Second device / Export backup).
   Locked tabs: a running flow owns the modal until it finishes. */
.pm-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 12px; }
.pm-tab {
  flex: 1; padding: 8px 4px; border-radius: 8px; font-size: 13.5px;
  background: none; border: 1px solid transparent; color: var(--text-dim);
}
.pm-tab.active { background: var(--bg-hover); color: var(--text); border-color: var(--border); }
.pm-tabs.locked .pm-tab { opacity: .45; pointer-events: none; }
.pm-hint { font-size: 14.5px; line-height: 1.5; margin-bottom: 10px; }
.pm-pane .text-field { width: 100%; margin-bottom: 10px; }
.pm-actions { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.pm-actions .btn-primary { flex: 1; }
/* #178 Privacy & security: PIN + biometric actions share one row as equal
   columns and wrap to two lines on narrow widths (labels never truncate). */
.ps-lock-actions > div { display: contents; }
.ps-lock-actions button { flex: 1 1 10rem; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
.pm-col { flex-direction: column; margin: 10px 0; }
.pm-progress { margin-top: 10px; font-size: 14.5px; color: var(--text-dim); }
.modal .chat-item { margin: 0; }
.btn-primary {
  background: var(--accent); color: var(--on-accent);
  border-radius: 10px; padding: 11px 22px;
  font-size: 15px; font-weight: 600;
  width: 100%;
}
.btn-primary:disabled { opacity: .5; cursor: default; }
/* Combined classes: .btn-text (later in file, equal specificity) would
   otherwise win color, and .btn-text:hover would replace the accent
   background — rendering primaries as plain text buttons. */
.btn-text.btn-primary { color: var(--on-accent); background: var(--accent); }
.btn-text.btn-primary:active { background: var(--accent-active); }
@media (hover: hover) and (pointer: fine) {
  .btn-text.btn-primary:hover { background: var(--accent-hover); }
}
.btn-text { color: var(--accent-text); font-weight: 600; padding: 10px 14px; border-radius: 8px; }
/* Language card (lang-cards.js): theme tokens, not the dark .ivc panel.
   Surface --bg-reply (--bg-elev equals the incoming bubble in dark/brutal).
   Button = .btn-text.btn-primary (accent fill, --on-accent ink; hover/press
   change the surface only), at radius 8 / 15px / btn-primary padding.
   "Already enabled" state: informative, full --text on the card surface. */
.lang-card {
  margin: 8px 0 2px; padding: 10px 12px; max-width: 360px; white-space: normal;
  background: var(--bg-reply); border: 1px solid var(--border); border-radius: 10px;
}
.lang-card .lang-card-btn {
  display: flex; align-items: center; justify-content: center; width: 100%;
  min-height: 44px; padding: 11px 22px; border-radius: 8px;
  font: inherit; font-size: 15px; font-weight: 600; line-height: 1.3; text-align: center;
}
/* Pin colors against bubble .btn-text overrides (.msg-row .btn-text, theme variants). */
html .msg-row .lang-card .lang-card-btn.btn-primary:not([disabled]) { color: var(--on-accent); background: var(--accent); border: 0; }
html .msg-row .lang-card .lang-card-btn.btn-primary:not([disabled]):active { background: var(--accent-active); }
@media (hover: hover) and (pointer: fine) {
  html .msg-row .lang-card .lang-card-btn.btn-primary:not([disabled]):hover { background: var(--accent-hover); }
}
.lang-card .lang-card-on {
  background: transparent; color: var(--text); border: 1px solid var(--text-dim); cursor: default;
}
html[data-theme="brutal"] .lang-card { border: 2px solid var(--nb-ink); box-shadow: 3px 3px 0 var(--nb-ink); border-radius: var(--radius-bubble); }
html[data-theme="brutal"] .msg-row .lang-card .lang-card-btn.btn-primary:not([disabled]) {
  border: 2px solid var(--nb-ink); background: var(--nb-main); color: var(--nb-main-ink); box-shadow: 3px 3px 0 var(--nb-ink);
}
html[data-theme="brutal"] .msg-row .lang-card .lang-card-btn.btn-primary:not([disabled]):active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--nb-ink); }
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .msg-row .lang-card .lang-card-btn.btn-primary:not([disabled]):hover { background: color-mix(in srgb, var(--nb-main) 88%, #fff); }
}
html[data-theme="brutal"] .lang-card .lang-card-on { border: 2px solid var(--nb-ink); }
/* Battery prompt + drawer dontkillmyapp.com link: --accent-text is the
   per-theme AA link color on --bg-sidebar (dark 6.5, light 5.1, brutal
   5.9 : 1; pinned by tests/android-background.test.mjs). Underlined so the
   link is not told apart by color alone (WCAG 1.4.1). */
.bat-prompt-text { font-size: 15px; line-height: 1.45; margin: 0 0 10px; }
.bat-dkma { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; font-weight: 600; }
.bat-dkma:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }
.btn-text:has(> svg) { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.btn-text > svg { width: 18px; height: 18px; flex: none; }
/* Diagnostics chat switches (Logging / DevTools): same size and tone as the
   .btn-text actions they share the #diagnostic-actions bar with. */
#diagnostic-actions { flex-wrap: wrap; align-items: center; }
.diag-filter { display: inline-flex; flex-wrap: wrap; align-items: center; }
.diag-filter .diag-switch { padding: 8px; }
.diag-switch { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); font-weight: 600; padding: 10px 14px; border-radius: 8px; cursor: pointer; user-select: none; -webkit-user-select: none; }
.diag-switch input { accent-color: var(--accent); width: 15px; height: 15px; margin: 0; cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .diag-switch:hover { background: var(--bg-hover); }
}
/* Restart group in #diagnostic-actions: "Restart: [Core|UI]" — one visual
   control so the bar fits narrow phone widths. */
.btn-group { display: inline-flex; align-items: center; min-width: 0; }
.btn-group-label { font-size: 13.5px; color: var(--text-dim); padding: 0 6px 0 10px; white-space: nowrap; }
.btn-group .btn-text { padding: 10px 12px; border-radius: 0; }
.btn-group .btn-text:first-of-type { border-radius: 8px 0 0 8px; }
.btn-group .btn-text:last-of-type { border-radius: 0 8px 8px 0; }
/* Inline .btn-text actions inside bubbles ("Show Full Message…", transfer
   Retry): the accent blue fails AA on the blue outgoing bubble (3.0:1 dark,
   2.85:1 light, 1.67:1 brutal), on light incoming (3.0:1) and is marginal
   on brutal incoming (4.19:1). Per-surface colors instead: meta-out is the
   token made for the out bubble (4.59–4.67:1 in every theme), light
   incoming takes a darkened accent (5.14:1), brutal its accent-2 (7.29:1). */
.msg-row.out .btn-text { color: var(--text-meta-out); }
html[data-theme="light"] .msg-row:not(.out) .btn-text { color: var(--accent-text-in); }
html[data-theme="brutal"] .msg-row:not(.out) .btn-text { color: var(--accent-2); }
@media (hover: hover) and (pointer: fine) {
  .btn-text:hover { background: var(--bg-hover); }
}
.modal-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; padding: 4px 18px 12px; } /* sides match .modal-body so buttons align with body content */
/* Pairing request: Deny keeps its natural width, the primary Pair action
   stretches over the rest of the row. */
.modal.pair-request .modal-foot .btn-primary { flex: 1; }
.text-field::placeholder { color: var(--text-dim); opacity: 1; }
.text-field {
  width: 100%; background: var(--bg-hover);
  border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px; outline: 0; color: var(--text);
  margin: 8px 0;
}
.text-field:focus { border-color: var(--accent); }

/* Identity backup modal (V2.5): hint text, labeled fields, status line. */
.modal-hint { margin: 0 0 10px; font-size: 13px; opacity: .8; }
.modal-field { display: block; margin: 10px 0; font-size: 13px; opacity: .9; }
.modal-field .text-field { width: 100%; margin-top: 4px; }
.modal-status { margin: 10px 0 0; font-size: 13px; opacity: .85; white-space: pre-wrap; overflow-wrap: anywhere; }
.modal details.backup-export, .modal details.backup-restore { margin: 8px 0; }
.modal details.backup-export summary, .modal details.backup-restore summary { cursor: pointer; font-weight: 600; padding: 6px 0; }

.info-reacts { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
.info-react > summary { cursor: pointer; }
.info-react-who { padding: 2px 4px 2px 12px; font-size: 14px; }
/* Key/value rows wrap instead of clipping on narrow screens / big scales:
   the value may shrink (min-width: 0), break long tokens and drops under the
   key when the row is too narrow for both (flex-wrap). */
.info-row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; column-gap: 12px; row-gap: 2px; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 14.5px; }
.info-row:last-child { border-bottom: 0; }
/* Contact relay list: each address copies on click/Enter; the primary
   (sending) address carries a badge. Theme tokens only (AA in every theme). */
.info-row.contact-addr { cursor: pointer; }
.info-row.contact-addr:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.info-row.contact-addr .v { font-weight: 500; }
.info-row.contact-addr-head + .contact-addr .k:empty,
.info-row.contact-addr .k:empty { display: none; }
.addr-primary-badge {
  flex: none; padding: 1px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 1.5;
  color: var(--text); background: var(--bg-sidebar); border: 1px solid var(--border);
}
.info-row .k { color: var(--text-dim); min-width: 0; overflow-wrap: anywhere; }
.info-row .v { font-weight: 500; display: flex; align-items: center; gap: 6px; min-width: 0; margin-left: auto; text-align: right; overflow-wrap: anywhere; }
.info-row.wrap { align-items: flex-start; gap: 12px; }
.info-row.wrap .k { flex: none; }
.info-row.wrap .v { display: block; margin-left: auto; max-width: 70%; text-align: right; overflow-wrap: anywhere; white-space: pre-line; }
.info-row .v svg { width: var(--ico-2); height: var(--ico-2); }
.info-row .v .avatar-profile-fpr { text-align: right; font-size: 12px; }
/* disappearing-messages + mute editors (chat info sheet): option rows */
.eph-row { display: flex; align-items: center; gap: 12px; padding: 10px 2px; cursor: pointer; font-size: 15px; }
.eph-row input { accent-color: var(--accent); width: 17px; height: 17px; margin: 0; flex: none; }
.eph-note { color: var(--text-dim); font-size: 13px; line-height: 1.45; padding: 10px 2px 4px; }
@media (hover: hover) and (pointer: fine) {
  .eph-row:hover { background: var(--bg-active); }
}
/* Collapsible "Chats in common" section in the profile sheet (showChatInfo).
   .info-details[hidden] guard: author display rules would beat the UA rule. */
.info-details[hidden] { display: none; }
.info-details > summary { list-style: none; cursor: pointer; }
.info-details > summary::-webkit-details-marker { display: none; }
/* The chevron must ride INSIDE the value span: as a summary::after it is a
   third flex item in .info-row's space-between layout, which centered the
   (n) count instead of pinning it to the right edge. */
.info-details > summary .v::after { content: "\203A"; color: var(--text-dim); margin-left: 6px; }
.info-details[open] > summary .v::after { content: "\2304"; }
/* Clickable chat rows (chats in common). */
.modal-list .info-row.clickable { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .modal-list .info-row.clickable:hover { background: var(--bg-hover); }
}
/* Profile modal action buttons under the avatars. */
.profile-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 2px 8px; padding: 0 0 12px; }
.profile-actions .btn-text { padding: 8px 10px; font-size: 13.5px; }
/* Captioned identity tile beside the velta-avatar in the chat-info modal
   (the global 24px svg rule would otherwise shrink it). */
.chat-info-tile svg { width: 168px; height: 168px; display: block; border-radius: 22%; }

/* About → Show device info: native <details>; the <pre> wraps (pre-wrap +
   anywhere) so a long user agent never widens the modal on a narrow phone. */
.device-info { margin: 12px 0 0; }
.device-info > summary { cursor: pointer; color: var(--accent-text-in); font-weight: 600; padding: 8px 0; }
.device-info-text {
  margin: 4px 0 0; padding: 10px 12px; max-width: 100%; box-sizing: border-box;
  background: var(--bg-hover); border-radius: 10px;
  font: 12px/1.45 var(--font-mono);
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
  -webkit-user-select: text; user-select: text;
}
.device-info-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; }
.device-info-actions [hidden] { display: none; }
.about-log-actions { margin: 4px 0 8px; }
.info-row[data-file-logging] { cursor: pointer; }
.info-row[data-file-logging] input { accent-color: var(--accent); width: 18px; height: 18px; margin: 0; cursor: pointer; }

/* settings drawer */
.drawer {
  /* stops above the list action bar so the bar stays visible and clickable */
  position: fixed; inset: env(safe-area-inset-top) auto var(--list-bar-h) 0; z-index: 55;

  /* same width as the chat list (.sidebar) */
  width: clamp(300px, calc(var(--vw) * .33), 420px);
  background: var(--bg-sidebar);
  /* no box shadow: the drawer slides over the sidebar's own edge and any
     offset shadow bleeds past it; a right border does the separation */
  border-right: 1px solid var(--border);
  transform: translateX(-100%);
  transition: transform .22s ease-out;
  display: flex; flex-direction: column;
  pointer-events: none;
}

.drawer.open { transform: none; pointer-events: auto; }
.drawer-head {
  padding: 22px 18px 16px;
  background: linear-gradient(135deg, #2b5278, #3d3a6e);
  color: #f4f4f4; display: flex; gap: 14px; align-items: center;
}
.drawer-head .velta-avatar-tile { width: 56px; height: 56px; font-size: 22px; }
.drawer-name { font-size: 17px; font-weight: 600; }
.drawer-links { display: flex; gap: 8px; margin-top: 8px; position: relative; }
.drawer-links button,
.acct-switch > summary {
  display: inline-flex; align-items: center; gap: 4px;
  background: rgba(244, 244, 244, .14);
  border: 1px solid rgba(244, 244, 244, .28);
  color: #f4f4f4; font-size: 13px;
  padding: 5px 11px; border-radius: 999px; cursor: pointer;
  transition: background .15s;
}
.drawer-links button:active,
.acct-switch > summary:active,
.acct-switch[open] > summary { background: rgba(244, 244, 244, .24); }
.drawer-links button svg,
.acct-switch > summary svg { width: var(--ico-1); height: var(--ico-1); }
/* account switcher: native <details>; the summary is the pill button */
.acct-switch > summary { list-style: none; user-select: none; -webkit-tap-highlight-color: transparent; }
.acct-switch > summary::-webkit-details-marker { display: none; }
.acct-switch > summary svg { transition: transform .15s; }
.acct-switch[open] > summary svg { transform: rotate(180deg); }

/* profile editor modal */
.edit-profile .ep-avatar { display: flex; justify-content: center; padding: 6px 0 2px; }
.edit-profile .ep-avatar .velta-avatar-tile { border-radius: 18%; }
.edit-profile .ep-avatar-actions {
  display: flex; justify-content: center; gap: 6px; padding: 2px 0 6px;
}
.edit-profile .ep-avatar-actions .btn-text { padding: 6px 10px; font-size: 14px; }
/* footer buttons: matched heights, compact primary (btn-primary defaults to
   the full-width onboarding style, which stretches into a bar inside a modal) */
/* Description textarea in the merged profile editor (issue #16): textareas
   default to a monospace system font — inherit the app font. */
.edit-profile .ep-desc { display: block; font: inherit; resize: vertical; min-height: 58px; }
.edit-profile-foot {
  display: flex; justify-content: flex-end; align-items: center;
  gap: 10px; padding: 4px 0 2px;
}
.edit-profile-foot .btn-text { padding: 10px 12px; font-size: 14.5px; }
.edit-profile-foot .btn-primary {
  width: auto; padding: 10px 22px; border-radius: 8px;
  font-size: 14.5px; font-weight: 600;
}
.drawer-items { padding: 8px; overflow-y: auto; flex: 1; min-height: 0; }
.drawer-foot { padding: 8px 16px 14px; border-top: 1px solid rgba(244, 244, 244, .08); }
.drawer-ver { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; color: var(--text-dim); padding: 2px 0; }
.drawer-sec { font-size: 12.5px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; padding: 14px 12px 6px; }
/* Update banner: pinned at the drawer's bottom (drawer-items scrolls, this
   and the foot do not). Rendered by ui.js renderUpdateBanner when a newer
   release is published (version.txt asset on GitHub releases). */
.update-banner {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  padding: 10px 12px;
  border-top: 1px solid var(--border);
  background: var(--bg-sidebar);
  font-size: 13.5px; color: var(--text);
}
.update-banner-text { flex: 1; min-width: 0; }
.update-banner-btn {
  flex: none; width: auto;
  padding: 8px 14px; border-radius: 10px;
  background: var(--accent); color: var(--on-accent); font-weight: 600;
}
.update-banner-btn:active { transform: scale(.97); }
/* While an update is available the bar-menu button grows a polite pulsing
   ring (no layout shift — box-shadow, not border) to invite a tap. */
#bar-menu.update { box-shadow: inset 0 0 0 2px var(--accent); animation: update-nudge 2.4s ease-in-out infinite; }
@keyframes update-nudge {
  0%, 100% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent-tint) 50%, transparent); }
  50% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 8px color-mix(in srgb, var(--accent-tint) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { #bar-menu.update { animation: none; } }

/* attach pop reuses ctx-menu; Apps / Commands disclosures (#137) */
.attach-pop { min-width: 190px; max-width: 280px; max-height: calc(var(--dvh) * .70); overflow: auto; }
.attach-sub { max-height: 220px; overflow: auto; }
.attach-sub .ctx-item { min-width: 0; }
.attach-sub .ctx-item span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-empty { padding: 2px 12px 8px; color: var(--text-dim); font-size: 13px; }
.app-browser { display: flex; flex-direction: column; gap: 8px; }
.app-browser .text-field { margin: 0; }
.app-row {
  display: block; width: 100%; text-align: left;
  padding: 8px 2px; border-bottom: 1px solid var(--border);
  background: none; color: inherit; font: inherit; cursor: pointer;
}
.app-row small { display: block; color: var(--text-dim); font-size: 13px; margin-top: 2px; }
.mirror-row { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
.mirror-row span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; }
.mirror-add { display: flex; gap: 8px; align-items: center; }
.mirror-add .text-field { flex: 1; }

/* toasts — bar anchored above the composer / bottom action bar
   (same edge the modals respect); z-index 80 keeps it visible over
   full-screen modal overlays. Timer bar (top, accent/danger) shrinks with
   the remaining auto-hide time; animationend is the dismiss trigger. */
.toasts {
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--list-bar-h) + 8px);
  z-index: 80; display: flex; flex-direction: column; gap: 8px;
  padding: 0 10px; pointer-events: none;
}
/* Desktop two-pane: the bar spans the ACTIVE context column only, not the
   whole window — list context keeps it inside the sidebar pane (the column
   desktop modals fill), a chat keeps it over the chat area above the
   composer. #toasts sits outside #app in the DOM, hence :has.
   Phones (<=820px, one pane) keep the full-width bar. */
@media (min-width: 821px) {
  .toasts { right: calc(100% - clamp(300px, calc(var(--vw) * .33), 420px)); }
  body:has(.app.chat-open) .toasts { left: clamp(300px, calc(var(--vw) * .33), 420px); right: 0; }
  body:has(.app.list-collapsed) .toasts { left: 0; right: 0; }
}
.toast {
  pointer-events: auto;
  position: relative; overflow: hidden;
  display: block;
  background: #22222c; color: #eee;
  border: 1px solid var(--border);
  padding: 10px 12px; border-radius: 10px;
  font-size: 14px; box-shadow: var(--shadow);
  animation: toastIn .18s ease-out;
}
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } }
.toast-timer {
  position: absolute; top: 0; left: 0; height: 3px; width: 100%;
  background: var(--accent); transform-origin: left;
  animation-name: toastTimer; animation-timing-function: linear;
  animation-fill-mode: forwards;
}
.toast.danger .toast-timer { background: var(--danger); }
@keyframes toastTimer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
.toast-summary {
  display: flex; align-items: center; gap: 10px;
  list-style: none; cursor: pointer;
}
.toast-summary::-webkit-details-marker { display: none; }
.toast-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.toast-more {
  display: flex; align-items: center; gap: 10px;
  margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--border);
  word-break: break-word;
}
.toast-more-text { flex: 1; max-height: calc(var(--vh) * .40); overflow-y: auto; }
.toast-chip, .toast-copy, .toast-undo {
  flex: none; cursor: pointer;
  font-size: 11px; padding: 3px 8px; border-radius: 999px;
  border: 1px solid var(--border); background: none; color: var(--text-dim);
}
.toast-copy:hover, .toast-undo:hover { color: var(--text); }
.toast-close {
  flex: none; cursor: pointer; background: none; border: 0;
  color: inherit; line-height: 1; padding: 2px 3px; /* the toast surface differs from the page (light theme) */
}
.toast-close svg { width: var(--ico-2); height: var(--ico-2); }
.toast-close:hover { color: var(--text); }

/* Welcome splash (first boot, unconfigured account) */
.splash {
  position: fixed; inset: 0; z-index: 40; /* above app UI (≤10), below modal overlays (50+) and toasts (80) */
  background: var(--bg);
  display: flex; flex-direction: column;
  overflow: hidden;
}
.splash-main {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; align-items: center;
  /* safe: when the content overflows (short screens, open form) keep the top
     (version + logo) reachable instead of clipping it above the scroll area */
  justify-content: safe center;
  text-align: center; padding: 32px 24px 12px;
}
.splash-logo { width: 132px; height: 132px; margin-bottom: 18px; }
/* Version line (#127): its own flex:none row of the .splash column, between
   the scrolling .splash-main and the App log bar, so it stays pinned just
   above the bar and never overlaps the buttons (they scroll above it).
   Left padding matches the log summary's 16px. */
.splash-ver {
  flex: none; margin: 0; padding: 0 16px 6px;
  font-size: 12px; line-height: 1.4; color: var(--text-dim);
  text-align: left; overflow-wrap: anywhere;
}
.splash-title { font-size: 30px; font-weight: 700; margin: 0 0 10px; }
.splash-tag { font-size: 14.5px; line-height: 1.55; color: var(--text-dim); max-width: 420px; margin: 0 0 26px; }
.splash-actions, .splash-form { width: min(420px, 100%); display: flex; flex-direction: column; gap: 10px; }
/* display:flex above would defeat the hidden attribute (author style beats the UA rule) */
.splash-actions[hidden], .splash-form[hidden] { display: none; }
.splash-btn { width: 100%; }
.splash-hint { font-size: 14.5px; line-height: 1.5; margin: 0 0 4px; }
/* Splash onboarding steps: spinner sits mid-splash, its text stacked below
   it, both centered. */
.splash .ob-steps {
  text-align: center; width: min(420px, 100%);
  margin: 14px auto 0; align-items: center;
}
.splash .ob-steps li { flex-direction: column; justify-content: center; gap: 8px; }
.splash-log {
  flex: none; max-height: calc(var(--vh) * .38);
  border-top: 1px solid var(--border);
  background: var(--bg-sidebar);
  font-family: var(--font-mono); font-size: 12px;
}
.splash-log summary { cursor: pointer; padding: 10px 16px; color: var(--text-dim); user-select: none; }
.splash-log-bar { display: flex; justify-content: flex-end; padding: 0 12px; }
.splash-log pre {
  margin: 0; padding: 4px 16px 14px;
  max-height: calc(var(--vh) * .30); overflow-y: auto;
  white-space: pre-wrap; word-break: break-all;
  color: var(--text-dim); text-align: left;
}

/* QR invite */
.qr-box {
  position: relative;
  container-type: inline-size;
  /* Fills the dialog's content width, always square; the cap only bites in
     wide desktop dialogs. The SVGs carry their own white quiet zone. */
  width: 100%; max-width: 520px; aspect-ratio: 1; margin: 12px auto;
  display: flex; align-items: center; justify-content: center;
}
/* Fit inside the square (meet): plain QRs fill it, the core's 515x630
   invite card keeps its proportions, centred. */
.qr-box svg { width: 100%; height: 100%; display: block; }
/* Dark badge over the circle the core reserves in the QR center (circle
   center is at 50% / 43.65% of the 515x630 design card). Opaque on purpose:
   it covers the avatar the core embeds underneath and carries the Velta logo. */
.qr-self {
  position: absolute; left: 50%; top: 43.65%;
  width: 18%; aspect-ratio: 1; /* card is 515/630 of the square's width */
  transform: translate(-50%, -50%);
  background: #1c1c1c;
  border-radius: 50%;
  pointer-events: none;
}
.qr-self img { display: block; width: 100%; height: 100%; transform: translate(0, 2px); }
.qr-loading { color: #555; font-size: 13px; text-align: center; padding: 16px 8px; }
.invite-link {
  font-family: var(--font-mono); font-size: 12.5px;
  word-break: break-all; color: var(--accent);
  background: var(--bg-hover); border-radius: 8px; padding: 10px; margin-top: 8px;
}

/* scrollbars */
::-webkit-scrollbar { width: 8px; height: 8px; }
/* styled webkit scrollbars paint unstyled track/corner light even under
   color-scheme: dark (the white strip on Windows); transparent tracks both themes */
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(128,128,128,.35); border-radius: 4px; }
@media (hover: hover) and (pointer: fine) {
  ::-webkit-scrollbar-thumb:hover { background: rgba(128,128,128,.55); }
}

.only-mobile { display: none; }

/* ---------- Responsive ---------- */
/* #282: collapsible chat list (two-pane only). The toggle floats above the
   list bar's Menu button; collapsed, the sidebar slides out and the chevron
   flips. Narrow layouts never render it. */
.list-toggle {
  position: fixed; z-index: 6;
  left: calc(clamp(300px, calc(var(--vw) * .33), 420px) - 52px);
  bottom: calc(46px + 24px + env(safe-area-inset-bottom, 0px));
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--bg-sidebar); color: var(--text);
  border: 1px solid var(--border); box-shadow: var(--shadow);
  cursor: pointer;
  transition: left .22s ease-out, background .15s;
}
.list-toggle[hidden] { display: none; }
.list-toggle svg { width: var(--ico-3, 20px); height: var(--ico-3, 20px); transition: transform .22s ease-out; }
@media (hover: hover) and (pointer: fine) { .list-toggle:hover { background: var(--bg-hover); } }
.list-toggle:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.list-toggle-badge { position: absolute; top: -6px; right: -8px; min-width: 20px; height: 20px; font-size: 11.5px; padding: 0 5px; }
.sidebar { transition: margin-left .22s ease-out; }
.app.list-collapsed .sidebar { margin-left: calc(-1 * clamp(300px, calc(var(--vw) * .33), 420px)); }
.app.list-collapsed .list-toggle { left: 12px; }
.app.list-collapsed .list-toggle svg { transform: rotate(180deg); }
@media (max-width: 820px) { .list-toggle { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .sidebar, .list-toggle, .list-toggle svg { transition: none; }
}
@media (max-width: 820px) {
  .sidebar { width: 100%; min-width: 0; max-width: none; border-right: 0; }
  .drawer { width: 100%; }
  .main { position: fixed; inset: 0; transform: translateX(100%); transition: transform .22s ease-out; z-index: 10; }
  .app.chat-open .main { transform: none; }
  .app.chat-open .sidebar { visibility: hidden; }
  /* Swipe-back (#35) reveals the list under the sliding column. */
  .app.chat-open.swipe-back .sidebar { visibility: visible; }
  .history-scroll { overflow-x: hidden; }
  .only-mobile { display: grid; }
  .no-chat { display: none; }
  .history { padding-left: 6px; padding-right: 6px; }
}

/* ===== onboarding steps ===== */
.ob-steps { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.ob-steps:empty { display: none; }
.ob-steps li { display: flex; align-items: center; gap: 10px; font-size: 13.5px; color: var(--text-dim); }
.ob-steps li.active { color: var(--text); }
.ob-steps li.done { color: var(--text-dim); }
.ob-steps li.failed { color: #e0685d; }
.step-ico { width: var(--ico-2); height: var(--ico-2); flex: none; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; }
li.active .step-ico { border: 2px solid var(--accent); border-top-color: transparent; animation: ob-spin .8s linear infinite; }
li.done .step-ico { background: var(--accent); color: var(--on-accent); }
li.done .step-ico::after { content: "\2713"; }
/* Unicode ✕ is hairline-thin in the UI font, same as the close buttons. */
li.failed .step-ico { background: #e0685d; position: relative; }
li.failed .step-ico::before,
li.failed .step-ico::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 8px; height: 2px; margin: -1px 0 0 -4px;
  background: #f4f4f4; border-radius: 1px;
}
li.failed .step-ico::before { transform: rotate(45deg); }
li.failed .step-ico::after { transform: rotate(-45deg); }
@keyframes ob-spin { to { transform: rotate(360deg); } }
.btn-text:disabled { opacity: .55; pointer-events: none; }
.btn-loading::before { content: ""; display: inline-block; width: var(--ico-1); height: var(--ico-1); margin-right: 7px; vertical-align: -2px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: ob-spin .8s linear infinite; }

/* drawer close button */
.drawer-head { position: relative; }
.drawer-close { position: absolute; top: 10px; right: 10px; z-index: 2; }

/* account switcher dropdown: the <details class="acct-switch"> content,
   anchored under its summary (.drawer-links is the positioned box) */
.acct-pop {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
  background: var(--bg-sidebar); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: var(--shadow);
  padding: 8px; display: flex; flex-direction: column; gap: 8px;
  min-width: min(220px, calc(var(--vw) - 90px)); max-width: calc(var(--vw) - 90px);
}
/* list entries styled like the head's pill buttons. The pop lives inside
   .drawer-head, whose light text would vanish on a light sidebar. */
.acct-pop .ctx-item {
  background: rgba(244, 244, 244, .14);
  border: 1px solid rgba(244, 244, 244, .28);
  border-radius: 999px; padding: 7px 12px;
  font-size: 13.5px;
  color: var(--text);
}
@media (hover: hover) and (pointer: fine) {
  .acct-pop .ctx-item:hover { background: rgba(244, 244, 244, .24); }
}
html[data-theme="light"] .acct-pop .ctx-item {
  background: rgba(16, 16, 16, .06);
  border-color: rgba(16, 16, 16, .14);
}
@media (hover: hover) and (pointer: fine) {
  html[data-theme="light"] .acct-pop .ctx-item:hover { background: rgba(16, 16, 16, .1); }
}
.acct-pop .ctx-item svg { width: var(--ico-2); height: var(--ico-2); }

/* ---------- image lightbox ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 2000;
  background: rgba(16, 16, 16, .96);
  display: flex; flex-direction: column;
  touch-action: none; user-select: none;
}
.lightbox-bar {
  /* App runs edge-to-edge: keep the close button below the status bar,
     otherwise the transparent system bar swallows taps on it. */
  display: flex; justify-content: space-between; align-items: center;
  padding: max(env(safe-area-inset-top), 10px) 14px 10px;
  color: #f4f4f4; flex: 0 0 auto;
}
.lightbox-cap { flex: 1; min-width: 0; font-size: 14px; opacity: .85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lightbox-tools { display: flex; align-items: center; gap: 4px; flex: none; }
.lightbox-stretch, .lightbox-speed {
  background: none; border: 0; color: #f4f4f4; font: inherit; font-size: 13px;
  padding: 4px 6px; cursor: pointer;
}
.lightbox-speed { color-scheme: dark; }
.lightbox-close {
  background: none; border: none; color: #f4f4f4; font-size: 22px;
  padding: 4px 10px; cursor: pointer;
}
.lightbox-stage {
  flex: 1; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.lightbox-img {
  max-width: 100%; max-height: 100%;
  will-change: transform;
  transition: transform .12s ease-out;
  transform-origin: center center;
}
/* video lightbox: the stage's touch handlers are off (player controls own
   gestures), so re-enable default touch behavior for the video surface */
.lightbox-video .lightbox-stage { touch-action: auto; user-select: none; }
.lightbox-video .lightbox-video {
  max-width: 100%; max-height: 100%;
  background: #101010; outline: none;
}
/* Stretch: the picture covers the overlay, bar stays tappable on top. */
.lightbox.is-stretch .lightbox-stage { position: absolute; inset: 0; }
.lightbox.is-stretch .lightbox-video {
  width: 100%; height: 100%; max-width: none; max-height: none; object-fit: cover;
}
.lightbox.is-stretch .lightbox-bar {
  position: relative; z-index: 1;
  background: linear-gradient(rgba(0, 0, 0, .55), transparent);
}

/* ---------- failed media placeholder ---------- */
.media-fail {
  width: 100%; min-height: 130px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; padding: 18px 12px;
  color: var(--text-dim); font-size: 13px; text-align: center;
}
.media-fail-ico svg { width: 30px; height: 30px; opacity: .55; }

/* ===== Boot error banner (pre-app.js safety net, see js/boot-net.js) ===== */
#boot-error {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: 12px;
  z-index: 1000;
  max-width: 560px;
  margin: 0 auto;
  padding: 10px 12px;
  border: 1px solid var(--danger);
  border-radius: 10px;
  background: #221416;
  color: #f2d7d5;
  font-size: 13px;
  line-height: 1.4;
}
#boot-error code {
  display: block;
  margin: 6px 0;
  white-space: pre-wrap;
  word-break: break-word;
  font-size: 12px;
  opacity: 0.9;
}
#boot-error button {
  margin-top: 4px;
  padding: 4px 10px;
  border: 1px solid var(--danger);
  border-radius: 8px;
  background: transparent;
  color: inherit;
  cursor: pointer;
}
#boot-error[hidden] { display: none; }

/* ===== Calls (audio + video, Delta Chat-style screen) =====
   Every color comes from the --call-* tokens (defined per theme in :root,
   light and brutal); sizes sit on the icon ladder. No pure black/white. */
#call-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: var(--call-bg);
  color: var(--call-fg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
#call-overlay [hidden] { display: none !important; }
.call-remote-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--call-bg);
}
.call-top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding: calc(env(safe-area-inset-top, 0px) + 18px) 20px 28px;
  text-align: left;
  z-index: 2;
}
#call-overlay.video .call-top { background: linear-gradient(var(--call-scrim), transparent); color: var(--call-video-fg); }
.call-name {
  font-size: 20px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.call-status { margin-top: 4px; font-size: 15px; color: var(--call-fg-dim); font-variant-numeric: tabular-nums; }
#call-overlay.video .call-status { color: inherit; opacity: .8; }
/* #239 Direct / Via relay chip: neutral in every theme (a relay is normal).
   Text is --call-fg on a 10% --call-fg tint of --call-bg (>= 12:1 light, dark,
   brutal); over remote video the scrim tokens carry it. Native <details>. */
.call-net { margin-top: 8px; font-size: 13px; }
.call-net-chip {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 28px; padding: 2px 10px 2px 8px; border-radius: 999px;
  list-style: none; cursor: pointer; user-select: none;
  color: var(--call-fg);
  background: color-mix(in srgb, var(--call-fg) 10%, var(--call-bg));
  border: 1px solid color-mix(in srgb, var(--call-fg) 28%, var(--call-bg));
  transition: background-color .3s, box-shadow .3s;
}
.call-net-chip::-webkit-details-marker { display: none; }
.call-net-chip::marker { content: ""; }
/* Expand marker on the right: rotates when the detail is open. */
.call-net-chip svg.call-net-chevron { width: 1em; height: 1em; margin-left: -2px; transition: transform .2s; }
.call-net[open] .call-net-chevron { transform: rotate(180deg); }
.call-net-chip svg { width: 16px; height: 16px; flex: none; }
.call-net-chip:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.call-net-chip.flash { box-shadow: 0 0 0 2px var(--accent-text); background: color-mix(in srgb, var(--accent-text) 16%, var(--call-bg)); }
.call-net-detail {
  margin-top: 6px; padding: 8px 10px; border-radius: 10px; max-width: 320px;
  color: var(--call-fg); background: color-mix(in srgb, var(--call-fg) 8%, var(--call-bg));
  line-height: 1.4;
}
#call-overlay.video .call-net-chip,
#call-overlay.video .call-net-detail { color: var(--call-video-fg); background: var(--call-scrim); border-color: color-mix(in srgb, var(--call-video-fg) 35%, transparent); }
@media (prefers-reduced-motion: reduce) { .call-net-chip, .call-net-chevron { transition: none; } }
.call-avatar {
  position: relative;
  width: min(168px, calc(var(--vw) * .46));
  height: min(168px, calc(var(--vw) * .46));
  border-radius: 50%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 40px;
  background: var(--call-avatar-bg);
}
.call-avatar-initial { font-size: min(72px, calc(var(--vw) * .2)); font-weight: 600; color: var(--call-avatar-fg); line-height: 1; }
.call-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.call-local-video {
  position: absolute;
  right: 12px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 104px);
  width: min(110px, calc(var(--vw) * .3));
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: var(--radius-media);
  background: var(--call-pip-bg);
  box-shadow: var(--shadow);
  z-index: 3;
  touch-action: none;
  cursor: grab;
}
.call-local-video.mirror { transform: scaleX(-1); }
.call-actions {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 24px);
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 14px;
  padding: 0 8px;
  z-index: 3;
}
.call-btn {
  width: var(--call-btn-size);
  height: var(--call-btn-size);
  flex: none;
  border-radius: 50%;
  border: 0;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--call-btn);
  color: var(--call-fg);
  cursor: pointer;
}
.call-btn svg { width: var(--ico-5); height: var(--ico-5); }
.call-btn.on { background: var(--call-btn-on); color: var(--call-btn-on-fg); }
.call-btn.danger { background: var(--call-danger); color: var(--call-danger-fg); }
.call-btn.accept { background: var(--call-accept); color: var(--call-accept-fg); }
.call-btn:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
@media (max-width: 430px) {
  /* Six buttons (mute, camera, end, speaker, earpiece, flip) fit 320px. */
  .call-actions { gap: 6px; }
  .call-btn { --call-btn-size: 44px; }
  .call-btn svg { width: var(--ico-4); height: var(--ico-4); }
}
html[data-theme="brutal"] .call-btn { border: 2px solid var(--nb-ink); box-shadow: 3px 3px 0 var(--nb-ink); }
html[data-theme="brutal"] .call-btn:active { transform: translate(3px, 3px); box-shadow: none; }
html[data-theme="brutal"] .call-avatar { border-radius: var(--radius-bubble); border: 2px solid var(--nb-ink); box-shadow: var(--shadow); }
html[data-theme="brutal"] .call-local-video { border: 2px solid var(--nb-ink); border-radius: var(--radius-bubble); }
#call-remote-audio { display: none; }
/* Header video-call icon only where the chat name keeps its room; phones
   start video calls from the chat menu (⋮ → Video call). */
@media (max-width: 479px) { #btn-video-call { display: none; } }

/* ===== Bot command chips ===== */
.msg-cmds {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.msg-cmd {
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.07);
  color: inherit;
  font-size: 13px;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .msg-cmd:hover { background: rgba(255, 255, 255, 0.14); }
}

/* ===== Link preview card (first link in a text message) ===== */
.msg-link-preview { margin-top: 8px; }
.msg-link-preview .link-preview { width: 100%; }
.bubble .link-preview { border-radius: var(--radius-media); } /* follows Chat appearance corners */
.link-preview {
  display: block;
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-hover);
  color: inherit;
  text-decoration: none;
  /* A finger that starts on the card must reach the bubble's swipe handlers,
     not start a native link/image drag (#35). */
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
.link-preview .lp-img {
  -webkit-user-drag: none;
  display: block;
  /* width/height auto plus both maxes: a max-height alone widens a landscape
     image past the card, and the card's overflow:hidden then clips the sides. */
  width: auto; height: auto;
  max-width: 100%; max-height: 280px;
  margin-inline: auto;
  object-fit: contain;
  background: #1c1c26;
}
.link-preview .lp-body {
  display: block;
  padding: 8px 10px;
}
.link-preview .lp-host {
  display: block;
  font-size: 12px;
  color: var(--text-meta-in);
  margin-bottom: 2px;
}
/* Out-bubble card text fails AA with the generic tokens (measured on the
   composited card surface: --bg-hover over --bg-bubble-out, AGENTS.md 6.4):
   dark desc 2.11 / host 4.00, light desc 3.64 / host 4.21, brutal title 1.64
   / desc 1.37. Scoped replacements, all >= 4.5. */
.msg-row.out .link-preview .lp-host { color: #b9d1e8; } /* 4.51 on out card */
.msg-row.out .link-preview .lp-title { color: var(--text); } /* 6.35 dark / 13.8 light */
.msg-row.out .link-preview .lp-desc { color: #d1d1d5; } /* 4.66 on out card */
html[data-theme="light"] .msg-row.out .link-preview .lp-host { color: #28699e; } /* 4.53 */
html[data-theme="light"] .msg-row.out .link-preview .lp-desc { color: #5e636d; } /* 4.69 */
html[data-theme="brutal"] .msg-row.out .link-preview .lp-host { color: var(--text-meta-out); } /* 4.84 */
html[data-theme="brutal"] .msg-row.out .link-preview .lp-title,
html[data-theme="brutal"] .msg-row.out .link-preview .lp-desc { color: #3d3405; } /* 6.63 */
.link-preview .lp-title {
  display: block;
  font-weight: 600;
  font-size: 14px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.link-preview .lp-desc {
  display: block;
  font-size: 13px;
  color: var(--text-dim);
  margin-top: 2px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  /* long unbreakable URLs never line-break, so the clamp never fires and
     the ellipsis is lost — force break opportunities */
  overflow-wrap: anywhere;
}
@media (hover: hover) and (pointer: fine) {
  .link-preview:hover { background: rgba(255, 255, 255, 0.12); }
}
/* Composer ghost: shorter than the in-bubble card so it doesn't eat the input. */
.lp-draft-body .link-preview { width: 100%; max-width: 420px; }
.lp-draft-body .lp-img { max-height: 120px; }

/* ===== Webxdc mini-apps ===== */
.msg-webxdc {
  position: relative;
  /* icon | name+summary; the Start button gets its own row, bottom-right
     (it used to float absolutely over the summary). */
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  column-gap: 10px; row-gap: 6px;
  align-items: center;
  min-width: min(220px, var(--bubble-inner-max));
  padding: 8px;
  border-radius: 10px;
  background: var(--bg-hover); /* visible card surface on the bubble */
  cursor: pointer;
}
.webxdc-start {
  grid-column: 1 / -1; justify-self: end;
  padding: 3px 12px;
  border: none; border-radius: 999px;
  background: var(--accent);
  /* Ink on accent (--on-accent is near-black for the theme blues; a custom
     accent derives its own) — white on accent blue was 2.6 / 3.1:1. */
  color: var(--on-accent);
  font-size: 12.5px; font-weight: 600; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .webxdc-start:hover { filter: brightness(1.15); }
}

/* Isolated HTML attachment viewer */
#html-view-overlay {
  position: fixed; inset: 0; z-index: 850;
  display: flex; flex-direction: column;
  background: var(--bg);
}
.html-view-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  /* App runs edge-to-edge: keep the title below the status bar. */
  padding: max(env(safe-area-inset-top), 8px) 12px 8px; color: var(--text); font-size: 14px;
  border-bottom: 1px solid var(--border);
}
.html-view-bar > span {
  /* Long subjects must truncate, not shove the close button out. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 500;
}
.html-view-frame {
  flex: 1; width: 100%; border: 0;
  background: var(--bg);
  /* Dark canvas + dark scrollbar for the frame; the attachment can still
     paint itself white once it loads. Light theme overrides. */
  color-scheme: dark;
}
html[data-theme="light"] .html-view-frame { color-scheme: light; }
.webxdc-ico {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  overflow: hidden;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  color: inherit;
  opacity: 0.9;
}
.webxdc-ico img { width: 100%; height: 100%; object-fit: cover; display: none; }
/* No icon in the app manifest: a letter tile (app initial on accent) stands
   in for the icon — always something app-specific, never a generic glyph. */
.webxdc-ico .webxdc-ico-letter {
  width: 100%; height: 100%;
  display: grid; place-items: center;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 20px; font-weight: 700; line-height: 1;
}
.file-sub { font-size: 12px; opacity: 0.7; margin-top: 2px; }
#webxdc-overlay {
  position: fixed;
  inset: 0;
  z-index: 850;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}
.webxdc-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* App runs edge-to-edge: keep the title below the status bar. */
  padding: max(env(safe-area-inset-top), 8px) 12px 8px;
  color: #eef2f7;
  background: rgba(16, 20, 26, 0.95);
}
.webxdc-title { font-weight: 600; font-size: 14px; }
.webxdc-frame {
  flex: 1; border: 0; width: 100%;
  background: var(--bg); /* no white flash before the app paints itself */
  color-scheme: dark; /* dark canvas + dark scrollbars; app can override */
}
html[data-theme="light"] .webxdc-frame { color-scheme: light; background: var(--bg-sidebar); }

/* ---------- Local chat hub card (sidebar) ---------- */
.lc-card {
  margin: 8px 10px; padding: 10px 12px;
  background: var(--bg-hover); border: 1px solid var(--border); border-radius: 14px;
  flex: none; z-index: 5;
}
.lc-card-head { display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; }
.lc-card-title { font-weight: 600; font-size: 14px; color: var(--text); flex: 1; }
.lc-card-chevron { color: var(--text-dim); transition: transform .15s; }
.lc-card.open .lc-card-chevron { transform: rotate(180deg); }
.lc-card-body { display: none; margin-top: 8px; }
.lc-card.open .lc-card-body { display: block; }
.lc-device { font-size: 13px; color: var(--text); }
.lc-device b { color: var(--text); font-weight: 600; }
.lc-actions { display: flex; gap: 8px; margin-top: 8px; }
.lc-sec { font-size: 12px; color: var(--text-dim); margin-top: 10px; opacity: .8; }
.lc-row { display: flex; align-items: center; gap: 10px; padding: 8px 6px; border-radius: 10px; cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .lc-row:hover { background: rgba(127,127,127,.14); }
}
.lc-row-name { flex: 1; font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-row-queued { font-size: 12px; opacity: .7; }
.lc-dot { width: 9px; height: 9px; border-radius: 50%; background: #7a7a85; flex: none; }
.lc-dot.on { background: #2ecc71; }

/* node id suffix on the device line — secondary info stays dim */
.lc-nodeid { font-size: 12px; color: var(--text-dim); }

/* ---------- Local chat offline media queue tray ---------- */
/* In-flow flex item at the right edge of .composer-input-wrap: the input
   shrinks a little instead of the chip overlapping the typed text. */
#lc-queue-chip {
  flex: none; align-self: center; margin: 0 10px 0 4px;
  border: 0; border-radius: 12px; padding: 3px 8px;
  background: var(--accent); color: var(--on-accent);
  font-size: 12px; font-weight: 600; cursor: pointer;
  box-shadow: var(--shadow);
}
#lc-queue-pop {
  position: fixed; z-index: 920;
  min-width: min(300px, calc(var(--vw) - 24px)); max-width: calc(var(--vw) - 24px);
  background: var(--bg-sidebar); border: 1px solid var(--border);
  border-radius: 14px; box-shadow: var(--shadow); padding: 10px;
}
.lq-head { font-size: 12.5px; color: var(--text-dim); padding: 2px 4px 8px; }
.lq-row { display: flex; align-items: center; gap: 8px; padding: 6px 4px; }
.lq-row + .lq-row { border-top: 1px solid var(--border); }
.lq-name { flex: 1; font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Transfer-in-progress card inside a bubble (local chat media) */
.msg-transfer { padding: 8px 6px; min-width: min(180px, var(--bubble-inner-max)); }
.mfp-name { font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
progress.mfp-bar, progress.pair-bar {
  appearance: none; -webkit-appearance: none;
  display: block; width: 100%; height: 4px; border: 0;
  border-radius: 2px; background: rgba(127,127,127,.25);
  overflow: hidden; margin-top: 8px; color: var(--accent);
}
progress.mfp-bar::-webkit-progress-bar,
progress.pair-bar::-webkit-progress-bar { background: transparent; border-radius: 2px; }
progress.mfp-bar::-webkit-progress-value,
progress.pair-bar::-webkit-progress-value { background: var(--accent); border-radius: 2px; }
progress.mfp-bar::-moz-progress-bar,
progress.pair-bar::-moz-progress-bar { background: var(--accent); border-radius: 2px; }
progress.pair-bar.is-fail { color: #ff6b6b; }
progress.pair-bar.is-fail::-webkit-progress-value { background: #ff6b6b; }
progress.pair-bar.is-fail::-moz-progress-bar { background: #ff6b6b; }
.update-progress { flex: 1 0 100%; height: 6px; }
.mfp-pct { font-size: 12px; color: var(--text-dim); margin-top: 4px; text-align: right; }
.mfp-fail { font-size: 12.5px; color: #e5484d; margin-top: 6px; }
/* Inline in-bubble / hint actions share one size (AGENTS 6.4 in-bubble rules). */
.msg-transfer.is-failed .btn-text, .msg-settings-act .btn-text, .md-note-acts .btn-text { padding: 2px 8px; font-size: 13px; }
.msg-transfer.is-failed .btn-text { margin-top: 4px; }

/* ---------- Neobrutalist dark theme ("brutal") ---------- */
/* Neobrutalism (neobrutalism.dev) translated to Velta's token set: flat
   panels, 2px ink borders, hard offset shadows, small radii, hover
   press-down. Dark only; like every Velta theme it avoids pure black
   (#000) and pure white (#fff). Selected chat rows and outgoing bubbles
   share the vivid main yellow with dark ink text. */
html[data-theme="brutal"] {
  --bg: #22222b;
  --bg-sidebar: #2a2a35;
  --bg-chat: #1b1b22;
  --bg-hover: rgba(244, 244, 244, .07);
  --bg-active: #e0b52f;
  --selection-bg: #e0b52f;
  --bg-bubble-in: #2d2d38;
  --bg-bubble-out: #e0b52f;
  --bg-reply: rgba(18, 16, 22, .08);
  --accent: #8f7ff0;
  --accent-tint: #8f7ff0; /* #182: derived per theme — translucent fills followed the dark blue here */
  --focus-ring: var(--accent-text);
  --accent-2: #4fd1c5;
  --text: #f2f0e9;
  --text-dim: #a3a0ad;
  --text-meta-in: #a19eab; /* 5.18:1 on #2d2d38 (was #918e9c, 4.24) */
  --text-meta-out: #55480a;
  --green: #3ecf5a;
  --danger: #f47c79; /* 4.9:1 on #313139 (was #ef6461, 4.1) */
  --border: #45434f;
  --shadow: 4px 4px 0 #121017;
  --radius-bubble: 6px;
  --nb-ink: #121017;      /* strong borders + hard shadow ink */
  --nb-main: #e0b52f;     /* the vivid main yellow */
  --nb-main-ink: #211d05; /* text on main yellow */
  --accent-default: #8f7ff0;
  --accent-text: #a99cf5; /* #8f7ff0 is 4.2:1 on #2a2a35 */
  --accent-text-in: var(--accent-2); /* accent fails 4.5 on incoming */
  --quote-name: var(--accent-2);
  --quote-name-out: #2b2406;
  --bg-elev: #2d2d38;
  --call-btn: var(--bg-bubble-in);
  --call-danger-fg: var(--nb-ink);
  --call-accept: var(--green);
  --call-accept-fg: var(--nb-ink);
  color-scheme: dark;
}

/* Frame: heavy ink rules instead of hairlines */
html[data-theme="brutal"] .sidebar { border-right: 2px solid var(--nb-ink); }
html[data-theme="brutal"] .sidebar-head,
html[data-theme="brutal"] .chat-head { border-bottom: 2px solid var(--nb-ink); }
html[data-theme="brutal"] .icon-btn {
  border: 2px solid transparent; border-radius: 6px;
  transition: background .15s, color .15s, border-color .15s, box-shadow .15s;
}
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .icon-btn:hover {
    border-color: var(--nb-ink); background: var(--nb-main);
    color: var(--nb-main-ink); box-shadow: 2px 2px 0 var(--nb-ink);
  }
}

/* Chat rows: bordered cards that press into their shadow */
html[data-theme="brutal"] .chat-item {
  margin: 6px 8px; border: 2px solid var(--nb-ink);
  border-radius: 8px; background: var(--bg-sidebar);
  transition: border-color .15s, box-shadow .15s;
}
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .chat-item:hover {
    border-color: var(--nb-main);
  }
}
html[data-theme="brutal"] .chat-item.active {
  background: var(--nb-main); box-shadow: 3px 3px 0 var(--nb-ink);
  transform: none;
}
html[data-theme="brutal"] .chat-item.active .ci-name,
html[data-theme="brutal"] .chat-item.active .ci-last { color: var(--nb-main-ink); }
html[data-theme="brutal"] .chat-item.active .ci-time,
html[data-theme="brutal"] .chat-item.active .ci-mute { color: rgba(18, 16, 22, .7); }
html[data-theme="brutal"] .ci-badge {
  border: 2px solid var(--nb-ink); background: var(--nb-main);
  color: var(--nb-main-ink); font-weight: 600;
}
html[data-theme="brutal"] .day-chip {
  border: 2px solid var(--nb-ink); border-radius: 6px;
  background: var(--bg-sidebar); box-shadow: 2px 2px 0 var(--nb-ink);
}

/* Bubbles: bordered cards with hard shadows; outgoing = main yellow */
html[data-theme="brutal"] .bubble {
  border: 2px solid var(--nb-ink);
  box-shadow: 3px 3px 0 var(--nb-ink);
}
html[data-theme="brutal"] .msg-row.out .bubble { background: var(--nb-main); }
html[data-theme="brutal"] .msg-row.out .bubble .msg-text,
html[data-theme="brutal"] .msg-row.out .msg-fwd,
html[data-theme="brutal"] .msg-row.out .reaction-chip,
html[data-theme="brutal"] .msg-row.out .msg-file { color: var(--nb-main-ink); }
html[data-theme="brutal"] .msg-row.out .msg-meta { color: var(--text-meta-out); }
html[data-theme="brutal"] .msg-meta .ticks.read { color: var(--nb-main-ink); }
html[data-theme="brutal"] .msg-row.out .msg-text a,
html[data-theme="brutal"] .msg-row.out .msg-text .hashtag {
  color: var(--nb-main-ink); text-decoration-thickness: 2px;
}
/* The 1.4.3 light-on-blue quote palettes don't apply on yellow — ink text */
html[data-theme="brutal"] .msg-row.out .msg-quote {
  border-left-color: var(--nb-main-ink); background: rgba(18, 16, 22, .1);
}
html[data-theme="brutal"] .msg-row.out .msg-quote .q-text { color: #3d3410; }
html[data-theme="brutal"] .msg-row.out .msg-text blockquote {
  border-left-color: var(--nb-main-ink); color: #332b08;
}
html[data-theme="brutal"] .msg-row.out .msg-text blockquote,
html[data-theme="brutal"] .msg-row.out .msg-text blockquote * { color: #332b08; }

/* Controls: bordered, hard-shadowed, press-down */
html[data-theme="brutal"] .text-field,
html[data-theme="brutal"] input.text-field {
  border: 2px solid var(--nb-ink); border-radius: 8px;
  background: var(--bg-chat); box-shadow: 2px 2px 0 var(--nb-ink);
}
html[data-theme="brutal"] .btn-primary {
  border: 2px solid var(--nb-ink); border-radius: 8px;
  background: var(--nb-main); color: var(--nb-main-ink);
  box-shadow: 4px 4px 0 var(--nb-ink);
  transition: border-color .15s, box-shadow .15s;
}
html[data-theme="brutal"] .btn-primary:active {
  border-color: var(--accent-2); box-shadow: 1px 1px 0 var(--nb-ink);
}
html[data-theme="brutal"] .modal {
  border: 2px solid var(--nb-ink); border-radius: 10px;
  box-shadow: 6px 6px 0 var(--nb-ink);
}
html[data-theme="brutal"] .drawer { border-right: 2px solid var(--nb-ink); }
html[data-theme="brutal"] .ctx-item { border-radius: 6px; }
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .ctx-item:hover {
    background: var(--bg-hover);
  }
}
html[data-theme="brutal"] .scale-opt input[type="radio"] {
  accent-color: var(--nb-main);
}

/* Bottom action bar: joins the ink frame */
html[data-theme="brutal"] .list-bar {
  border-top: 2px solid var(--nb-ink);
  padding-left: 12px; padding-right: 12px;
}
html[data-theme="brutal"] .bar-btn {
  border: 2px solid var(--nb-ink); border-radius: 8px;
  background: var(--bg-bubble-in); box-shadow: 2px 2px 0 var(--nb-ink);
  transition: background .15s, color .15s, border-color .15s, box-shadow .15s;
}
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .bar-btn:hover { background: var(--nb-main); color: var(--nb-main-ink); }
}
html[data-theme="brutal"] .bar-btn:active { border-color: var(--accent-2); box-shadow: none; }
html[data-theme="brutal"] .bar-btn.bar-accent {
  background: var(--accent); color: #17131f;
}
@media (hover: hover) and (pointer: fine) {
  html[data-theme="brutal"] .bar-btn.bar-accent:hover { background: var(--accent); }
}

/* "Your QR code" screen (#37): copy/share row and the in-page scanner tab. */
.side-view .qr-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.side-view .qr-scan { width: 100%; max-width: 360px; aspect-ratio: 1; margin: 4px auto; border-radius: 12px; overflow: hidden; background: #0b0b10; }
.side-view .qr-scan video { width: 100%; height: 100%; object-fit: cover; display: block; }
.side-view .qr-scan-status { text-align: center; font-size: 13.5px; color: var(--text-dim); }
.side-view .qr-view > .side-tabs { margin-bottom: 4px; }

/* #135 recent "/" commands: listbox anchored above #main-composer. */
#main-composer { position: relative; }
.recent-commands-pop {
  position: absolute; left: 14px; right: 14px; bottom: 100%;
  margin: 0 0 4px; padding: 4px 0; list-style: none;
  max-height: min(calc(var(--vh) * .50), 340px); overflow-y: auto; z-index: 30;
  background: var(--bg-sidebar); color: var(--text);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow);
}
.recent-commands-pop [role=option] {
  padding: 10px 14px; cursor: pointer; font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.recent-commands-pop [role=option]:hover,
.recent-commands-pop [role=option].active { background: color-mix(in srgb, var(--accent) 18%, transparent); }

/* #110: relay confirmed UnifiedPush support (Relays modal). */
.relay-push-mark { font-size: .85em; opacity: .7; margin-left: 4px; }

/* Relays modal (#15): each relay is a two-line card — address + capability
   flags on the first line, action buttons on the second. Flags are silver
   (not offered / not known yet) and turn green when the relay advertised
   the capability. C calls/TURN, I iroh, P push, D instant delivery (IDLE), W web/C3 endpoints. */
.relay-card { display: flex; flex-direction: column; gap: 6px; padding: 8px 0; border-bottom: 1px solid var(--border); }
.relay-card:last-child { border-bottom: 0; }
/* VENDORISSUES #23: opt-in automatic relay onboarding — profile-level
   switch under the relay list, above the flag legend. */
.relay-auto { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
.relay-auto label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13.5px; }
.relay-auto input { width: 15px; height: 15px; accent-color: var(--accent); flex: none; margin: 0; }
.relay-auto-hint { margin: 4px 0 0 23px; font-size: 12.5px; line-height: 1.35; color: var(--text-dim); }
/* Paused relay (Velta #24, #189): dimmed card + off chip. */
.relay-card.relay-off .relay-addr { opacity: .55; }
.relay-off-chip {
  display: inline-block; margin-inline-start: 4px; padding: 1px 8px;
  border-radius: 999px; border: 1px solid var(--border);
  color: var(--muted); font-size: 11px; vertical-align: middle;
}
.relay-line { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.relay-addr { font-weight: 600; overflow-wrap: anywhere; }
.relay-sending { font-weight: 400; opacity: .75; }
.relay-flags { display: inline-flex; gap: 4px; flex: none; }
/* Neutral key-caps: the letter never carries color, and a green dot pinned
   to the corner marks capabilities the relay advertises — presence/absence
   of the dot is the state, so no green/grey pairs to tune per theme. */
.relay-flag {
  position: relative;
  font-size: 10.5px; font-weight: 700; line-height: 1;
  padding: 3px 5px; border-radius: 5px;
  color: var(--text-dim); background: var(--bg-hover);
  cursor: default; user-select: none;
}
.relay-flag.on::before {
  content: ""; position: absolute; top: -2px; left: -2px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--green);
}
/* Relay details (admin/comment METADATA, quota): dim small text; the quota
   bar fill is --text-dim on --bg-hover (same pair as the key-caps, AA as
   text), --danger from 80% — the % is always printed too, so color is
   never the only signal. */
.relay-meta { font-size: 12px; color: var(--text-dim); overflow-wrap: anywhere; }
.relay-quota { display: flex; align-items: center; gap: 6px; }
.relay-quota-bar { flex: none; width: 56px; height: 6px; border-radius: 3px; background: var(--bg-hover); overflow: hidden; }
.relay-quota-bar > span { display: block; height: 100%; background: var(--text-dim); }
.relay-quota-bar > span.high { background: var(--danger); }
.relay-actions { display: flex; flex-wrap: wrap; gap: 6px; }
/* Flag key: the letters mean the same on every relay, say so once. Chip and
   label travel as a pair (tight); the legend gap separates the pairs. */
.relay-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px;
  margin-top: 12px; font-size: 12px; color: var(--text-dim);
}
.relay-pair { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.relay-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); flex: none; }

/* #138: Velta Beam opens as a panel in the chat view's flow, directly below
   the chat header: the history shrinks between it and the composer, which
   stays fully visible (also with the Android keyboard open — the cap keeps
   the panel under half of the chat view). Expanded = full-screen overlay. */
:root { --webxdc-panel-h: min(max(220px, calc(var(--dvh) * .30)), 45%); }
#webxdc-overlay.webxdc-panel:not(.webxdc-expanded) {
  position: relative; inset: auto; z-index: 5;
  flex: 0 0 var(--webxdc-panel-h);
  height: var(--webxdc-panel-h);
  border-bottom: 1px solid rgba(255,255,255,.12);
  box-shadow: 0 4px 12px rgba(0,0,0,.25);
}
#webxdc-overlay.webxdc-panel:not(.webxdc-expanded) .webxdc-bar { padding-top: 6px; padding-bottom: 6px; }
.webxdc-bar .webxdc-title { flex: 1; }
.webxdc-expand span { font-size: 18px; line-height: 1; }

/* #136: menu buttons show an X while their menu is open. */
.icon-btn.menu-open svg { animation: menu-x-in .16s ease-out; }
@keyframes menu-x-in { from { transform: rotate(-90deg) scale(.6); opacity: .4; } to { transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .icon-btn.menu-open svg { animation: none; } }

/* Chat info member list (member-list.js): search row + fixed-height virtual rows. */
.member-search { display: flex; gap: 6px; align-items: center; margin: 6px 0; }
.member-search .text-field { flex: 1; min-width: 0; }
.member-scroll { position: relative; }
.member-row { display: flex; align-items: center; gap: 10px; padding: 0 6px; box-sizing: border-box; cursor: pointer; border-radius: 8px; }
.member-row:hover { background: var(--hover); }
.member-row:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.member-row velta-avatar { flex: 0 0 auto; }
.member-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.member-name, .member-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.member-sub { font-size: 12.5px; opacity: .7; }
.member-you { opacity: .7; font-weight: normal; }
.member-empty { padding: 12px 6px; opacity: .7; text-align: center; }

/* Chat info media sections (chat-media.js) */
.media-grid .media-tile { padding: 2px; box-sizing: border-box; overflow: hidden; }
.media-tile .media-open { display: block; width: 100%; height: 100%; padding: 0; margin: 0; border: 0; background: none; cursor: pointer; }
.media-tile .media-goto { position: absolute; top: 4px; right: 4px; z-index: 2; width: 28px; height: 28px; min-width: 0; padding: 0; border-radius: 50%; background: rgba(0,0,0,.55); color: #fff; display: grid; place-items: center; }
.media-tile .media-goto svg { width: 16px; height: 16px; }
.media-row .media-goto { flex: none; }
.media-tile .media-tile-inner { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 6px; background: var(--bg-elev); }
.media-tile.media-loading::after { content: ""; display: block; width: 100%; height: 100%; border-radius: 6px; background: var(--bg-elev); opacity: .6; }
.media-tile .media-video { position: relative; display: grid; place-items: center; color: #fff; overflow: hidden; }
.media-video .media-poster { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.media-video .media-play-glyph { position: relative; z-index: 1; display: grid; place-items: center; line-height: 0; }
.media-video .media-play-glyph svg { width: 28px; height: 28px; }
.media-video .media-dur { z-index: 1; line-height: 1.4; white-space: nowrap; position: absolute; right: 4px; bottom: 3px; font-size: 11px; background: rgba(0,0,0,.55); padding: 0 4px; border-radius: 4px; }
.media-open:focus-visible, .media-goto:focus-visible, .media-row:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }
.media-app-icon { width: 36px; height: 36px; flex: none; border-radius: 8px; object-fit: cover; }
.media-app-letter { display: grid; place-items: center; background: var(--bg-elev); font-weight: 600; }
.media-play { flex: none; }
.media-audio { display: block; width: 100%; }
.media-details summary[aria-disabled="true"] { opacity: .6; }
/* Media list rows: fixed heights = mediaRowHeight() in chat-media.js; never scroll inside a row. */
.media-row { overflow: visible; }
.media-row .member-text { overflow: visible; justify-content: center; gap: 2px; }
.media-row-audio { height: 80px; align-items: center; }
.media-row-audio .media-audio { margin-top: 2px; }
.media-audio .vp { height: 30px; background: none; padding: 0; }
.media-audio .vp-play { width: 26px; height: 26px; }
/* Text-only mode (#300): covered gallery items show no media, only a reveal
   button (same in-memory exception as the chat bubble). */
.media-tile .media-textonly-cover { display: block; width: 100%; height: 100%; border-radius: 6px; background: var(--bg-elev); }
.media-tile .media-reveal { position: absolute; inset: 0; margin: auto; height: 30px; max-width: calc(100% - 12px); border-radius: 15px; background: rgba(0,0,0,.55); color: #fff; font-size: 12.5px; }
.msg-textonly .btn-text { flex: none; }
.msg-link-preview .lp-hidden-note { display: block; font-size: 12.5px; color: var(--text-dim); margin-bottom: 2px; }

/* ---------- Settings transfer (settings-transfer.js) ---------- */
/* "Apply these settings" inside a Saved Messages bubble: the scoped
   .msg-row.out .btn-text color keeps it at AA on the own bubble (6.4). */
.msg-settings-act { margin-top: 6px; }
/* Apply confirm: what changes, current → new. Scrolls inside the modal body
   when long (inner scroll areas are fine, AGENTS.md §11). */
.settings-diff p { font-size: 15px; line-height: 1.45; }
.settings-diff-list { list-style: none; margin: 8px 0 4px; display: flex; flex-direction: column; gap: 6px; }
.settings-diff-list li {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 10px;
  font-size: 14px; padding: 6px 0; border-bottom: 1px solid var(--border); min-width: 0;
}
.settings-diff-list .sd-label { min-width: 0; overflow-wrap: anywhere; }
.settings-diff-list .sd-vals { min-width: 0; overflow-wrap: anywhere; color: var(--text); }
.settings-diff-list .sd-from { color: var(--text-dim); }

/* Group call in-call bar (group-call-ui.js): host chrome, outside the app
   iframe; only while in a call AND the call screen is not visible. A
   full-width strip ABOVE the app: .app (and the phone's fixed .main) move
   down by its height, so it never covers a header (KEEP, small screens).
   Design-system tokens only (design/velta-design-system.html). */
:root { --gcb-h: 40px; } /* = icon-btn height */
#group-call-bar {
  position: fixed; left: 0; right: 0; top: 0; z-index: 30; /* above app UI (≤10), below popups (40+) */
  height: calc(var(--gcb-h) + env(safe-area-inset-top, 0px));
  padding: env(safe-area-inset-top, 0px) 8px 0 4px; box-sizing: border-box;
  display: flex; align-items: center; gap: 6px;
  background: var(--bg-sidebar); color: var(--text); border-bottom: 1px solid var(--border); box-shadow: var(--shadow);
  font-size: 13.5px;
}
html.in-group-call .app { margin-top: calc(var(--gcb-h) + env(safe-area-inset-top, 0px)); height: calc(100% - var(--gcb-h) - env(safe-area-inset-top, 0px)); }
@media (max-width: 820px) { html.in-group-call .main { top: calc(var(--gcb-h) + env(safe-area-inset-top, 0px)); } }
#group-call-bar .gcb-open { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden; padding: 6px 8px; border-radius: 8px; text-align: left; }
#group-call-bar .gcb-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#group-call-bar .gcb-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex: none; }
#group-call-bar.speaking .gcb-dot { box-shadow: 0 0 0 4px color-mix(in srgb, var(--green) 35%, transparent); }
#group-call-bar .btn-text { padding: 6px 12px; flex: none; }
#group-call-bar .gcb-mute[aria-pressed="true"] { background: var(--bg-active); color: var(--on-accent); }
#group-call-bar .btn-text.gcb-leave { color: var(--danger); }
@media (hover: hover) and (pointer: fine) { #group-call-bar .gcb-open:hover { background: var(--bg-hover); } }

/* Group call card in the chat (group-call-card.js): bubble content, existing
   .btn-text / .btn-primary buttons and <velta-avatar>s, tokens only. */
.msg-groupcall { display: flex; flex-direction: column; gap: 8px; min-width: min(260px, calc(var(--vw) - 110px)); padding: 2px 0; }
.msg-groupcall .gc-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
.msg-groupcall .gc-text { min-width: 0; }
.msg-groupcall .gc-title { font-weight: 600; font-size: 15px; overflow-wrap: anywhere; }
.msg-groupcall .gc-sub { font-size: 12.5px; color: var(--text-dim); }
.msg-groupcall .gc-people { display: flex; gap: 4px; }
.msg-groupcall .gc-more { font-size: 12.5px; color: var(--text-dim); align-self: center; }
.msg-groupcall .gc-actions { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.msg-groupcall .btn-text { padding: 6px 14px; border-radius: 999px; }
/* Join / Start again: NOT an accent fill. On the own (accent-tinted) bubble
   the scoped .msg-row.out .btn-text color (--text-meta-out) beat
   .btn-text.btn-primary's --on-accent and the fill sat on an accent bubble:
   blue on blue. The card's primary action is the in-bubble .btn-text (its
   per-surface AA colors: --text-meta-out out, --accent-text-in light in,
   --accent-2 brutal in, all recomputed for a custom accent) with a
   currentColor ring to mark it primary. Contrast: e2e-group-call-contrast. */
/* In a bubble, --danger / --green fail AA (2.2–4.3:1 across themes), so
   Leave is a regular in-bubble .btn-text and "You're in" takes the bubble's
   text color; dim text on the own bubble is --text-meta-out (as .msg-meta),
   brutal's own bubble uses --nb-main-ink like .msg-text. */
.msg-row .msg-groupcall .gc-in { color: inherit; }
.msg-row.out .msg-groupcall .gc-sub, .msg-row.out .msg-groupcall .gc-more, .msg-row.out .msg-groupcall .gc-ended-label { color: var(--text-meta-out); }
html[data-theme="brutal"] .msg-row.out .msg-groupcall { color: var(--nb-main-ink); }
.msg-groupcall .btn-text.gc-primary { box-shadow: inset 0 0 0 1px currentColor; }
.msg-groupcall .gc-in { color: var(--green); font-weight: 600; font-size: 13px; }
.msg-groupcall .gc-ended-label { color: var(--text-dim); font-size: 13px; }
.gc-name-label { display: block; font-size: 13px; color: var(--text-dim); margin-bottom: 6px; }
.gc-name-input { width: 100%; box-sizing: border-box; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-hover); color: var(--text); }
.gc-name-hint { font-size: 12.5px; color: var(--text-dim); margin: 8px 0 0; }

/* @mentions (docs/mentions.md). Soft tinted chip (Telegram-like): text uses
   the per-surface AA link tokens (in: --accent-text-in, out: --out-link,
   brutal out: ink); the background is a light tint of that same colour
   (currentColor), so it follows every theme without new tokens. No fixed
   height and no outline: line-height normal + em padding let the chip grow
   with glyph extents (Й breve, Д descender, Zalgo marks, emoji). Wrapped
   names get a rounded chip per line (box-decoration-break: clone). Brutal
   keeps a thin 1px outline instead of the tint. The fingerprint code stays
   inside the chip, muted by size/weight (smaller, regular, mono) but full
   colour so it still meets 4.5:1 on the tint, with line-height 0 so it never
   changes the chip height. */
.msg-text .mention {
  color: var(--accent-text-in); font-weight: 600; -webkit-hyphens: manual; hyphens: manual;
  line-height: normal; padding: .08em .35em; border-radius: .45em;
  background: color-mix(in srgb, currentColor 8%, transparent);
  -webkit-box-decoration-break: clone; box-decoration-break: clone;
}
.msg-row.out .msg-text .mention { color: var(--out-link); background: color-mix(in srgb, currentColor 14%, transparent); }
html[data-theme="brutal"] .msg-text .mention { background: transparent; box-shadow: inset 0 0 0 1px currentColor; border-radius: 4px; }
html[data-theme="brutal"] .msg-row.out .msg-text .mention { color: var(--nb-main-ink); }
.msg-text .mention-me { font-weight: 700; text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: .18em; }
.msg-text .mention .mention-code { line-height: 0; font-weight: 400; }
.mention-code { font-family: var(--font-mono); font-size: .72em; font-weight: 400; margin-left: .3em; letter-spacing: .02em; }
.mention-pop [role=option] {
  display: flex; align-items: center; gap: 10px; font-family: inherit; padding: 6px 14px;
}
.mention-pop .mention-opt-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.mention-pop .mention-code { color: inherit; }
/* Loading status row: muted, not an option (AA: --text-dim on --bg-sidebar, all themes ≥ 5.2:1). */
.mention-pop .mention-loading { padding: 8px 14px; color: var(--text-dim); font-size: .9em; cursor: default; }

/* Inline house icons that replaced text glyphs (✕/✓/✗): size to the text. */
.btn-text > svg, .pin-unpin > svg, .gc-in > svg, .info-row .v > svg {
  width: 1.1em; height: 1.1em; vertical-align: -0.2em; flex: none;
}

/* SOCKS5 proxy settings: saved proxies (proxy.js proxyListHtml). The
   active row is marked by a check AND an "In use" badge, not colour alone. */
.tunnel-list-h { font-size: 13px; font-weight: 600; color: var(--text-dim); margin: 0 0 6px; }
.tunnel-list { list-style: none; margin: 0 0 6px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.tunnel-row { display: flex; align-items: center; gap: 8px; padding: 6px 4px 6px 6px; border-radius: 10px; border: 1px solid var(--border); }
.tunnel-row.is-active { border-color: var(--accent); }
.tunnel-mark { width: 22px; flex: none; display: grid; place-items: center; color: var(--accent); }
.tunnel-mark svg { width: 20px; height: 20px; }
.tunnel-text { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; }
.tunnel-name { font-weight: 600; overflow-wrap: anywhere; }
.tunnel-badge { font-size: 12px; font-weight: 600; padding: 1px 6px; border-radius: 6px; border: 1px solid currentColor; }
.tunnel-sub { flex-basis: 100%; font-size: 12px; color: var(--text-dim); overflow-wrap: anywhere; }
.tunnel-acts { display: flex; align-items: center; flex: none; }
.tunnel-acts .btn-text { min-height: 40px; padding: 0 10px; }
.tunnel-acts .icon-btn { width: 40px; height: 40px; }
.tunnel-acts button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }

/* ===== Scheduled messages ("Send later", app/js/scheduled-ui.js) ===== */
.sched-chip {
  display: flex; align-items: center; gap: 6px; align-self: flex-start;
  margin: 4px 10px 0; padding: 4px 12px; min-height: 32px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg-sidebar); color: var(--text); font: inherit; font-size: 13px; cursor: pointer;
}
.sched-chip[hidden] { display: none; }
.sched-chip svg { width: 16px; height: 16px; color: var(--accent); flex: none; }
.sched-chip:hover { background: var(--bg-hover); }
.sched-chip:focus-visible, .sched-preset:focus-visible, .sched-row .btn-text:focus-visible, .sched-dt:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
.sched-pick { display: flex; flex-direction: column; gap: 8px; min-width: min(340px, calc(var(--vw) - 48px)); }
.sched-presets { display: flex; flex-direction: column; }
.sched-preset { width: 100%; }
.sched-preset .sched-preset-l { flex: 1; text-align: start; }
.sched-preset .sched-preset-t { color: var(--text-dim); font-size: 13px; }
.sched-custom-l { font-size: 13px; color: var(--text-dim); margin-top: 6px; }
.sched-dt, .sched-text {
  font: inherit; font-size: 15px; color: var(--text); background: var(--bg);
  border: 1px solid var(--text-dim); border-radius: 8px; padding: 8px 10px; color-scheme: light dark;
}
.sched-text { width: 100%; box-sizing: border-box; resize: vertical; min-width: min(340px, calc(var(--vw) - 48px)); }
.sched-hint { font-size: 13px; color: var(--text-dim); line-height: 1.4; margin: 0; }
.sched-err { font-size: 13px; color: var(--danger); margin: 0; }
.sched-sheet { display: flex; flex-direction: column; gap: 10px; }
.sched-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sched-row { border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; background: var(--bg-sidebar); }
.sched-row-err { border-color: var(--danger); }
.sched-chat { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
.sched-when { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-dim); }
.sched-row-err .sched-when { color: var(--danger); }
.sched-when svg { width: 15px; height: 15px; flex: none; }
.sched-body { margin: 6px 0; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 7.5em; overflow: hidden; color: var(--text); }
.sched-acts { display: flex; flex-wrap: wrap; gap: 4px 12px; }
.sched-acts .sched-del { color: var(--danger); }
.sched-empty { color: var(--text-dim); text-align: center; padding: 16px 0; }

/* Invite link domain pills above invite QRs (invite-domains.js); same chip
   palette as the chat category bar (AA in light/dark/brutal). */
.domain-tabs { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 10px 0 0; }
.domain-tab {
  padding: 6px 12px; font-size: 12.5px; line-height: 1.2; border: none; border-radius: 999px;
  color: var(--text-dim); background: var(--bg-hover); cursor: pointer;
}
.domain-tab[aria-selected="true"] { background: var(--accent); color: var(--on-accent-chip); }
.domain-tab:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .domain-tab:hover:not([aria-selected="true"]) { color: var(--text); }
}

/* Admin groups (#177): badge on the admin's member row, info line under the
   group name, the "Admin group" switch in the new-group dialog. Text uses
   --accent-text / --text (AA on every theme surface: accent-text >= 4.65:1
   light, 7.2 dark, 5.3 brutal); the shield is an outline 24-grid icon. */
.admin-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 8px 1px 6px; border: 1px solid currentColor; border-radius: 999px;
  color: var(--accent-text); font-size: 12px; font-weight: 600; line-height: 18px;
  white-space: nowrap;
}
.admin-badge svg { width: 14px; height: 14px; flex: none; }
.member-actions .admin-badge + .btn-text { margin-left: 6px; }
.admin-group-info {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  margin: 2px 0 6px; font-size: 13px; color: var(--text);
}
.admin-group-info svg { width: 16px; height: 16px; color: var(--accent-text); flex: none; }
.admin-group-opt { align-items: flex-start; margin: 10px 2px 0; }
.admin-group-opt-text { display: flex; flex-direction: column; gap: 2px; }
.admin-group-opt-title { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
.admin-group-opt-title svg { width: 18px; height: 18px; color: var(--accent-text); flex: none; }
.admin-group-hint { font-size: 12.5px; color: var(--text-dim); }
.info-row[data-admin-only] { cursor: default; }
.info-row[data-admin-only] .v { color: var(--text-dim); }

/* #178 database locked: blocking screen for accounts the keychain key
   cannot open. Sits over everything, including onboarding. */
#db-locked {
  position: fixed; inset: 0; z-index: 1000;
  display: grid; place-items: center;
  background: var(--bg); color: var(--text);
  padding: 16px;
}
#db-locked .db-locked-card { max-width: 420px; display: grid; gap: 12px; text-align: center; }
#db-locked h2 { margin: 0; font-size: 18px; }
#db-locked p { margin: 0; opacity: .85; font-size: 14px; line-height: 1.45; }
#db-locked .db-locked-hint { opacity: .65; font-size: 13px; }
#db-locked .db-locked-retry { margin-top: 4px; }

/* #178 encryption off / key missing: non-blocking notice. Tokens only:
   --text on --bg-sidebar (AA in dark, light and high-contrast themes),
   --danger only as a non-text accent bar. */
#db-key-notice {
  position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 900;
  display: flex; align-items: flex-start; gap: 12px;
  max-width: 560px; margin: 0 auto; padding: 12px 14px;
  background: var(--bg-sidebar); color: var(--text);
  border: 1px solid var(--border); border-left: 4px solid var(--danger);
  border-radius: var(--radius-media); box-shadow: 0 4px 16px rgba(0,0,0,.25);
  font-size: 14px; line-height: 1.45;
}
#db-key-notice .db-key-notice-text { display: grid; gap: 4px; flex: 1; }
#db-key-notice .db-key-notice-close {
  min-width: 32px; min-height: 32px; border: 0; border-radius: 6px;
  background: transparent; color: var(--text); font-size: 20px; cursor: pointer;
}
#db-key-notice .db-key-notice-close:hover { background: var(--bg-hover); }
#db-key-notice .db-key-notice-close:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Profile export / transfer / import progress (transfer-progress.js) */
.xfer { display: grid; gap: 6px; margin-top: 10px; }
.xfer-bar { width: 100%; height: 8px; accent-color: var(--accent); appearance: none; -webkit-appearance: none; border: 1px solid var(--border); border-radius: 999px; background: var(--bg-hover); overflow: hidden; }
.xfer-bar::-webkit-progress-bar { background: var(--bg-hover); border-radius: 999px; }
.xfer-bar::-webkit-progress-value { background: var(--accent); border-radius: 999px; }
.xfer-bar::-moz-progress-bar { background: var(--accent); border-radius: 999px; }
.xfer-row { display: flex; gap: 10px; align-items: baseline; font-size: 14px; color: var(--text); }
.xfer-stage { flex: 1; min-width: 0; }
.xfer-size, .xfer-pct { color: var(--text-dim); font-variant-numeric: tabular-nums; }
.xfer.is-failed .xfer-stage { color: var(--danger); }
.xfer.is-failed .xfer-bar::-webkit-progress-value { background: var(--danger); }
.xfer.is-failed .xfer-bar::-moz-progress-bar { background: var(--danger); }
.xfer-pill { position: fixed; z-index: 900; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-sidebar); color: var(--text); font: inherit; font-size: 13px; font-variant-numeric: tabular-nums; box-shadow: 0 4px 16px rgba(0,0,0,.25); cursor: pointer; }
.xfer-pill:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.xfer-pill[hidden] { display: none; }

/* ===== #240 Core watchdog banner: non-blocking, top of the chat pane.
   Tokens only (design-token-guard); --danger is AA-tuned per theme. ===== */
.core-banner {
  position: fixed; top: calc(10px + env(safe-area-inset-top, 0px)); left: 50%; transform: translateX(-50%);
  z-index: 900; width: max-content; max-width: min(560px, calc(var(--vw) - 24px));
  display: flex; align-items: center; gap: 10px;
  padding: 8px 8px 8px 12px; border-radius: 12px;
  background: var(--bg-sidebar); color: var(--text);
  border: 1px solid var(--border); border-left: 4px solid var(--danger);
  box-shadow: 0 4px 18px color-mix(in srgb, var(--bg) 60%, transparent);
  font-size: 13.5px; line-height: 1.35;
}
.core-banner[hidden] { display: none; }
.core-banner-ico { width: 20px; height: 20px; flex: none; color: var(--danger); }
.core-banner-text { flex: 1; min-width: 0; }
.core-banner-btn {
  flex: none; width: auto; padding: 7px 12px; border-radius: 9px; border: 0;
  background: var(--accent); color: var(--on-accent); font-weight: 600; cursor: pointer;
}
.core-banner-btn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.core-banner-btn:disabled { opacity: .6; cursor: progress; }
.core-banner[data-status="dead"] .core-banner-btn { display: none; }
html[data-theme="brutal"] .core-banner { border: 2px solid var(--nb-ink); border-left-width: 6px; border-left-color: var(--danger); border-radius: 4px; box-shadow: 4px 4px 0 var(--nb-ink); }
html[data-theme="brutal"] .core-banner-btn { border: 2px solid var(--nb-ink); border-radius: 4px; box-shadow: 2px 2px 0 var(--nb-ink); }

/* Stall-safe loads (load-retry.js): failed list load banner. Ink is --text
   on the panel (AA in dark/light/brutal, tests/load-retry.test.mjs); the
   danger edge is decoration only. */
.load-error { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 8px 10px; padding: 6px 6px 6px 12px; border-left: 3px solid var(--danger); border-radius: 8px; background: var(--bg-sidebar); color: var(--text); font-size: 14px; }
.load-error .btn-text { flex: none; }
html[data-theme="brutal"] .load-error { border: 2px solid var(--nb-ink, var(--text)); border-left-width: 4px; }
/* #286: send failover / strict-wait status in the Relays modal. Same
   surface as .load-error (--text on --bg-sidebar: AA in every theme). */
.relay-failover { margin: 0 0 10px; padding: 8px 12px; border-left: 3px solid var(--accent); border-radius: 8px; background: var(--bg-sidebar); color: var(--text); font-size: 13.5px; line-height: 1.35; }
html[data-theme="brutal"] .relay-failover { border: 2px solid var(--nb-ink, var(--text)); border-left-width: 4px; }
