/* ===========================================================================
   CodeTalk · Styles
   Goal: on the landing page there is exactly one thing for the user to do
   (enter the passcode); once inside, exactly one thing (talk).
   =========================================================================== */

:root {
  --bg: #f5f6f8;
  --surface: #ffffff;
  --surface-2: #f0f1f4;
  --text: #14171a;
  --muted: #697586;
  --line: #e3e6ea;
  --accent: #2f6df6;
  --accent-ink: #ffffff;
  --ok: #12a150;
  --warn: #e08d0b;
  --danger: #d93a3a;
  --radius: 14px;
  --radius-sm: 10px;
  --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 8px 24px rgba(16, 24, 40, .06);

  /* Chat bubbles: your own use brand blue (not WeChat's green, so this
     product keeps a consistent look). The peer's bubbles are pure white
     with a hairline border, lifting them off the light-gray background. */
  --bubble-me: #2f6df6;
  --bubble-me-ink: #ffffff;
  --bubble-them: #ffffff;
  --chat-bg: #f5f6f8;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1115;
    --surface: #171a21;
    --surface-2: #1e222b;
    --text: #e8eaed;
    --muted: #98a2b3;
    --line: #262b36;
    --accent: #4a83ff;
    --ok: #2fbf6d;
    --warn: #e8a33d;
    --danger: #f05656;
    --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 8px 24px rgba(0, 0, 0, .35);

    --bubble-me: #3b6fe0;
    --bubble-me-ink: #ffffff;
    --bubble-them: #1e222b;
    --chat-bg: #0f1115;
  }
}

[hidden] { display: none !important; }   /* Required! A class selector's display overrides the UA's [hidden] */

* { box-sizing: border-box; }

html, body { height: 100%; }

/*
 * ⚠ Must be locked to 100%, or font sizes on Android run away.
 * Huawei / UC / QQ browsers apply "Font Boosting" (web font scaling) to pages
 * by default: body text is re-enlarged based on screen width, and if the
 * system "font size" setting is bumped up a notch as well, the two compound
 * until px stops meaning px — long lines like the trust bar can be stretched
 * to half the screen, leaving the chat area a sliver.
 * Locked to 100%: **the page no longer drifts with the system web font
 * size**, so it looks the same across devices and browsers.
 */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
        "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior: none;
}

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

button { cursor: pointer; }

/* ============================ Buttons ============================ */

.primary {
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  padding: 11px 18px;
  font-weight: 600;
  transition: filter .15s, opacity .15s;
}
.primary:hover:not(:disabled) { filter: brightness(1.06); }
.primary:disabled { opacity: .45; cursor: default; }

.icon {
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  font-size: 16px;
  line-height: 1;
  transition: background .15s, border-color .15s, transform .1s;
}
.icon:hover { background: var(--surface-2); }
.icon:active { transform: scale(.94); }
.icon.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.icon.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, var(--line)); }
.icon.danger:hover { background: color-mix(in srgb, var(--danger) 10%, var(--surface)); }

/* ---------------------------------------------------------------------------
   Icons are always inline SVG, **never emoji**.

   How an emoji looks is decided by the system font: the same "📹" is three
   completely different things on iOS, Android and Windows, and some of them
   (especially the Windows set) are ugly enough to look like a different
   product. Stroked SVG follows currentColor: change one state color and the
   whole icon follows, and it always aligns crisply.
   --------------------------------------------------------------------------- */
.icon svg { width: 19px; height: 19px; display: block; }

/* Quality tier. Deliberately a native select instead of a hand-drawn menu:
   on phones it opens the system picker, with a large tap target and no
   positioning or click-outside-to-close logic of our own to handle. */
.quality {
  flex: none;
  height: 38px;
  padding: 0 6px 0 9px;
  font: inherit;
  font-size: 12.5px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 19px;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.quality:hover { background: var(--surface-2); }
.quality:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Small inline button (used on the identity bar) */
.mini {
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  /* Same clamp adaptation: on small screens the button shrinks one size so
     more fit per row, without bursting the bar */
  padding: 3px 8px;
  padding: clamp(3px, 0.9vw, 4px) clamp(8px, 2.4vw, 11px);
  font-size: 11px;
  font-size: clamp(11px, 2.9vw, 12px);
  line-height: 1.45;
  white-space: nowrap;
  transition: background .15s, border-color .15s;
}
.mini:hover { background: var(--surface-2); }
.mini.danger {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 35%, var(--line));
}
.mini.danger:hover { background: color-mix(in srgb, var(--danger) 10%, var(--surface)); }

/* ============================ Entry ============================ */

/*
 * ⚠ `justify-content: center` has an old trap: **when the content is taller
 * than the viewport, the overflowing end gets clipped, and it cannot be
 * scrolled back** (with flex centering the start sits outside the viewport,
 * out of reach of the scrollbar).
 * With large system fonts / Huawei's setTextZoom scaling up, the landing
 * card grows taller than the screen and the top title and hint simply vanish
 * — what the user sees is "the hint is cut off".
 *
 * `safe center` fixes exactly this: center when it fits, fall back to
 * flex-start when it doesn't (so the top stays reachable).
 * Legacy browsers that don't support `safe` drop the second declaration and
 * fall back to the first line's flex-start — the safe side either way.
 */
.gate {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  justify-content: safe center;
  gap: 14px;
  gap: clamp(14px, 3.6vw, 22px);
  padding: 16px;
  padding: clamp(16px, 4vw, 24px);
}

.card {
  width: 100%;
  max-width: 380px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  /* clamp: on narrow screens, give padding over to the text so it isn't
     squeezed out */
  padding: 20px 18px 18px;
  padding: clamp(20px, 5.4vw, 30px) clamp(18px, 4.8vw, 26px) clamp(18px, 4.8vw, 26px);
}

.card h1 {
  margin: 0 0 6px;
  font-size: 19px;
  font-size: clamp(19px, 5.2vw, 21px);
  font-weight: 650;
  letter-spacing: .01em;
  text-align: center;
}

.lede {
  margin: 0 0 14px;
  margin: 0 0 clamp(14px, 3.6vw, 22px);
  color: var(--muted);
  /* Was hard-coded 13.5px: one step up in system font size pushed it out of
     the card. clamp lets it tighten automatically on small screens / large
     fonts. */
  font-size: 12.5px;
  font-size: clamp(12.5px, 3.4vw, 13.5px);
  line-height: 1.6;
}

.field {
  position: relative;
  margin-bottom: 14px;
}

.field input {
  width: 100%;
  /* Explicit 16px for two reasons: ① inheriting body's size would squeeze
     the placeholder into an ellipsis at large font sizes; ② iOS Safari
     zooms the whole page on focus for inputs < 16px — 16px is its no-zoom
     threshold. */
  font-size: 16px;
  padding: 12px 62px 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  outline: none;
  transition: border-color .15s, background .15s;
}
.field input:focus {
  border-color: var(--accent);
  background: var(--surface);
}
.field input::placeholder { color: var(--muted); }

/* Passcode masking: only swaps the "display", never touches the input's type.
   ⚠ type="password" makes Android IMEs switch to the English keyboard and
   disables candidate words → Chinese becomes impossible to type.
   -webkit-text-security is supported in the engines behind Chrome / Edge /
   Safari and all the domestic browsers; where unsupported (Firefox) it falls
   back to plain text — a deliberate trade: a missing mask beats an input
   users cannot type into. */
.masked {
  -webkit-text-security: disc;
}

.reveal {
  position: absolute;
  top: 50%; right: 8px;
  transform: translateY(-50%);
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 12.5px;
  padding: 6px 8px;
  border-radius: 6px;
}
.reveal:hover { color: var(--text); background: var(--surface-2); }

.card .primary { width: 100%; }

/*
 * Hint text ("the more unique the passcode, the safer…"). Same approach as
 * the trust bar: font size / line height / margins all clamp, **and no
 * max-height cap** — it is static copy, shown in full when it fits, unlike
 * the trust bar it needs no fallback. What actually truncated it was the
 * .gate centering overflow above (already fixed).
 */
.hint {
  margin: 0 0 12px;
  margin: 0 0 clamp(12px, 3.2vw, 18px);
  color: var(--muted);
  font-size: 11px;
  font-size: clamp(11px, 3vw, 12px);
  line-height: 1.55;
}

/* The inline link inside the "More" hint ("…or click to contact us").
   Accent color + no underline: it stands out inside 12px gray text without
   looking like a 1990s link.
   ⚠ It must be clickable — written as plain text with no entry point, users
   follow the words and find nothing. */
.hint .hl {
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
}
.hint .hl:hover,
.hint .hl:focus-visible {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.error {
  margin: 12px 0 0;
  color: var(--danger);
  font-size: 12px;
  font-size: clamp(12px, 3.2vw, 13px);
  line-height: 1.5;
  text-align: center;
}

.foot {
  max-width: 420px;
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.7;
  text-align: center;
}

/* Footer: privacy note + contact. Wrapped so .gate's 22px gap doesn't push
   the two blocks of small text apart. */
.page-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 420px;
}

.contact {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.8;
  text-align: center;
}
.contact a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: color-mix(in srgb, currentColor 32%, transparent);
}
.contact a:hover { color: var(--text); }

/* ---------------------------------------------------------------------------
   "More": the landing page keeps only "enter passcode → enter"; everything
   else is folded in here (expanded on tap).
   Same styling as the self-test block below, just one layer deeper — more
   <details> can nest inside it.
   --------------------------------------------------------------------------- */
.more {
  width: 100%;
  max-width: 420px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}
.more > summary {
  padding: 10px 14px;
  font-size: 12.5px;
  color: var(--muted);
  text-align: center;
  cursor: pointer;
  list-style: none;
}
.more > summary::-webkit-details-marker { display: none; }
.more > summary::before { content: '▸ '; }
.more[open] > summary {
  color: var(--text);
  border-bottom: 1px solid var(--line);
}
.more[open] > summary::before { content: '▾ '; }
.more > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* Vertical stack inside: safety tips → install on phone → self-test →
   privacy / contact */
.more-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px 14px 14px;
}
.more-body .hint { margin-bottom: 0; text-align: left; }
.more-body .page-foot { max-width: none; }

/* ---------------------------------------------------------------------------
   The self-test entry on the landing page (expandable).
   Deliberately a <details>: the landing page keeps "enter passcode" as its
   one job; people hitting trouble will expand it themselves.
   --------------------------------------------------------------------------- */
.selftest {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  overflow: hidden;
}

.selftest > summary {
  padding: 10px 14px;
  font-size: 12.5px;
  color: var(--muted);
  text-align: center;
  cursor: pointer;
  list-style: none;                 /* Drop the system triangle in favor of the ::before below */
}
.selftest > summary::-webkit-details-marker { display: none; }
.selftest > summary::before { content: '▸ '; }
.selftest[open] > summary {
  color: var(--text);
  border-bottom: 1px solid var(--line);
}
.selftest[open] > summary::before { content: '▾ '; }
.selftest > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.selftest-body { padding: 12px 14px 14px; }
.selftest-body p {
  margin: 0 0 10px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.7;
  text-align: left;
}
.selftest-body p:last-child { margin-bottom: 0; }
.selftest-body b { color: var(--text); font-weight: 600; }

.selftest-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.selftest-links a {
  flex: 1 1 120px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 12px;
  text-align: center;
  text-decoration: none;
  transition: border-color .15s, color .15s, background .15s;
}
.selftest-links a:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--surface);
}

.selftest-tip { font-size: 11.5px; }

/* ---------------------------------------------------------------------------
   The two floating buttons at bottom right: Download App (lower) +
   Contact us (upper).

   2026-10-13: changed from "a button in the landing form" to a persistent
   FAB. Reason: the landing page keeps only "enter passcode" as its one job;
   installing is a parallel path — sitting in the form it just steals layout
   from the first screen. The floating ball costs no layout and is reachable
   at any moment.

   All install notes (Huawei interception / iOS must add to home screen from
   Safari) are folded into the overlay — **exactly one copy**. "More" used to
   carry a second copy; two entries guarantee one rots first: version
   number, size and Huawei's interception steps each written twice, fix one
   and miss the other.
   --------------------------------------------------------------------------- */

.fab-dl,
.fab-support {
  position: fixed;
  right: 14px;
  right: max(14px, env(safe-area-inset-right));
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(16, 24, 40, .22);
  transition: transform .12s, filter .15s, box-shadow .15s;
}
.fab-dl:active,
.fab-support:active { transform: scale(.93); }
.fab-dl:focus-visible,
.fab-support:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Lower one: Download App. Solid accent circle — the one most meant to be
   tapped in the overlay entry. */
.fab-dl {
  bottom: 16px;
  bottom: max(16px, env(safe-area-inset-bottom));
  width: 54px;
  height: 54px;
  background: var(--accent);
  color: var(--accent-ink);
}
.fab-dl:hover { filter: brightness(1.06); }
.fab-dl svg { width: 25px; height: 25px; }

/* Upper one: Contact us. Outlined white, one size smaller — it is an exit,
   not the main path, and should not compete with "Download App" for
   attention. */
.fab-support {
  bottom: calc(16px + 64px);
  bottom: calc(max(16px, env(safe-area-inset-bottom)) + 64px);
  width: 48px;
  height: 48px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--accent);
}
.fab-support:hover { border-color: var(--accent); background: var(--surface-2); }
.fab-support svg { width: 22px; height: 22px; }

/* Once inside the room, both are hidden: no installing in the room, and the
   floating balls must not cover the composer.
   ⚠ This uses a **sibling selector**, so both buttons must come after #room
     in index.html (they do, right after </main> today). Move them before
     #room and this rule fails silently. */
#room:not([hidden]) ~ .fab-dl,
#room:not([hidden]) ~ .fab-support { display: none; }

/* ---------------- QR code overlay ---------------- */

/* Full-screen translucent backdrop. Same approach as the incoming-call
   overlay (.ring): tapping the empty area closes it.
   ⚠⚠ Centering must be **safe** center, never bare center: this card is
     650px tall; a slightly shorter viewport (iPhone SE 667, small Android
     640) gets its top pushed negative by flex centering — **clipped top
     with no way to scroll back** — the same trap .gate once hit (see the
     notes there). Main axis (horizontal) uses center, cross axis (vertical)
     uses safe center: when over-tall it degrades to laying out from the
     top, and with the overflow-y:auto below you can scroll all the way
     down. */
.modal {
  position: fixed;
  /* ⚠⚠ All four sides must be written out; `inset` is only a convenient
     shorthand, **inset alone is not enough**: Chrome 55 (vivo X9's stock
     browser) doesn't know inset, so the whole declaration is dropped → the
     overlay gets no position and no size, pushed to the end of the document
     flow (outside the viewport) → tapping "Download App" **looks like
     nothing happened at all** (localized on a real device 2026-10-24).
     Side note: z-index still comes from .modal's own 60. */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0;
  z-index: 60;
  display: flex;
  /* safe center is Chrome 93+; legacy browsers drop the whole declaration →
     align-items returns to its default stretch and the card is stretched to
     full screen height. flex-start is the fallback: the card is taller than
     the viewport anyway (650px), and on new browsers safe center also
     degrades to flex-start — the two are equivalent. */
  align-items: flex-start;
  align-items: safe center;
  justify-content: center;
  padding: 14px;
  padding: clamp(14px, 4vw, 24px);
  background: rgba(10, 14, 22, .58);
  overflow-y: auto;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.modal[hidden] { display: none; }

.modal-card {
  position: relative;
  width: 100%;
  max-width: 340px;
  padding: 20px 18px 18px;
  padding: clamp(20px, 5.4vw, 26px) clamp(18px, 5vw, 24px) clamp(18px, 5vw, 22px);
  border-radius: var(--radius);
  background: var(--bg);
  box-shadow: 0 18px 50px rgba(8, 12, 24, .28);
  text-align: center;
}

/* The ✕ at top right. A 32dp square so it is easy to hit — a 16px × on a
   phone is a miss. */
.modal-x {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: 21px;
  line-height: 1;
  cursor: pointer;
}
.modal-x:hover { background: var(--surface-2); color: var(--text); }
.modal-x:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.modal-title {
  margin: 0 0 14px;
  font-size: 17px;
  font-size: clamp(17px, 4.6vw, 19px);
  font-weight: 700;
  letter-spacing: -.01em;
}

/* ===========================================================================
   Download overlay (rewritten 2026-10-19 to the layout the boss specified)
   ---------------------------------------------------------------------------
   Layout: QR code → one version line (version + APK size) → direct-link line
   → iOS line → "Close".

   Why we backed off from "three background blocks" to "a few lines of text":
     The 2026-10-14 version chopped the content into three blocks (QR code /
     "or" divider + button / notice block with platform labels) to squeeze a
     22px overflow on 360px screens. Height came down, but the price was
     **capped information density** — a caption under the QR code, then a
     version badge, then a thin "—— or ——" rule and a button, then two
     labeled lines in a gray box. Users had to read through 4 visual layers
     to learn "how do I actually download this".

   Those layers are all gone now, text only:
     · The version line merges "version + size" into one sentence — what the
       user really wants to know is "how big is this and which version is
       it"; that is one question, not two lines.
     · The "or" rule and button are gone; the direct link is an inline link —
       it is a fallback path, and giving it button shape tells the user "you
       have two main paths" when only one is real.
     · Platform labels (Android / iOS) are gone — only Android ships an APK
       and iOS is add-to-home-screen only; the labels implied both sides had
       something to choose.
   Height drops by another third along the way; 360px screens breathe easier.

   ⚠ QR size stays 200px and shrinks with the viewport (min(200px, 52vw)):
     it is the **only main path**, but what makes it scannable is
     black-white contrast (the white background is set explicitly below),
     not size; enlarging it further only pushes the text below out of the
     viewport.
   =========================================================================== */

/* ---- ① QR code ---- */
.dl-qr-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.modal-qr {
  display: block;
  width: 200px;
  width: min(200px, 52vw);
  height: auto;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: #fff;      /* 9 times out of 10, scan failures are a background that isn't white enough, not a broken pattern */
}

/* ---- ② Version line: Android APK 1.0.32 (6.9M) ----
   ⚠ The whole line is **one sentence**, not a badge: version and size are
     both part of that sentence; making it a badge would demote "what should
     I download" to decoration.
   ⚠ Font size uses clamp, not a hard value: on 320dp devices with 175% font
     scaling this line will wrap — wrapping is the **correct** behavior (two
     lines beat a horizontal truncation). */
.dl-ver-row {
  margin: 11px 0 0;
  color: var(--text);
  font-size: 13px;
  font-size: clamp(13px, 3.6vw, 14.5px);
  font-weight: 600;
  line-height: 1.55;
}
/* The version number alone gets bolder + accent color: it is the only thing
   in this line that ever changes, and tabular-nums keeps the digits from
   jittering left-right between versions. */
.dl-ver-row b {
  color: var(--accent);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* When the size is unavailable (older version.json without sizeMB), hide
   only the parenthesized part and keep showing the version — better a
   missing size than swallowing the whole version line. */
.dl-size-wrap.hide { display: none; }
/* When version.json can't be read, hide the whole line (JS adds the hidden
   class; :has() is not used: that pseudo-class fails outright on old
   WebViews, and failing leaves a dangling "Android APK" line, which is
   worse than nothing). */
.dl-ver-row.hide { display: none; }

/* ---- ③ Text lines: direct link + notes ----
   ⚠ No white-space:nowrap: on narrow screens with large fonts wrapping must
     be allowed; truncation (ellipsis / overflow clipping) would eat the
     "install unknown apps" notice outright. */
.dl-line {
  margin: 11px 0 0;
  color: var(--text);
  font-size: 12px;
  font-size: clamp(12px, 3.3vw, 13px);
  line-height: 1.65;
}
.dl-line-muted { color: var(--muted); }

/* The direct link is an inline text link: gold (not accent blue) separates
   the "fallback path" from the primary button, without being too faint to
   see. One weight step up keeps it looking tappable on narrow screens. */
.dl-link {
  color: #a9640b;
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
}
.dl-link:hover,
.dl-link:focus-visible {
  color: #8a520a;
  outline: none;
}
/* In dark mode that gold muddies against the dark background, so it switches
   to a brighter amber — the direct link is the only element that is "off the
   main path but must stay visible", and it cannot disappear with the
   theme. */
@media (prefers-color-scheme: dark) {
  .dl-link { color: #e2a94c; }
  .dl-link:hover,
  .dl-link:focus-visible { color: #f2c47a; }
}

/* "Close" button: phones have no Esc key and not everyone dares tap the
   dark area, so give them an obvious button. Secondary background color —
   it is not "start download" and should not steal the accent. */
.dl-done {
  display: block;
  margin: 16px auto 2px;
  min-width: 116px;
  padding: 9px 24px 10px;
  border: 0;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
}
.dl-done:hover { background: var(--line); }
.dl-done:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ===========================================================================
   Support message session (tap "Contact us" and a room opens on the spot)
   ===========================================================================
   The rules differ too much from a normal room, so the whole UI is scoped
   under a single .support class:

     · Removed: quality tier, attachments, call initiation — support sessions
       take text and voice only; no calls, no files;
     · Removed: device-name bar, trust bar — the other side is support, not a
       "peer" whose identity needs checking;
     · Added: the support info bar at the top (session ID S-XXXXXXXX + where
       messages go, **no countdown**).

   Kept: the top bar (connection status + leave room), the message log, the
   composer (text / hold-to-talk).
   =========================================================================== */

.room.support #video-quality,
.room.support .meta,
.room.support .trust,
.room.support #btn-attach,
.room.support #btn-call-audio,
.room.support #btn-call-video { display: none !important; }

/* Support info bar. Same position as a normal room's "device-name bar",
   padded under the top bar. */
.support-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 8px 14px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  font-size: 12.5px;
  color: var(--muted);
}
.support-dot {
  flex: none;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--ok);
  animation: pulse 1.8s ease-in-out infinite;
}
.support-agent { font-weight: 600; color: var(--text); }
.support-sep { color: var(--line); }
.support-timer {
  font-variant-numeric: tabular-nums;
  color: var(--muted, var(--text));
}
.support-timer.low { color: var(--danger, #c00); }

/* ⚠ Deleted (2026-10-17): the entire .support-hint block — the "messages are
   saved on the server…" hint is gone: messages are now emailed the moment
   the session ends and are not stored server-side (see the 10-minute
   countdown rework). Styles removed with it; dead code alone would make
   people think the line still exists in the UI. */

/* ⚠ Deleted (2026-10-14): .support-timer / .support-timer.low — the
   "5 minutes per room" countdown is gone (one tap opens a room now,
   unlimited).
   ⚠ Deleted: the entire .support-busy block (40 lines) — the "all 10 rooms
     busy / keep waiting" overlay. The queueing concept itself no longer
     exists: tapping "Contact us" always gets you a room.
     Keeping these styles would only suggest the overlay still exists. */

/* Secondary button ("not now"). Defined standalone instead of relying on the
   .ring-btn.ghost set — ⚠ it comes **before** .ring-btn.ghost, so the
   incoming-call overlay's "Decline" button is unaffected (at equal
   specificity, later wins). */
.ghost {
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font-weight: 600;
  transition: filter .15s;
}
.ghost:hover { filter: brightness(.96); }

/* ============================ Room ============================ */

.room {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
}

.bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  padding-top: 10px;
  padding-top: max(10px, env(safe-area-inset-top));
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.dot {
  flex: none;
  width: 10px; height: 10px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--muted);
  transition: background .2s;
}
.dot.waiting { background: var(--warn); animation: pulse 1.8s ease-in-out infinite; }
.dot.online  { background: var(--ok); }
.dot.warn    { background: var(--danger); }

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .3; }
}

.status {
  flex: 1;
  min-width: 0;
  font-size: 13.5px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bar-actions { display: flex; gap: 8px; }

/* ---------------------------- Device-name bar ---------------------------- */

/* One line of small text, permanently showing what device "me / peer" is on.
   Deliberately quiet: its job is **to let both sides verify out loud**
   ("what does yours show?"), not to make its presence felt. Hence the muted
   color and 12px, never competing with the chat area for attention. */
.meta {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 5px 14px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.who {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.who b {
  margin-left: 5px;
  font-weight: 600;
  color: var(--text);
}
.who.self { flex: none; max-width: 44%; }
.who.other { flex: 1; min-width: 0; }

.meta-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Checkbox switch. Native input + accent-color, no hand-drawn control —
   on phones the native control's tap target and feel beat a custom one, and
   there is less code. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.switch input {
  width: 14px; height: 14px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* ---------------------------- Peer identity bar ---------------------------- */

/*
 * All dimensions clamp-adaptive: tighter on small screens, capped at the
 * original tier on large ones.
 * ⚠ Two deliberate designs, both in service of "don't let the bar eat the
 *   chat area":
 *   ① flex-wrap + `flex: 1 1 240px` — on narrow screens **the buttons wrap
 *      to the next row as a group** and the text gets the full row. The text
 *      used to be squeezed by three buttons into a narrow vertical line,
 *      doubling its line count; after wrapping the lines are wider, the
 *      count drops, and the bar gets shorter overall.
 *   ② max-height fallback — however large the system font, the bar takes at
 *      most min(18dvh, 120px) and any overflow scrolls inside it, so **the
 *      chat area (.log) is never squeezed away**.
 */
.trust {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  gap: clamp(6px, 1.6vw, 10px);
  padding: 6px 10px;
  padding: clamp(6px, 1.6vw, 9px) clamp(10px, 3.2vw, 14px);
  font-size: 11px;
  font-size: clamp(11px, 2.9vw, 12.5px);
  line-height: 1.45;
  /* ⚠ The 100px cap was measured, not guessed: at normal font sizes this
     bar is only ~74px and never comes close; Huawei-style setTextZoom (web
     font scaling) can blow 11px up to 22px, with the text alone taking
     80px — only then does the cap catch it, with the overflow scrolling
     inside the bar and the chat area never squeezed away. */
  max-height: 100px;
  max-height: 15dvh;
  max-height: min(15dvh, 100px);
  overflow-y: auto;
  overscroll-behavior: contain;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--muted);
}
.trust.new {
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  color: var(--text);
}
/* "First call with this device" — deliberately lighter than a warning and a
   notch heavier than a familiar-peer hint. It is neither a fault nor a
   threat, just a reminder that "this device has not been confirmed by
   you". */
.trust.first {
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  color: var(--text);
}
.trust.warn {
  background: color-mix(in srgb, var(--warn) 14%, var(--surface));
  color: var(--text);
}
.trust-text { flex: 1 1 240px; min-width: 0; }
.trust-actions { display: flex; flex-wrap: wrap; gap: 4px; gap: clamp(4px, 1.2vw, 6px); flex: none; }

/* ---------------------------- Video stage ----------------------------
 * Appears only in a **video call** (app.js's syncStage decides visibility).
 *
 * The base layout is two equal windows side by side (landscape and portrait
 * alike) — it is the fallback form outside of .pip, used e.g. when only one
 * side has video. Real calls default to .pip below: **one side full-bleed,
 * the other shrunk into a top-right small window** (WeChat video calls work
 * exactly this way); tap the small window to swap.
 *
 * Every cell uses object-fit: contain (show it whole, letterbox rather than
 * crop): in a video call "seeing the whole person" matters far more than
 * "filling every pixel". The top-right small window is the lone exception
 * (cover instead): a palm-sized thumbnail with another ring of black bars
 * would show nothing.
 *
 * ⚠ The height is back-derived from the camera's rough aspect ratio, not a
 *   made-up number:
 *   side by side, each cell is ~50vw wide, so
 *     · portrait (phone held upright, camera gives 3:4 vertical) → cell
 *       height ≈ 50vw × 4/3 ≈ 67vw
 *     · landscape / desktop (camera gives 16:9) → ≈ 50vw × 9/16 ≈ 28vw
 *   With that, contain leaves almost no black bars, the picture fills the
 *   cell, and the vertical space is used up.
 *   Stacking portrait cells vertically would give each ~43vw wide × ~21vw
 *   tall; a vertical picture dropped in there is barely palm-sized — less
 *   than half the area, hence no stacking.
 */
.stage {
  flex: none;
  display: grid;
  gap: 2px;
  background: #000;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr;
  height: 29vw;
  height: 50dvh;
  height: min(50dvh, 29vw);
  overflow: hidden;
}

@media (orientation: portrait) {
  .stage { height: 68vw; height: min(46dvh, 68vw); }
}

.pane {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: #0b0d11;
  cursor: pointer;           /* whole cell is clickable: one tap means "zoom into this cell" */
}
.pane:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.pane video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;       /* show whole, no cropping, no enlargement */
  background: #0b0d11;
}

/* Your own cell is mirrored horizontally: seeing yourself should feel like a
   mirror (the picture sent to the peer is unaffected).
   ⚠ This class is added only for the **front** camera — keeping the mirror
   on the rear camera would render captured text backwards. */
.pane video#local-video.mirror { transform: scaleX(-1); }

.pane-tag {
  position: absolute;
  left: 8px;
  bottom: 8px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .5);
  color: #fff;
  font-size: 11.5px;
  line-height: 1.5;
  max-width: calc(100% - 16px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}

.stage-empty {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  display: grid;
  place-items: center;
  padding: 0 14px;
  color: #8a93a3;
  font-size: 12.5px;
  text-align: center;
  pointer-events: none;
}

/* ---------------------------------------------------------------------------
   Tap a cell to zoom (WeChat-style: big window full-bleed + the other cell
   shrunk into a top-right small window)

   The default is the equal layout above; only the .pip tier switches to
   big/small — if the user never taps, the two cells stay symmetric.

   ⚠ The small window deliberately uses object-fit: cover (slight cropping):
     at a glance-sized thumbnail, the black ring contain leaves behind would
     shrink the picture to a fingernail, illegible.
     The big window stays strictly contain, uncropped — it is the one that
     must "show the whole person".
   --------------------------------------------------------------------------- */
.stage.pip {
  display: block;
  position: relative;
  height: 96vw;
  height: 62dvh;
  height: min(62dvh, 96vw);
}

.stage.pip .pane {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
}

.stage.pip .pane.small {
  top: 10px; right: 10px; bottom: auto; left: auto;
  width: 30%;
  max-width: 140px;
  /* ⚠ Chrome 55 doesn't know aspect-ratio (88+) → the declaration is
     dropped whole, and here top/bottom are both auto with no size source
     beyond width → **the small window's height collapses to 0, the
     picture-in-picture vanishes**. The fallback is "30% wide ⇒ 40% tall"
     (the same 3:4 ratio), with max-height following max-width 140. New
     browsers still honor aspect-ratio. */
  height: 40vw;
  max-height: 186px;
  aspect-ratio: 3 / 4;      /* same vertical framing as WeChat's small window */
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, .22);
  box-shadow: 0 3px 12px rgba(0, 0, 0, .5);
  z-index: 2;
}

.stage.pip .pane.small video { object-fit: cover; }

/* The small window is too small for a full sentence like "peer has not
   turned the camera on" — let it be a plain block rather than squeezed
   noise */
.stage.pip .pane.small .stage-empty { display: none; }

.stage.pip .pane.small .pane-tag {
  left: 4px;
  bottom: 4px;
  padding: 1px 6px;
  font-size: 10.5px;
  max-width: calc(100% - 8px);
}

/* ===========================================================================
   Call screen (fullscreen) — modeled on WeChat's call window

   During a call there is "one thing to do": look at the peer and talk. So
   this state hides everything chat-related (top bar, device-name bar,
   identity bar, message flow, composer), leaving only the video plus
   hang-up / mute / flip camera.

   It also solves a product problem by the way: with the composer hidden,
   "already in a call and tapping start-call again" cannot happen.
   =========================================================================== */

.room.call-mode { background: #0b0d11; }

.room.call-mode > .bar,
.room.call-mode > .meta,
.room.call-mode > .trust,
.room.call-mode > .log,
.room.call-mode > .composer { display: none; }

/* ===================== Call floating-window mode (2026-10-22) =====================
   Collapsed into the small window, the room page must **stay usable** — type,
   send files, watch the message flow. That is exactly why the web client
   "cannot send text during a call": .call-mode hides both .log (messages)
   and .composer (input).

   So floating-window mode drops call-mode and adds call-float-mode:
   the message list and composer come back, while the video stage stays
   collapsed into the small window (the picture is illegible inside 116px,
   so it is simply not shown when collapsed — tap the small window to bring
   it back).
   ================================================================== */
.room.call-float-mode > .stage,
.room.call-float-mode > .stage.pip {
  display: none;
}
.room.call-float-mode { background: inherit; }

/* Stage fills the whole screen. !important overrides any inline height
   fitStage may have written — in fullscreen the height is decided by the
   viewport alone, not recomputed from the camera ratio. */
.room.call-mode .stage,
.room.call-mode .stage.pip {
  position: fixed;
  top: 0; right: 0; bottom: 0; left: 0;
  height: auto;
  z-index: 60;
  background: #000;
}

/* The small window is a bit bigger: 30% is legible in fullscreen, yet still
   clearly smaller than the big window. Pinned to the top right — matching
   WeChat, and clear of the button row below. */
.room.call-mode .stage.pip .pane.small {
  top: 14px;
  top: max(14px, env(safe-area-inset-top));
  right: 14px;
  width: 32%;
  max-width: 150px;
}

/* Call info (peer device name + duration). Top left; the right side is
   reserved for the small window, hence the width cap + ellipsis. */
/* ⚠ Four sides written out (inset is a no-op on Chrome 55, see the long note
   at .modal) */
.call-ui { position: fixed; top: 0; right: 0; bottom: 0; left: 0; inset: 0; z-index: 70; pointer-events: none; }

.call-head {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding: 14px 16px 0;
  padding: max(14px, env(safe-area-inset-top)) 16px 0;
  color: #fff;
}
.call-peer {
  max-width: calc(100% - 160px);   /* the top-right small window sits on the right — don't overlap it */
  font-size: 15px;
  font-weight: 600;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: 0 1px 4px rgba(0, 0, 0, .6);
}
.call-sub {
  display: flex;
  gap: 8px;
  align-items: baseline;
  font-size: 12.5px;
  color: rgba(255, 255, 255, .78);
  text-shadow: 0 1px 4px rgba(0, 0, 0, .6);
}
.call-timer { font-variant-numeric: tabular-nums; }

/* Placeholder view for voice calls (or video calls where neither side
   enabled video) */
.call-audio {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0;
  /* grid is a no-op on Chrome 55 (default only from 57); flex is the
     fallback.
     ⚠ flex's gap only exists from 84+, so on legacy browsers the 14px
       spacing here is lost — this layer is just "the avatar placeholder
       during a voice call", and losing a little spacing doesn't hurt
       usability. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 14px;
  color: #fff;
}
/* The badge in the middle of a voice call: bright green solid circle + white
   "280k.com", item-for-item matching the Android side's screen_call.xml
   callBadge (112dp circle there, 96px here; the ratio is consistent).
   ⚠ It used to be a "translucent gray ring + mic icon" — against the pure
     black call background that read as an empty gray ring, and users' first
     reaction was that the UI was broken (the same pitfall the Android side
     already hit; see the app.css notes).
   ⚠ Font sizes in px, not em/rem: this is a logo and must not distort with
     the user's font settings. "6" at 28px, the rest at 17px — a 1.65 ratio,
     roughly the Android side's 1.45 scaled up a notch (the circle is
     bigger). */
.call-avatar {
  width: 96px; height: 96px;
  display: flex; align-items: baseline; justify-content: center;
  border-radius: 50%;
  background: #2ea44f;
  box-shadow: 0 2px 12px rgba(46, 164, 79, .28);
  user-select: none;
}
.call-badge-big {
  font-size: 28px; font-weight: 600; color: #fff;
  line-height: 1;
}
.call-badge-dim {
  font-size: 17px; color: rgba(255, 255, 255, .72);
  line-height: 1;
}
.call-audio-hint {
  margin: 0;
  font-size: 13px;
  color: rgba(255, 255, 255, .72);
  letter-spacing: .04em;
}

/* ===================== Call floating window (2026-10-22) =====================
   Modeled on WeChat's floating call window: when collapsed, the call shrinks
   into a top-right small window, the conversation shows underneath, and the
   user can keep typing / sending files; tap the small window to restore
   fullscreen.

   ⚠ Pinned top right, with pointer-events enabled only on the window itself
     — it floats above the conversation, and everything underneath must stay
     tappable, scrollable and typable.
   ⚠ z-index above call-ui (70) but below toast — the small window must not
     be covered by a toast. */
.call-float {
  position: fixed;
  top: 12px;
  top: max(12px, env(safe-area-inset-top));
  right: 12px;
  z-index: 80;
  width: 116px;
  padding: 12px 8px 10px;
  border-radius: 14px;
  background: rgba(20, 22, 26, .92);
  border: 1px solid rgba(255, 255, 255, .34);
  box-shadow: 0 6px 22px rgba(0, 0, 0, .38);
  color: #fff;
  text-align: center;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}
.call-float-avatar {
  width: 52px; height: 52px;
  margin: 0 auto;
  border-radius: 50%;
  background: #07C160;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.call-float-avatar .call-badge-big { font-size: 19px; }
.call-float-avatar .call-badge-dim { font-size: 11px; }
.call-float-timer {
  margin: 8px 0 6px;
  font-size: 12px;
  color: rgba(255, 255, 255, .86);
  font-variant-numeric: tabular-nums;
  min-height: 15px;
}
.call-float-actions {
  display: flex;
  gap: 6px;
  justify-content: center;
}
.call-float-actions .cbtn {
  width: 34px; height: 34px;
  background: rgba(255, 255, 255, .16);
  border: none;
  color: #fff;
  padding: 8px;
  cursor: pointer;
}
.call-float-actions .cbtn:hover { background: rgba(255, 255, 255, .26); }
.call-float-actions .cbtn:active { transform: scale(.92); }
.call-float-actions .cbtn svg { width: 18px; height: 18px; display: block; }
.call-float-actions .cbtn:not(.on) { color: rgba(255, 255, 255, .62); }
.call-float-actions .cbtn.hangup { background: #FA5151; }

/* The "collapse" button in the call header: right-aligned, one size smaller
   than the call key row */
.call-head .call-shrink {
  position: absolute;
  right: 12px;
  top: 10px;
  top: max(10px, env(safe-area-inset-top));
  width: 36px; height: 36px;
  background: rgba(255, 255, 255, .16);
  border: none;
  color: #fff;
  padding: 9px;
  cursor: pointer;
  z-index: 2;
}
.call-head .call-shrink:hover { background: rgba(255, 255, 255, .26); }
.call-head .call-shrink svg { width: 18px; height: 18px; display: block; }

/* The row of round buttons at the bottom: hang-up center-right, the rest on
   the left — matching WeChat's feel */
.call-actions {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 18px 16px 20px;
  padding: 18px 16px max(20px, env(safe-area-inset-bottom));
  background: linear-gradient(to top, rgba(0, 0, 0, .55), rgba(0, 0, 0, 0));
}

.call-actions .cbtn {
  pointer-events: auto;
  width: 54px; height: 54px;
  border-radius: 50%;
  border: 0;
  background: rgba(255, 255, 255, .16);
  color: #fff;
  backdrop-filter: blur(6px);
  transition: background .15s, transform .1s, color .15s;
}
.call-actions .cbtn:hover { background: rgba(255, 255, 255, .24); }
.call-actions .cbtn:active { transform: scale(.92); }
.call-actions .cbtn svg { width: 23px; height: 23px; display: block; }
/* Mic / camera / speaker turned off: gray base + a crossed-out feel
   (conveyed by reduced opacity) */
.call-actions .cbtn:not(.on) { color: rgba(255, 255, 255, .62); }
.call-actions .cbtn.on { background: rgba(255, 255, 255, .92); color: #14171a; }

/* On / off are two different faces, not one icon recolored — see the paired
   .i-on / .i-off in index.html. Consistent with the Android side (it also
   swaps drawables, not tints).

   ⚠⚠ Here .on points the **opposite** way from #btn-voice above — do not
     copy from there:
     · #btn-voice — .on means "switched to the other mode", so .on shows the
       **second** icon;
     · here       — .on means "this device is on", so .on shows **.i-on
       itself**.
     The consequence of copying wrong is concrete: a live mic drawn as a
     "slashed microphone", and users believe they are muted mid-speech.

   ⚠ Scope with `.call-actions .cbtn`, never globally: the composer's .i-mic /
     .i-kbd use the same "one button, two faces" trick, but the switching
     condition differs, and mixing them makes them step on each other. */
.call-actions .cbtn .i-on { display: none; }
.call-actions .cbtn.on .i-on { display: block; }
.call-actions .cbtn.on .i-off { display: none; }

/* Hang-up: red on white, the single most prominent button on screen, same
   as WeChat's.
   ⚠ This one is **no longer** rotated 135°: the old one was a vertical mic
   that had to be turned to read as "hung up"; it now uses the Android
   side's same ic_hangup, which already draws a hung-up handset — rotating
   again would tilt it. */
.call-actions .cbtn.hangup { background: var(--danger); color: #fff; }
.call-actions .cbtn.hangup:hover { filter: brightness(1.08); background: var(--danger); }

/* Unlock bar shown when the browser blocks audio */
.audio-unlock {
  flex: none;
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--warn) 18%, var(--surface));
  color: var(--text);
  padding: 10px 14px;
  font-size: 12.5px;
  text-align: center;
}
.audio-unlock:hover { filter: brightness(.98); }
/* In a fullscreen call it must float above everything — otherwise the
   fullscreen stage covers it, and "can't hear the peer" is exactly when this
   hint matters most in a call.
   Placed at the bottom (above the buttons), not the top: the top row is
   "who is on the other end + how long we've been on"; covering that would
   only make people more anxious. */
.room.call-mode .audio-unlock {
  position: fixed;
  top: auto;
  bottom: 104px;
  left: 12px; right: 12px;
  width: auto;
  z-index: 80;
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow);
}

/* ---------------------------- Messages ----------------------------
 * Structure follows WeChat: light gray background, rounded bubbles split
 * left/right, a small pointer on each bubble, system notices centered as
 * pills. Only the palette is our own brand color, not WeChat's green.
 */

.log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--chat-bg);
  scrollbar-width: thin;
  overscroll-behavior: contain;
}

/* Time divider (only inserted when adjacent messages are more than 5 minutes
   apart) */
.time {
  align-self: center;
  padding: 2px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--muted) 16%, transparent);
  color: var(--muted);
  font-size: 11.5px;
  line-height: 1.6;
}

.msg {
  position: relative;
  max-width: 76%;
  max-width: min(76%, 520px);
  padding: 9px 13px;
  border-radius: 8px;
  font-size: 15px;
  line-height: 1.5;
  word-break: break-word;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  box-shadow: 0 1px 1px rgba(16, 24, 40, .05);
  animation: rise .16s ease-out;
}

/* The bubble's little pointer — the WeChat feel of "sticking out toward the
   speaker" */
.msg::after {
  content: '';
  position: absolute;
  bottom: 10px;
  width: 0;
  height: 0;
  border: 5px solid transparent;
}

@keyframes rise {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

.msg.me {
  align-self: flex-end;
  background: var(--bubble-me);
  color: var(--bubble-me-ink);
}
.msg.me::after { right: -9px; border-left-color: var(--bubble-me); }

.msg.them {
  align-self: flex-start;
  background: var(--bubble-them);
  border: 1px solid var(--line);
}
.msg.them::after { left: -9px; border-right-color: var(--bubble-them); }

/* Support room only: the "server received this message" marker.
   ⚠ The content is generated by ::after, **not** written as a real text
     node — __rt.messages reads textContent, and mixing "delivered" in would
     wreck every m.text === '…' assertion in the e2e suite. */
.msg .ack { font-size: 10.5px; opacity: .78; }
.msg.acked .ack::after { content: ' delivered'; }

.notice {
  align-self: center;
  max-width: 90%;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 12px;
  text-align: center;
}

/* ---- Voice messages ---- */
.msg.voice {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 104px;
  cursor: pointer;
  user-select: none;
  padding: 9px 14px;
}
.voice-wave {
  flex: none;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 2px solid currentColor;
  position: relative;
  opacity: .85;
}
.voice-wave::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 0; height: 0;
  transform: translate(-45%, -50%);
  border: 4px solid transparent;
  border-left-color: currentColor;
}
.msg.voice.playing .voice-wave { animation: pulse 1.1s ease-in-out infinite; }
.voice-dur { font-size: 12.5px; opacity: .85; }

/* ---- File messages ---- */
.msg.file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
}
.file-ico {
  flex: none;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: 8px;
  background: rgba(127, 127, 127, .16);
  font-size: 18px;
}
.file-meta { min-width: 0; display: flex; flex-direction: column; line-height: 1.35; }
.file-name {
  font-size: 14px;
  max-width: 46vw;
  max-width: min(46vw, 210px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-size { font-size: 11.5px; opacity: .72; }
.msg.file a { color: inherit; text-decoration: none; }

/* ---- Transfer progress bubble ---- */
.msg.progress { display: flex; align-items: center; gap: 10px; padding: 10px 14px; }
.progress-bar {
  flex: 1;
  height: 4px;
  min-width: 90px;
  border-radius: 999px;
  background: rgba(127, 127, 127, .28);
  overflow: hidden;
}
.progress-bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: currentColor;
  transition: width .12s linear;
}
.progress-pct { flex: none; font-size: 11.5px; opacity: .8; font-variant-numeric: tabular-nums; }

/* ---------------------------- Composer ----------------------------
 * Structure modeled on WeChat: "voice/keyboard" and "attach" on the left,
 * the input in the middle, "call" and "send" on the right.
 */

.composer {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  padding-bottom: 8px;
  padding-bottom: max(8px, env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--line);
}

.cbtn {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font-size: 19px;
  line-height: 1;
  transition: background .15s, color .15s, transform .1s;
}
.cbtn:hover { background: var(--surface-2); }
.cbtn:active { transform: scale(.92); }
.cbtn.on {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.cbtn.call.on {
  background: color-mix(in srgb, var(--ok) 16%, transparent);
}

/* Icons are uniformly 21px stroked SVG (see the note near the top of this
   file: no emoji). */
.cbtn svg { width: 21px; height: 21px; display: block; }

/* "Voice / keyboard" are two faces of one button: only in voice mode does
   the mic swap for a keyboard.
   (Switched via CSS rather than replacing innerHTML — that rebuilds nodes
   and interrupts an in-flight pointer press.) */
#btn-voice .i-kbd { display: none; }
#btn-voice.on .i-mic { display: none; }
#btn-voice.on .i-kbd { display: block; }

/* The two "start call" entries each carry a color of their own:
   voice is green (connected), video is blue (matches the brand). */
#btn-call-audio { color: var(--ok); }
#btn-call-audio:hover { background: color-mix(in srgb, var(--ok) 12%, transparent); }
#btn-call-video { color: var(--accent); }
#btn-call-video:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }

.composer input#text {
  flex: 1;
  min-width: 0;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-2);
  outline: none;
  font-size: 15px;
  transition: border-color .15s, background .15s;
}
.composer input#text:focus { border-color: var(--accent); background: var(--surface); }
.composer input#text::placeholder { color: var(--muted); }

.composer .primary {
  flex: none;
  border-radius: 8px;
  padding: 9px 16px;
  font-size: 14px;
}

/* "Hold to talk" — the button that replaces the input in voice mode.
   ⚠ Deliberately a **centered, enlarged circle**, not the old flat bar:
     the bar looked easy to press, but once the thumb was down, the slightest
     slide left its bounds and pointerup never arrived — presenting as
     "pressed but won't release / pressed but nothing happens", in the
     user's own words "often can't hold it continuously to trigger". A
     circle's landing is far more stable, and the diameter is generous.
   62px leaves margin for the pad of a thumb's contact area, not a number
   picked at random. */
.hold-talk {
  flex: none;
  width: 62px;
  height: 62px;
  margin: 0 auto;              /* the two button groups flanking it have equal counts, so auto margins center it exactly */
  display: grid;
  place-items: center;
  align-content: center;
  gap: 2px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 10.5px;
  line-height: 1;
  user-select: none;
  touch-action: none;          /* long-press must not open the system menu or trigger scrolling (children too, see below) */
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  transition: background .15s, border-color .15s, color .15s, transform .1s;
}
.hold-talk svg { width: 22px; height: 22px; display: block; }

/* ⚠ touch-action must be written on the children too. What the finger
   actually hits is the svg / span inside, and touch-action **is not
   inherited** — written only on the button, some implementations go by "the
   hit element's own copy", and this none goes for nothing: the browser can
   still take the touch away on long-press, and "taken away" = pointercancel
   = the recording voice cut off on the spot (while the finger is still
   down). */
.hold-talk svg,
.hold-talk span {
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}
.hold-talk:active { transform: scale(.96); }
.hold-talk.recording {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
  transform: scale(1.06);
}

/* Narrow screens: tighten the buttons to make room for the input */
@media (max-width: 420px) {
  .composer { gap: 4px; padding-left: 8px; padding-right: 8px; }
  .cbtn { width: 32px; height: 32px; font-size: 17px; }
  .cbtn svg { width: 20px; height: 20px; }
  .composer .primary { padding: 9px 12px; }
  .hold-talk { width: 58px; height: 58px; }
}

/* ---------------------------- Recording overlay ---------------------------- */

.rec-hud {
  position: fixed;
  left: 50%;
  bottom: 96px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 18px;
  border-radius: 14px;
  background: rgba(20, 23, 26, .9);
  color: #fff;
  font-size: 13px;
  z-index: 25;
  animation: rise .16s ease-out;
}
.rec-dot {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: #ff4d4f;
  animation: pulse 1s ease-in-out infinite;
}
.rec-hint { opacity: .75; font-size: 12px; }

/* ---------------------------- Incoming-call overlay ---------------------------- */

.ring {
  position: fixed;
  /* Four sides written out: inset is a no-op on Chrome 55 (vivo X9), leaving
     the overlay with no position and no size → on an incoming call nothing
     appears on screen at all (see the long note at .modal) */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0;
  /* Must sit above the fullscreen call UI (z-index 70) — even though calls
     don't ring mid-call, "the incoming-call overlay covered by the call
     screen" would be fatal and hard to debug the moment it happened */
  z-index: 90;
  /* Chrome 55's grid is not on by default (only from 57) → display:grid is
     inert whole, and place-items fails with it. flex is the fallback
     (perfectly supported on 55), same effect. */
  display: flex;
  align-items: center;
  justify-content: center;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(10, 12, 16, .55);
  animation: rise .18s ease-out;
}

.ring-card {
  width: 84vw;
  width: min(84vw, 300px);
  padding: 22px 20px 16px;
  border-radius: 16px;
  background: var(--surface);
  box-shadow: var(--shadow);
  text-align: center;
}

.ring-avatar {
  width: 56px; height: 56px;
  margin: 0 auto 12px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 14%, var(--surface));
  font-size: 26px;
  color: var(--accent);
}
.ring-avatar svg { width: 26px; height: 26px; display: block; }
/* The overlay swaps icons by call type: mic for voice, camcorder for video */
.ring-avatar .i-video { display: none; }
#ring.kind-video .ring-avatar .i-audio { display: none; }
#ring.kind-video .ring-avatar .i-video { display: block; }
.ring-title { margin: 0 0 4px; font-size: 16px; font-weight: 600; }
.ring-sub { margin: 0 0 16px; font-size: 12.5px; color: var(--muted); line-height: 1.5; }
.ring-actions { display: flex; gap: 10px; }
.ring-btn {
  flex: 1;
  padding: 10px 0;
  border: 0;
  border-radius: 10px;
  font-weight: 600;
  font-size: 14px;
}
.ring-btn.accept { background: var(--ok); color: #fff; }
.ring-btn.accept:hover { filter: brightness(1.06); }
.ring-btn.ghost { background: var(--surface-2); color: var(--text); }
.ring-btn.ghost:hover { filter: brightness(.97); }

/* ============================ Toast ============================ */

.toast {
  position: fixed;
  left: 50%;
  bottom: 78px;
  bottom: max(78px, calc(env(safe-area-inset-bottom) + 78px));
  transform: translateX(-50%);
  max-width: 88vw;
  max-width: min(88vw, 420px);
  padding: 9px 16px;
  border-radius: 999px;
  background: rgba(20, 23, 26, .92);
  color: #fff;
  font-size: 13px;
  text-align: center;
  /* Always on top: a hint covered by anything is no hint at all */
  z-index: 95;
  animation: rise .18s ease-out;
}

/* ==================== Free-time wall (#92, see wall-layer in index.html) ==================== */

/* Room countdown: lives in the device-name bar, small and unobtrusive — it
   is "background info", not something to stare at. It only changes color to
   warn with under 60 seconds left. */
.wall-timer {
  margin-left: auto;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-2, #8a94a6);
  white-space: nowrap;
}
.wall-timer.urgent { color: #e8590c; font-weight: 600; }

/* "Free time remaining" during a call (#92 call wall) — the local estimate
   is only a hint; at the deadline the server's wall-hit is authoritative;
   ≤30 seconds turns warning-colored, same language as the room countdown. */
#call-state.urgent { color: #e8590c; font-weight: 600; }

/* Wall-reached layer: centered card, mask over the whole page (unskippable =
   nowhere else to click). */
.wall-layer {
  position: fixed;
  /* Four sides written out (inset is a no-op on Chrome 55). This layer is
     the fallback for both "call time up" and "support unreachable"; getting
     it wrong leaves the user stuck with no visible notice. */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  inset: 0;
  z-index: 120;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(10, 12, 16, .55);
  animation: rise .16s ease-out;
}
.wall-card {
  width: 92vw;
  width: min(92vw, 360px);
  background: var(--surface, #fff);
  color: var(--text, #1a1d21);
  border-radius: 16px;
  padding: 22px 20px 18px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .28);
  text-align: center;
}
.wall-title { font-size: 17px; font-weight: 700; margin-bottom: 8px; }
.wall-body { font-size: 13.5px; line-height: 1.65; opacity: .85; margin-bottom: 18px; }
.wall-actions { display: flex; flex-direction: column; gap: 10px; }
.wall-actions .primary, .wall-actions .ghost {
  padding: 11px 0;
  border: 0;
  border-radius: 10px;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}
.wall-actions .primary { background: var(--ok, #07c160); color: #fff; }
.wall-actions .ghost { background: var(--surface-2, #f2f3f5); color: var(--text, #1a1d21); }

/* The WeChat QR-code bubble in the support room (boss, 2026-10-17: pushed
   automatically on entering a support room; a locally bundled asset, no
   protocol, no DB — changing the code requires a release). */
.msg.qr { max-width: 240px; padding: 10px; }
.msg.qr img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  background: #fff;
}
.msg.qr .qr-cap {
  margin-top: 8px;
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  opacity: .85;
}

/* ==================== Call wall layer (#92) ====================
   (standby silent mode #91's styles were deleted along with the feature on
   2026-10-19) */
