/* ═══════════════════════════════════════════════════════════════════════
   LiveLink Live — application stylesheet.

   A port of the QML surfaces, not a reinterpretation. Where a value looks
   arbitrary it was copied from the QML and the source file is named in a
   comment, so the two can be diffed when either moves.
   ═══════════════════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }

html, body {
    height: 100%;
    margin: 0;
    overflow: hidden;
}

/* ── UI locale ─────────────────────────────────────────────────────────
   The gate js/core/i18n.js arms in <head> and js/app.js clears after the
   string pass, so a Korean user never sees a frame of English. It is only
   ever armed when the locale is NOT English — an English page needs no pass,
   and gating it would mean a blank screen if the scripts failed to arrive. */
html.i18n-pending body { visibility: hidden; }

/* Korean breaks at any character by default, which puts half a word on the
   next line of every button and card in the app. `keep-all` moves the break
   to the spaces between eojeol, which is where a Korean reader expects it —
   and `overflow-wrap` is the escape hatch for the one string that is longer
   than its box (a URL, a long display name) so keep-all cannot overflow it. */
html:lang(ko) {
    word-break: keep-all;
    overflow-wrap: anywhere;
}

body {
    background: var(--window-bg);
    color: var(--text-primary);
    font-family: var(--font-stack);
    font-size: var(--type-body);
    line-height: 1.35;
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
    overscroll-behavior: none;
}

button, input { font: inherit; color: inherit; }
canvas { display: block; }

/* Han and Nastaliq cannot be selected by unicode-range, because the whole
   point is that several fonts claim the same codepoints and only the language
   says which drawing is right. So these are the one place a font is chosen by
   `lang` instead: the transcript rows and subtitle cards carry the code of the
   text they hold, and js/fonts.js links the matching sheet the first time one
   appears. `|=` matches the tag and any region suffix — zh-Hans-CN too.

   Order matters in the fallback: the Han family first, then "LL Sans" for the
   Latin and punctuation mixed into the same line, then the system stack.

   The `i` flag matters: `lang` is not one of the attributes CSS matches
   case-insensitively by default, and the script subtag is conventionally
   capitalised, so `zh-Hant` would slip past a bare `[lang|="zh-hant"]`.
   The Traditional rule follows the Simplified one deliberately — `|="zh"`
   matches zh-Hant as well, and equal specificity means the later rule wins. */
[lang|="zh" i]      { font-family: "LL Han SC", var(--font-stack); }
[lang|="zh-hant" i] { font-family: "LL Han TC", var(--font-stack); }
[lang|="ja" i]      { font-family: "LL Han JP", var(--font-stack); }
[lang|="ur" i]      { font-family: "LL Urdu",   var(--font-stack); }

/* Korean is not in that list. Modern Hangul lives in "LL Sans" and is already
   loaded, so ordinary Korean needs no override; this only adds the Hanja
   family behind it, for the occasional 漢字 in a name or a headline. */
[lang|="ko" i] { font-family: "LL Sans", "LL Han KO", var(--font-stack); }

/* Qt's ScrollView draws a thin, unobtrusive bar. Match it rather than
   letting each platform impose its own. */
.page-scroll, .sheet-list, .log-text, .login-scroll, .transcript-scroll,
.popup-layer {
    scrollbar-width: thin;
    scrollbar-color: #33415f transparent;
}
.page-scroll::-webkit-scrollbar,
.sheet-list::-webkit-scrollbar,
.log-text::-webkit-scrollbar,
.login-scroll::-webkit-scrollbar,
.popup-layer::-webkit-scrollbar,
.transcript-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.page-scroll::-webkit-scrollbar-thumb,
.sheet-list::-webkit-scrollbar-thumb,
.log-text::-webkit-scrollbar-thumb,
.login-scroll::-webkit-scrollbar-thumb,
.popup-layer::-webkit-scrollbar-thumb,
.transcript-scroll::-webkit-scrollbar-thumb {
    background: #33415f; border-radius: 4px;
}
.page-scroll::-webkit-scrollbar-track { background: transparent; }

.screen { position: fixed; inset: 0; display: none; }
.screen.is-active { display: block; }
/* D210 — js/core/invite-gate.js: a browser opening an invite never sees the
   login landing, not even for the first paint; showScreen() lifts this. */
html.invite-pending #login-screen { display: none; }

[hidden] { display: none !important; }


/* ═══════════════════════════════════════════════════════════════════════
   THE RAISED-SURFACE RECIPE  (VISUAL_STYLE.md § "standard raised-surface")

   1. a dark rounded rectangle shifted down 5 px  →  ::before
   2. the main face with a static gradient        →  ::after
   3. a one-pixel top highlight                   →  a background layer on
                                                     that same ::after, so
                                                     no extra element is
                                                     needed per control
   4. content translated down with the face while pressed

   Per-control variation is by custom property, so every control shares one
   implementation of the depth model instead of re-deriving it.
   ═══════════════════════════════════════════════════════════════════════ */

.lift {
    --lift-radius: var(--radius-standard);
    --lift-g0: #35445f;
    --lift-g1: #253149;
    --lift-g2: #161d30;
    --lift-mid: 50%;
    --lift-border: #7183a4;
    --lift-border-width: 1px;
    --lift-highlight: rgba(255, 255, 255, 0.27);
    --lift-inset: 24px;          /* highlight is narrower than the face */
    --lift-opacity: 1;

    position: relative;
    display: block;
    border: 0;
    padding: 0;
    background: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

/* The dark offset base — the part that stays put while the face sinks. */
.lift::before {
    content: "";
    position: absolute;
    inset: var(--face-reveal) 0 0 0;
    border-radius: var(--lift-radius);
    background: var(--offset-base);
}

.lift::after {
    content: "";
    position: absolute;
    left: 0; right: 0; top: 0;
    height: calc(100% - var(--face-reveal));
    border-radius: var(--lift-radius);
    border: var(--lift-border-width) solid var(--lift-border);
    opacity: var(--lift-opacity);
    background-image:
        linear-gradient(var(--lift-highlight), var(--lift-highlight)),
        linear-gradient(180deg,
            var(--lift-g0) 0%, var(--lift-g1) var(--lift-mid), var(--lift-g2) 100%);
    background-repeat: no-repeat;
    background-position: center 1px, 0 0;
    background-size: calc(100% - var(--lift-inset)) 1px, 100% 100%;
    transition: transform var(--press-duration) var(--ease-out),
                border-color var(--hover-duration) var(--ease-out);
}

.lift > * {
    position: relative;
    z-index: 1;
    transition: transform var(--press-duration) var(--ease-out);
}

/* "On press, move the face down by 2–3 px … Do not move the control's
   layout bounds." */
.lift:active:not(:disabled)::after,
.lift:active:not(:disabled) > * { transform: translateY(var(--press-offset)); }

/* "Keyboard focus: persistent 2 px focus ring, not just a colour shift." */
.lift:focus-visible { outline: none; }
.lift:focus-visible::after {
    border-color: var(--accent-focus);
    border-width: 2px;
}

.lift:disabled { cursor: default; }
.lift:disabled::after { opacity: 0.58; }


/* ═══════════════════════════════════════════════════════════════════════
   LOGIN LANDING  (qml/LoginLanding.qml)
   ═══════════════════════════════════════════════════════════════════════ */

#login-screen, #join-screen { overflow: hidden; }

/* Behind both landings, outside either, so switching does not restart the
   shader or re-seed the starfield. Earlier in the DOM than the screens, so
   their content paints over it without needing a stacking context. */
.landing-backdrop {
    position: fixed; inset: 0; display: none;
    background: var(--login-base);
}
.landing-backdrop.is-active { display: block; }

.login-base   { position: absolute; inset: 0; background: var(--login-base); }
.login-ambient{ position: absolute; inset: 0; width: 100%; height: 100%; }

.login-stars  { position: absolute; inset: 0; pointer-events: none; }
.star {
    position: absolute;
    background: #dcecff;
    border-radius: 50%;
    opacity: 0.30;
    animation: star-twinkle var(--twinkle) ease-in-out infinite alternate;
}
@keyframes star-twinkle { from { opacity: 0.30; } to { opacity: 1; } }

/* The QML lays a top-and-bottom darkening gradient over the shader so the
   wordmark and the card edges keep their contrast wherever the waves are. */
.login-vignette {
    position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(180deg,
        rgba(0, 0, 0, 0.094) 0%,       /* #18000000 */
        rgba(0, 0, 0, 0) 50%,
        rgba(2, 4, 13, 0.369) 100%);   /* #5e02040d */
}

.login-scroll {
    position: absolute; inset: 0;
    overflow-y: auto; overflow-x: hidden;
    /* The shader backdrop behind this deliberately keeps running under the
       insets — only the content is pulled in. */
    padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
    -webkit-overflow-scrolling: touch;
}
.login-content {
    position: relative;
    min-height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding-bottom: 40px;
}

/* The QML anchors this to the top of the scroll content rather than putting
   it in the column, so it does not push the hero down; `heroColumn.y` is
   measured from the content top, not from below the brand. */
.brand-row {
    position: absolute; top: 26px; left: 50%;
    transform: translateX(-50%);
    display: flex; align-items: center; gap: 10px;
}
.brand-word {
    font-size: 12px; font-weight: 600; letter-spacing: 2.2px;
    color: #dce5f7;
}

/* ── Partner mark ──────────────────────────────────────────────────────
   Craftive, between the orb and the app-language switch.

   It is this element, not the switch, that now carries `margin-top: auto`:
   the column is min-height:100%, and two flex items with auto top margins
   *split* the free space between them, which would have floated the logo
   halfway up the empty half of the landing. One claim on the free space,
   and the mark and the switch travel down to the bottom padding as a pair.

   Native artwork is 281x101 with alpha, drawn here at a third of that.
   `height` leads and the width follows, so the pair keeps the height it was
   laid out at whatever the artwork's aspect turns out to be — and the
   width/height attributes on the <img> reserve the box before it decodes,
   so nothing under it jumps. Full opacity and no filter: a partner's mark
   is not ours to tint. */
.partner-mark {
    display: block;
    margin-top: auto;
    height: 34px; width: auto;
}
@media (max-height: 589px) { .partner-mark { height: 26px; } }

/* ── The app-language switch ───────────────────────────────────────────
   Two segments over one setting, in three placements: cornered on the login
   landing, inline at the top of the join card, and full width in Settings.
   Only the placement differs — the segments themselves are one rule, so the
   control reads as the same control wherever somebody meets it. */
.locale-switch {
    display: inline-flex;
    padding: 2px;
    border-radius: 999px;
    background: rgba(12, 19, 35, 0.55);
    border: 1px solid rgba(117, 141, 180, 0.28);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}
/* Foot of the login column, where an auto top margin used to put it — the
   column is min-height:100%, so something down here has to eat whatever the
   hero left over for the footer to ride the bottom padding however tall the
   screen is. That job moved up one element to .partner-mark, which now
   precedes the switch, and this is the gap between the two. Still no
   absolute positioning, so nothing can end up under the wordmark. */
.locale-switch.is-footer { margin-top: 14px; }
.locale-switch.is-inline { align-self: flex-end; }
/* Settings: flags instead of names, riding the end of the heading rather than
   spanning a row of its own. The names are what make the control legible on a
   landing that has no heading — here the heading does that job, so the
   segments can shrink to the one thing a two-state switch needs to show. */
.locale-switch.is-compact { flex: 0 0 auto; }
.locale-switch.is-compact .locale-option {
    display: flex; align-items: center;
    padding: 5px 10px;
}
/* Same treatment as the picker's region flags, at two-thirds the height: a
   16 px flag keeps the segment the height two words gave it. The inactive one
   is dulled because the pill alone is a quiet cue, and with the words gone it
   was the only one left. */
.locale-flag {
    display: block; height: 16px; width: auto;
    border-radius: 2px;
    opacity: 0.45; filter: grayscale(0.55);
    transition: opacity var(--press-duration) var(--ease-out),
                filter var(--press-duration) var(--ease-out);
}
.locale-option.is-current .locale-flag { opacity: 1; filter: none; }

.locale-option {
    padding: 6px 13px;
    border: 0; border-radius: 999px;
    background: transparent;
    color: var(--text-secondary);
    font-size: 12.5px; font-weight: 600;
    cursor: pointer;
    transition: background-color var(--press-duration) var(--ease-out),
                color var(--press-duration) var(--ease-out);
}
.locale-option.is-current {
    background: rgba(117, 141, 180, 0.34);
    color: var(--text-primary);
}
/* The switch lives on two shader-backed landings where a focus ring drawn in
   the accent would disappear into the background. */
.locale-option:focus-visible {
    outline: 2px solid var(--text-primary);
    outline-offset: -2px;
}

.hero {
    /* LoginLanding.qml: y = max(104, contentHeight * 0.19), and the pieces
       the orb-centring arithmetic below needs to subtract. */
    --hero-top: max(104px, 19vh);
    --wordmark-height: 65px;
    --descriptor-height: 13px;
    --hero-gap: 16px;

    width: min(470px, calc(100% - 52px));
    margin-top: var(--hero-top);
    display: flex; flex-direction: column; align-items: center;
    gap: var(--hero-gap);
}

/* `shortViewport` (height < 590) drops the hero to a fixed 40 px and tightens
   the column, exactly as the QML ternaries do. */
@media (max-height: 589px) {
    .brand-row { top: 10px; }
    .hero { --hero-top: 40px; --hero-gap: 8px; }
}

.wordmark { display: flex; align-items: center; gap: 11px; }
.wordmark-text {
    font-size: 54px; font-weight: 700; letter-spacing: -1.3px;
    /* Qt sizes a Text item by font metrics (ascent + descent, ~1.2em), not by
       the glyph box, so `line-height: 1` made the hero 11 px short and pulled
       everything below it up. --wordmark-height tracks this. */
    color: var(--ink); line-height: 1.2;
}
.wordmark-badge {
    display: inline-flex; align-items: center; justify-content: center;
    width: 72px; height: 34px; border-radius: 10px;
    background: linear-gradient(90deg, var(--coral) 0%, #a77cff 100%);
    color: #090914; font-size: 14px; font-weight: 700; letter-spacing: 1.2px;
}

.hero-descriptor {
    font-size: 11px; font-weight: 600; letter-spacing: 2.4px;
    color: var(--aqua); text-align: center;
}

/* ── The orb ── */
.voice-mark {
    position: relative;
    width: 141px; height: 141px;
    margin-top: 15px;
    transition: transform 150ms var(--ease-out);
}

/* `centerLoginVertically` (height >= 620) puts the orb on the vertical centre
   of the viewport. QML has to reach that with a computed Layout.topMargin
   because ColumnLayout cannot centre one child; the same arithmetic works
   here, and keeps the busy card and rejoin button in flow underneath.
       topMargin = viewportH/2 - heroY - orbRadius - wordmark - descriptor - 2*gap */
@media (min-height: 620px) {
    .voice-mark {
        margin-top: max(8px, calc(50vh - var(--hero-top) - 70.5px
            - var(--wordmark-height) - var(--descriptor-height)
            - var(--hero-gap) * 2));
    }
}
.voice-mark:hover  { transform: scale(1.045); }
.voice-mark:active { transform: scale(0.95); }

.pulse-ring {
    position: absolute; inset: 0;
    border-radius: 50%;
    border: 1px solid #5ed7c7;
    opacity: 0;
    animation: orb-pulse 2700ms linear infinite;
    pointer-events: none;
}
.pulse-ring[data-ring="1"] { border-color: #8f76ed; animation-delay: 900ms; }

@keyframes orb-pulse {
    0%     { transform: scale(0.76); opacity: 0; }
    12.9%  { opacity: 0.42; }
    66.6%  { transform: scale(1.34); opacity: 0; }
    100%   { transform: scale(1.34); opacity: 0; }
}

.orb {
    position: absolute; inset: 12px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    border: 1px solid rgba(125, 167, 199, 0.392);   /* #647da7c7 */
    background: rgba(20, 32, 59, 0.722);            /* #b814203b */
    cursor: pointer;
    padding: 0;
    transition: border-color var(--hover-duration) var(--ease-out);
}
.orb:hover:not(:disabled) { border-color: #a9fff4; border-width: 2px; }
.orb:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: 3px; }
/* Storage-blocked is the one state that disables a visible orb (busy hides
   the whole voice mark instead), and a disabled control has to look like
   one: this button glowing invitingly while ignoring taps read as "the app
   is broken", with the actual reason sitting unread in the notice below. */
.orb:disabled { cursor: default; opacity: 0.48; }

.orb-inner {
    position: absolute; inset: 9px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: linear-gradient(90deg,
        rgba(97, 234, 209, 0.831) 0%,    /* #d461ead1 */
        rgba(146, 119, 255, 0.816) 100%);/* #d09277ff */
}
.orb-label { color: #07101e; font-size: 20px; font-weight: 700; }

/* Whose credential the Continue orb would resume. */
.landing-account { margin: 0; font-size: 14px; color: var(--muted-ink); }

/* ── Busy / status cards on the landing ── */
.busy-card {
    width: 100%; max-width: 390px;
    border-radius: var(--radius-standard);
    background: rgba(17, 27, 50, 0.659);            /* #a8111b32 */
    border: 1px solid rgba(95, 113, 150, 0.251);    /* #405f7196 */
    padding: 14px;
    text-align: center;
}
.busy-dots { display: flex; justify-content: center; gap: 7px; margin-bottom: 9px; }
.busy-dots i {
    width: 7px; height: 7px; border-radius: 4px;
    background: var(--aqua); opacity: 0.35;
    animation: busy-dot 1040ms ease-in-out infinite;
}
.busy-dots i:nth-child(2) { background: var(--violet); animation-delay: 150ms; }
.busy-dots i:nth-child(3) { animation-delay: 300ms; }
@keyframes busy-dot {
    0%, 100% { opacity: 0.3; }
    31%      { opacity: 1; }
    71%      { opacity: 0.3; }
}
.busy-text { margin: 0; font-size: 13px; color: var(--muted-ink); }

.rejoin-button {
    width: 100%; max-width: 390px; height: 50px;
    border-radius: 16px;
    background: #184f48;
    border: 1px solid rgba(88, 222, 200, 0.533);    /* #8858dec8 */
    color: #eafff9; font-size: 15px; font-weight: 700;
    cursor: pointer;
}
.rejoin-button:active { background: #1f6f64; }

.status-notice {
    width: 100%; max-width: 390px;
    border-radius: 12px;
    background: rgba(23, 35, 59, 0.451);            /* #7317233b */
    border: 1px solid rgba(93, 113, 144, 0.275);    /* #465d7190 */
    padding: 10px;
}
.status-notice p {
    margin: 0; font-size: 12px; color: var(--muted-ink); text-align: center;
}
.status-notice.is-warning p { color: #ffc177; }

/* The iOS Home Screen hint — the status-notice look with a dismissal. The ×
   is the one control, sized for a thumb (the hint exists only on phones). */
.a2hs-hint {
    width: 100%; max-width: 390px;
    display: flex; align-items: center; gap: 4px;
    border-radius: 12px;
    background: rgba(23, 35, 59, 0.451);
    border: 1px solid rgba(93, 113, 144, 0.275);
    padding: 4px 4px 4px 12px;
}
.a2hs-hint p {
    margin: 0; font-size: 12px; color: var(--muted-ink);
    text-align: left; flex: 1;
}
.a2hs-dismiss {
    flex: none; width: 40px; height: 40px;
    border: 0; border-radius: 10px; background: transparent;
    color: var(--muted-ink); font-size: 18px; line-height: 1;
    cursor: pointer;
}
.a2hs-dismiss:hover { background: rgba(93, 113, 144, 0.2); }

/* ── Terminal guest overlay (Main.qml, z: 12) ──
   A room ending is allowed to interrupt Transcript and QR: otherwise their
   full-screen layers hide the one event the reader cannot infer from text
   that simply stops arriving. The confirm dialog remains the topmost modal. */
.guest-exit {
    position: fixed; inset: 0; z-index: 68;
    display: flex; align-items: center; justify-content: center;
    background: rgba(5, 9, 22, 0.902);              /* #e6050916 */
    padding: 24px;
}
.guest-exit[hidden] { display: none; }
/* D210 — a Counter tablet's ending is terminal: the login screen behind it
   is covered, not dimmed, and there is no OK to reach it. */
.guest-exit.is-terminal { background: var(--window-bg); }
.guest-exit-card {
    display: flex; flex-direction: column; align-items: center;
    gap: 14px; width: 100%; max-width: 380px; text-align: center;
}
.guest-exit-title {
    margin: 0; font-size: 22px; font-weight: 700; color: var(--primary-ink);
}
.guest-exit-message {
    margin: 0; font-size: 14px; color: var(--muted-ink);
}
.guest-exit .rejoin-button { max-width: 200px; height: 46px; }
.guest-exit-ok {
    width: 200px; height: 44px;
    border-radius: 14px;
    background: transparent;
    border: 1px solid #3a4a63;
    color: var(--muted-ink); font-size: 14px;
    cursor: pointer;
}
.guest-exit-ok:active { background: #1a2230; }

/* Landing content is hidden outright while the popup owns the surface.
   The locale switch belongs in that list and was missed when it was added:
   D048 emptied the landing so nothing competed with the card and nothing
   focusable sat behind it, and the switch is both. Now that the card is tall
   enough to reach the foot of the column (D057), it also showed *through* the
   card's 93% background, which is how the omission surfaced. Switching
   language mid-sign-in costs a tap on the close button; reading the form
   through a ghost of the footer was worse. The partner mark is in the list
   for the same reason, and because a logo showing through a sign-in card is
   the one place a partner's mark should never appear. */
#login-screen.form-open .brand-row,
#login-screen.form-open #login-locale,
#login-screen.form-open .partner-mark,
#login-screen.form-open .hero { visibility: hidden; }


/* ── Credential popup (D048) ── */
/* Fixed, not absolute: the layer lives at body level now (the guest review
   state opens it over the workspace), so it anchors to the viewport rather
   than to a screen that may be display:none.

   And it scrolls. `interactive-widget=resizes-content` (index.html) has the
   on-screen keyboard shrink the viewport instead of covering it, so on a
   phone this layer is routinely shorter than the card inside it — a provider
   button, two fields, submit and the create-an-account line do not fit in
   what a keyboard leaves. Whatever does not fit has to be reachable, and the
   create-an-account line is last in the card and first to go.

   Which is why the card is in normal flow with `auto` side margins rather
   than absolutely centred: an in-flow child's margin box counts as
   scrollable overflow, an out-of-flow one's is clipped and unreachable. The
   gutters moved to the layer so the full width answers a drag, and the
   backdrop turned fixed so the dimming stays put while the card moves under
   it. --viewport-height is how iOS, which ignores interactive-widget, is
   told the same thing (see app.js). */
.popup-layer {
    position: fixed;
    top: var(--viewport-offset, 0px); left: 0; right: 0;
    height: var(--viewport-height, 100%);
    z-index: 30;
    padding: 0 22px;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.popup-backdrop {
    position: fixed; inset: 0;
    background: rgba(4, 10, 24, 0.702);             /* #b3040a18 */
}

.login-popup {
    position: relative;
    margin: 26px auto;
    width: min(370px, 100%);
    border-radius: var(--radius-modal);
    background: rgba(9, 19, 38, 0.929);             /* #ed091326 */
    border: 1px solid rgba(117, 141, 180, 0.396);   /* #65758db4 */
    padding: 21px;
    display: flex; flex-direction: column; gap: 13px;
    overflow: hidden;
    animation: popup-in var(--state-duration) var(--ease-out);
}
@keyframes popup-in {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: none; }
}

/* The aqua→violet hairline across the top edge of the card. */
.popup-accent {
    position: absolute; top: 1px; left: 1px; right: 1px; height: 2px;
    border-radius: 1px;
    background: linear-gradient(90deg,
        rgba(97, 234, 209, 0) 0%,
        var(--aqua) 28%,
        var(--violet) 72%,
        rgba(146, 119, 255, 0) 100%);
}

.popup-header { display: flex; align-items: center; gap: 8px; }
.popup-title  { flex: 1; margin: 0; font-size: var(--type-card); font-weight: 600; color: var(--ink); }

.icon-button {
    display: flex; align-items: center; justify-content: center;
    width: 34px; height: 34px;
    border: 0; border-radius: 11px;
    background: transparent; cursor: pointer;
    transition: background var(--hover-duration) var(--ease-out);
}
.icon-button:hover  { background: rgba(59, 73, 103, 0.133); }
.icon-button:active { background: rgba(64, 95, 119, 0.2); }
.icon-button:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: 2px; }

/* ── Text fields ──
   QML draws a 1 px coloured ring by stacking two rectangles; the focus
   colour differs per field (aqua for username, violet for password). */
.field {
    position: relative;
    border-radius: 16px;
    padding: 1px;
    background: rgba(81, 104, 134, 0.251);          /* #40516886 */
    transition: background var(--hover-duration) var(--ease-out);
}
.field:focus-within { background: var(--aqua); }
.field[data-field="password"]:focus-within { background: var(--violet); }

.text-field {
    display: block;
    width: 100%; height: 52px;
    border: 0; border-radius: 15px;
    background: rgba(16, 27, 50, 0.902);            /* #e6101b32 */
    color: var(--ink);
    font-size: var(--type-body);
    padding: 0 16px 0 50px;
    outline: none;
}
.field[data-field="password"] .text-field { padding-right: 56px; }
.text-field::placeholder { color: #7f90b1; }
.text-field::selection { background: var(--violet); color: #07101e; }

/* The little person / padlock marks, built from boxes exactly as the QML
   builds them from Rectangles. */
.field-glyph {
    position: absolute; left: 16px; top: 50%;
    width: 20px; height: 20px;
    transform: translateY(-50%);
    z-index: 2; pointer-events: none;
}
.field-glyph i { position: absolute; display: block; background: #8495b5; }
.glyph-head {
    left: 50%; top: 0; width: 7px; height: 7px;
    margin-left: -3.5px; border-radius: 50%;
}
.glyph-shoulders {
    left: 50%; bottom: 0; width: 16px; height: 8px;
    margin-left: -8px; border-radius: 4px;
    background: transparent !important; border: 1px solid #8495b5;
}
.glyph-shackle {
    left: 50%; top: 2px; width: 11px; height: 10px;
    margin-left: -5.5px; border-radius: 5px;
    background: transparent !important; border: 1px solid #8495b5;
}
.glyph-body {
    left: 50%; bottom: 0; width: 17px; height: 12px;
    margin-left: -8.5px; border-radius: 4px;
}
.glyph-body::after {
    content: ""; position: absolute; left: 50%; top: 50%;
    width: 3px; height: 5px; margin: -2.5px 0 0 -1.5px;
    border-radius: 1.5px; background: #101b32;
}
.field:focus-within .glyph-head,
.field:focus-within .glyph-body { background: var(--aqua); }
.field:focus-within .glyph-shoulders,
.field:focus-within .glyph-shackle { border-color: var(--aqua); }
.field[data-field="password"]:focus-within .glyph-body { background: var(--violet); }
.field[data-field="password"]:focus-within .glyph-shackle { border-color: var(--violet); }

.reveal {
    position: absolute; right: 7px; top: 50%;
    width: 44px; height: 44px;
    transform: translateY(-50%);
    z-index: 3;
}

.login-status {
    border-radius: 11px;
    background: rgba(23, 35, 59, 0.333);            /* #5517233b */
    border: 1px solid rgba(81, 104, 134, 0.251);
    padding: 8px;
}
.login-status.is-error {
    background: rgba(255, 159, 112, 0.196);         /* #32ff9f70 */
    border-color: rgba(255, 193, 119, 0.467);       /* #77ffc177 */
}
.login-status p {
    margin: 0; font-size: 12px; line-height: 1.2;
    color: var(--muted-ink); text-align: center;
}
.login-status.is-error p { color: #ffd09b; }

.submit-button {
    position: relative;
    height: 54px;
    border: 0; border-radius: 16px;
    background: linear-gradient(90deg, var(--aqua) 0%, var(--violet) 100%);
    color: #07101e; font-size: var(--type-body); font-weight: 700;
    cursor: pointer;
    transition: transform 120ms var(--ease-out), opacity var(--hover-duration);
}
/* The inner 1 px white rim the QML draws inside the gradient. */
.submit-button::after {
    content: ""; position: absolute; inset: 1px;
    border-radius: 15px; border: 1px solid rgba(255, 255, 255, 0.333);
    pointer-events: none;
}
.submit-button:hover:not(:disabled)  { transform: scale(1.008); }
.submit-button:active:not(:disabled) { transform: scale(0.985); }
.submit-button:disabled { opacity: 0.48; color: #71809a; cursor: default; }

/* Sign in ⇄ create an account, and the way back off the promo-code step. */
.popup-switch {
    margin: 0; text-align: center;
    font-size: 12.5px; line-height: 1.45; color: var(--muted-ink);
}
.linkish {
    border: 0; background: none; padding: 0 0 0 4px;
    font: inherit; color: var(--aqua); cursor: pointer;
    text-decoration: underline; text-underline-offset: 2px;
}
.linkish:hover { color: #fff; }
.linkish:disabled { opacity: 0.48; cursor: default; text-decoration: none; }

/* Grouping wrappers only, so each step can be shown or hidden as a unit.
   `display: contents` keeps their children as direct flex items of the form,
   which is what preserves the gap between the username and password fields. */
#credential-fields, #promo-fields, #provider-block { display: contents; }
#credential-fields[hidden], #promo-fields[hidden],
#provider-block[hidden] { display: none; }

.enrollment-removed {
    border: 1px solid rgba(255, 181, 71, 0.38);
    border-radius: 12px;
    background: rgba(255, 181, 71, 0.08);
    color: var(--muted-ink);
    font-size: 13px;
    line-height: 1.55;
    padding: 13px 14px;
}
.enrollment-removed[hidden] { display: none; }

/* ── Federated sign-in ──────────────────────────────────────────────────
   The provider buttons are the one part of this UI we do not get to design.
   Each provider publishes a button specification — mark, wording, minimum
   size, permitted colourways — and following it is a condition of using the
   API, checked at review. So these deliberately do NOT inherit the app's
   gradient-and-glow language: they are the vendor's dark colourway, sized to
   the vendor's metrics, sitting inside our card.

   What is ours is the stacking and the divider. Buttons are full width and
   identical to each other so no provider reads as the endorsed one. */
#provider-buttons { display: flex; flex-direction: column; gap: 9px; }

.provider-button {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    width: 100%; height: 44px; padding: 0 12px;
    border-radius: 12px; cursor: pointer;
    font-size: 14px; font-weight: 600; letter-spacing: 0.1px;
    transition: filter var(--hover-duration), transform 120ms var(--ease-out);
}
.provider-button:hover:not(:disabled) { filter: brightness(1.14); }
.provider-button:active:not(:disabled) { transform: scale(0.985); }
.provider-button:disabled { opacity: 0.48; cursor: default; }
.provider-button svg { flex: none; }

/* Google's dark colourway: #131314 surface, #8E918F rule, #E3E3E3 label —
   the variant intended for a dark UI, rather than a recolour of the light one. */
.provider-button[data-provider="google"] {
    background: #131314; border: 1px solid #8e918f; color: #e3e3e3;
}

/* Named for what it is rather than styled as a heading: it separates two ways
   of doing the same thing, so it must not read as a section break. */
.popup-or {
    display: flex; align-items: center; gap: 10px;
    font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.09em;
    color: var(--muted-ink);
}
.popup-or::before, .popup-or::after {
    content: ""; flex: 1 1 auto; height: 1px;
    background: rgba(117, 141, 180, 0.34);
}

/* Which provider this person used last time. The single cheapest fix for the
   commonest federated-login support ticket: signing up with one provider,
   returning through another, and arriving at an empty second account. */
.provider-button .provider-hint {
    font-size: 11px; font-weight: 500; opacity: 0.62;
}

/* A promo code is read off a slide or a phone screen and typed once — worth
   the monospace and the letter spacing that make a transcription error
   visible while it is being made. */
#promo-code {
    font-family: ui-monospace, "SF Mono", Consolas, monospace;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
#promo-code::placeholder { letter-spacing: normal; text-transform: none; }

@media (max-height: 589px) {
    .voice-mark { width: 87px; height: 87px; }
    .orb { inset: 8px; } .orb-inner { inset: 6px; }
    .orb-label { font-size: 15px; }
    .wordmark-text { font-size: 40px; }
    .wordmark-badge { width: 58px; height: 27px; font-size: 11px; }
    .hero { margin-top: 40px; gap: 8px; }
    .brand-row { margin-top: 10px; }
    /* This one now also fires with the keyboard up, which is the moment the
       card most needs to be compact. */
    .login-popup { margin: 14px auto; padding: 16px; gap: 10px; }
    .text-field, .submit-button { height: 48px; }
}
@media (max-width: 419px) {
    .hero { width: calc(100% - 30px); gap: 13px; }
    .wordmark-text { font-size: 43px; }
    .wordmark-badge { width: 61px; height: 29px; font-size: 12px; }
    .hero-descriptor { font-size: 10px; letter-spacing: 1.8px; }
    /* Narrower gutters, on the layer: the card is `min(370px, 100%)` of
       whatever the layer leaves it. */
    .popup-layer { padding: 0 12px; }
}

/* The partner mark belongs in the step-down above with the rest of the hero:
   a fixed 34 px is 7% of a 1280 px window but 24% of a 412 px phone, so
   leaving it out did not keep it the same size — it kept it the same number
   of pixels while everything beside it shrank.

   Phone portrait only, and that is what the min-height is for. The
   short-viewport size (26 px, up beside .partner-mark) is written earlier in
   the file, so on a window that is both narrow and short it would lose the
   cascade to this rule and undo the compaction the height is asking for.
   The two conditions are made disjoint instead of left to fight. */
@media (max-width: 419px) and (min-height: 590px) {
    .partner-mark { height: 33px; }
}


/* ═══════════════════════════════════════════════════════════════════════
   WORKSPACE CHROME  (qml/Main.qml, qml/AppTabButton.qml)
   ═══════════════════════════════════════════════════════════════════════ */

/* `.screen` hides by default and `.screen.is-active` reveals. An id selector
   out-specifies both, so the flex layout has to hang off the active state --
   otherwise the workspace paints over the login landing permanently. */
#workspace { background: var(--window-bg); }
#workspace.is-active { display: flex; flex-direction: column; }

/* The header holds the tab bar, so it is what sits under a status bar or a
   landscape notch. Grow the box by the top inset rather than only padding
   it: the tabs are centred in their own height, and padding alone would
   push them off-centre by however tall the notch is. The background stays
   full-bleed either way, which is the point of covering the viewport. */
.app-header {
    position: relative;
    flex: 0 0 calc(66px + var(--safe-top));
    background: var(--header-bg);
    padding: calc(2px + var(--safe-top)) calc(5px + var(--safe-right))
             2px calc(5px + var(--safe-left));
    z-index: 2;
}
/* The 2 px horizontal gradient rule under the header. */
.app-header::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: linear-gradient(90deg, #26344e 0%, #52617d 50%, #26344e 100%);
}
@media (max-height: 499px) { .app-header { flex-basis: 60px; } }

.tab-bar { display: flex; gap: 2px; height: 100%; }

.tab {
    position: relative;
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 4px;
    border: 0; background: none; cursor: pointer;
    padding: 0 2px;
    -webkit-tap-highlight-color: transparent;
}
.tab:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: -2px; border-radius: 8px; }
.tab-label {
    font-size: var(--type-tab); font-weight: 500; color: var(--tab-idle);
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tab[aria-selected="true"] .tab-label { color: var(--tab-selected); font-weight: 600; }

/* The selected tab is lit from behind rather than underlined (D106). The
   underline was a second badge for a state the icon and the label already
   carry in colour, and it spent the bottom 5 px of a header that wanted to
   be shorter. A glow says the same thing in the space the tab already
   occupies.

   The accents live here as channels rather than as `var(--accent-solo)`,
   because the glow needs three alphas of one hue and `rgb(… / …)` is the
   portable way to get them — color-mix is not on every iOS this app still
   meets. Behind the content (`z-index: -1`) but still above the header's
   own background: a negative-z child paints after its stacking context's
   background, and .app-header is that context. */
.tab[data-accent="solo"]    { --tab-glow: 255 111 145; }   /* --accent-solo */
.tab[data-accent="multi"]   { --tab-glow:  88 222 200; }   /* --accent-multi */
.tab[data-accent="account"] { --tab-glow: 255 179 107; }   /* --accent-account */
.tab::before {
    content: ""; position: absolute; z-index: -1;
    inset: 1px 3px;
    border-radius: 13px;
    background: radial-gradient(50% 74% at 50% 48%,
        rgb(var(--tab-glow) / 26%) 0%,
        rgb(var(--tab-glow) / 10%) 56%,
        rgb(var(--tab-glow) / 0%) 78%);
    opacity: 0;
    transform: scale(0.88);
    transition: opacity 170ms var(--ease-out), transform 170ms var(--ease-out);
}
.tab[aria-selected="true"]::before { opacity: 1; transform: none; }

.page-stack { position: relative; flex: 1 1 auto; min-height: 0; }

.page { position: absolute; inset: 0; display: none; }
.page.is-active { display: block; }

/* Each page owns a vertical gradient; they are what makes the tabs feel
   like different rooms rather than one scrolling document. The hub blends
   the two session families; the mode pages keep their family's room so
   the old Solo/Multi identities survive as accents (D076). */
.page-hub      { background: linear-gradient(180deg, #1c1428 0%, #121a2c 46%, #0c1624 100%); }
.page-solo     { background: linear-gradient(180deg, #1c1428 0%, #11182b 42%, #0d1322 100%); }
.page-multi    { background: linear-gradient(180deg, #10252c 0%, #0d1a26 48%, #0b1421 100%); }
.page-transcript { background: linear-gradient(180deg, #151a2c 0%, #0d1424 100%); }
.page-account  { background: linear-gradient(180deg, #271b25 0%, #191620 48%, #11131d 100%); }

.page-scroll {
    position: absolute; inset: 0;
    overflow-y: auto; overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
}
.page-column {
    /* --page-gutter is the page padding plus both safe-area insets; see
       css/tokens.css for why the insets are spent here and not on an
       ancestor. */
    width: min(760px, calc(100% - var(--page-gutter)));
    margin: 0 auto;
    /* The home indicator sits over the last row of a scrolled page. */
    padding: var(--page-padding) 0 calc(var(--page-padding) + var(--safe-bottom));
    display: flex; flex-direction: column;
}


/* ═══════════════════════════════════════════════════════════════════════
   LANGUAGE PAIR  (LanguageRoleLabel.qml, LanguagePicker.qml, SoloPage.qml)
   ═══════════════════════════════════════════════════════════════════════ */

.language-row {
    display: flex; align-items: flex-start; gap: var(--row-spacing);
}
/* Multi's single row moved below the connect panel, where it needs the gap it
   used to get for free as the first thing on the page. */
.page-multi .language-row { margin-top: 12px; }

/* D237 — sized by its words, never narrower than the old fixed slot:
   Korean's 말할 언어 was cut to 말할 … in the 88 px slot at every width.
   The picker beside it takes what is left. */
.role-label {
    flex: 0 0 auto;
    display: flex; align-items: center; gap: 9px;
    height: 48px;
    min-width: var(--role-label-width);
    max-width: 45%;
}
.role-label span {
    font-size: var(--type-body); font-weight: 600;
    color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.language-picker {
    --lift-radius: 17px;
    --lift-g0: #364563; --lift-g1: #25324c; --lift-g2: #151c30;
    --lift-mid: 48%;
    --lift-border: #586b8a;
    flex: 1 1 auto; min-width: 0;
    height: 52px;
    display: flex; align-items: center;
    padding: 0 var(--field-pad-right) 0 var(--field-pad-left);
    gap: var(--chevron-spacing);
}
.language-picker:hover:not(:disabled)::after { border-color: rgba(255, 142, 170, 0.47); }
.picker-value {
    flex: 1 1 auto; min-width: 0;
    text-align: left;
    font-size: var(--type-value); font-weight: 500;
    color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.language-picker:disabled .picker-value { color: var(--text-disabled); }

/* D107 — the swap moved off the seam between the two language rows and onto
   the status card above them, where the sentence it answers already is
   ("speak, then tap Listen"). On the seam it was a 44 px control floating in
   a 10 px gap, which read as decoration between two rows rather than as the
   act that reverses them; on the card it is the card's one control, and the
   pair it acts on is still the next thing down the page.

   Square rather than the seam's full label-column width: it now sits beside
   a line of text that owns the width, and a wide button there would read as
   the card's primary act instead of its modifier. */
/* D117 removed the direction swap: the four keys declare the direction, so
   there is nothing left to reverse. `.swap-button` went with it. */


/* ═══════════════════════════════════════════════════════════════════════
   SESSION AUDIO CONTROLS  (qml/SessionAudioControls.qml)
   ═══════════════════════════════════════════════════════════════════════ */

.session-controls { margin-top: 14px; display: flex; flex-direction: column; gap: 5px; }

/* D122 moves Solo's waveform and server orbit into the mode header, where
   they remain visible in every half. The transport keeps its established
   height when it appears; there is no longer an empty instrument row to
   balance against it. */
.session-controls.is-solo .advanced-transport {
    min-height: 92px;
    justify-content: center;
}

.action-row {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: start;
    gap: var(--column-spacing);
    height: 58px;
}
/* A 2-way room is a call, not a mixing console. Its timing and peer activity
   are already legible in the card above, so Mute gets the full thumb-width
   row and the three instrument dials disappear. Group keeps the full cluster;
   Solo relocates its two relevant instruments into the header. */
.session-controls.is-duo .action-row {
    grid-template-columns: minmax(0, 1fr);
    height: 54px;
}
.session-controls.is-duo .capture-button { height: 54px; }
.session-controls.is-duo .indicator-cluster,
.session-controls.is-duo .buffer-readout { display: none; }
/* Post-session review: the room is gone, so the capture button, the VAD
   meter and the peer indicator are promises the page can no longer keep.
   The transport below stays — it serves the retained message. */
.session-controls.is-review .action-row { display: none; }
/* `display: grid` above outranks the UA stylesheet's `[hidden]` — the same
   trap `.clear-button` and `.mode-row` both fall into. */
.action-row[hidden] { display: none; }

/* Solo between sessions (D055): same reasoning as `.is-review` above, one
   stage earlier. There is no microphone to meter and no session to stop, but
   the clip the last session left is still worth replaying and saving. */
.session-controls.is-idle .action-row { display: none; }

.capture-button, .clear-button {
    height: 58px;
    display: flex; align-items: center; justify-content: center;
}
/* Solo hides Clear outright (D055), and the `display: flex` just above
   outranks the UA stylesheet's `[hidden]` rule — so the attribute on its own
   would leave the button sitting there. Same trap, same fix, as
   `.mode-row[hidden]` further down. */
.clear-button[hidden] { display: none; }
/* The resting face: no microphone yet, or one that is still coming up. It
   used to wear a plum border a shade off the alarm colour, which on the
   waiting screen — where this button is disabled and is the only control on
   the page — read as a warning about a microphone that had not been asked
   for yet. Now that pink means "the microphone is shut", neutral is the only
   honest thing for "there is no microphone". */
.capture-button {
    --lift-g0: #333f57; --lift-g1: #242e44; --lift-g2: #161d2e;
    --lift-mid: 52%;
    --lift-border: #5d6b86;
    --lift-border-width: 1.5px;
    --lift-highlight: rgba(255, 255, 255, 0.196);
}
/* A live, well-behaved microphone. Deliberately quiet: this is the normal
   condition of every running session, and for most of a conversation it is
   the state the button is in. It reads as "on" through an accent ring and a
   lit glyph rather than through a filled alarm colour, so the loudest thing
   on the page can go back to being whatever the user is meant to do next. */
.capture-button.is-live {
    --lift-g0: #3d5566; --lift-g1: #26384a; --lift-g2: #141d2c;
    --lift-border: var(--accent-multi);
    --lift-highlight: rgba(255, 255, 255, 0.302);
}
/* The microphone is shut — muted, or faulted so nothing is leaving the
   device. This is the one state worth shouting, because the person it
   affects is usually still talking. */
.capture-button.is-alert {
    --lift-g0: #ff89a4; --lift-g1: #ef5279; --lift-g2: #bd244d;
    --lift-border: rgba(255, 255, 255, 0.753);
    --lift-highlight: rgba(255, 255, 255, 0.565);
}
.capture-button[hidden] { display: none; }
.capture-icon[hidden] { display: none; }
.clear-button {
    --lift-g0: #3a4965; --lift-g1: #27334d; --lift-g2: #161d30;
    --lift-border: #7185a5;
    --lift-border-width: 1.5px;
    --lift-highlight: rgba(255, 255, 255, 0.259);
}
.clear-button:disabled { --lift-border: #4b5871; --lift-highlight: rgba(255,255,255,0.125); }

.button-inner { display: flex; align-items: center; gap: 9px; }
.button-label { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.capture-button.is-alert .button-label { color: #fff7fa; }
.capture-button:disabled .button-label,
.clear-button:disabled .button-label { color: #697189; }
@media (max-width: 520px) { .button-label { font-size: 14px; } .button-inner { gap: 5px; } }

/* Record dot → stop square. */
.record-glyph {
    width: 23px; height: 23px;
    display: flex; align-items: center; justify-content: center;
}
.record-glyph i {
    display: block; width: 17px; height: 17px;
    background: var(--accent-solo);
    border: 2px solid rgba(255, 255, 255, 0.376);
    border-radius: 50%;
    transition: border-radius var(--state-duration) var(--ease-out),
                background var(--state-duration) var(--ease-out);
}
/* Solo only. The button now sits on the calm `is-live` face, so the stop
   square carries the colour instead of the whole pill — the shape still says
   "stop", the page no longer shouts it. */
.capture-button.is-live .record-glyph i {
    border-radius: 3px; background: var(--accent-solo);
    border-color: rgba(255, 255, 255, 0.502);
}
.capture-button:disabled .record-glyph i { background: #697189; }

/* ── Indicator cluster: VAD, peer turn, buffering ── */
.indicator-cluster { display: flex; }
.indicator { width: 48px; height: 58px; display: flex; align-items: center; justify-content: center; }
/* Recessed, where every button on this row is raised. These are meters, not
   controls — the VAD waveform, the peer's turn, the buffering orbit — and as
   flat discs beside a lifted Stop button they read as empty holes waiting for
   something rather than as instruments showing something. Sinking them makes
   "you cannot press this" a property of the drawing instead of a fact you
   discover by trying. */
/* Sinking them was not enough on a phone: at 42 px they matched the transport
   circles one row down almost exactly, so "same size, same shape, one row
   apart" said *button* louder than the inset shadow said meter. They are
   smaller and dimmer than any control on the page now, which is the size
   difference an instrument is allowed to have. */
.indicator { width: 40px; }
.indicator-disc {
    position: relative;
    width: 34px; height: 34px; border-radius: 50%;
    background: #0a0f1b;
    border: 1px solid #1e2739;
    box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.5);
    display: flex; align-items: center; justify-content: center;
    transition: border-color var(--hover-duration) var(--ease-out);
}
.indicator.is-active .indicator-disc { border-color: var(--accent-utility); }

/* One box drives every number below it. The Solo header draws this same
   orbit larger (D143), and a second hand-computed set of coordinates is a
   second chance to put 120° in the wrong place — so the radius is kept as
   the QML's 8.5-in-25 ratio and the dots are placed off it. The literals
   these calc()s replace are reproduced exactly at the default size. */
.orbit {
    --orbit-size: 25px;
    --orbit-dot: 5px;
    --orbit-lead: 6px;
    --orbit-r: calc(var(--orbit-size) * 0.34);
    --orbit-c: calc(var(--orbit-size) / 2);
    position: relative;
    width: var(--orbit-size); height: var(--orbit-size);
}
.orbit.is-spinning { animation: orbit-spin 880ms linear infinite; }
@keyframes orbit-spin { to { transform: rotate(360deg); } }
.orbit i {
    position: absolute; border-radius: 50%;
    width: var(--orbit-dot); height: var(--orbit-dot);
    background: var(--text-secondary); opacity: 0.34;
}
.orbit.is-spinning i { background: var(--accent-utility); }
/* Three dots on that radius at 0°/120°/240° — the positions the QML
   Repeater computes (cos 120° = -0.5, sin 120° = 0.866). */
.orbit i:nth-child(1) {
    width: var(--orbit-lead); height: var(--orbit-lead);
    left: calc(var(--orbit-c) + var(--orbit-r) - var(--orbit-lead) / 2);
    top:  calc(var(--orbit-c) - var(--orbit-lead) / 2);
}
.orbit i:nth-child(2) {
    left: calc(var(--orbit-c) - var(--orbit-r) * 0.5 - var(--orbit-dot) / 2);
    top:  calc(var(--orbit-c) + var(--orbit-r) * 0.866 - var(--orbit-dot) / 2);
}
.orbit i:nth-child(3) {
    left: calc(var(--orbit-c) - var(--orbit-r) * 0.5 - var(--orbit-dot) / 2);
    top:  calc(var(--orbit-c) - var(--orbit-r) * 0.866 - var(--orbit-dot) / 2);
}
.orbit.is-spinning i:nth-child(1) { opacity: 1; }
.orbit.is-spinning i:nth-child(2) { opacity: 0.78; }
.orbit.is-spinning i:nth-child(3) { opacity: 0.56; }

/* ── PeerActivityIndicator.qml ──────────────────────────────────────────
   D039's turn-taking cue. Two glyphs sharing one disc: a speaker that
   pulses while they are talking, and messenger-style composing dots while
   their words are being translated. The distinction is the feature — the
   listener learns a reply is coming seconds before any audio arrives — so
   collapsing both into one "busy" state would lose it. */
.peer-indicator .indicator-disc { position: relative; }
.peer-indicator.is-active .indicator-disc {
    border-color: var(--accent-peer);
    box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.5),
                0 0 0 3px rgba(126, 167, 255, 0.14),
                0 0 16px rgba(126, 167, 255, 0.32);
}

.peer-speaker {
    /* Drawn dim at rest rather than removed: the slot must not collapse and
       reflow the cluster every time the peer stops talking. */
    opacity: 0.4;
    transition: opacity 120ms var(--ease-out);
}
.peer-indicator.is-speaking .peer-speaker {
    opacity: 1;
    animation: peer-speak 860ms ease-in-out infinite;
}
.peer-indicator.is-translating .peer-speaker { display: none; }
@keyframes peer-speak {
    50% { transform: scale(1.12); }
}

.typing-dots { display: none; gap: 4px; }
.peer-indicator.is-translating .typing-dots { display: flex; }
.typing-dots i {
    width: 6px; height: 6px; border-radius: 3px;
    background: var(--accent-peer);
    opacity: 0.25;
    /* One fixed-length cycle per dot with a staggered lead-in, so the wave
       travels left to right and never drifts out of phase — three
       independent loops of different lengths would. */
    animation: typing-dot 1470ms ease-in-out infinite;
}
.typing-dots i:nth-child(2) { animation-delay: 170ms; }
.typing-dots i:nth-child(3) { animation-delay: 340ms; }
@keyframes typing-dot {
    0%   { opacity: 0.25; }
    16%  { opacity: 1; }
    33%  { opacity: 0.25; }
    100% { opacity: 0.25; }
}

/* ── Multi conversation cue ─────────────────────────────────────────────
   D039's first version asked a tiny peer instrument to carry the whole
   conversational handoff. On a phone that reduced the most important state
   in Multi to three 6 px dots.

   This card is the primary cue: phase name, instruction, live caption and
   local delivery feedback. The colour changes reinforce the words but never
   carry meaning alone. "Ready" is permission, not ownership — the room
   remains full duplex. */
/* ═══ The 2-way status widget (D164) ═══════════════════════════════════
   `.mode-status` does the frame — height, padding, radius, hairline, the
   14/600 text, and the `is-restarting` / `is-caution` / `is-error` states
   every mode page shares. What is left here is only what a call has and a
   one-phone page does not.

   Gone with the redesign: a 44 px signal disc drawing animated bars, dots
   and a tick; a breathing radial glow behind the card; and a border, a
   background gradient and a left rail all re-tinted per phase. It was a
   second visual language for the job the other three pages did plainly, and
   the one card in the app a user had to learn twice.

   What survives of the per-phase colour is a 3 px rail, because the phase is
   real information and losing it entirely would cost the glance that told a
   speaker "still translating" from across the table. Everything else about
   the card now looks the same on every page. */
.turn-cue {
    --turn-color: var(--accent-peer);
    --turn-rgb: 126, 167, 255;
    /* The rows below the frame — caption, clip status, delay lesson — wrap
       onto full-width lines of their own rather than squeezing beside the
       icon. `.mode-status` is a flex row; this is the one thing it needs
       adding for a surface that sometimes carries more than two lines. */
    flex-wrap: wrap;
    /* `.session-controls` supplies the gap to the microphone button; the
       card's own page margin would double it. */
    margin-bottom: 0;
    box-shadow: inset 3px 0 0 var(--turn-color);
    transition: box-shadow var(--state-duration) var(--ease-out);
}
.turn-cue[hidden] { display: none; }
.turn-cue[data-phase="translating"] {
    --turn-color: var(--accent-settings);
    --turn-rgb: 167, 139, 250;
}
.turn-cue[data-phase="playing"] {
    --turn-color: #8bd8ff;
    --turn-rgb: 139, 216, 255;
}
.turn-cue[data-phase="ready"] {
    --turn-color: var(--accent-multi);
    --turn-rgb: 88, 222, 200;
}
.turn-cue[data-phase="mic-off"],
.turn-cue[data-phase="mic-blocked"] {
    --turn-color: var(--warning);
    --turn-rgb: 255, 179, 107;
}
.turn-cue[data-phase="mic-starting"],
.turn-cue[data-phase="reconnecting"],
.turn-cue[data-phase="waiting"],
.turn-cue[data-phase="ending"] {
    --turn-color: var(--text-secondary);
    --turn-rgb: 174, 185, 214;
}
.turn-cue[data-phase="self-speaking"],
.turn-cue[data-phase="self-sending"],
.turn-cue[data-phase="self-delivered"] {
    --turn-color: var(--accent-multi);
    --turn-rgb: 88, 222, 200;
}
.turn-cue[data-phase="overlap"] {
    --turn-color: var(--warning);
    --turn-rgb: 255, 179, 107;
}
.turn-cue[data-phase="mic-fault"],
.turn-cue[data-phase="media-down"] {
    --turn-color: #ffb6c2;
    --turn-rgb: 255, 182, 194;
}
/* The card's own state classes outrank the phase rail: an unreadable
   microphone and a dead one are both about to be acted on, and the rail
   arguing with the border was the sort of detail that makes a warning look
   decorative. */
.turn-cue.is-caution { --turn-color: var(--warning); }
.turn-cue.is-error { --turn-color: #ff6f91; }

/* A `<strong>` in a card that already sets 14/600: without this it renders a
   step heavier than Solo's line, which is the whole thing this redesign was
   trying to stop doing. */
.turn-cue-title { font-weight: 600; }

/* Captions and delivery feedback stay inside the surface that explains their
   timing. Full-width rows under the frame, so a long translated script uses
   the whole phone rather than the column beside the icon. (D165 took the
   one-time delay lesson out of this stack: see js/core/settings.js.) */
.turn-cue-transcript,
.turn-cue-status {
    flex: 1 0 100%;
    min-width: 0;
}
.turn-cue-transcript {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-top: 1px;
    padding: 9px 10px;
    border: 1px solid rgba(var(--turn-rgb), 0.24);
    border-radius: 10px;
    background: rgba(5, 12, 22, 0.36);
}
.turn-cue-transcript[hidden],
.turn-cue-status[hidden] { display: none; }
.turn-cue-transcript.is-live { border-color: rgba(var(--turn-rgb), 0.58); }
/* D237 — your own words (what the model heard) read a step quieter than
   the other person's: they are a check, not the conversation. */
.turn-cue-transcript.is-own .turn-cue-transcript-text {
    color: var(--text-secondary);
}
.turn-cue-transcript-who {
    color: var(--turn-color);
    font-size: 10.5px;
    line-height: 1.2;
    font-weight: 750;
}
.turn-cue-transcript-text {
    color: var(--text-primary);
    font-size: 14px;
    line-height: 1.4;
    overflow-wrap: anywhere;
}
.turn-cue-status {
    color: var(--text-secondary);
    font-size: 10.5px;
    line-height: 1.3;
}
.turn-cue-status.is-error { color: #ffb6c2; font-weight: 700; }

@media (max-width: 399px) {
    .turn-cue { gap: 8px; padding: 9px 10px; }
}
/* ── Transport row ── */
/* ── The take-turns person cards (D121, refining D118's columns) ────────
   Each person owns a separate surface. The previous design put both columns
   inside one recessed well; the four raised faces then competed with that
   well, the picker faces, and the centre divider, and at phone width the
   result looked like one dense 2×4 keypad. Two quiet cards keep the hand-off
   model but make the ownership boundary do the grouping work.

   Action faces use `.lift` through its variables. Do not paint a second
   gradient on the button itself: `.lift::after` is the visible face, and a
   second face underneath it is what made the older button graphic muddy. */
.turn-desk {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin: 10px 0;
}
.turn-desk[hidden] { display: none; }

.turn-column {
    --person-rgb: 255, 111, 145;
    position: relative;
    display: flex; flex-direction: column; gap: 9px;
    min-width: 0;
    padding: 10px;
    border: 1px solid rgba(var(--person-rgb), 0.24);
    border-radius: 20px;
    background:
        linear-gradient(145deg, rgba(var(--person-rgb), 0.08), transparent 42%),
        linear-gradient(180deg, #1b2236 0%, #12192b 100%);
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
    overflow: hidden;
}
.turn-column[data-turn-side="b"] { --person-rgb: 126, 167, 255; }
.turn-column::before {
    content: "";
    position: absolute; left: 16px; right: 16px; top: 0; height: 1px;
    background: rgba(var(--person-rgb), 0.44);
}

.turn-person-head {
    min-width: 0;
    display: flex; align-items: center; gap: 8px;
}
.turn-person-marker {
    flex: 0 0 auto;
    width: 25px; height: 25px;
    display: grid; place-items: center;
    border-radius: 50%;
    border: 1px solid rgba(var(--person-rgb), 0.62);
    background: rgba(var(--person-rgb), 0.13);
    color: rgb(var(--person-rgb));
    font-size: 12px; font-weight: 800;
}
.turn-person {
    font-size: 13px; font-weight: 700;
    color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.turn-column .language-picker {
    height: 48px;
    padding: 0 10px 0 12px;
    --lift-radius: 14px;
    --lift-g0: #303b54;
    --lift-g1: #222c42;
    --lift-g2: #151c2d;
    --lift-border: rgba(var(--person-rgb), 0.42);
}
.turn-column .picker-value { font-size: 15px; }

.turn-key-stack {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
}
.turn-key {
    min-height: 62px;
    padding: 0 10px;
    display: flex; align-items: center; justify-content: center; gap: 8px;
    color: var(--text-secondary);
    --lift-radius: 14px;
    --lift-g0: #2b354c;
    --lift-g1: #1e283c;
    --lift-g2: #131a2a;
    --lift-border: #465674;
    --lift-highlight: rgba(255, 255, 255, 0.18);
    --lift-inset: 18px;
}
.turn-key-icon {
    flex: 0 0 auto;
    width: 27px; height: 28px;
    display: grid; place-items: center;
}
.turn-key canvas { display: block; }
.turn-key:disabled { color: #66728c; }
.turn-key:disabled .turn-key-icon { opacity: 0.58; }
.turn-key-verb {
    font-size: 15px; font-weight: 700;
    min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.turn-key.is-selected {
    color: var(--text-primary);
}
.turn-key.is-selected[data-turn-role="speak"] {
    --lift-g0: #814155;
    --lift-g1: #5d2d40;
    --lift-g2: #391c2a;
    --lift-border: rgba(255, 111, 145, 0.78);
    --lift-highlight: rgba(255, 255, 255, 0.25);
}
.turn-key.is-selected[data-turn-role="listen"] {
    --lift-g0: #2c6b62;
    --lift-g1: #1d5049;
    --lift-g2: #11332f;
    --lift-border: rgba(88, 222, 200, 0.78);
    --lift-highlight: rgba(255, 255, 255, 0.25);
}

/* Server-confirmed and unplayed. The dot is the whole visual treatment: a
   pill and a tinted button face crowded the compact two-column controls.
   The button's accessible name still announces "New audio". */
.turn-key-new {
    position: absolute;
    top: 10px; right: 11px;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--accent-utility);
    box-shadow: 0 0 0 3px rgba(88, 222, 200, 0.12),
                0 0 10px rgba(88, 222, 200, 0.44);
}
.turn-key-new[hidden] { display: none; }
@media (max-width: 399px) {
    .turn-desk { gap: 7px; }
    .turn-column { gap: 8px; padding: 8px; border-radius: 18px; }
    .turn-person-head { gap: 6px; }
    .turn-person-marker { width: 23px; height: 23px; }
    .turn-column .language-picker { padding-left: 9px; padding-right: 7px; }
    .turn-column .picker-value { font-size: 14px; }
    .turn-key { min-height: 60px; gap: 6px; padding: 0 7px; }
    .turn-key-icon { width: 25px; height: 27px; }
    .turn-key-verb { font-size: 14px; }
    .turn-key-new { top: 8px; right: 8px; }
}
@media (min-width: 600px) {
    .turn-key-stack { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.transport-row { display: flex; align-items: center; gap: 8px; }
.transport-spacer { flex: 1 1 auto; }
/* D118 — Solo runs the bar *inside* the transport row, in the gap between
   Replay and Save, so playback is one row instead of two. The row is the
   whole of that mode's transport and it was 52 px of buttons with a 58 px
   bar under it; on a phone that second row is what pushed the tail below the
   fold. The bar keeps its own height off the row so the circles stay on one
   optical centre line, and `.seek-row` is left in the template for Multi,
   whose expanded playback tools are a deliberately separate block. */
.transport-row > .seek-bar { height: 52px; }
.seek-row[hidden] { display: none; }
.advanced-transport {
    display: flex;
    flex-direction: column;
    gap: 5px;
}
.advanced-transport[hidden] { display: none; }

/* Multi's default playback shape mirrors a phone call: one useful shortcut,
   then an explicitly secondary way into the complete retained-audio tools. */
.multi-replay-row {
    display: flex;
    align-items: center;
    gap: 9px;
}
.multi-replay-row[hidden] { display: none; }
.replay-last-button,
.replay-tools-button {
    min-height: 46px;
    border-radius: 13px;
    color: var(--text-primary);
    font-family: inherit;
    font-weight: 700;
}
.replay-last-button {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    --lift-g0: #37665f;
    --lift-g1: #234b46;
    --lift-g2: #132d2b;
    --lift-border: rgba(88, 222, 200, 0.66);
}
.replay-last-button canvas { flex: 0 0 auto; }
.replay-tools-button {
    flex: 0 0 auto;
    min-width: 106px;
    padding: 0 13px;
    border: 0;
    background: none;
    color: var(--text-secondary);
    cursor: pointer;
    --lift-g0: #2d3a50;
    --lift-g1: #202a3d;
    --lift-g2: #151d2d;
    --lift-border: #4d607c;
    --lift-radius: 13px;
}
.replay-last-button:disabled,
.replay-tools-button:disabled { opacity: 0.46; cursor: default; }
@media (max-width: 399px) {
    .multi-replay-row { gap: 7px; }
    .replay-last-button { gap: 5px; font-size: 13px; }
    .replay-tools-button { min-width: 92px; padding: 0 9px; font-size: 11px; }
}

/* Clear lives on the transport row rather than in the action row — it acts
   on the retained buffer, and that is what this row is about — but at the
   far right end, as far from Play as the row allows. 52 px, not the action
   row's 58, so its face lines up with the circular media faces beside it.

   The word is the whole control now, so the padding is what gives it its
   size: 18 px a side puts a ~70 px button around a ~34 px word, which reads
   as deliberately wider than the 50 px circles beside it rather than
   accidentally different. It no longer sheds anything at phone widths — a
   label that disappears on the commonest screen there is was never really
   the label — and at 390 px the row still leaves a ~65 px spacer, which is
   plainly a gap between two groups rather than an uneven gutter. */
.transport-row .clear-button {
    flex: 0 0 auto;
    height: 52px;
    padding: 0 18px;
    margin-left: 4px;
}
@media (max-width: 420px) {
    .transport-row { gap: 7px; }
    .transport-row .clear-button { padding: 0 15px; margin-left: 0; }
}

/* SoloMediaButton is a circle with no dark base: the face itself carries
   the offset, so ::before is suppressed. */
/* Square, so the face is a circle. The face takes `calc(100% - 6px)` in both
   axes, which off a 50×52 box gave 44×46 — a two-pixel egg under
   `border-radius: 50%`, small enough to pass a diff and visible as
   irregularity on the row the eye spends most of its time on. The height is
   the number that cannot move (46 of face, plus the 5 px of dark base the
   reveal leaves below it, as `.lift` does), so the width follows it. */
.media-button {
    position: relative;
    width: 52px; height: 52px; min-width: 52px;
    border: 0; background: none; padding: 0; cursor: pointer;
    flex: 0 0 auto;
    -webkit-tap-highlight-color: transparent;
}
.media-face {
    position: absolute; left: 3px; top: 1px;
    width: calc(100% - 6px); height: calc(100% - 6px);
    border-radius: 50%;
    border: 1.5px solid #7085a4;
    background: linear-gradient(180deg, #35445e 0%, #202c42 52%, #111827 100%);
    transition: transform var(--press-duration) var(--ease-out);
}
.media-face::after {
    content: ""; position: absolute; inset: 3px;
    border-radius: 50%; border: 1px solid rgba(79, 99, 128, 0.157);
}
.media-button.is-emphasized .media-face {
    border-color: rgba(255, 255, 255, 0.69);
    background: linear-gradient(180deg, #82ead7 0%, var(--accent-utility) 52%, #32aa96 100%);
}
.media-button.is-emphasized .media-face::after { border-color: rgba(255, 255, 255, 0.22); }
.media-button canvas {
    position: absolute; left: 50%; top: 50%;
    margin: -14px 0 0 -12px;
    transition: transform var(--press-duration) var(--ease-out);
}
.media-button:active:not(:disabled) .media-face,
.media-button:active:not(:disabled) canvas { transform: translateY(var(--press-offset)); }
.media-button:disabled { cursor: default; }
.media-button:disabled .media-face { opacity: 0.58; }
.media-button:focus-visible { outline: none; }
.media-button:focus-visible .media-face { border-color: var(--text-primary); border-width: 2px; }

/* ── Seek bar (qml/SoloMediaSeekBar.qml) ── */
.seek-row { display: flex; align-items: center; gap: 8px; }
.seek-bar {
    flex: 1 1 auto; min-width: 0;
    position: relative; height: 58px;
    display: flex; align-items: center;
    cursor: pointer;
    border: 0; background: none; padding: 0;
    /* Unconditional, not only while .is-dragging: the class lands after the
       first pointermove, and by then the browser may already have claimed
       the gesture for a page scroll and cancelled the drag. */
    touch-action: none;
}
/* SoloMediaSeekBar.qml: an 11 px track on a 13 px box, so a dark base shows
   below it — the same offset-base idea as every other raised surface here.
   The inner 2 px rule near the top is the track's own highlight. */
.seek-track {
    position: relative;
    width: 100%; height: 11px; border-radius: 5.5px;
    background: #090f18;
    border: 1px solid #343c54;
    box-shadow: 0 4px 0 -2px rgba(0, 0, 0, 0.659);   /* #a8000000 */
}
.seek-track::before {
    content: "";
    position: absolute; left: 2px; right: 2px; top: 1px;
    height: 2px; border-radius: 1px;
    background: rgba(37, 49, 69, 0.086);             /* #16253145 */
}
/* Once there is buffered audio the track picks up the utility accent. */
.seek-bar.has-buffer .seek-track { border-color: #28564f; }
.seek-bar.has-buffer .seek-track::before {
    background: rgba(77, 141, 130, 0.188);           /* #304d8d82 */
}
.seek-fill {
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 0;
    border-radius: 4px;
    background: var(--accent-utility);
    transition: width 120ms linear;
}
.seek-bar.is-replay .seek-fill { background: var(--accent-solo); }
/* The QML handle is `visible: mode !== 0 && boundedDuration > 0`, so at rest
   there is no knob at all — the empty track is just a track. */
.seek-knob {
    position: absolute; top: 50%; left: 0;
    width: 14px; height: 14px; margin: -7px 0 0 -7px;
    border-radius: 50%;
    background: #eaf0ff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    transition: left 120ms linear;
    display: none;
}
.seek-bar.has-buffer .seek-knob { display: block; }
.seek-bar:disabled { cursor: default; opacity: 0.5; }
/* The knob follows the finger directly during a drag; the 120 ms ease that
   smooths playback progress would lag behind it. */
.seek-bar.is-dragging .seek-knob { transition: none; transform: scale(1.12); }

/* D023's drag tooltip. Committing on release means nothing moves while the
   finger is down, so without a readout the drag looks broken. */
.seek-tip {
    position: absolute; left: 50%; bottom: 100%;
    transform: translateX(-50%);
    margin-bottom: 9px; padding: 5px 9px;
    border-radius: 9px;
    background: #f7f8ff;
    border: 1px solid var(--accent-solo);
    font-family: var(--font-mono); font-size: 12px; font-weight: 700;
    color: #171222;
    white-space: nowrap;
    display: none;
}
.seek-bar.is-dragging .seek-tip { display: block; }
/* Now the action row's third cell, where Clear used to be. Right-aligned
   rather than centred in its 1fr column: it reads as a readout hung off the
   row's edge, and centring it would leave it floating mid-gap. The face
   reveal is subtracted from the height so it sits on the buttons' optical
   centre line, not 2.5 px below it. */
.buffer-readout {
    justify-self: end;
    height: calc(58px - var(--face-reveal));
    display: flex; flex-direction: column;
    align-items: flex-end; justify-content: center;
    padding-right: 2px;
}
.buffer-readout[hidden] { display: none; }
.buffer-caption {
    font-size: 9px; font-weight: 800; letter-spacing: 0.09em;
    line-height: 1.1;
    color: var(--text-secondary); opacity: 0.72;
    text-transform: uppercase; white-space: nowrap;
}
.buffer-caption:empty { display: none; }
.buffer-duration {
    min-width: 44px;
    display: flex; align-items: center; justify-content: flex-end;
    font-family: var(--font-mono); font-size: 15px; font-weight: 700;
    color: var(--text-secondary);
}
.buffer-duration.has-audio { color: var(--text-primary); }
/* Riding the present, in follow-along's version of this readout (D056). The
   accent is the same one the seek fill and the transport wear, so "LIVE" and
   a full bar are visibly the same statement; a lag figure stays in the plain
   readout colour, because being a few seconds behind is the normal condition
   of this mode and not a warning. */
.buffer-duration.is-live { color: var(--accent-utility); }
/* The last 10 s before the retention cap trims the clip's head — the same
   amber the warning notification wears, one step short of the error pink. */
.buffer-duration.is-near-cap { color: #ffd09b; }

/* ═══════════════════════════════════════════════════════════════════════
   TRANSCRIPT PANEL  (ExpandableTranscriptPanel.qml, TranscriptHistoryView.qml)
   ═══════════════════════════════════════════════════════════════════════ */

/* The panel's own fill is #0b101e on both pages; `cardColor` (#121b2c on
   Solo, #11242d on Multi) is the turn cards alone, which is why they read as
   raised against it. */
.transcript-panel {
    margin-top: 12px;
    border-radius: 18px;
    background: #0b101e;
    border: 1px solid #26344f;
    overflow: hidden;
}

/* ExpandableTranscriptPanel.qml uses the same header recipe as the Connect
   panel: the collapsed blue-grey gradient, the open teal one, a 1 px top
   highlight inset 28 px, and (open) an 18 px flat strip plus a hairline rule
   where the body starts. */
.transcript-header {
    display: flex; align-items: center; gap: 11px;
    width: 100%; height: 54px;
    padding: 0 var(--panel-padding);
    border: 0; cursor: pointer;
    border-radius: 18px;
    -webkit-tap-highlight-color: transparent;
    background-repeat: no-repeat;
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.227), rgba(255, 255, 255, 0.227)),
        linear-gradient(180deg, #35445f 0%, #253149 50%, #161d30 100%);
    background-position: center 1px, 0 0;
    background-size: calc(100% - 28px) 1px, 100% 100%;
}
.transcript-panel.is-open .transcript-header {
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.396), rgba(255, 255, 255, 0.396)),
        linear-gradient(rgba(111, 127, 139, 0.263), rgba(111, 127, 139, 0.263)),
        linear-gradient(#142c2a, #142c2a),
        linear-gradient(180deg, #355c58 0%, #254641 50%, #142c2a 100%);
    background-position: center 1px, 0 100%, 0 100%, 0 0;
    background-size:
        calc(100% - 28px) 1px, 100% 1px, 100% 18px, 100% 100%;
}
.transcript-header:active > * { transform: translateY(3px); }
.transcript-header > * { transition: transform 90ms var(--ease-out); }

.transcript-doc { flex: 0 0 auto; }
.transcript-title {
    flex: 1; text-align: left;
    font-size: 16px; font-weight: 700; color: var(--text-primary);
}
.transcript-chevron {
    flex: 0 0 auto;
    transition: transform 170ms var(--ease-out);
}
.transcript-panel.is-open .transcript-chevron { transform: rotate(180deg); }

/* ── TranscriptHistoryView.qml ──────────────────────────────────────────
   The panel body: a header (title, storage line, count pill), the Show
   filter row, the actions toolbar, then the list. Every value below is the
   QML's own. `--transcript-accent` carries the page tint so Solo's pink and
   Multi's teal drive the same rules. */
.transcript-body { padding: 0 var(--panel-padding) var(--panel-padding); }

/* `center`, not `flex-start`: with the "Transcript history" title gone the
   text side is one 11 px line, and top-aligning it against a 30 px pill left
   the line hanging off the pill's shoulder. */
.transcript-head {
    display: flex; align-items: center; gap: 10px;
    padding-top: 12px;
    border-top: 1px solid color-mix(in srgb, var(--transcript-accent) 32%, transparent);
}
.transcript-head-text { flex: 1 1 auto; min-width: 0; }
.transcript-storage {
    margin: 2px 0 0;
    font-size: 11px; color: #8f9bb5;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.transcript-count {
    flex: 0 0 auto;
    padding: 5px 10px; border-radius: 999px;
    font-weight: 700;
    color: var(--transcript-accent);
    background: color-mix(in srgb, var(--transcript-accent) 16%, transparent);
}

.transcript-filters, .transcript-actions {
    display: flex; align-items: center; gap: 5px;
    margin-top: 10px;
}
.transcript-filters-label, .transcript-actions-label {
    font-size: 11px; color: #8f9bb5;
}
.transcript-actions-label {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* FilterButton: the shared lift, but with its own three-stop face and a
   selection state that swaps both the gradient and the border. */
.filter-button {
    position: relative;
    flex: 1 1 0; min-width: 0; height: 36px;
    border: 0; background: none; padding: 0;
    color: var(--text-primary); font-size: 12px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.filter-button::before {
    content: ""; position: absolute; inset: 0 0 3px 0;
    border-radius: 11px;
    border: 1px solid #465572;
    background: linear-gradient(180deg, #34425c 0%, #222e45 52%, #141b2c 100%);
    transition: transform var(--press-duration) var(--ease-out);
}
.filter-button > span { position: relative; }
.filter-button:active::before,
.filter-button:active > span { transform: translateY(var(--press-offset)); }
.filter-button.is-selected::before {
    border-width: 1.5px;
    border-color: var(--filter-accent, var(--transcript-accent));
    background: linear-gradient(180deg, #42625f 0%, #294b47 52%, #17322f 100%);
}

.toolbar-button {
    flex: 0 0 auto;
    width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    border: 1px solid #3a4a63; border-radius: 12px;
    background: #1a2230;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.toolbar-button:active { transform: translateY(2px); }
.toolbar-button:disabled { opacity: 0.45; cursor: default; transform: none; }

.transcript-action-message {
    margin: 8px 0 0;
    text-align: center; font-size: 12px;
    color: var(--transcript-accent);
}

.transcript-scroll {
    max-height: 0;
    overflow-y: auto;
    transition: max-height var(--state-duration) var(--ease-out);
}
.transcript-panel.is-open .transcript-scroll { max-height: 420px; }

.transcript-empty {
    padding: 26px var(--panel-padding);
    text-align: center;
}
.transcript-empty-title {
    margin: 0;
    font-size: 14px; font-weight: 700; color: #d9e2f4;
}

/* The panel can open before the fonts its stored rows are written in have
   arrived, and an empty panel reads as a bug rather than as a wait. Same busy
   dots the connecting screen uses, tinted with the panel's own accent. */
.transcript-loading { padding: 24px var(--panel-padding); text-align: center; }
.transcript-loading .busy-dots i { background: var(--transcript-accent); }

.transcript-list {
    display: flex; flex-direction: column; gap: 9px;
    margin: 0; padding: 10px 0 4px; list-style: none;
}

.session-head { margin: 7px 0 0; }
.session-title { margin: 0; font-size: 15px; font-weight: 700; }
.session-pair { margin: 2px 0 0; font-size: 11px; color: #8f9bb5; }

/* "For frequently repeated transcript rows … omit RectangularShadow and use
   only the offset base, face gradient, and hairline." No blur per row. */
.turn {
    padding: 10px;
    border-radius: 12px;
    background: var(--transcript-card, #151c30);
}
.turn.is-incomplete { border: 1px solid #ffca6b; }
/* One line: speaker, state, languages, time, and the actions toggle. The
   speaker no longer takes the slack — the languages do — because a name is a
   fixed short string and a language pair is the part that has to be allowed
   to run out of room and ellipsis rather than push the clock off the row. */
.turn-meta {
    display: flex; align-items: baseline; gap: 8px;
    font-size: 11px; color: #8f9bb5;
    margin-bottom: 6px;
}
.turn-speaker {
    flex: 0 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 14px; font-weight: 700; color: var(--text-primary);
}
.turn-langs {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.turn-time { flex: 0 0 auto; }
.turn-flag { font-size: 10px; font-weight: 700; color: #ffca6b; }
.turn-label { font-size: 10px; font-weight: 700; color: #8f9bb5; margin-bottom: 3px; }
.turn-label.is-translation { color: var(--transcript-accent); }
.turn-source { font-size: 15px; color: var(--text-primary); margin: 0; }
/* In the compact both-languages view the original demotes to a lead-in. */
.turn.has-both .turn-source { font-size: 15px; color: #bdc7dc; }
.turn-rule { height: 1px; margin: 6px 0; background: #2a344d; }
.turn-target { font-size: 15px; color: var(--text-primary); margin: 0; }
.turn-source[dir="rtl"], .turn-target[dir="rtl"] { text-align: right; }

/* ── the full layout (the phone sheet) ──────────────────────────────────
   All that separates the two now is the card's own generosity. The meta line
   used to be sheet-only, on the grounds that the in-page panel had no width
   for a speaker row plus two caption lines — but there are no caption lines
   any more, and the one line that replaced all three is also where a row's
   Copy and Share live, so hiding it inline would have put them out of reach
   on every desktop. */
.transcript-sheet .turn { padding: 14px; border-radius: 17px; }

/* ── per-message actions ────────────────────────────────────────────────
   The whole card is the tap target; ⋯ is that target made visible and
   focusable. 30 px rather than the toolbar's 44: it repeats on every row,
   and the row itself is the generous hit area. */
.turn-more {
    flex: 0 0 auto;
    width: 30px; height: 24px; margin: -4px -4px -4px 0;
    display: flex; align-items: center; justify-content: center;
    border: 0; border-radius: 8px; background: none;
    color: #8f9bb5; font-size: 15px; line-height: 1;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.turn.is-selected .turn-more { color: var(--transcript-accent); }
/* Outside the card, not on its edge: an incomplete turn already carries a
   1 px amber border there, and an inset selection ring landed exactly on it
   and mixed into a third colour that meant neither thing. The 9 px row gap
   has room for it. */
.turn.is-selected {
    outline: 1px solid color-mix(in srgb, var(--transcript-accent) 55%, transparent);
    outline-offset: 2px;
}
.turn-actions {
    display: flex; gap: 6px;
    margin-top: 9px; padding-top: 8px;
    border-top: 1px solid #2a344d;
}
.turn-action {
    flex: 1 1 0;
    height: 34px; border-radius: 10px;
    border: 1px solid #3a4a63; background: #1a2230;
    color: var(--text-primary); font-size: 12px; font-weight: 700;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.turn-action:active { transform: translateY(2px); }

/* A draft is a turn being spoken right now (§3.6). Dashed rather than solid
   so it never reads as a finished record, and it carries no timestamp —
   the one it would get is the moment it started, which is not when it
   happened. */
.turn.is-draft {
    border: 1px dashed color-mix(in srgb, var(--transcript-accent) 55%, transparent);
    background: color-mix(in srgb, var(--transcript-card, #151c30) 88%, #ffffff);
}
.turn-flag.is-live { color: var(--transcript-accent); }

/* D015's session grouping, which the Qt build declared and never rendered. */
.session-head:not(:first-child) { margin-top: 9px; }


/* ═══════════════════════════════════════════════════════════════════════
   TRANSCRIPT SHEET  (ExpandableTranscriptPanel.qml's transcriptPopup)

   Below 600 px the panel's body is re-parented in here. Nesting a scrolling
   list inside a scrolling page is the actual problem — on a phone the two
   fight for every touch — so the sheet takes the whole viewport and there is
   only one scroller again.
   ═══════════════════════════════════════════════════════════════════════ */

/* The shell is shared with the connect sheet: same geometry, same insets,
   same header bar. Only the body differs, and each sheet styles its own. */
.full-sheet {
    position: fixed; inset: 0;
    z-index: 60;
    display: flex; flex-direction: column;
    background: linear-gradient(180deg, #17243a 0%, #0b1020 100%);
    /* Covers the whole viewport, so it owns all four insets: the keyboard is
       never up in here, but the status bar, the home indicator and — turned
       sideways — the notch all are. */
    padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
}
.full-sheet[hidden] { display: none; }
/* The page underneath must not scroll behind a modal sheet. */
body.has-sheet #workspace { overflow: hidden; }

/* ── Swipe left to close (js/app.js bindSwipeToClose) ──────────────────────
   `pan-y` is the whole contract: the browser keeps vertical panning, which is
   the sheet's own scrolling, and leaves horizontal to the listener. Without
   it a downward flick that drifts sideways would be both a scroll and a
   drag. Only the connect sheet is bound; the declaration lives with it. */
.connect-sheet { touch-action: pan-y; }
/* Under the finger there is no transition — the sheet has to be exactly where
   the touch is. It comes back, or leaves, on a released one. */
.full-sheet { transition: transform 170ms var(--ease-out); }
/* No transition under the finger, and no text selection under a mouse doing
   the same drag. */
.full-sheet.is-swiping {
    transition: none;
    -webkit-user-select: none; user-select: none;
}
/* Leaving accelerates rather than settling: the finger has already decided,
   and OutCubic on the way out reads as the sheet hesitating. */
.full-sheet.is-leaving { transition: transform 170ms cubic-bezier(0.4, 0, 1, 1); }

.full-sheet-header {
    flex: 0 0 auto;
    display: flex; align-items: center;
    height: 60px;
    padding-left: var(--panel-padding);
    background: #11192a;
    border-bottom: 1px solid #465572;
}
.full-sheet-title {
    flex: 1 1 auto; min-width: 0; margin: 0;
    font-size: 19px; font-weight: 700; color: var(--text-primary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.full-sheet-close {
    flex: 0 0 52px; height: 52px;
    background: none; border: 0; cursor: pointer;
    font-size: 25px; line-height: 1; color: var(--text-secondary);
}
.full-sheet-close:active { color: var(--transcript-accent, #5dd6c0); }

/* The transcript owns the viewport on a phone, but it does not own the call.
   This fixed-height strip keeps the covered conversation state visible and
   leaves the list below as the sheet's one vertical scroller. */
.transcript-live-strip {
    flex: 0 0 auto;
    display: flex; align-items: stretch; gap: 8px;
    margin: 10px 12px 0;
}
.transcript-live-strip[hidden] { display: none; }
.transcript-live-return,
.transcript-live-mute,
.transcript-live-action {
    min-height: 60px;
    border: 1px solid rgba(88, 222, 200, 0.42);
    border-radius: 15px;
    background: linear-gradient(180deg, #17373a 0%, #10292d 100%);
    color: var(--text-primary);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.transcript-live-return {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: center; gap: 9px;
    padding: 8px 9px 8px 10px;
    text-align: left;
}
.transcript-live-return:active,
.transcript-live-mute:active,
.transcript-live-action:active {
    background: #1b4648;
    transform: translateY(1px);
}
.transcript-live-signal {
    position: relative;
    flex: 0 0 34px; width: 34px; height: 34px;
    display: grid; place-items: center;
    border-radius: 50%;
    background: rgba(88, 222, 200, 0.12);
}
.transcript-live-signal i {
    position: absolute; right: 0; bottom: 1px;
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--transcript-live-accent, var(--accent-multi));
    box-shadow: 0 0 0 2px #10292d;
}
.transcript-live-copy {
    flex: 1 1 auto; min-width: 0;
    display: flex; flex-direction: column; gap: 2px;
}
.transcript-live-copy strong,
.transcript-live-copy > span {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.transcript-live-copy strong { font-size: 13px; line-height: 1.25; }
.transcript-live-copy > span {
    color: #aebdd0; font-size: 11px; line-height: 1.25;
}
.transcript-live-chevron {
    flex: 0 0 auto;
    color: var(--accent-multi); font-size: 25px; line-height: 1;
}
.transcript-live-mute,
.transcript-live-action {
    flex: 0 0 66px;
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 2px;
    padding: 5px;
}
.transcript-live-mute {
    color: #ffd8e2;
    border-color: rgba(255, 111, 145, 0.48);
    background: linear-gradient(180deg, #42243a 0%, #2e1a2b 100%);
}
/* Follow Along's Back-10 / Skip-to-live pair keeps the strip's teal shell:
   neither is destructive, and the pink coat is mute's alone. */
.transcript-live-action { color: var(--text-primary); }
.transcript-live-action:disabled {
    opacity: 0.45; cursor: default; transform: none; background: none;
}
.transcript-live-mute[hidden],
.transcript-live-action[hidden] { display: none; }
.transcript-live-mute span,
.transcript-live-action span {
    max-width: 58px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 10px; font-weight: 700;
}
.transcript-live-mute.is-muted {
    color: #fff7fa;
    background: linear-gradient(180deg, #81334f 0%, #59253c 100%);
}

/* A reader who scrolls away from the tail keeps their place. This appears
   above the list rather than inside it, so the arriving row cannot push its
   own recovery control off screen. */
.transcript-new-message {
    position: absolute;
    z-index: 2;
    right: calc(var(--safe-right) + 18px);
    bottom: calc(var(--safe-bottom) + 16px);
    min-height: 42px;
    display: inline-flex; align-items: center; gap: 7px;
    padding: 0 15px;
    border: 1px solid rgba(88, 222, 200, 0.62);
    border-radius: 999px;
    background: #173b3b;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.34);
    color: #eafffb;
    font-size: 12px; font-weight: 750;
    cursor: pointer;
}
.transcript-new-message[hidden] { display: none; }
.transcript-new-message:active {
    background: #215452;
    transform: translateY(1px);
}

/* The invite sheet is an interruption to the live conversation, not a new
   destination. A conventional, labelled back affordance makes that
   relationship obvious and keeps the way back within an easy 48 px target. */
.connect-sheet .full-sheet-header {
    height: 68px;
    padding: 8px 14px 8px 10px;
    gap: 12px;
}
.connect-sheet-return {
    flex: 0 0 auto;
    min-width: 92px; min-height: 48px;
    padding: 0 15px 0 11px;
    display: inline-flex; align-items: center; justify-content: center;
    gap: 7px;
    border: 1px solid rgba(88, 222, 200, 0.48);
    border-radius: 15px;
    background: rgba(88, 222, 200, 0.12);
    color: var(--accent-multi);
    font: inherit; font-size: 15px; font-weight: 750;
    letter-spacing: 0.01em;
    cursor: pointer;
}
.connect-sheet-return:active {
    background: rgba(88, 222, 200, 0.24);
    transform: translateY(1px);
}
.connect-sheet-return-icon {
    flex: 0 0 22px;
    transform: rotate(90deg);
}

.full-sheet-body {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
    padding: 12px;
}
/* The panel's own collapse animation and height cap do not apply in here:
   the sheet *is* the expanded state, and it gets the whole viewport. */
.transcript-sheet-body .transcript-scroll {
    flex: 1 1 auto; min-height: 0;
    max-height: none;
    transition: none;
}
.transcript-sheet-body .transcript-body { padding-bottom: 0; }


/* ═══════════════════════════════════════════════════════════════════════
   MULTI — connect panel and incoming subtitle
   ═══════════════════════════════════════════════════════════════════════ */

/* ExpandableConnectPanel.qml. The QML draws the outline as a separate rect
   stacked above everything (z: 10) so it stays crisp over the header
   gradient; a border on the container plus a clipped header does the same. */
/* The guest-review card (the last survivor of the idle stages D076
   retired — the start paths live on the Speak hub now). */
.multi-idle {
    margin-top: 10px;
    display: flex; flex-direction: column; gap: 12px;
}
.multi-idle[hidden] { display: none; }
.multi-idle .primary-button { width: 100%; }

.page-multi.is-multi-idle .page-column > #multi-language-row {
    margin-top: 0;
    margin-bottom: 2px;
}

/* The one Solo page serves both entrances (D076): `.is-follow` swaps the
   Speak/Listen device pair for the Speaker/Listen-in people pair — and
   takes the swap seam with it, because handing the phone over is a
   turn-based act (the follow swap is chooseLanguage's ordinary edit). */
.page-solo:not(.is-follow) [data-solo-lane="follow"] { display: none; }
.page-solo.is-follow [data-solo-lane="turns"] { display: none; }
/* D231 — the hands-free lane hides both of the others; they hide it. */
.page-solo.is-converse [data-solo-lane="turns"],
.page-solo.is-converse [data-solo-lane="follow"] { display: none; }
.page-solo:not(.is-converse) [data-solo-lane="converse"] { display: none; }
/* D234 — the lane fills the page and the conversation takes whatever
   height is left, so the key row sits on the bottom edge of any phone:
   the feed's own cap (52vh, D231) measured the viewport with the
   browser's bars retracted, ignored the rows around it, and on a phone
   pushed the keys below the fold. The status card leaves the view
   (screen readers keep its live line; the header's microphone is its
   face), and so do the other lanes' header instruments, which never
   light in this one.
   D235 — and the page itself is not a scroll view (Denis: the
   conversation is, the page is not): on a screen too short for the lane
   the conversation gives up height, never the keys, and a phone on its
   side tightens the rows around it (the short-screen block below). */
.page-solo.is-converse .page-scroll { overflow: hidden; }
.page-solo.is-converse .page-column { height: 100%; }
.page-solo.is-converse .mode-head-activity { display: none; }
.page-solo.is-converse #solo-recent { display: none; }
.page-solo.is-converse #solo-status {
    position: absolute; width: 1px; height: 1px; min-height: 0;
    padding: 0; margin: -1px; border: 0;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
}

/* ── Take Turns, hands free (D231) ──
   Two people at the top, the conversation in the middle, one key at the
   bottom. The bubbles borrow the group feed's shape at phone width; a
   Person A bubble sits left in A's tint, a Person B bubble right in B's,
   so a glance says whose words they are without reading the name. */
.converse-desk {
    display: flex; flex-direction: column; gap: 10px;
    flex: 1 1 0; min-height: 0; margin: 6px 0 0;
}
.converse-people {
    display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
}
.converse-person {
    --person-rgb: 255, 111, 145;
    display: flex; align-items: center; gap: 8px; min-width: 0;
}
.converse-person[data-converse-side="b"] { --person-rgb: 88, 222, 200; }
.converse-person .language-picker { flex: 1 1 auto; min-width: 0; }
.converse-feed {
    list-style: none; margin: 0; padding: 10px;
    display: flex; flex-direction: column; gap: 8px;
    flex: 1 1 0; min-height: 0; overflow-y: auto;
    /* D235 — a fling past either end stays in the conversation. */
    overscroll-behavior-y: contain;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-standard, 14px);
    background: rgba(13, 20, 36, 0.45);
}
.converse-empty {
    margin: auto; padding: 18px 8px; text-align: center;
    color: var(--text-secondary); font-size: var(--type-body);
}
/* D234 — the conversation's last line when it ended on its own; alone
   in an empty feed it takes the placeholder's place. */
.converse-sys {
    align-self: center; max-width: 92%;
    padding: 4px 10px; text-align: center;
    color: var(--text-secondary); font-size: var(--type-caption, 12px);
}
.converse-sys.is-error { color: #ffd5df; }
.converse-sys[hidden] { display: none; }
.converse-empty[hidden] + .converse-sys { margin: auto; font-size: var(--type-body); }
.converse-msg {
    --person-rgb: 255, 111, 145;
    position: relative;
    align-self: flex-start;
    max-width: 88%;
    padding: 8px 11px 7px;
    border: 1px solid rgba(var(--person-rgb), 0.28);
    border-radius: 16px 16px 16px 5px;
    background: rgba(var(--person-rgb), 0.08);
    cursor: default;
}
.converse-msg.is-b {
    --person-rgb: 88, 222, 200;
    align-self: flex-end;
    border-radius: 16px 16px 5px 16px;
}
.converse-msg.is-replayable { cursor: pointer; }
.converse-msg.is-playing {
    box-shadow: 0 0 0 2px rgba(var(--person-rgb), 0.55);
    /* D236 — a tap on the message being read skips it. */
    cursor: pointer;
}
/* D234 — a message still being heard belongs to nobody yet: full width
   and grey until the gateway settles whose it is, then it moves to that
   side in that person's tint. The live translation (D233) reads inside
   it. No bubble carries a name: the side and the tint say whose. */
.converse-msg.is-pending {
    --person-rgb: 174, 185, 214;
    align-self: stretch;
    max-width: none;
    border-radius: 14px;
}
.converse-msg-text {
    color: var(--text-primary); font-size: var(--type-body);
    white-space: pre-wrap; overflow-wrap: anywhere;
}
.converse-msg.is-listening .converse-msg-text,
.converse-msg.is-translating .converse-msg-text { color: var(--text-secondary); }
.converse-msg-translation {
    margin-top: 4px; padding-top: 4px;
    border-top: 1px solid rgba(var(--person-rgb), 0.22);
    color: var(--text-primary); font-size: var(--type-body);
    white-space: pre-wrap; overflow-wrap: anywhere;
}
.converse-msg-note {
    margin-top: 3px; font-size: var(--type-caption, 12px);
    color: var(--text-secondary); font-style: italic;
}
.converse-msg.is-lost .converse-msg-note,
.converse-msg.is-untranslated .converse-msg-note { color: #ffb6c2; font-style: normal; }
/* D233 — the translation of the message so far reads as provisional
   until the message ends; the minute's card sits over the conversation,
   in the speaker's tint, large enough to read from across the table. */
.converse-msg-translation.is-partial { color: var(--text-secondary); }
.converse-stage {
    position: relative; min-width: 0;
    flex: 1 1 0; min-height: 0;
    display: flex; flex-direction: column;
}
.converse-wait {
    --person-rgb: 255, 111, 145;
    position: absolute; inset: 10px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 8px; padding: 16px; text-align: center;
    border: 2px solid rgba(var(--person-rgb), 0.75);
    border-radius: var(--radius-standard, 14px);
    background: rgba(13, 20, 36, 0.93);
}
.converse-wait[data-converse-side="b"] { --person-rgb: 88, 222, 200; }
.converse-wait[hidden] { display: none; }
.converse-wait-text {
    color: #fff; font-size: clamp(26px, 7vw, 34px); font-weight: 700;
    line-height: 1.15; overflow-wrap: anywhere;
}
.converse-wait-count {
    color: rgba(var(--person-rgb), 0.95);
    font-size: clamp(32px, 10vw, 48px); font-weight: 700; line-height: 1;
    font-variant-numeric: tabular-nums;
}
.converse-bar {
    display: grid; grid-template-columns: auto 1fr 1.6fr; gap: 10px; align-items: stretch;
}
.converse-pause {
    display: flex; align-items: center; gap: 4px;
    padding: 0 4px;
    border: 1px solid var(--hairline); border-radius: 14px;
    background: rgba(13, 20, 36, 0.55);
}
.converse-pause button {
    width: 34px; height: 34px; padding: 0;
    border: 0; border-radius: 12px; background: transparent;
    color: var(--text-primary); font-size: 20px; line-height: 1;
}
.converse-pause button:disabled { color: var(--text-secondary); opacity: 0.55; }
.converse-pause-value {
    min-width: 44px; text-align: center;
    font-size: var(--type-caption, 12px); color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
.converse-key {
    min-height: 62px; padding: 0 10px;
    display: flex; align-items: center; justify-content: center; gap: 8px;
    color: var(--text-secondary);
    --lift-radius: 14px;
    --lift-g0: #2b354c;
    --lift-g1: #202a3f;
    font-size: var(--type-body); font-weight: 600;
}
.converse-key:disabled { opacity: 0.55; }
.converse-main { color: var(--text-primary); }
.converse-main[data-converse-action="done"],
.converse-main[data-converse-action="start"] {
    --lift-g0: #ff6f91; --lift-g1: #d9466e; color: #fff;
}
/* D234 — Done is pink only while there is a message to end (somebody
   talking, the microphone open); otherwise it wears the grey of the keys
   beside it, so the pink key is always the one worth tapping. */
.converse-main[data-converse-action="done"]:disabled {
    --lift-g0: #2b354c; --lift-g1: #202a3f;
    color: var(--text-disabled); opacity: 1;
}
/* D236 — Skip while the phone reads aloud, in the blue of its voice
   (the screen's edge and the header's ring wear it too). */
.converse-main[data-converse-action="skip"] {
    --lift-g0: #7ac9ff; --lift-g1: #4aa3e0; color: #0b1826;
}
.converse-mute[aria-pressed="true"] { color: #ffb6c2; }
.converse-key-verb { white-space: nowrap; }
.converse-key-icon { display: inline-flex; }
.converse-key-icon canvas { display: block; }
/* D235 — a screen this short (a phone on its side) keeps the whole lane
   in view without a page scroll: the page's margins, the header's gap,
   the pickers and the keys tighten so the conversation keeps a strip
   between them, and the minute's card lies along the conversation
   instead of standing up in it. */
@media (max-height: 500px) {
    .page-solo.is-converse .page-column {
        padding-top: 8px; padding-bottom: calc(8px + var(--safe-bottom));
    }
    .page-solo.is-converse .mode-head { margin-bottom: 4px; }
    .converse-desk { margin-top: 0; gap: 8px; }
    .converse-person .language-picker { height: 44px; }
    .converse-key { min-height: 48px; }
    .converse-wait { inset: 0; flex-direction: row; gap: 14px; padding: 4px 14px; }
    .converse-wait-text { font-size: 22px; }
    .converse-wait-count { font-size: 28px; }
}
/* D236 — a narrow phone keeps the whole key row on the screen, whatever
   its words (Korean's Skip is four syllables; a muted Mute says two
   words in Korean): the pause control, the keys' padding and the row's
   gaps tighten, and Mute's label may take two lines. The −/+ keep their
   34 px. */
@media (max-width: 374px) {
    .converse-bar { gap: 6px; }
    .converse-pause { padding: 0 2px; gap: 2px; }
    .converse-pause-value { min-width: 32px; }
    .converse-key { padding: 0 6px; gap: 5px; }
    .converse-mute .converse-key-verb {
        white-space: normal; word-break: keep-all; text-align: center;
        line-height: 1.15;
    }
}
/* D234 — the lane's microphone, in the header's centre: a disc in the
   header buttons' idiom holding one glyph. Pink with two rings rippling
   out while the phone listens, and a glow that swells with the voice it
   hears (`--mic-level`, from the level feed) — nothing else on the row
   moves, so motion there means an open microphone. Grey in a blue ring
   while the phone talks and the microphone is paused; amber and struck
   through when muted; struck through in pink when the microphone or the
   gateway ended the conversation. A tap mutes and unmutes. */
.converse-mic {
    --mic-rgb: 135, 148, 175;
    --mic-level: 0;
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: 48px; height: 48px;
    padding: 0; border: 0; background: transparent;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.converse-mic:disabled { cursor: default; }
.converse-mic > span {
    position: absolute; inset: 3px;
    border-radius: 50%;
    pointer-events: none;
}
.converse-mic-disc {
    display: grid; place-items: center;
    background: rgba(13, 20, 36, 0.55);
    border: 1px solid var(--hairline, #2a3550);
    transition: border-color var(--hover-duration) var(--ease-out),
                transform 120ms var(--ease-out);
}
.converse-mic:active:not(:disabled) .converse-mic-disc { transform: scale(0.94); }
.converse-mic:focus-visible { outline: none; }
.converse-mic:focus-visible .converse-mic-disc {
    outline: 2px solid var(--accent-utility); outline-offset: 2px;
}
.converse-mic-ring { opacity: 0; border: 2px solid rgba(var(--mic-rgb), 0.65); }
.converse-mic-glow {
    background: radial-gradient(closest-side, rgba(var(--mic-rgb), 0.6),
                                rgba(var(--mic-rgb), 0));
    opacity: calc(var(--mic-level) * 1.2);
    transform: scale(calc(1 + var(--mic-level) * 0.6));
    transition: opacity 100ms linear, transform 100ms linear;
}
.converse-mic[data-converse-mic="listening"] { --mic-rgb: 255, 111, 145; }
.converse-mic[data-converse-mic="listening"] .converse-mic-disc,
.converse-mic[data-converse-mic="error"] .converse-mic-disc {
    border-color: rgba(255, 111, 145, 0.75);
}
.converse-mic[data-converse-mic="listening"] .converse-mic-ring {
    animation: converse-mic-ripple 2.4s ease-out infinite;
}
.converse-mic[data-converse-mic="listening"] .converse-mic-ring + .converse-mic-ring {
    animation-delay: 1.2s;
}
@keyframes converse-mic-ripple {
    from { transform: scale(1); opacity: 0.85; }
    to   { transform: scale(1.5); opacity: 0; }
}
.converse-mic[data-converse-mic="connecting"] canvas {
    animation: converse-mic-breathe 1.4s ease-in-out infinite alternate;
}
@keyframes converse-mic-breathe { from { opacity: 0.35; } to { opacity: 1; } }
.converse-mic[data-converse-mic="paused"] .converse-mic-disc {
    border-color: rgba(122, 201, 255, 0.8);
}
.converse-mic[data-converse-mic="muted"] .converse-mic-disc {
    border-color: rgba(255, 179, 107, 0.8);
}
/* D236 — while the phone reads a translation aloud its microphone is
   closed, so a word said over the voice is never heard: the edge of the
   whole screen glows in the voice's blue until the microphone opens
   again, on whichever tab is showing. It is the Service Counter's page
   edge (the frame, the guest turn's weight and slow pulse) in the colour
   the Counter gives reading aloud; the Counter's green says "speak now",
   the opposite. It takes no tap: Skip is the key, and the bubble being
   read.

   D237 — a 2-way call wears the same edge while the other person's
   translation plays and the microphone is ducked under it (only while
   the microphone is live: a muted one has nothing to lose). */
#workspace.is-duo-voice::after,
#workspace.is-converse-voice::after {
    content: "";
    position: absolute;
    z-index: 30;
    inset: max(3px, var(--safe-top)) max(3px, var(--safe-right))
           max(3px, var(--safe-bottom)) max(3px, var(--safe-left));
    border: 5px solid #7ac9ff;
    border-radius: 16px;
    box-shadow: inset 0 0 28px rgba(122, 201, 255, 0.32),
                0 0 18px rgba(122, 201, 255, 0.5);
    pointer-events: none;
    animation: converse-voice-glow 1500ms ease-in-out infinite alternate;
}
@keyframes converse-voice-glow {
    from { opacity: 0.66; box-shadow: inset 0 0 18px rgba(122, 201, 255, 0.24), 0 0 10px rgba(122, 201, 255, 0.36); }
    to { opacity: 1; box-shadow: inset 0 0 34px rgba(122, 201, 255, 0.42), 0 0 22px rgba(122, 201, 255, 0.58); }
}
@media (prefers-reduced-motion: reduce) {
    #workspace.is-converse-voice::after { animation: none; }
}
@media (forced-colors: active) {
    #workspace.is-converse-voice::after { border-color: Highlight; box-shadow: none; }
}
/* D237: the 2-way edge keeps both promises too. */
@media (prefers-reduced-motion: reduce) {
    #workspace.is-duo-voice::after { animation: none; }
}
@media (forced-colors: active) {
    #workspace.is-duo-voice::after { border-color: Highlight; box-shadow: none; }
}

.connect-panel[hidden] { display: none; }
.connect-panel {
    position: relative;
    margin-top: 4px;
    border-radius: 18px;
    background: transparent;
    border: 1px solid rgba(92, 113, 141, 0.667);   /* #5c718daa */
    overflow: hidden;
}
.connect-panel.is-open {
    border-width: 1.5px;
    border-color: rgba(93, 214, 192, 0.545);       /* #8b5dd6c0 */
}

/* The header is the standard raised surface, but with its own two gradients:
   a blue-grey one when collapsed and a teal one when open, plus the 1 px top
   highlight inset 28 px, and (open only) an 18 px flat strip along the bottom
   so the header squares off where the body begins. */
/* The gradient is on the row, not on the toggle: the End button shares the
   header now, and a background that stopped where the toggle stops would cut
   the raised surface off two thirds of the way across. */
.connect-head {
    display: flex; align-items: center;
    height: 54px;
    border-radius: 18px;
    background-repeat: no-repeat;
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.227), rgba(255, 255, 255, 0.227)),
        linear-gradient(180deg, #35445f 0%, #253149 50%, #161d30 100%);
    background-position: center 1px, 0 0;
    background-size: calc(100% - 28px) 1px, 100% 100%;
}
.connect-panel.is-open .connect-head {
    background-image:
        linear-gradient(rgba(255, 255, 255, 0.396), rgba(255, 255, 255, 0.396)),
        linear-gradient(#142c2a, #142c2a),
        linear-gradient(180deg, #355c58 0%, #254641 50%, #142c2a 100%);
    background-position: center 1px, 0 100%, 0 0;
    background-size: calc(100% - 28px) 1px, 100% 18px, 100% 100%;
}
.connect-toggle {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: center; gap: 11px;
    height: 100%;
    padding: 0 8px 0 var(--panel-padding);
    border: 0; cursor: pointer;
    background: none;
}
/* `pressOffset`: the content translates, the background does not. */
.connect-toggle:active > * { transform: translateY(3px); }
.connect-toggle > * { transition: transform 90ms var(--ease-out); }

/* Reads as a pill so it is unmistakably the action, not a third status word
   sitting beside the two above it. */
.connect-cta {
    flex: 0 0 auto;
    padding: 5px 11px;
    border-radius: 999px;
    border: 1px solid rgba(88, 222, 200, 0.45);
    background: rgba(88, 222, 200, 0.13);
    color: var(--accent-multi);
    font-size: 13px; font-weight: 700; white-space: nowrap;
}
.connect-cta[hidden] { display: none; }
.connect-panel.is-open .connect-cta { opacity: 0; }

/* A guest's header: it still names the room and still carries the way out,
   it just does not open. Losing the press animation and the chevron is what
   stops it looking like it should. */
.connect-toggle.is-static { cursor: default; }
.connect-toggle.is-static:active > * { transform: none; }
.connect-toggle.is-static .connect-chevron { visibility: hidden; }

/* The one destructive control in the header, so it is the one thing in it
   that is not on the disclosure's press animation — a glyph that sinks with
   the row beside it reads as part of the row.

   It used to be grey until pressed, on the argument that an always-red × on
   a live session reads as an error state. It does not: red here is the same
   thing it is on the microphone button two rows down — this ends something —
   and grey made the only way out of a conversation the quietest mark in the
   panel. It is now the header's one coloured glyph, on its own tinted
   ground, at 38 px rather than 26. */
.connect-end {
    flex: 0 0 auto;
    width: 52px; height: 44px;
    margin-right: 8px;
    display: flex; align-items: center; justify-content: center;
    /* A chip inset from the edge, where there used to be a hairline rule
       against the panel's own corner. The rule was there to say where the row
       stops being a disclosure; a tinted, separated shape says it louder, and
       it does not have to be talked out of the 18 px radius the head curves
       away with. */
    border: 1px solid rgba(255, 255, 255, 0.48); border-radius: 14px;
    background: linear-gradient(180deg, #ef5c80 0%, #c52d55 100%);
    box-shadow: 0 4px 0 #761a36, inset 0 1px rgba(255, 255, 255, 0.3);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: transform var(--press-duration) var(--ease-out),
                filter var(--state-duration) var(--ease-out);
}
.connect-end canvas { display: block; }
.connect-end:active {
    transform: translateY(3px);
    box-shadow: 0 1px 0 #761a36, inset 0 1px rgba(255, 255, 255, 0.2);
}
.connect-end:focus-visible { outline: 2px solid #fff2f5; outline-offset: 2px; }
.connect-end[hidden] { display: none; }

.connect-dish { flex: 0 0 auto; }
.connect-heading {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
}
.connect-title { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.connect-status {
    font-size: 11px; color: var(--text-secondary);
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.connect-chevron {
    flex: 0 0 auto;
    transition: transform 170ms var(--ease-out);
}
.connect-panel.is-open .connect-chevron { transform: rotate(180deg); }

.connect-body {
    max-height: 0; overflow: hidden; opacity: 0;
    transition: max-height 220ms var(--ease-out), opacity 150ms linear;
}
.connect-panel.is-open .connect-body { max-height: 620px; opacity: 1; }
/* Inside the sheet the panel is no longer an ancestor, so the `is-open` rule
   above cannot reach the body and it would sit collapsed at zero height. The
   sheet *is* the open state: no cap, no fade, no animation to sit through. */
.connect-sheet-body .connect-body {
    max-height: none; opacity: 1; overflow-y: auto;
    transition: none;
}
.connect-sheet { --transcript-accent: var(--accent-multi); }
/* The QR wants the room the sheet just bought it, and the way out belongs at
   the bottom of the scroll rather than floating over it. */
.connect-sheet-body .connect-waiting,
.connect-sheet-body .connect-live { gap: 16px; }
.connect-sheet-body .qr-card { width: min(320px, 74vw); margin: 0 auto; }
.connect-sheet-body .connect-copy { font-size: 12px; }
/* Ending the session is not the next step after Copy link; it is the opposite
   of everything above it. Inline, the panel border did the separating. */
.connect-sheet-body .connect-body > .primary-button {
    margin-top: 20px;
    border-top: 0;
}

.connect-waiting, .connect-live {
    padding: 12px;
    display: flex; flex-direction: column; gap: 12px;
}
.connect-copy { margin: 0; font-size: 11px; color: var(--text-secondary); }
.connect-copy.center { text-align: center; }

/* `sessionActionButton`: a flat teal fill with a bright border, not a
   gradient — it darkens rather than scales on press. */
.primary-button {
    height: 46px; border-radius: 14px;
    background: #184f48;
    border: 1px solid rgba(88, 222, 200, 0.533);   /* #8858dec8 */
    color: var(--text-primary); font-size: 15px; font-weight: 700;
    cursor: pointer;
    transition: background-color var(--press-duration) var(--ease-out);
}
.primary-button:active { background: #1f6f64; }
.primary-button:disabled { background: #1a2230; cursor: default; }
/* Hosting flips it to the destructive "End conversation" colours. */
/* Scoped to the body rather than the panel: it is the body that carries the
   button, and the body is re-parented into the sheet below 600 px. */
.connect-body.is-live .primary-button,
.connect-body.is-waiting .primary-button {
    background: #4a2438;
    border-color: rgba(255, 111, 145, 0.533);      /* #88ff6f91 */
}
.connect-body.is-live .primary-button:active,
.connect-body.is-waiting .primary-button:active { background: #6a3048; }

/* `showInviteQrButton`: outlined and secondary — it re-opens the invite
   after a guest leaves, so it must not compete with the session action. */
.ghost-button {
    width: 100%;
    height: 44px; border-radius: 12px;
    background: transparent;
    border: 1px solid #3a4a63;
    color: var(--text-secondary); font-size: 13px;
    cursor: pointer;
}
.ghost-button:active { background: #1a2230; }

/* The two Multi recovery buttons. Not ghosts: they appear only when
   something is broken, and the affordance has to read as the way out rather
   than as another secondary option. */
.recovery-button {
    width: 100%;
    height: 44px; border-radius: 12px;
    margin-top: 2px;
    background: #1a2a36;
    border: 1px solid rgba(88, 222, 200, 0.29);
    color: var(--text-primary); font-size: 14px; font-weight: 700;
    cursor: pointer;
}
.recovery-button:active { background: #2a3d4a; }
.recovery-button[hidden] { display: none; }

.invite-row { display: flex; gap: 8px; }

/* InviteButton. Share carries the accent on a phone; where no share sheet
   exists, Copy inherits it instead — hence `.is-primary` rather than a rule
   keyed on position. */
.invite-button {
    flex: 1 1 0; min-width: 0;
    height: 44px; border-radius: 12px;
    background: transparent;
    border: 1px solid #3a4a63;
    color: var(--text-secondary); font-size: 13px; font-weight: 700;
    cursor: pointer;
}
.invite-button:active { background: #1a2230; }
.invite-button.is-primary {
    background: #184f48;
    border-color: rgba(88, 222, 200, 0.533);
    color: var(--text-primary);
}
.invite-button.is-primary:active { background: #1f6f64; }

/* The QR is a button because tapping it enlarges it. `cardSize` in the QML
   is clamped to [200, 288] and follows the panel width. */
.qr-card {
    align-self: center;
    width: clamp(200px, 100%, 288px); aspect-ratio: 1;
    padding: 15px;                       /* the code's quiet zone */
    border: 0; border-radius: 12px;
    background: #ffffff;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.qr-card canvas { display: block; width: 100%; height: 100%; }
.qr-card:active { filter: brightness(0.94); }

/* The affordance that replaced "Tap the code to enlarge it for scanning."
   Four viewfinder brackets — the one shape both platforms' camera apps use
   for this exact gesture, so it needs no sentence.

   Outside the white card, not inside it: the card's 15 px padding is the
   code's quiet zone, and it is already tighter than the four-module
   recommendation. Marks in there would be bought with scan reliability.
   The negative inset stays well within .connect-waiting's own 12 px, so
   .connect-body's overflow never clips it. Eight gradients, two per
   corner — one element, and no support surprises on old Safari. */
.qr-card { position: relative; }
.qr-card::after {
    content: "";
    position: absolute; inset: -7px;
    pointer-events: none;
    --bracket: rgba(88, 222, 200, 0.62);
    background-repeat: no-repeat;
    background-image:
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket));
    background-size: 16px 2px, 2px 16px, 16px 2px, 2px 16px,
                     16px 2px, 2px 16px, 16px 2px, 2px 16px;
    background-position:
        left top, left top, right top, right top,
        left bottom, left bottom, right bottom, right bottom;
    transition: opacity var(--press-duration) var(--ease-out);
}
.qr-card:active::after { opacity: 0.45; }

.connect-expiry {
    margin: 0 12px 12px;
    text-align: center;
    font-size: 11px; color: var(--text-secondary);
}

/* The expiry line and the session action sit below the state blocks rather
   than inside one, because both survive the waiting → connected switch. They
   are the only direct children of the body, so they carry their own gutter. */
.connect-body > .primary-button {
    display: block;
    width: calc(100% - 24px);
    margin: 0 12px 12px;
}

/* ── The connect header's second row ──────────────────────────────────────
   The peer used to be the first thing inside the panel body, which below
   600 px means the first thing inside a full-screen sheet — and the sheet now
   closes itself the moment the guest arrives, so that is the one moment the
   row was guaranteed not to be on screen. As a header row it survives the
   collapse, which is the state a conversation is actually held in.

   It continues the header's raised surface rather than sitting on the page:
   two rows of one block, so the panel grows downward instead of the header
   appearing to sprout a card. Both gradients have an open variant for the
   same reason the head does — teal while the body is out. */
/* ═══ The peer half of the language row (D164) ══════════════════
   It used to be the second row of the connect panel, with that panel's
   raised gradient and a hairline joining it to the head above. The panel is
   gone from the call screen and this came down to sit beside the picker, so
   it drops the seam and the gradient and becomes what it is: the right-hand
   end of one row. No background of its own — the picker beside it is the
   lit control, and a second filled block on the same row would compete with
   it for the tap. */
.peer-row {
    flex: 0 1 auto;
    display: flex; align-items: center; gap: 9px;
    min-width: 0;
    height: 52px;
    padding-left: 2px;
}
.peer-row[hidden] { display: none; }
.peer-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.peer-name {
    min-width: 0;
    font-size: 14px; font-weight: 700;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Amber rather than the session teal, and deliberately not red: a room that
   passes audio through is working exactly as designed and faster than a
   translated one — it is just not doing the thing the app is for, which is
   worth noticing and is nobody's error. */
.peer-badge {
    position: relative;
    flex: 0 0 auto;
    padding: 2px 8px 3px;
    border-radius: 999px;
    border: 1px solid rgba(230, 176, 90, 0.5);
    background: rgba(230, 176, 90, 0.12);
    font-size: 10px; font-weight: 700; letter-spacing: 0.02em;
    color: #e8bd76;
    white-space: nowrap;
    /* It is a <button> now, so it has to stop inheriting one. */
    font-family: inherit; line-height: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.peer-badge::after {
    content: "";
    position: absolute;
    inset: -11px -6px;
    border-radius: 999px;
}
.peer-badge:active { background: rgba(230, 176, 90, 0.24); }
.peer-badge[hidden] { display: none; }
/* Its own line under the row it is about. Inline it had to fit between a
   name and a language on a phone, which is how a warning ends up two
   characters wide and read by nobody. */
.language-row > .peer-badge {
    flex: 1 0 100%;
    align-self: flex-start;
    text-align: center;
    padding-block: 5px 6px;
    font-size: 11px;
}
/* The second line, and only one of the two candidates is ever drawn (see
   renderConnectPanel). Two lines before eliding: at 390 px "Audio connected
   · ready to speak · speaks Korean" truncated the peer's language — the one
   fact the host actually needs from this line. */
.peer-presence {
    font-size: 11px; color: var(--text-secondary);
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden;
}
.peer-presence[hidden] { display: none; }

/* ═══ One row, two speakers (D164) ═════════════════════════
   Once somebody is on the other end, the row that says what you speak also
   says who you are speaking to and what they speak. `align-items: center`
   rather than the row's usual `flex-start`, because the two halves are now
   the same kind of thing and should sit on one line; and the row wraps so
   the passthrough badge can break underneath the pair it is about instead of
   stealing width from a name.

   The words "I speak" go with the peer's arrival. The microphone glyph
   beside the picker already says whose language it is, and on a 390 px phone
   those two words were being paid for out of somebody's name. */
.language-row.has-peer {
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}
.language-row.has-peer .role-label {
    flex: 0 0 auto;
    width: auto;
    min-width: 0;
    height: 52px;
}
.language-row.has-peer .role-label span { display: none; }
.language-row.has-peer .language-picker {
    flex: 1 1 108px;
    min-width: 96px;
    padding-left: 12px;
    padding-right: 10px;
}
.language-row.has-peer .picker-value { font-size: 14px; }
/* The unhealthy states, where the peer's second line is the presence
   sentence rather than one word of language: it takes a line of its own
   under the picker instead of wrapping to four beside it. */
.language-row.has-peer.peer-line .peer-row {
    flex: 1 0 100%;
    height: auto;
    padding-block: 2px;
}

/* ── The full-screen QR (MOBILE_UX §3.3) ── */
/* 65, above the sheets at 60: the QR card that opens this now lives inside
   the connect sheet, and at 40 the enlarged code opened behind the sheet that
   asked for it. Still below the confirm dialog at 70. */
.qr-overlay { position: fixed; inset: 0; z-index: 65; }
.qr-overlay-scrim { position: absolute; inset: 0; background: rgba(5, 9, 22, 0.85); }
.qr-overlay-body {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 14px;
    padding: var(--page-padding);
}
.qr-overlay-body .qr-card { width: min(360px, 78vw); }
.qr-overlay-body .invite-row { width: min(360px, 78vw); }

/* PresenceAvatar.qml. The disc, the initials and the status ring are all
   painted by js/avatar.js, which also sets width and height — the ring pad
   is part of the item's size, so CSS must not fix it. The font-family here
   is not decoration: the painter reads it back to letter the initials. */
.presence-avatar {
    flex: 0 0 auto;
    font-family: var(--font-stack);
}



/* ═══════════════════════════════════════════════════════════════════════
   SETTINGS  (qml/SettingsPage.qml)

   No page of its own since D106 — the app-language switch and the
   diagnostics drawer are the last of it, and both now sit at the bottom of
   the Account column. What survives here is what they are made of.
   ═══════════════════════════════════════════════════════════════════════ */

/* Flowed, not stretched: the Account column scrolls as one page, so the
   drawer is as tall as what is open inside it — collapsed, it is a heading
   row and nothing else. (The old page gave this section the leftover height
   and a 144 px floor, which here would have been a hole under the toggle.) */
.settings-diagnostics {
    display: flex; flex-direction: column; gap: 10px;
}
.settings-diagnostics[hidden] { display: none; }
.section-title { margin: 0; font-size: 21px; font-weight: 700; }
.section-title.spaced { margin-top: 14px; }
/* A heading with its own actions on the end of it. `align-items: end` rather
   than center: the buttons carry a 5 px offset face, so their optical bottom
   is 5 px above their box, and centring on the box leaves them sitting high
   against the heading's baseline. */
.section-head {
    display: flex; align-items: end; gap: 10px;
    min-height: 34px;
}
.section-head.spaced { margin-top: 14px; }
.section-head .section-title { flex: 1 1 auto; min-width: 0; }
.section-copy { margin: 0; font-size: var(--type-meta); color: var(--text-secondary); }
/* Copy that follows a toggle rather than a heading needs the gap the column
   does not give it: the toggle's own 5 px offset shadow sits inside its box. */
.section-copy.spaced { margin-top: 9px; }
.settings-toggle.spaced { margin-top: 16px; }

/* The disclosure that turns the diagnostics dump back into furniture. */
.diagnostics-toggle {
    flex: 1 1 auto; min-width: 0;
    display: flex; align-items: center; gap: 8px;
    border: 0; background: none; padding: 0; cursor: pointer;
    text-align: left;
}
.diagnostics-chevron {
    flex: 0 0 auto;
    transition: transform var(--state-duration) var(--ease-out);
}
.diagnostics-toggle.is-open .diagnostics-chevron { transform: rotate(180deg); }

.settings-toggle, .settings-button {
    position: relative;
    border: 0; background: none; padding: 0; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.settings-toggle { height: 55px; margin-top: 5px; }
.settings-toggle-face, .settings-button-face {
    position: absolute; left: 0; right: 0; top: 0;
    height: calc(100% - 5px);
    border-radius: 16px;
    background: #24203a;
    border: 1px solid var(--accent-settings);
    display: flex; align-items: center;
    transition: transform var(--press-duration) var(--ease-out);
}
.settings-toggle:active .settings-toggle-face { transform: translateY(var(--press-offset)); }
.settings-toggle-label {
    flex: 1; text-align: left; padding-left: 16px;
    font-size: 14px; font-weight: 700;
}
.settings-pill {
    margin-right: 16px;
    width: 64px; height: 28px; border-radius: 14px;
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: 700;
    background: #33415e; color: var(--text-secondary);
    transition: background var(--state-duration) var(--ease-out);
}
.settings-toggle.is-on .settings-pill { background: var(--accent-settings); color: #111526; }

/* A definite height, because the frame's contents are all absolute and a
   flowed column would otherwise give it none. The log scrolls inside it;
   `dvh` keeps a landscape phone from handing the whole viewport to a
   support surface. */
.log-frame {
    position: relative; flex: 0 0 auto;
    height: min(320px, 46dvh); min-height: 140px;
    margin-top: 5px;
}
.log-offset {
    position: absolute; inset: 5px 0 0 0;
    border-radius: 20px; background: #080a13;
}
.log-face {
    position: absolute; left: 0; right: 0; top: 0;
    height: calc(100% - 5px);
    border-radius: 20px;
    background: #111526;
    border: 1px solid #59477f;
    overflow: hidden;
}
.log-highlight {
    position: absolute; top: 1px; left: 50%;
    width: calc(100% - 30px); height: 1px;
    transform: translateX(-50%);
    background: rgba(255, 255, 255, 0.27);
}
.log-text {
    position: absolute; inset: 12px;
    margin: 0; padding: 4px;
    overflow: auto;
    font-family: var(--font-mono); font-size: 11px; line-height: 1.45;
    color: #dce5ff;
    white-space: pre-wrap; word-break: break-all;
}

/* On the heading row now, so there is no width to fill and no top margin to
   clear. Small enough to read as a tool rather than a decision, and sized by
   its own word rather than by the row.

   The face leaves the absolute positioning it shares with the toggle: an
   absolutely positioned child gives the button no intrinsic width at all,
   which is why the full-width pair could get away with it and these two
   cannot. So here the face flows, and the 5 px the press moves down into
   becomes the button's own bottom padding.

   28 px of visible face is under the 44 px target minimum, deliberately and
   on the record in MOBILE_UX §9.1: they sit alone at the end of a heading
   with nothing else within reach to mis-hit, and a pair at full size is what
   made a diagnostics dump look like the point of the screen. */
.log-actions { flex: 0 0 auto; display: flex; gap: 8px; }
.settings-button { flex: 0 0 auto; padding-bottom: 5px; }
.settings-button-face {
    position: static;
    height: 28px;
    padding: 0 12px;
    border-radius: 10px;
    justify-content: center;
    white-space: nowrap;
    font-size: 12px; font-weight: 700;
}
.settings-button:active .settings-button-face { transform: translateY(var(--press-offset)); }
.settings-button:disabled { cursor: default; }
.settings-button:disabled .settings-button-face { opacity: 0.45; color: #687189; }


/* ═══════════════════════════════════════════════════════════════════════
   ACCOUNT  (qml/AccountPage.qml)
   ═══════════════════════════════════════════════════════════════════════ */

.account-column {
    width: min(560px, calc(100% - var(--page-gutter)));
    margin: 0 auto;
    padding: var(--page-padding) 0 calc(var(--page-padding) + var(--safe-bottom));
    display: flex; flex-direction: column; gap: 12px;
}

/* A heading row landing among cards needs air on both sides, not only
   above: the settings column stacked headings with rows, the Account column
   stacks them with 56 px buttons and bordered cards, and 12 px of column gap
   under "App language" put the flags on the lip of Log out. */
.account-column .section-head.spaced { margin-top: 18px; margin-bottom: 6px; }

.persona-card {
    display: flex; align-items: center; gap: 10px;
    min-height: 76px; padding: 14px;
    border-radius: var(--radius-card);
    background: #1a1520;
    border: 1px solid rgba(255, 179, 107, 0.29);   /* #4affb36b */
}
/* DisplayNameField.qml. Typing is a draft: the store is not written until
   the check is pressed (or Enter), and the × puts the old name back. The
   apply/revert pair sits inside the field's right padding. */
.name-draft { position: relative; flex: 1 1 auto; min-width: 0; }
.name-field {
    width: 100%; height: 48px;
    border-radius: var(--radius-compact);
    border: 1px solid #33415f;
    background: #0d1420;
    color: var(--text-primary);
    /* 16px like the Chat composer (D093): under that, iOS Safari zooms
       the page the moment the field is tapped — and the guest name field
       is the first tap of the join ceremony. */
    font-size: 16px;
    padding: 0 14px;
    outline: none;
}
.persona-card .name-field { border-color: #3a3144; background: #12101a; }
.name-draft.is-editing .name-field { padding-right: 92px; }
.name-field:focus { border-color: var(--draft-accent, var(--accent-multi)); }
.persona-card .name-field:focus { border-color: var(--accent-account); }

.draft-controls {
    position: absolute; right: 5px; top: 50%;
    transform: translateY(-50%);
    display: flex; gap: 4px;
}
.draft-apply, .draft-revert {
    width: 36px; height: 36px;
    border-radius: 10px;
    font-size: 18px; font-weight: 700;
    cursor: pointer;
}
.draft-apply {
    background: #1d3229; color: #8ce8b0;
    border: 1px solid rgba(140, 232, 176, 0.29);
}
.draft-apply:active { background: #28453a; }
.draft-revert {
    background: #321f24; color: #ff9d9d; font-size: 20px;
    border: 1px solid rgba(255, 157, 157, 0.29);
}
.draft-revert:active { background: #45282e; }
.avatar-reroll {
    flex: 0 0 auto;
    width: 48px; height: 48px;
    border-radius: var(--radius-compact);
    background: #2a2433;
    border: 1px solid rgba(255, 179, 107, 0.29);
    color: var(--text-primary); font-size: 22px;
    cursor: pointer;
    transition: transform 90ms var(--ease-out);
}
.avatar-reroll:active { transform: scale(0.94); background: #3a3144; }
/* Same rule the join card carries: while a name draft is pending the reroll
   stands down so the field — which has just given 92 px of its right edge to
   the apply/revert pair — keeps its width. */
.persona-card.is-drafting .avatar-reroll { display: none; }
.persona-card[hidden] { display: none; }

/* ── The same card, on the 2-way invite (D163) ──
   The Account page owns identity, so the card there wears the account
   orange. Here it is one row of a teal screen, and a lone orange-bordered
   tile in the middle of the invite reads as a warning rather than as
   "this is you". Only the accents move; the geometry is shared, so the
   two cards stay recognisably the same control. */
.persona-card.is-duo {
    background: var(--surface);
    border-color: var(--hairline);
    margin-bottom: 12px;
}
.persona-card.is-duo .name-field { border-color: var(--hairline); background: #0d1420; }
.persona-card.is-duo .name-field:focus { border-color: var(--accent-multi); }
.persona-card.is-duo .avatar-reroll {
    background: #1b2b3a;
    border-color: rgba(88, 222, 200, 0.29);       /* #4a58dec8 */
}
.persona-card.is-duo .avatar-reroll:active { background: #243b4d; }

.account-card {
    padding: 14px;
    border-radius: var(--radius-standard);
    background: #161520;
    border: 1px solid #34415b;
    display: flex; flex-direction: column; gap: 5px;
}
.account-name { font-size: var(--type-value); font-weight: 700; }
.account-credits { font-size: 14px; font-weight: 700; color: var(--accent-account); }
/* Between the credits it is spent before and the licence summary it belongs
   to: lighter than the balance, because the shared wallet is the number that
   runs out for good, but not as quiet as the summary metadata. */
.account-daily { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.account-summary { font-size: var(--type-meta); color: var(--text-secondary); }

/* The review state's sign-in card shares the account card's skin; its button
   needs its own rhythm. */
#guest-signin-card .primary-button { width: 100%; margin-top: 6px; }

.logout-button {
    height: 56px; border: 0; border-radius: var(--radius-standard);
    background: linear-gradient(90deg, #ffb36b 0%, #ff758d 100%);
    color: #1c0f08; font-size: var(--type-body); font-weight: 700;
    cursor: pointer;
    transition: transform 120ms var(--ease-out);
}
.logout-button:active { transform: scale(0.985); }
.logout-button:disabled { opacity: 0.45; color: #776b67; cursor: default; }


/* ═══════════════════════════════════════════════════════════════════════
   NOTIFICATION STACK  (qml/NotificationStack.qml)
   ═══════════════════════════════════════════════════════════════════════ */

.notification-stack {
    /* The home-indicator inset: without it the bottom card sits half under
       the gesture bar on a notched phone. */
    position: fixed; left: 50%; bottom: calc(12px + var(--safe-bottom));
    transform: translateX(-50%);
    width: min(440px, calc(100% - 24px));
    display: flex; flex-direction: column; gap: 8px;
    /* Above the content sheets (sheet-layer 40, full-sheet 60): on a phone
       the transcript sheet is the reading posture, and the warnings this
       stack carries fired invisibly under it. Below the QR overlay (65) and
       everything modal above it (help 66/67, guest-exit 68, dialogs 70) —
       a toast must not paint over a surface that demands an answer. */
    z-index: 64;
    pointer-events: none;
}
/* The stack itself ignores pointer events so it cannot block the controls
   underneath; each card takes them back, because each card has a button. */
.notification {
    position: relative;
    display: flex; align-items: center;
    min-height: 48px;
    padding: 9px 0 9px 16px;
    border-radius: var(--radius-compact);
    background: rgba(16, 22, 40, 0.94);
    border: 1px solid var(--hairline);
    font-size: 12px;
    color: var(--text-primary);
    box-shadow: var(--shadow-2);
    animation: notify-in 200ms var(--ease-out);
    pointer-events: auto;
    overflow: hidden;
}
/* The QML's 4 px accent rail down the left edge. */
.notification-bar {
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 4px; border-radius: 2px;
    background: var(--accent-utility);
}
.notification-text {
    flex: 1 1 auto; min-width: 0; margin: 0;
    /* Three lines, then elide: a notification is a headline, and one that
       grows to eight lines covers the controls it is describing. */
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
    overflow: hidden;
}
.notification-dismiss {
    flex: 0 0 44px;
    align-self: stretch;
    background: none; border: 0; cursor: pointer;
    font-size: 20px; line-height: 1;
    color: #b7c2d9;
}
.notification-dismiss:active { color: inherit; }
.notification.is-warning { border-color: rgba(255, 179, 107, 0.55); color: #ffd09b; }
.notification.is-warning .notification-bar { background: #ffd28a; }
.notification.is-error   { border-color: rgba(255, 113, 140, 0.6);  color: #ffb6c2; }
.notification.is-error .notification-bar { background: #ff7897; }
.notification.is-leaving { animation: notify-out 180ms var(--ease-out) forwards; }
@keyframes notify-in  { from { opacity: 0; transform: translateY(10px); } }
@keyframes notify-out { to   { opacity: 0; transform: translateY(6px); } }


/* ═══════════════════════════════════════════════════════════════════════
   GUEST COACH MARKS  (js/coach.js)

   Between the notification stack (20) and the sheets (40): a coach mark
   outranks a passing notice, and any sheet outranks a coach mark.
   ═══════════════════════════════════════════════════════════════════════ */

.coach-layer { position: fixed; inset: 0; z-index: 35; pointer-events: none; }

/* `position: fixed` inside the fixed layer, so the coordinates js/coach.js
   writes are the same viewport coordinates getBoundingClientRect returns. */
.coach-bubble {
    position: fixed;
    width: min(300px, calc(100vw - 24px));
    padding: 12px 16px 0;
    border-radius: var(--radius-compact);
    background: rgba(18, 26, 46, 0.97);
    border: 1px solid rgba(88, 222, 200, 0.45);
    box-shadow: var(--shadow-3);
    /* Never takes a tap. Dismissal is a document listener that lets the tap
       reach the control underneath — see the comment in index.html. */
    pointer-events: none;
}
/* Scrolled off its own control: fade rather than tear down, so scrolling
   back brings the control and its explanation back together. */
.coach-bubble.is-orphaned { opacity: 0; }

.coach-arrow {
    position: absolute;
    width: 12px; height: 12px;
    margin-left: -6px;
    background: rgba(18, 26, 46, 0.97);
    transform: rotate(45deg);
}
.coach-bubble.is-below .coach-arrow {
    top: -7px;
    border-top: 1px solid rgba(88, 222, 200, 0.45);
    border-left: 1px solid rgba(88, 222, 200, 0.45);
}
.coach-bubble.is-above .coach-arrow {
    bottom: -7px;
    border-bottom: 1px solid rgba(88, 222, 200, 0.45);
    border-right: 1px solid rgba(88, 222, 200, 0.45);
}

.coach-step {
    margin: 0 0 4px;
    font-size: 10px; letter-spacing: 0.09em; text-transform: uppercase;
    color: var(--accent-multi);
}
.coach-title {
    margin: 0 0 5px;
    font-size: 15px; font-weight: 600; line-height: 1.25;
    color: var(--text-primary);
}
.coach-body {
    margin: 0;
    font-size: 12.5px; line-height: 1.4;
    color: var(--text-secondary);
}
/* The escape routes, stated. Without them a first-time guest has a card on
   screen and no visible way to be rid of it — and no way at all to say they
   do not want the rest. */
.coach-actions {
    margin: 11px -16px 0;
    padding: 8px 16px 12px;
    border-top: 1px solid rgba(88, 222, 200, 0.18);
}
.coach-hint {
    margin: 0 0 8px;
    font-size: 11px; text-align: center;
    color: var(--text-disabled);
}
/* Full width and a full touch target: "skip" is a decision the guest is
   entitled to make on the first bubble, so it must not be the small print. */
.coach-skip {
    display: block; width: 100%;
    min-height: var(--touch-min);
    padding: 0 12px;
    border-radius: 10px;
    border: 1px solid rgba(88, 222, 200, 0.5);
    background: rgba(88, 222, 200, 0.12);
    color: var(--accent-multi);
    font-size: 13px; font-weight: 600;
    cursor: pointer;
    /* The one element in the layer that takes a tap. */
    pointer-events: auto;
}
.coach-skip:active { background: rgba(88, 222, 200, 0.22); }
.coach-skip:focus-visible {
    outline: 2px solid var(--accent-focus); outline-offset: 2px;
}

/* Two keyframes rather than one, because the bounce has to travel *toward*
   the control: up when the bubble hangs below it, down when it sits above.
   The pair is comma-joined with the fade so the entrance and the loop
   animate different properties and never fight over `transform`.

   9 px, not the 6 it started at: on a phone at arm's length the smaller
   travel was easy to miss entirely, and a hint nobody notices moving is a
   hint that only works if it is read. The period stays slow — this is a
   nudge toward a control, not a thing demanding to be tapped. */
@keyframes coach-in { from { opacity: 0; } }
@keyframes coach-bounce-below {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-9px); }
}
@keyframes coach-bounce-above {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(9px); }
}
.coach-bubble.is-entering.is-below {
    animation: coach-in 220ms var(--ease-out),
               coach-bounce-below 1400ms 220ms ease-in-out infinite;
}
.coach-bubble.is-entering.is-above {
    animation: coach-in 220ms var(--ease-out),
               coach-bounce-above 1400ms 220ms ease-in-out infinite;
}

/* The ring on the control being pointed at. `outline` rather than a
   box-shadow: every one of these controls already has the raised-surface
   recipe's shadow, and outline does not have to be merged with it. */
.is-coach-target {
    outline: 2px solid var(--accent-multi);
    outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
    .coach-bubble.is-entering.is-below,
    .coach-bubble.is-entering.is-above {
        animation: coach-in 220ms var(--ease-out);
    }
    /* Every looping decoration. State changes still repaint; only the
       perpetual motion stops. (`.orbit.is-spinning`, not `.orbit i`: the
       spin is declared on the parent, and the old selector matched only
       the motionless dots inside it.) */
    /* D164 took the 2-way status widget's animated bars, dots and breathing
       glow with the rest of its bespoke frame, so there is nothing of it
       left to stop here. */
    .pulse-ring, .star, .busy-dots i, .orbit.is-spinning, .typing-dots i,
    .peer-indicator.is-speaking .peer-speaker,
    .converse-mic-ring, .converse-mic canvas {
        animation: none !important;
    }
    /* D234 — the listening microphone keeps one still ring instead. */
    .converse-mic[data-converse-mic="listening"] .converse-mic-glow + .converse-mic-ring {
        opacity: 0.6; transform: scale(1.2);
    }
    /* The swipe still tracks the finger — that is direct manipulation, not
       decoration — but the sheet does not fly out on release. */
    .full-sheet, .full-sheet.is-leaving { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   LANGUAGE PICKER SHEET
   ═══════════════════════════════════════════════════════════════════════ */

/* Overwritten per invocation by openPicker(); the default keeps the sheet
   legible if it is ever inspected without one. */
.sheet-layer { position: fixed; inset: 0; z-index: 40; --picker-accent: #58dec8; }
.sheet-scrim { position: absolute; inset: 0; background: rgba(7, 10, 23, 0.769); }

/* Full height, so it takes the vertical insets; the horizontal ones fall to
   the centred width, which never reaches a landscape notch. */
.sheet {
    position: absolute; top: 0; left: 50%;
    transform: translateX(-50%);
    width: min(560px, 100%);
    height: 100%;
    padding-top: var(--safe-top);
    padding-bottom: var(--safe-bottom);
    display: flex; flex-direction: column;
    background: linear-gradient(180deg, #20263c 0%, #151b2f 32%, #101628 100%);
    border: 1px solid rgba(100, 124, 157, 0.345);
    border-radius: 0;
    overflow: hidden;
}
@media (min-width: 561px) {
    .sheet { border-radius: var(--radius-modal); margin: 0 5px; }
}
.sheet-accent {
    position: absolute; top: 1px; left: 50%;
    transform: translateX(-50%);
    width: calc(100% - 42px); height: 2px; border-radius: 1px;
    background: linear-gradient(90deg,
        rgba(97, 234, 209, 0) 0%, var(--aqua) 28%,
        var(--violet) 72%, rgba(146, 119, 255, 0) 100%);
}
.sheet-header {
    display: flex; align-items: center; gap: 8px;
    padding: 18px var(--panel-padding) 10px;
}
.sheet-title { flex: 1; margin: 0; font-size: var(--type-card); font-weight: 600; }

/* `flex: 0 0 auto` for the same reason .sheet-section carries it: the sheet
   is a flex column whose list takes the slack, and a shrinkable input gets
   squeezed to a sliver on a short viewport — which is every phone once the
   keyboard is up, i.e. exactly when this field is in use. 16 px type also
   keeps iOS from zooming the sheet on focus. */
.sheet-search {
    flex: 0 0 auto;
    margin: 0 var(--panel-padding) 12px;
    height: 56px;
    border-radius: 16px;
    border: 1px solid rgba(81, 104, 134, 0.5);
    background: rgba(16, 27, 50, 0.902);
    color: var(--ink);
    font-size: 16px;
    padding: 0 16px;
    outline: none;
}
.sheet-search:focus { border-color: var(--aqua); }

/* The sheet is a flex column whose list takes the slack; without an explicit
   basis these two get squeezed to nothing behind it. */
.sheet-section {
    flex: 0 0 auto;
    margin: 0; padding: 0 12px 6px 16px;
    font-size: 10px; font-weight: 700; letter-spacing: 1.4px;
    color: var(--text-secondary);
}
.sheet-section[hidden] { display: none; }

/* Search results live in a strip beneath the field, not as a filter on the
   list: on a phone the keyboard covers the list entirely while typing. */
.chip-strip {
    display: flex; flex-wrap: nowrap; gap: 6px;
    flex: 0 0 44px;
    height: 44px; margin: 4px 12px 12px 16px;
    /* The QML Flickable scrolls sideways once the chips outgrow the line. */
    overflow-x: auto; overflow-y: hidden;
    scrollbar-width: none;
}
.chip-strip::-webkit-scrollbar { display: none; }
/* Only while the strip actually overflows (app.js toggles it): a fade on a
   strip that fits would gnaw the last chip's edge for nothing. */
.chip-strip.is-scrollable {
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
}
/* `display: flex` above would otherwise beat the UA sheet's [hidden] rule. */
.chip-strip[hidden] { display: none; }

/* LanguageChipStrip.qml. The same sunken base as everything else, but the
   face is flat, so this spells the lift as a hard `box-shadow` rather than
   borrowing `.lift` — that recipe bakes in a gradient face and needs a child
   element to hold the label above it. */
.chip {
    flex: 1 0 auto;                  /* an equal share of the line, but never
                                        narrower than the name it holds */
    height: 41px; margin-bottom: 3px;
    padding: 0 12px;
    border: 1px solid #41516c; border-radius: 12px;
    background: #202a42;
    box-shadow: 0 3px 0 0 #080c18;
    color: var(--text-primary);
    font-size: 13px; font-weight: 500;
    white-space: nowrap;
    cursor: pointer;
}
.chip.is-current {
    background: var(--picker-accent);
    border-color: rgba(255, 255, 255, 0.376);   /* QML #60ffffff */
    color: #10131f;
}
.chip:active { transform: translateY(2px); box-shadow: 0 1px 0 0 #080c18; }

/* Not in the QML: the Qt strip simply stops at four. Kept because the search
   is otherwise silent about how much it left behind. */
.chip-note {
    flex: 0 0 auto; align-self: center;
    font-size: 11px; color: var(--text-secondary);
}

/* The QML list sits on black with a 1 px rule along the top, inset 10 px on
   the left and 4 px on the right, and starts 8 px down. */
.sheet-list {
    flex: 1 1 auto; min-height: 0;
    margin: 8px 0 0; padding: 8px 4px 24px 10px;
    list-style: none; overflow-y: auto;
    background: #000000;
    border-top: 1px solid #53627d;
}

/* LanguageResultRow.qml. Two stacked rounded rectangles: a sunken base 3 px
   lower, and the face on top of it — the same lift the panels use, but with
   its own radius and palette, so it is spelled out rather than sharing
   `.lift`. The delegate is `view.width - 30`, leaving the scrollbar gutter. */
.sheet-list li {
    position: relative;
    width: calc(100% - 30px);
    height: 52px;
    margin-bottom: 4px;
}
.sheet-list li::before {
    content: "";
    position: absolute; top: 3px; left: 0; right: 0; bottom: 0;
    border-radius: 15px;
    background: #080c18;
}
.sheet-row {
    position: absolute; top: 0; left: 0; right: 0; height: 49px;
    display: flex; align-items: center; gap: 12px;
    padding: 0 14px 0 15px;
    border: 1px solid #30405b; border-radius: 15px;
    background: #171f34;
    color: var(--text-primary);
    font-size: 15px; text-align: left;
    cursor: pointer;
}
.sheet-row:hover { background: #202a42; }
.sheet-row:active { top: 2px; }
/* Only the weight changes — the QML keeps the label at `primaryText` and
   signals the current language through the fill and the border. */
.sheet-row.is-current {
    font-weight: 600;
    background: color-mix(in srgb, var(--picker-accent) 14%, transparent);
    border-color: color-mix(in srgb, var(--picker-accent) 60%, transparent);
}

.row-name {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A common 24 px height with the width left free: the PNGs are all 96 px
   tall at their true aspect, so a square flag stays square. */
.row-flags { flex: 0 0 auto; display: flex; align-items: center; gap: 5px; }
.row-flag { display: block; height: 24px; width: auto; }


/* ═══════════════════════════════════════════════════════════════════════
   JOIN LANDING  (qml/JoinLanding.qml)

   One centred card over the login shader. The QML clamps it to 420 px and
   centres it vertically, growing the scroll area only when the content
   (or a raised keyboard) needs it.
   ═══════════════════════════════════════════════════════════════════════ */

.join-scroll {
    position: absolute; inset: 0;
    overflow-y: auto;
    display: flex; align-items: center; justify-content: center;
    padding: var(--page-padding);
}
.join-card {
    --draft-accent: var(--accent-multi);
    width: min(420px, 100%);
    display: flex; flex-direction: column; gap: 14px;
    padding: var(--card-padding, 18px);
    border-radius: 24px;
    background: rgba(16, 24, 42, 0.8);              /* #cc10182a */
    border: 1px solid rgba(88, 222, 200, 0.4);      /* #6658dec8 */
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}
.join-title { margin: 0; font-size: 22px; font-weight: 700; text-align: center; }
.join-status {
    margin: 0; text-align: center;
    font-size: 13px; color: var(--text-secondary);
}
.join-host-avatar { align-self: center; margin-top: 4px; }
.join-host-name { margin: 0; text-align: center; font-size: 18px; font-weight: 700; }

.join-persona {
    display: flex; align-items: center; gap: 10px;
    margin-top: 4px; padding: 14px;
    min-height: 76px;
    border-radius: 22px;
    background: #121a28;
    border: 1px solid rgba(88, 222, 200, 0.29);     /* #4a58dec8 */
}
.join-persona .avatar-reroll {
    background: #1a2a36;
    border-color: rgba(88, 222, 200, 0.29);
}
.join-persona .avatar-reroll:active { background: #2a3d4a; }
/* The reroll yields its width to the apply/revert pair while a draft is
   pending, exactly as the QML hides it on `nameField.editing`. */
.join-persona.is-drafting .avatar-reroll { display: none; }

.join-language { display: flex; align-items: center; gap: var(--row-spacing, 10px); }
.join-language-label { font-size: 14px; color: var(--text-secondary); }
.join-language .language-picker { flex: 1 1 auto; min-width: 0; }

.join-disclosure {
    margin: 0; padding: 10px;
    border-radius: 14px;
    background: #101d2a;
    border: 1px solid #38586a;
    text-align: center;
    font-size: 11px; color: var(--text-secondary);
}
.join-warning {
    margin: 0; text-align: center;
    font-size: 12px; font-weight: 700; color: #ffd28a;
}
/* The in-app-webview advisory. Amber like .join-warning — a caveat, not an
   error: joining is still allowed, and listening may work. */
.join-webview {
    margin: 0; padding: 10px;
    display: flex; flex-direction: column; gap: 8px;
    border-radius: 14px;
    border: 1px solid rgba(255, 179, 107, 0.38);
    background: #221b12;
}
.join-webview-note {
    margin: 0; text-align: center;
    font-size: 12px; color: #ffd28a;
}
.join-webview-copy {
    align-self: center;
    height: 36px; padding: 0 16px;
    border-radius: 12px;
    border: 1px solid rgba(255, 179, 107, 0.38);
    background: transparent;
    color: #ffd28a; font-size: 13px; font-weight: 700;
    cursor: pointer;
}
.join-webview-copy:active { background: #33281a; }

.join-button {
    height: 48px; margin-top: 6px;
    border-radius: 16px;
    background: #184f48;
    border: 1px solid rgba(88, 222, 200, 0.533);
    color: var(--text-primary); font-size: 16px; font-weight: 700;
    cursor: pointer;
    transition: background-color var(--press-duration) var(--ease-out);
}
.join-button:active { background: #1f6f64; }
.join-button:disabled { background: #1a2230; cursor: default; }

.join-decline {
    height: 44px;
    border-radius: 14px;
    background: transparent;
    border: 1px solid #3a4a63;
    color: var(--text-secondary); font-size: 14px;
    cursor: pointer;
}
.join-decline:active { background: #1a2230; }
.join-decline:disabled { opacity: 0.5; cursor: default; }


/* ═══════════════════════════════════════════════════════════════════════
   CONFIRM DIALOG  (qml/ConfirmDialog.qml)
   ═══════════════════════════════════════════════════════════════════════ */

/* Above every sheet and overlay (transcript sheet is 60): the confirm dialog
   is the one true modal here, and "Clear transcript history?" is opened FROM
   the sheet — rendering under it left an invisible dialog holding focus. */
.dialog-layer { position: fixed; inset: 0; z-index: 70; }
.dialog-scrim { position: absolute; inset: 0; background: rgba(5, 9, 22, 0.65); }
.dialog {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: min(360px, calc(100vw - 48px));
    display: flex; flex-direction: column; gap: 12px;
    padding: 20px;
    border-radius: 20px;
    background: #10182a;
    border: 1px solid rgba(88, 222, 200, 0.4);
}
.dialog-title { margin: 0; font-size: 17px; font-weight: 700; }
.dialog-message { margin: 0; font-size: 13px; color: var(--text-secondary); }
.dialog-actions { display: flex; gap: 10px; margin-top: 6px; }
.dialog-cancel, .dialog-confirm {
    flex: 1 1 0; min-width: 0;
    height: 44px; border-radius: 14px;
    font-size: 14px; cursor: pointer;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dialog-cancel {
    background: transparent;
    border: 1px solid #3a4a63;
    color: var(--text-secondary);
}
.dialog-cancel:active { background: #1a2230; }
.dialog-confirm {
    background: #184f48;
    border: 1px solid rgba(88, 222, 200, 0.533);
    color: var(--text-primary); font-weight: 700;
}
.dialog-confirm:active { background: #1f6f64; }

/* The noise filter download's progress track, on the Speak hub (D102). The
   fill's width is driven from app.js as a percentage; when no
   Content-Length is available there is no percentage and the fill stays at
   zero while the message carries the byte count instead. */
.download-progress {
    height: 6px; border-radius: 3px;
    background: #1a2230; overflow: hidden;
}
.counter-voice-dialog {
    width: min(620px, calc(100vw - 32px)); max-height: calc(100dvh - 32px);
    box-sizing: border-box; overflow-y: auto; padding: 24px; border-radius: 20px;
    border: 1px solid #364656; background: #131c26; color: #eff6fa;
    box-shadow: 0 20px 80px #0008; line-height: 1.55;
}
.counter-voice-dialog::backdrop { background: #050b14d9; }
.counter-voice-dialog h2 { margin: 0 0 12px; font-size: 1.5rem; }
.counter-voice-dialog p { margin: 12px 0; }
.counter-voice-dialog progress { width: 100%; height: 9px; accent-color: #58dec8; }
.counter-voice-dialog select { display: block; margin-top: 8px; padding: 10px; border-radius: 8px; max-width: 100%; }
.counter-voice-script { margin: 16px 0; padding: 16px; border-left: 3px solid #58dec8; background: #1d2c37; font-size: 1.15rem; white-space: pre-line; }
.counter-voice-heard { min-height: 3em; color: #b6cbd8; overflow-wrap: anywhere; }
.counter-voice-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.counter-voice-button { min-height: 44px; padding: 10px 16px; margin: 4px 4px 4px 0; border: 1px solid #52716d; border-radius: 10px; background: #22433e; color: #e9fffa; cursor: pointer; font: inherit; }
.counter-voice-button:disabled { opacity: .45; cursor: default; }
.counter-voice-button:focus-visible { outline: 2px solid #58dec8; outline-offset: 3px; }
.download-progress-fill {
    height: 100%; width: 0; border-radius: 3px;
    background: rgba(88, 222, 200, 0.85);
    transition: width 0.15s linear;
}


/* ═══════════════════════════════════════════════════════════════════════
   TEXT-FIRST GROUP CHAT

   A live Group room is a small application of its own. The ordinary header
   destinations give way to People / Chat / Settings / Account in the same
   top bar, so room navigation stays consistent with every other mode.
   ═══════════════════════════════════════════════════════════════════════ */

.sr-only {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0;
}

/* The legacy Multi page yields to the Group shell while its room is live. */
#page-multi.is-group-room-open > .page-scroll { display: none; }

/* The app header stays put; only its destinations change while Group Chat is
   live. The room nav is a child of that header, so it naturally inherits the
   same safe-area placement and height as the ordinary application tabs. */
#workspace.is-group-room > .app-header > .tab-bar { display: none; }

.group-room-shell {
    position: absolute;
    inset: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    background: linear-gradient(180deg, #10252c 0%, #0d1a26 48%, #0b1421 100%);
}

/* The room's contextual bar now begins below the persistent app header, so
   its 44px content must not spend the safe-top inset a second time. */
.group-room-topbar {
    flex: 0 0 44px;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 calc(12px + var(--safe-right)) 0
             calc(12px + var(--safe-left));
    border-bottom: 1px solid var(--hairline);
    background: rgba(8, 15, 27, 0.9);
}
.group-room-topbar > canvas { flex: 0 0 auto; }
.group-room-heading {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.group-room-heading strong {
    color: var(--text-primary);
    font-size: var(--type-body);
    line-height: 1.15;
}
.group-room-heading strong::after {
    content: " ›";
    color: var(--text-secondary);
    font-weight: 500;
}
.group-room-heading > span {
    overflow: hidden;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    line-height: 1.3;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.group-room-stage {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
}
.group-room-view {
    position: absolute;
    inset: 0;
    display: none;
}
.group-room-view.is-active { display: block; }
.group-room-view.group-room-chat.is-active {
    display: flex;
    flex-direction: column;
}
.group-room-scroll {
    position: absolute;
    inset: 0;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    scrollbar-color: #33415f transparent;
}
.group-room-column {
    width: min(100%, 760px);
    min-height: 100%;
    margin: 0 auto;
    padding: 18px calc(var(--page-padding) + var(--safe-right))
             calc(22px + var(--safe-bottom))
             calc(var(--page-padding) + var(--safe-left));
    display: flex;
    flex-direction: column;
}
.group-room-help {
    position: relative;
    flex: 0 0 44px;
    width: 44px; height: 44px;
    margin-right: -7px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--text-secondary);
    font-size: 15px;
    font-weight: 650;
    cursor: pointer;
    isolation: isolate;
}
.group-room-topbar .group-room-help { margin-left: auto; }
.group-room-help::before {
    content: "";
    position: absolute;
    inset: 7px;
    z-index: -1;
    border: 1px solid var(--hairline);
    border-radius: 50%;
    background: var(--surface);
}
.group-room-help:active { color: var(--text-primary); }
.group-room-help:active::before { border-color: #4d5f80; }

.group-room-language { margin-top: 12px; }

.group-room-invite-card {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    margin-top: 12px;
    padding: 16px 12px 14px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: var(--surface);
}
.group-room-invite-head {
    min-height: 30px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    color: var(--text-primary);
    font-size: var(--type-body);
}
.group-room-invite-head > button {
    min-width: 72px;
    min-height: 36px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    padding: 0 4px;
    border: 0;
    background: transparent;
    color: #a5f0e3;
    font: inherit;
    font-size: var(--type-meta);
    cursor: pointer;
}
.group-room-invite-head i {
    font-style: normal;
    transition: transform var(--state-duration) var(--ease-out);
}
.group-room-invite-card.is-qr-collapsed .group-room-invite-head i {
    transform: rotate(180deg);
}
.group-room-invite-card.is-qr-collapsed .group-room-qr-panel,
.group-room-invite-card.is-qr-collapsed .group-room-invite-actions {
    display: none;
}
.group-room-qr-panel { display: flex; justify-content: center; }
.group-room-qr-card {
    width: min(228px, 66vw);
    height: min(228px, 66vw);
    padding: 0;
    overflow: hidden;
    border: 8px solid #f4f8ff;
    border-radius: 12px;
    background: #f4f8ff;
    cursor: zoom-in;
}
.group-room-qr-card canvas { width: 100%; height: 100%; }
.group-room-invite-actions {
    width: 100%;
    display: flex;
    gap: 8px;
}
.group-room-invite-actions button {
    flex: 1 1 0;
    min-width: 0;
    height: 48px;
    border: 1px solid #2b5a52;
    border-radius: var(--radius-compact);
    background: transparent;
    color: #7ee6d2;
    font-size: var(--type-body);
    font-weight: 650;
    cursor: pointer;
}
.group-room-invite-actions .is-primary {
    flex-grow: 1.25;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    border-color: transparent;
    background: #1f7d70;
    color: #eafffb;
}
.group-room-invite-actions button:disabled { opacity: 0.4; cursor: default; }
.group-room-invite-actions button:active:not(:disabled) { background: #113029; }
.group-room-invite-actions .is-primary:active:not(:disabled) { background: #1a6a60; }
.group-room-languages {
    margin-top: 14px;
    overflow: hidden;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-standard);
    background: var(--surface);
}
.group-room-languages summary {
    min-height: 48px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 0 var(--card-padding);
    color: var(--text-primary);
    font-size: var(--type-body);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}
.group-room-languages summary::-webkit-details-marker { display: none; }
.group-room-languages summary > span:last-child {
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-languages summary b { color: #a5f0e3; }
.group-room-languages summary i {
    display: inline-block;
    margin-left: 7px;
    font-style: normal;
    transform: rotate(90deg);
    transition: transform var(--state-duration) var(--ease-out);
}
.group-room-languages[open] summary i { transform: rotate(-90deg); }
.group-room-language-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 7px;
    padding: 10px var(--card-padding) 12px;
    border-top: 1px solid var(--hairline);
}
.group-room-language-chip {
    padding: 5px 11px;
    border: 1px solid var(--hairline);
    border-radius: 15px;
    background: var(--surface);
    color: var(--text-primary);
    font-size: var(--type-meta);
}
.group-room-language-chip.is-mine { border-color: #58dec8; color: #a5f0e3; }
.group-room-people {
    margin-top: 14px;
    overflow: hidden;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-standard);
    background: var(--surface);
}
.group-room-people-head {
    min-height: 48px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 0 var(--card-padding);
    color: var(--text-primary);
    font-size: var(--type-body);
    font-weight: 600;
}
.group-room-people-head b { color: #a5f0e3; }
.group-room-inline-controls {
    padding: 10px var(--card-padding) 12px;
    border-top: 1px solid var(--hairline);
    background: rgba(17, 48, 41, 0.52);
}
.group-room-mode-toggle {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
    padding: 4px;
    border: 1px solid #2b5a52;
    border-radius: 14px;
    background: #0d1724;
}
.group-room-mode-toggle button {
    min-height: 42px;
    padding: 0 8px;
    border: 0;
    border-radius: 10px;
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--type-body);
    font-weight: 650;
    cursor: pointer;
}
.group-room-mode-toggle button[aria-pressed="true"] {
    background: #1f7d70;
    color: #effffb;
}
.group-room-floor-picker {
    min-height: 48px;
    margin-top: 9px;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: 10px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-floor-picker select {
    width: 100%;
    height: 44px;
    padding: 0 34px 0 11px;
    border: 1px solid #397467;
    border-radius: 10px;
    background: #11182b;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--type-body);
}
.group-room-mute-all {
    width: 100%;
    min-height: 42px;
    margin-top: 7px;
    padding: 0 10px;
    border: 1px solid var(--hairline);
    border-radius: 10px;
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 600;
    cursor: pointer;
}
.group-room-mute-all:disabled { opacity: 0.42; cursor: default; }
/* The listen-only raise-hand beside the composer (D183): full-width
   pill above the locked input, mirroring the People page's control. */
.group-room-composer-request {
    display: block;
    width: calc(100% - 20px - var(--safe-left) - var(--safe-right));
    max-width: 764px;
    min-height: 40px;
    margin: 0 auto 7px;
    padding: 0 12px;
    border: 1px solid #397467;
    border-radius: 10px;
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 600;
    cursor: pointer;
}
.group-room-composer-request.is-on {
    background: #1f7d70;
    border-color: #1f7d70;
    color: #effffb;
}
.group-room-guest-controls {
    min-height: 48px;
    display: flex;
    align-items: center;
    gap: 10px;
}
.group-room-mode-status {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.group-room-mode-status small { color: var(--text-secondary); font-size: var(--type-meta); }
.group-room-mode-status strong { color: #a5f0e3; font-size: var(--type-body); }
#group-room-request-floor {
    flex: 0 0 auto;
    min-height: 44px;
    padding: 0 13px;
    border: 1px solid #397467;
    border-radius: 12px;
    background: #113029;
    color: #c8fff4;
    font-size: var(--type-meta);
    font-weight: 650;
    cursor: pointer;
}
#group-room-request-floor[aria-pressed="true"] {
    border-color: #ffb86b;
    background: #33210f;
    color: #ffd7a8;
}
/* The list flows with the page (D094): one scroll, never a porthole —
   a host moderating a full room should not fight nested scrolling. */
.group-room-roster {
    border-top: 1px solid var(--hairline);
}
.group-room-roster-row {
    min-height: 56px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px var(--card-padding);
    border-bottom: 1px solid rgba(51, 65, 95, 0.48);
}
.group-room-roster-row:last-child { border-bottom: 0; }
.group-room-roster-row.is-away { opacity: 0.56; }
.group-room-roster-avatar { flex: 0 0 auto; }
.group-room-roster-copy {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.group-room-roster-copy strong {
    overflow: hidden;
    color: var(--text-primary);
    font-size: var(--type-body);
    text-overflow: ellipsis;
    white-space: nowrap;
}
.group-room-roster-copy small { color: var(--text-secondary); font-size: var(--type-meta); }
/* Frequent, reversible audio moderation stays one tap away. The labelled
   participant sheet owns block and ban, keeping consequential actions out
   of the main scan path. */
.group-room-roster-actions {
    flex: 0 0 auto;
    display: flex;
    gap: 5px;
}
.group-room-roster-actions button {
    position: relative;
    width: 44px; height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--hairline);
    border-radius: 50%;
    background: transparent;
    color: var(--text-secondary);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}
/* The pseudo target keeps the hit contract explicit for source tests and
   future visual changes; the button itself already occupies all 44px. */
.group-room-roster-actions button::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
}
.group-room-roster-actions button.is-active {
    border-color: #ffb86b;
    background: #33210f;
    color: #ffd7a8;
}
.group-room-roster-actions canvas { pointer-events: none; }
.group-room-roster-request {
    color: #ffd7a8;
    font-weight: 650;
}
.group-room-roster-lang {
    flex: 0 0 auto;
    max-width: 38%;
    overflow: hidden;
    padding: 4px 8px;
    border: 1px solid var(--hairline);
    border-radius: 12px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    text-overflow: ellipsis;
    white-space: nowrap;
}
.group-room-banned {
    border-top: 1px solid var(--hairline);
    background: rgba(40, 20, 33, 0.32);
}
.group-room-banned summary {
    min-height: 46px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 0 var(--card-padding);
    color: #ffb6c2;
    font-size: var(--type-body);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}
.group-room-banned summary::-webkit-details-marker { display: none; }
.group-room-banned summary i {
    display: inline-block;
    margin-left: 7px;
    font-style: normal;
    transform: rotate(90deg);
    transition: transform var(--state-duration) var(--ease-out);
}
.group-room-banned[open] summary i { transform: rotate(-90deg); }
.group-room-banned-row {
    min-height: 56px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px var(--card-padding);
    border-top: 1px solid rgba(124, 47, 68, 0.42);
}
.group-room-banned-row .group-room-roster-copy small { color: #d695a2; }
.group-room-unban {
    min-width: 64px;
    min-height: 44px;
    padding: 0 10px;
    border: 1px solid #7c2f44;
    border-radius: 12px;
    background: transparent;
    color: #ffb6c2;
    font-size: var(--type-meta);
    font-weight: 650;
    cursor: pointer;
}

/* Chat */
.group-room-playback-mute {
    flex: 0 0 auto;
    min-width: 40px; height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 0 10px;
    border: 1px solid var(--hairline);
    border-radius: 20px;
    background: var(--surface);
    color: var(--text-secondary);
    cursor: pointer;
}
.group-room-playback-mute canvas { opacity: 0.75; }
.group-room-playback-mute span {
    font-size: var(--type-meta);
    font-weight: 650;
    white-space: nowrap;
}
.group-room-playback-mute.is-muted {
    border-color: #7c2f44;
    background: #281421;
    color: #ffb6c2;
}
.group-room-playback-mute:disabled { opacity: 0.4; cursor: default; }
.group-room-message-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-x: hidden;
    overflow-y: auto;
    padding: 8px calc(12px + var(--safe-right)) 6px calc(12px + var(--safe-left));
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    scrollbar-color: #33415f transparent;
}
.group-room-messages {
    width: 100%;
    max-width: 760px;
    min-height: 100%;
    margin: 0 auto;
    padding: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 6px;
    list-style: none;
}
.group-room-empty {
    margin: auto;
    max-width: 300px;
    padding: 24px 18px;
    color: var(--text-secondary);
    font-size: var(--type-body);
    text-align: center;
}
.group-room-empty > span { display: block; }
.group-room-empty button {
    min-height: 44px;
    margin-top: 12px;
    padding: 0 16px;
    border: 1px solid #2b5a52;
    border-radius: 22px;
    background: #113029;
    color: #a5f0e3;
    font: inherit;
    font-weight: 650;
    cursor: pointer;
}
.group-room-message {
    position: relative;
    align-self: flex-start;
    max-width: min(86%, 620px);
    padding: 8px 38px 7px 11px;
    border: 1px solid var(--hairline);
    border-radius: 16px 16px 16px 5px;
    background: var(--surface);
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    touch-action: manipulation;
}
.group-room-message-more {
    position: absolute;
    top: 1px;
    right: 1px;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--text-secondary);
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
}
.group-room-message-more::after {
    content: "";
    position: absolute;
    inset: -5px;
    border-radius: 50%;
}
.group-room-message-more:active { background: rgba(88, 222, 200, 0.1); }
/* Speaker identity at a glance (D095): the left edge carries the
   sender's avatar hue — the same hash the roster badge uses — on every
   bubble of a run, so runs read as people even when persona names
   near-collide at 30 heads. A cue, not a rainbow: 3px at 45% sat. */
.group-room-message:not(.is-mine) {
    border-left: 3px solid var(--group-sender-accent, var(--hairline));
}
.group-room-message.is-mine {
    align-self: flex-end;
    border-color: #2b5a52;
    border-radius: 16px 16px 5px 16px;
    background: #143733;
}
/* A send that outlived its 20 s with no echo and no rejection (D095):
   full opacity and an error tint — failed must not read as pending. */
.group-room-message.is-failed {
    border-color: #7c2f44;
    background: #281421;
}
.group-room-message-retry {
    position: relative;
    display: block;
    min-height: 26px;
    margin-top: 4px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #ff9db1;
    font-size: 11px;
    font-weight: 650;
    cursor: pointer;
}
.group-room-message-retry::after {
    content: "";
    position: absolute;
    inset: -9px -3px;
}
/* The last-read marker (D095): a keyed feed node above the first
   message of the batch that arrived while the user was away. */
.group-room-unread-divider {
    align-self: stretch;
    max-width: none;
    display: flex;
    align-items: center;
    gap: 10px;
    color: #7ee6d2;
    font-size: var(--type-meta);
    font-weight: 650;
}
.group-room-unread-divider::before,
.group-room-unread-divider::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: #2b5a52;
}
.group-room-day-divider,
.group-room-system-line {
    align-self: stretch;
    max-width: none;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 600;
    list-style: none;
}
.group-room-day-divider::before,
.group-room-day-divider::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--hairline);
}
.group-room-system-line {
    font-style: italic;
    font-weight: 500;
}
.group-room-message.is-held {
    border-color: #58dec8;
    box-shadow: 0 0 0 1px rgba(88, 222, 200, 0.2);
}
.group-room-message.is-reply-target {
    border-color: #58dec8;
    animation: group-room-reply-target 1.5s ease-out;
}
@keyframes group-room-reply-target {
    0%, 45% { box-shadow: 0 0 0 3px rgba(88, 222, 200, 0.28); }
    100% { box-shadow: none; }
}
.group-room-message.is-speaking { opacity: 0.75; }
/* The dead-air cue (D090): a talker whose speech just ended, their
   words still crossing recognition + translation. */
.group-room-message.is-translating .group-room-message-text {
    color: var(--text-secondary);
    font-style: italic;
}
/* An optimistic echo (D089): sent, not yet confirmed by the gateway. */
.group-room-message.is-pending { opacity: 0.65; }
/* A run of bubbles from one sender (D092): the repeats drop their name
   line and ride close under the first. */
.group-room-message.is-grouped { margin-top: -5px; }
/* The presence line (D091): who is speaking or typing right now, pinned
   above the composer. D173: hidden while idle so it does not tax the
   feed; `.has-presence` on the chat view lifts the jump pill over it. */
.group-room-presence {
    flex: 0 0 auto;
    padding: 2px calc(16px + var(--safe-right)) 2px calc(16px + var(--safe-left));
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-style: italic;
}
.group-room-presence[hidden] { display: none; }
/* Clips dropped for freshness (D088): one coalesced feed line. */
.group-room-skip-note {
    align-self: center;
    padding: 3px 12px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    text-align: center;
}
.group-room-message.is-playing {
    border-color: #58dec8;
    box-shadow: 0 0 0 1px rgba(88, 222, 200, 0.15);
}
.group-room-message-meta {
    margin-bottom: 3px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-lang-tag {
    position: relative;
    display: inline-block;
    margin: 0;
    padding: 0 1px;
    border: 0;
    background: transparent;
    color: #7ee6d2;
    font: inherit;
    font-weight: 700;
    letter-spacing: 0.04em;
    cursor: pointer;
}
.group-room-lang-tag::after {
    content: "";
    position: absolute;
    inset: -8px -4px;
}
.group-room-lang-tag.is-open { color: #a5f0e3; }
.group-room-message.is-playing .group-room-message-meta::after {
    content: " · ▶";
    color: #58dec8;
    animation: group-room-play-pulse 1s ease-in-out infinite;
}
@keyframes group-room-play-pulse { 50% { opacity: 0.35; } }
/* The reading-progress fill (D175): a thin bar under the playing
   bubble's words, filling linearly across the clip's length. */
.group-room-read-progress {
    height: 2px;
    margin-top: 5px;
    overflow: hidden;
    border-radius: 1px;
    background: rgba(88, 222, 200, 0.18);
}
.group-room-read-progress i {
    display: block;
    height: 100%;
    background: #58dec8;
    transform: scaleX(0);
    transform-origin: left;
    animation: group-room-read-fill linear forwards;
}
@keyframes group-room-read-fill { to { transform: scaleX(1); } }
/* A system line (D176): the room narrating its own events — centered,
   quiet, no bubble chrome. The is-playing ring and the progress fill
   still apply while the narrator's clip sounds. */
.group-room-message.is-system {
    align-self: center;
    max-width: 88%;
    padding: 4px 12px;
    background: none;
    border: 0;
    text-align: center;
}
.group-room-message.is-system.is-playing {
    border-radius: 10px;
    background: rgba(88, 222, 200, 0.08);
    box-shadow: 0 0 0 1px rgba(88, 222, 200, 0.35);
}
.group-room-system-text {
    color: var(--text-secondary);
    font-size: var(--type-meta);
    overflow-wrap: anywhere;
}
.group-room-message-text {
    color: var(--text-primary);
    font-size: var(--type-body);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.group-room-message-edit-status {
    margin-top: 3px;
    color: var(--text-disabled);
    font-size: var(--type-meta);
    text-align: right;
}
.group-room-message.is-deleted {
    opacity: 0.72;
    border-style: dashed;
}
.group-room-message.is-deleted .group-room-message-text {
    color: var(--text-secondary);
    font-style: italic;
}
.group-room-message-reply {
    width: 100%;
    margin: 0 0 6px;
    padding: 5px 8px;
    overflow: hidden;
    display: block;
    border: 0;
    border-left: 3px solid #58dec8;
    border-radius: 4px 8px 8px 4px;
    background: rgba(3, 13, 20, 0.28);
    color: var(--text-secondary);
    text-align: left;
    cursor: pointer;
}
.group-room-message-reply strong,
.group-room-message-reply span {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--type-meta);
}
.group-room-message-reply strong { color: #7ee6d2; }
.group-room-message-link {
    color: #7ee6d2;
    text-decoration: underline;
    text-underline-offset: 2px;
    -webkit-user-select: auto;
    user-select: auto;
}
.group-room-message-original {
    margin-top: 7px;
    padding-top: 7px;
    border-top: 1px solid rgba(77, 95, 128, 0.55);
    color: var(--text-secondary);
    font-size: var(--type-meta);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.group-room-message-original b {
    display: block;
    margin-bottom: 2px;
    color: var(--text-disabled);
    font-size: 10px;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}
/* The translation states (D172): a quiet in-bubble status while the
   D116 patch is in flight, and the untranslated marker with its
   tap-to-translate once the fallback is final. */
.group-room-message-translating {
    margin-top: 4px;
    color: var(--text-disabled);
    font-size: var(--type-meta);
    font-style: italic;
}
.group-room-message-untranslated {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 5px;
    color: var(--text-disabled);
    font-size: var(--type-meta);
}
.group-room-message-untranslated button {
    position: relative;
    min-height: 26px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #7ee6d2;
    font-size: 11px;
    font-weight: 650;
    cursor: pointer;
}
.group-room-message-untranslated button::after {
    content: "";
    position: absolute;
    inset: -9px -3px;
}
.group-room-message-translation-note {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 3px 10px;
    margin-top: 5px;
    color: var(--text-disabled);
    font-size: var(--type-meta);
}
.group-room-message-translation-note button {
    position: relative;
    min-height: 24px;
    padding: 0;
    border: 0;
    background: transparent;
    color: #7ee6d2;
    font-size: 11px;
    font-weight: 650;
    cursor: pointer;
}
.group-room-message-translation-note button::after {
    content: "";
    position: absolute;
    inset: -8px -4px;
}
.group-room-jump {
    position: absolute;
    left: 50%;
    bottom: calc(66px + var(--group-now-reading-lift, 0px));
    z-index: 2;
    padding: 7px 16px;
    border: 1px solid #2b5a52;
    border-radius: 16px;
    background: #113029;
    color: #7ee6d2;
    font-size: var(--type-meta);
    font-weight: 600;
    cursor: pointer;
    transform: translateX(-50%);
    box-shadow: var(--shadow-2);
}
.group-room-chat.has-now-reading { --group-now-reading-lift: 34px; }
.group-room-chat.has-presence .group-room-jump {
    bottom: calc(86px + var(--group-now-reading-lift, 0px));
}
.group-room-chat.has-compose-context .group-room-jump {
    bottom: calc(116px + var(--group-now-reading-lift, 0px));
}
.group-room-chat.has-presence.has-compose-context .group-room-jump {
    bottom: calc(136px + var(--group-now-reading-lift, 0px));
}
.group-room-now-reading {
    flex: 0 0 auto;
    align-self: center;
    max-width: calc(100% - 24px);
    min-height: 30px;
    margin: 2px 12px;
    padding: 3px 12px;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 7px;
    border: 1px solid #2b5a52;
    border-radius: 15px;
    background: #113029;
    color: #a5f0e3;
    font-size: var(--type-meta);
    font-weight: 650;
    cursor: pointer;
}
.group-room-now-reading[hidden] { display: none; }
.group-room-now-reading span:last-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.group-room-compose-context {
    flex: 0 0 auto;
    width: 100%;
    max-width: 784px;
    min-height: 48px;
    margin: 0 auto;
    padding: 6px calc(12px + var(--safe-right)) 5px
        calc(16px + var(--safe-left));
    display: flex;
    align-items: center;
    gap: 10px;
    border-top: 1px solid var(--hairline);
    background: rgba(9, 18, 24, 0.88);
}
.group-room-compose-context[hidden] { display: none; }
.group-room-compose-context > span {
    min-width: 0;
    flex: 1 1 auto;
    padding-left: 9px;
    border-left: 3px solid #58dec8;
}
.group-room-compose-context strong,
.group-room-compose-context small {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.group-room-compose-context strong {
    color: #7ee6d2;
    font-size: var(--type-meta);
}
.group-room-compose-context small { color: var(--text-secondary); }
.group-room-compose-context button {
    flex: 0 0 40px;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--text-secondary);
    font-size: 24px;
    cursor: pointer;
}
.group-room-compose-context:not([hidden]) + .group-room-composer {
    border-top: 0;
}
.group-room-composer {
    flex: 0 0 auto;
    width: 100%;
    max-width: 784px;
    min-height: 58px;
    margin: 0 auto;
    display: flex;
    align-items: flex-end;
    gap: 8px;
    padding: 6px calc(10px + var(--safe-right)) 7px calc(10px + var(--safe-left));
    border-top: 1px solid var(--hairline);
    background: rgba(9, 18, 24, 0.76);
}
.group-room-mic,
.group-room-send {
    flex: 0 0 46px;
    width: 46px; height: 46px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border-radius: 50%;
    cursor: pointer;
}
.group-room-mic { border: 1px solid var(--hairline); background: var(--surface); }
.group-room-mic canvas { opacity: 0.58; }
.group-room-mic.is-on {
    border-color: #58dec8;
    background: #113029;
    box-shadow: 0 0 0 1px rgba(88, 222, 200, 0.25);
}
.group-room-mic.is-on canvas { opacity: 1; }
.group-room-mic.is-muted {
    border-color: #7c2f44;
    background: #281421;
}
.group-room-mic.is-muted canvas { opacity: 0.82; }
.group-room-mic.is-faulted {
    border-color: #b94b65;
    background: #351622;
    box-shadow: 0 0 0 1px rgba(255, 182, 194, 0.18);
}
.group-room-mic.is-faulted canvas { opacity: 1; }
.group-room-mic:disabled { opacity: 0.32; cursor: default; }
.group-room-send { border: 0; background: #1f7d70; color: #eafffb; }
.group-room-send:active:not(:disabled) { background: #1a6a60; }
.group-room-send:disabled { opacity: 0.35; cursor: default; }
.group-room-send span { font-size: 18px; transform: translateX(1px) rotate(-5deg); }
.group-room-compose-field { position: relative; flex: 1 1 auto; min-width: 0; }
.group-room-compose-field textarea {
    display: block;
    width: 100%;
    min-height: 46px;
    max-height: 108px;
    resize: none;
    overflow-y: auto;
    padding: 11px 14px;
    border: 1px solid var(--hairline);
    border-radius: 23px;
    background: var(--canvas-raised);
    color: var(--text-primary);
    font: inherit;
    /* 16px, not the app's 15px (D093): iOS Safari auto-zooms the whole
       page when a focused input's font is under 16px, and the zoom
       survives the blur. Only the composer needs the exemption. */
    font-size: 16px;
    line-height: 22px;
}
.group-room-compose-field textarea::placeholder { color: var(--text-disabled); }
/* The character counter (D089), floating in the field's corner. While
   it shows, the draft's lines yield the corner (D095) — past ~400
   chars the counter sat on the last line's tail. */
.group-room-compose-field textarea.has-counter { padding-right: 62px; }
.group-room-counter {
    position: absolute;
    right: 10px;
    bottom: 4px;
    color: var(--text-secondary);
    font-size: 11px;
    pointer-events: none;
}
.group-room-listening {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 0 13px;
    border: 1px solid #58dec8;
    border-radius: 23px;
    background: #113029;
    color: #a5f0e3;
    font-size: var(--type-meta);
    font-weight: 600;
}
.group-room-listening > span { margin-left: 6px; }
.group-room-listening i {
    width: 2px; height: 7px;
    border-radius: 2px;
    background: #58dec8;
    animation: group-room-listen-bars 0.75s ease-in-out infinite alternate;
}
.group-room-listening i:nth-child(2) { height: 14px; animation-delay: 0.18s; }
.group-room-listening i:nth-child(3) { height: 10px; animation-delay: 0.36s; }
@keyframes group-room-listen-bars { to { transform: scaleY(0.45); opacity: 0.55; } }
/* The duck made visible (D174): while a read-aloud clip is audible the
   mic is paused — the pill dims and its bars stand still. */
.group-room-listening.is-paused {
    border-color: var(--hairline);
    background: var(--surface);
    color: var(--text-secondary);
}
.group-room-listening.is-paused i {
    background: var(--text-secondary);
    opacity: 0.35;
    animation: none;
}

/* Settings */
.group-room-feature-toggles {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    margin-bottom: 14px;
}
.group-room-feature-toggle {
    min-width: 0;
    min-height: 74px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-standard);
    background: var(--surface);
    color: var(--text-primary);
    text-align: left;
    cursor: pointer;
}
.group-room-feature-toggle canvas { flex: 0 0 auto; }
.group-room-feature-toggle > span {
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.group-room-feature-toggle strong {
    overflow: hidden;
    font-size: var(--type-body);
    font-weight: 650;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.group-room-feature-toggle small {
    margin-top: 3px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-feature-toggle.is-on {
    border-color: #2b7d6f;
    background: #113029;
}
.group-room-feature-toggle.is-on small { color: #7ee6d2; }
.group-room-feature-toggle:disabled { opacity: 0.42; cursor: default; }

.group-room-settings-card {
    margin-bottom: 14px;
    overflow: hidden;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-standard);
    background: var(--surface);
}
.group-room-settings-card > h2 {
    margin: 0;
    padding: 11px var(--card-padding) 7px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.group-room-setting-language,
.group-room-setting-row {
    width: 100%;
    min-height: 52px;
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 6px var(--card-padding);
    border: 0;
    border-top: 1px solid rgba(51, 65, 95, 0.7);
    background: transparent;
    color: var(--text-primary);
    text-align: left;
}
.group-room-settings-list > :first-child { border-top: 0; }
.group-room-setting-row { cursor: pointer; }
.group-room-setting-row:disabled { opacity: 0.45; cursor: default; }
.group-room-setting-language > span,
.group-room-setting-copy {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.group-room-setting-language strong,
.group-room-setting-copy strong { font-size: var(--type-body); font-weight: 620; }
.group-room-setting-language small,
.group-room-setting-copy small { margin-top: 2px; color: var(--text-secondary); font-size: var(--type-meta); }
.group-room-language-picker {
    flex: 0 0 auto;
    min-width: 108px;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    padding: 0 11px;
    border: 1px solid #2b5a52;
    border-radius: 14px;
    background: #113029;
    color: #a5f0e3;
    cursor: pointer;
}
.group-room-setting-chevron {
    flex: 0 0 auto;
    color: var(--text-secondary);
    font-size: 24px;
    line-height: 1;
}
.group-room-aa { font-size: 13px; font-weight: 750; }
.group-room-switch {
    position: relative;
    flex: 0 0 42px;
    width: 42px; height: 24px;
    border: 1px solid #52617d;
    border-radius: 13px;
    background: #11182b;
}
.group-room-switch i {
    position: absolute;
    top: 3px; left: 3px;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: #8794af;
    transition: transform var(--state-duration) var(--ease-out),
                background var(--state-duration) var(--ease-out);
}
.group-room-setting-row.is-on .group-room-switch { border-color: #2b5a52; background: #16483f; }
.group-room-setting-row.is-on .group-room-switch i { background: #78ead7; transform: translateX(18px); }
.group-room-setting-row:disabled { opacity: 0.38; cursor: default; }
.group-room-rate {
    min-height: 54px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 7px var(--card-padding);
    border-top: 1px solid rgba(51, 65, 95, 0.7);
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-rate > span:last-child { display: flex; gap: 4px; }
.group-room-rate button {
    min-width: 46px;
    height: 44px;
    padding: 0 7px;
    border: 1px solid var(--hairline);
    border-radius: 11px;
    background: #11182b;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    cursor: pointer;
}
.group-room-rate button.is-selected { border-color: #58dec8; background: #113029; color: #a5f0e3; }
/* Recognition model (D087): the host-only lever row in the Voice card,
   the same row rhythm as the reading speed above it. */
.group-room-model {
    min-height: 54px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 7px var(--card-padding);
    border-top: 1px solid rgba(51, 65, 95, 0.7);
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
/* My voice (D077 picker, D078 identity card): the gender pair and the
   premium combobox + audition button, in their own Settings card.
   The speaking-style chips (D083) wrap below on their own full row. */
.group-room-style {
    flex: 1 1 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 7px 12px;
    padding-top: 2px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-spark { font-size: 15px; margin-right: 4px; }
.group-room-identity-help {
    margin: 0;
    padding: 0 var(--card-padding) 4px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-premium-picker {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 9px 12px;
    padding: 9px var(--card-padding) 13px;
}
.group-room-gender { display: flex; gap: 4px; }
.group-room-gender button {
    min-width: 56px;
    height: 44px;
    padding: 0 11px;
    border: 1px solid var(--hairline);
    border-radius: 11px;
    background: #11182b;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    cursor: pointer;
}
.group-room-gender button.is-selected { border-color: #58dec8; background: #113029; color: #a5f0e3; }
.group-room-voice-row {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}
.group-room-voice-select { flex: 1 1 auto; min-width: 0; max-width: 190px; }
.group-room-voice-test {
    flex: 1 1 100%;
    min-width: 0;
    height: 44px;
    padding: 0 11px;
    border: 1px solid var(--hairline);
    border-radius: 14px;
    background: #11182b;
    color: var(--text-primary);
    font: inherit;
    /* 16px like the Chat composer (D094): under that, iOS Safari
       zooms the page the moment this field is tapped. */
    font-size: 16px;
}
.group-room-voice-test::placeholder { color: var(--text-tertiary); }
.group-room-voice-select select {
    width: 100%;
    height: 44px;
    padding: 0 11px;
    border: 1px solid #2b5a52;
    border-radius: 14px;
    background: #113029;
    color: #a5f0e3;
    font: inherit;
    /* iOS zooms focused selects under 16px too (D094). */
    font-size: 16px;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}
#group-room-voice-preview {
    flex: 0 0 44px;
    width: 44px; height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid #2b5a52;
    border-radius: 50%;
    background: #113029;
    color: #a5f0e3;
    font-size: 13px;
    cursor: pointer;
}
#group-room-voice-preview:disabled { opacity: 0.45; cursor: default; }
/* A pending preview waits in the gateway's TTS queue behind the room's
   read-aloud work, so the wait is long: the glyph yields to a spinner,
   at full strength — the disabled dim would read as broken, not busy. */
#group-room-voice-preview.is-busy:disabled { opacity: 1; }
#group-room-voice-preview.is-busy > span { display: none; }
#group-room-voice-preview.is-busy::after {
    content: "";
    width: 16px; height: 16px;
    border: 2px solid rgba(165, 240, 227, 0.25);
    border-top-color: #a5f0e3;
    border-radius: 50%;
    animation: group-preview-spin 0.9s linear infinite;
}
@keyframes group-preview-spin {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    /* No motion: a static ellipsis carries "working" instead. */
    #group-room-voice-preview.is-busy::after {
        content: "\2026";
        width: auto; height: auto;
        border: 0;
        border-radius: 0;
        animation: none;
    }
}
/* Long-press message actions (D173). */
.group-room-msg-layer { position: fixed; inset: 0; z-index: 68; }
.group-room-msg-dialog {
    position: absolute;
    left: max(10px, var(--safe-left));
    right: max(10px, var(--safe-right));
    bottom: max(10px, var(--safe-bottom));
    width: min(420px, calc(100% - 20px));
    max-height: min(88dvh, 700px);
    margin: 0 auto;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    padding: 8px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: linear-gradient(180deg, #1a2238 0%, #10182b 100%);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.group-room-msg-dialog button {
    width: 100%;
    min-height: 48px;
    padding: 0 16px;
    border: 0;
    border-radius: 12px;
    background: transparent;
    color: var(--text-primary);
    font-size: var(--type-body);
    font-weight: 620;
    text-align: left;
    cursor: pointer;
}
.group-room-msg-dialog button:active { background: rgba(88, 222, 200, 0.08); }
.group-room-msg-dialog button.is-danger { color: #ff9db1; }
.group-room-msg-dialog button.is-cancel {
    margin-top: 4px;
    color: var(--text-secondary);
    font-weight: 500;
    text-align: center;
}

/* Participant moderation keeps consequential choices in a bounded sheet. */
.group-room-participant-layer {
    position: fixed;
    inset: 0;
    z-index: 67;
}
.group-room-action-dialog {
    position: absolute;
    left: max(10px, var(--safe-left));
    right: max(10px, var(--safe-right));
    bottom: max(10px, var(--safe-bottom));
    width: min(560px, calc(100% - 20px));
    max-height: min(88dvh, 720px);
    margin: 0 auto;
    overflow-y: auto;
    padding: 14px 18px 16px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: linear-gradient(180deg, #1a2238 0%, #10182b 100%);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.group-room-action-dialog h3 {
    margin: 16px 2px 7px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.group-room-action-lead {
    margin: 2px 0 10px;
    color: #a5f0e3;
    font-size: var(--type-body);
}
.group-room-action-button,
.group-room-floor-choice {
    width: 100%;
    min-height: 52px;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 7px 12px;
    border: 1px solid var(--hairline);
    border-radius: 12px;
    background: #11182b;
    color: var(--text-primary);
    text-align: left;
    cursor: pointer;
}
.group-room-action-button + .group-room-action-button,
.group-room-floor-choice + .group-room-floor-choice { margin-top: 7px; }
.group-room-action-button > span,
.group-room-floor-choice > span:first-child {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.group-room-action-button strong,
.group-room-floor-choice strong {
    color: var(--text-primary);
    font-size: var(--type-body);
}
.group-room-action-button small,
.group-room-floor-choice small {
    margin-top: 2px;
    color: var(--text-secondary);
    font-size: var(--type-meta);
}
.group-room-action-button.is-selected,
.group-room-floor-choice.is-selected {
    border-color: #58dec8;
    background: #113029;
}
.group-room-action-button.is-danger {
    border-color: #7c2f44;
    background: #281421;
}
.group-room-action-button.is-danger strong { color: #ffb6c2; }
.group-room-action-button:disabled { opacity: 0.4; cursor: default; }
.group-room-floor-choice-state {
    flex: 0 0 auto;
    color: #7ee6d2;
    font-size: 20px;
}

/* Room destinations occupy the existing app header. `hidden` controls the
   inactive state; `.tab-bar` above is the complementary application nav. */
.group-room-tabs {
    width: 100%;
    height: 100%;
    display: flex;
}
.group-room-tab {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 2px 4px 0;
    border: 0;
    background: transparent;
    color: var(--tab-idle);
    font-size: var(--type-meta);
    cursor: pointer;
}
.group-room-tab::before {
    content: "";
    position: absolute;
    top: 0; left: 50%;
    width: 0; height: 3px;
    border-radius: 0 0 3px 3px;
    background: #58dec8;
    transform: translateX(-50%);
    transition: width 170ms var(--ease-out);
}
.group-room-tab.is-active { color: var(--text-primary); font-weight: 650; }
.group-room-tab.is-active::before { width: 34px; }
.group-room-tab > canvas,
.group-room-tab-icon { flex: 0 0 auto; }
.group-room-tab-icon { position: relative; }
.group-room-tab-icon b {
    position: absolute;
    top: -7px; right: -15px;
    min-width: 19px; height: 19px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 5px;
    border: 2px solid #080f1b;
    border-radius: 10px;
    background: #ff6f91;
    color: #111827;
    font-size: 10px;
    font-weight: 800;
}

/* Contextual help sheet for Invite and Settings. */
.group-room-help-layer { position: fixed; inset: 0; z-index: 66; }
.group-room-help-scrim { position: absolute; inset: 0; background: rgba(5, 9, 22, 0.7); }
.group-room-help-dialog {
    position: absolute;
    left: max(10px, var(--safe-left));
    right: max(10px, var(--safe-right));
    bottom: max(10px, var(--safe-bottom));
    width: min(560px, calc(100% - 20px));
    max-height: min(78dvh, 640px);
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    padding: 14px 18px 16px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: linear-gradient(180deg, #1a2238 0%, #10182b 100%);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.group-room-help-head { display: flex; align-items: center; justify-content: space-between; }
.group-room-help-head h2 { margin: 0; font-size: var(--type-hero); font-weight: 650; }
.group-room-help-head button {
    width: 40px; height: 40px;
    border: 0; border-radius: 50%;
    background: transparent;
    color: var(--text-secondary);
    font-size: 26px;
    cursor: pointer;
}
.group-room-help-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px; }
.group-room-help-body p,
.group-room-help-body li { color: var(--text-primary); font-size: var(--type-body); line-height: 1.48; }
.group-room-help-body p { margin: 8px 0; }
.group-room-help-body ol { margin: 8px 0; padding-left: 22px; }
.group-room-help-body li { margin: 7px 0; }
.group-room-help-body h3 {
    margin: 16px 0 4px;
    color: var(--text-primary);
    font-size: var(--type-value);
}
.group-room-help-tags { display: flex; flex-wrap: wrap; gap: 5px; }
.group-room-help-tags code {
    padding: 3px 8px;
    border: 1px solid var(--hairline);
    border-radius: 9px;
    background: #11182b;
    color: #a5f0e3;
    font-size: var(--type-meta);
    white-space: nowrap;
}
.group-room-help-done {
    flex: 0 0 auto;
    width: 100%; height: 48px;
    margin-top: 14px;
    border: 0;
    border-radius: var(--radius-standard);
    background: #1f7d70;
    color: #eafffb;
    font-size: var(--type-value);
    font-weight: 600;
    cursor: pointer;
}

.group-room-voice-layer { position: fixed; inset: 0; z-index: 67; }
.group-room-voice-dialog {
    position: absolute;
    left: max(10px, var(--safe-left));
    right: max(10px, var(--safe-right));
    bottom: max(10px, var(--safe-bottom));
    width: min(560px, calc(100% - 20px));
    max-height: min(88dvh, 700px);
    margin: 0 auto;
    overflow-y: auto;
    padding: 14px 18px 16px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: linear-gradient(180deg, #1a2238 0%, #10182b 100%);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.group-room-voice-dialog .group-room-help-head h2 {
    margin: 0;
    font-size: var(--type-hero);
}
.group-room-voice-dialog .group-room-identity-help {
    padding: 2px 0 8px;
}
.group-room-voice-dialog .group-room-premium-picker {
    padding: 8px 0 2px;
}

.group-room-shell button:focus-visible,
.group-room-shell textarea:focus-visible,
.group-room-shell select:focus-visible,
.group-room-tabs button:focus-visible,
.group-room-help-dialog button:focus-visible,
.group-room-voice-dialog button:focus-visible,
.group-room-voice-dialog input:focus-visible,
.group-room-voice-dialog select:focus-visible,
.group-room-action-dialog button:focus-visible,
.group-room-msg-dialog button:focus-visible {
    outline: 2px solid #58dec8;
    outline-offset: 2px;
}

@media (max-width: 360px) {
    .group-room-column { padding-left: 12px; padding-right: 12px; }
    .group-room-qr-card { width: min(208px, 62vw); height: min(208px, 62vw); }
}

@media (orientation: landscape) and (max-height: 500px) {
    .group-room-topbar { flex-basis: 34px; }
    .group-room-playback-mute { min-height: 30px; }
    .group-room-composer { min-height: 54px; padding-top: 4px; padding-bottom: 4px; }
    .group-room-tab { min-height: 46px; flex-direction: row; gap: 7px; padding-top: 0; }
    .group-room-column { padding-top: 10px; }
    .group-room-feature-toggle { min-height: 64px; }
}

@media (prefers-reduced-motion: reduce) {
    .group-room-listening i,
    .group-room-message.is-playing .group-room-message-meta::after {
        animation: none;
    }
    /* The fill parks full instead of moving: still says "this one is
       being read", without the motion. */
    .group-room-read-progress i { animation: none; transform: none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   SPEAK HUB + MODE PAGES  (D076, from the reviewed ui-prototype)
   ═══════════════════════════════════════════════════════════════════════ */

/* The four doors. Portrait phones (under 560px) get a vertical list of
   row-style cards; landscape phones, tablets and desktop get the 2x2 grid
   of centered cards. 560 sits above every portrait phone (~320-480 CSS px)
   and below every landscape one (~568+). */
.hub-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 10px;
    margin-top: 12px;
}
.hub-card {
    --lift-radius: var(--radius-standard, 14px);
    --lift-g0: #33415f; --lift-g1: #232f49; --lift-g2: #141b2f;
    --lift-border: #4d5f80;
    min-height: 68px;
    padding: 10px 15px;
    display: grid;
    grid-template-columns: 40px 1fr;
    grid-template-rows: auto auto;
    column-gap: 14px; row-gap: 3px;
    align-items: center;
    justify-items: start;
    text-align: left;
    font-family: inherit;
}
.hub-card canvas {
    grid-row: 1 / span 2;
    justify-self: center;
}
.hub-card.is-multi {
    --lift-g0: #2b4a50; --lift-g1: #1e3541; --lift-g2: #12202e;
    --lift-border: #3f6a6d;
}
.hub-card-name {
    font-size: 16px; font-weight: 600;
    color: var(--text-primary);
    align-self: end;
}
.hub-card-sub {
    font-size: 12px;
    color: var(--text-secondary);
    align-self: start;
}
/* The filter's download, at the top of the hub while it runs and gone
   afterwards. Deliberately not a card and not a dialog: it is the only
   thing on the page that is doing anything, but it is also temporary, so it
   states itself in one line and takes the height of one. */
.hub-filter {
    margin-bottom: 14px;
    padding: 12px 14px;
    border: 1px solid #33415f;
    border-radius: var(--radius-standard, 14px);
    background: #141b2f;
}
.hub-filter-message {
    margin: 0 0 9px;
    font-size: 14px;
    line-height: 1.35;
    color: var(--text-secondary);
}
/* Only after a failure, so it is the one thing in the widget that is ever
   tappable — and at the full 44 px, because it is the only way forward for
   somebody whose network dropped mid-download. */
.hub-filter-retry {
    margin-top: 11px;
    min-height: 44px;
    padding: 0 18px;
    border: 1px solid #3a4a63;
    border-radius: 10px;
    background: transparent;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 15px;
}
.hub-filter-retry:active { background: #1a2230; }

.hub-card:disabled { opacity: 0.5; cursor: default; }

@media (min-width: 560px) {
    .hub-grid { grid-template-columns: 1fr 1fr; gap: 14px; }
    .hub-card {
        min-height: 108px;
        padding: 16px 14px 14px;
        display: flex; flex-direction: column;
        align-items: center; justify-content: center;
        gap: 7px;
        text-align: center;
    }
    .hub-card canvas { flex: 0 0 auto; }
    .hub-card-name, .hub-card-sub { align-self: auto; }
}

.hub-disclosure {
    max-width: 620px;
    margin: 13px auto 0;
    color: #b7c3d9;
    font-size: 14px;
    line-height: 1.42;
}

@media (max-width: 340px) {
    .hub-grid { gap: 8px; margin-top: 10px; }
    .hub-card { min-height: 64px; padding-top: 8px; padding-bottom: 8px; }
    .hub-disclosure { margin-top: 10px; line-height: 1.35; }
}

/* ── Mode page header: back door, centred live activity, and Help. ── */
.mode-head {
    position: relative;
    display: flex; align-items: center; gap: 12px;
    margin-bottom: 10px;
}
.mode-title {
    margin: 0;
    font-size: 22px; font-weight: 650;
    color: var(--text-primary);
    letter-spacing: 0.2px;
}
.mode-back {
    position: relative;
    isolation: isolate;
    flex: 0 0 auto;
    width: 44px; height: 44px;
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 20px;
    font-weight: 600;
    line-height: 1;
    padding: 0 0 2px;
    cursor: pointer;
}
.mode-back::before,
.mode-help-btn::before {
    content: "";
    position: absolute;
    inset: 7px;
    z-index: -1;
    border: 1px solid var(--hairline, #2a3550);
    border-radius: 50%;
    background: rgba(13, 20, 36, 0.55);
}
.mode-back:active,
.mode-help-btn:active { color: var(--text-primary); }
.mode-back:active::before,
.mode-help-btn:active::before { border-color: #4d5f80; }
/* D158 — the listening-setup door, in the header's own idiom: the same
   44 px square, the same hairline disc behind it, a glyph instead of a
   letter. It takes the row's slack so Help sits beside it rather than a
   gap away, and gives it back on the take-turns lane, where the button is
   display:none and Help is the only thing on the right again. */
.mode-head-settings {
    position: relative;
    isolation: isolate;
    margin-left: auto;
    flex: 0 0 auto;
    width: 44px; height: 44px;
    border: 0;
    background: transparent;
    padding: 0;
    display: grid; place-items: center;
    cursor: pointer;
}
.mode-head-settings::before {
    content: "";
    position: absolute;
    inset: 2px;
    z-index: -1;
    border: 1px solid var(--hairline, #2a3550);
    border-radius: 50%;
    background: rgba(13, 20, 36, 0.55);
}
.mode-head-settings:active::before { border-color: #4d5f80; }
.mode-head-settings canvas { display: block; }
.page-solo.is-follow .mode-head .mode-help-btn { margin-left: 0; }
.mode-back[hidden] { display: none; }
/* The group room's shell covers the page under it, so the same control is
   repeated on its topbar — same glyph, same corner, same act (D107). */
.group-room-back {
    width: 44px; height: 44px;
    margin-left: -8px;
    font-size: 22px;
}
.group-room-back::before { inset: 5px; }
.mode-head-activity {
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    display: flex; align-items: center; justify-content: center;
    gap: 6px;
    height: 44px;
}
/* D143, extended to the 2-way page by D144 — these are the mode headers
   whose title is screen-reader-only, so these four round things ARE the row,
   and at a 30 px halo they read as small print above the page they head.
   Everything in it is drawn about a third larger. The row is not taller for
   it: the back and Help buttons already reserved 44 px of tap target around
   a 30 px drawing, so the halo simply fills more of the square it was always
   sitting in, and the two instruments grow into the same height. The gap
   below is trimmed to pay for the extra weight above it.

   Group is deliberately not in this list: its room shell hides this whole
   page column and heads itself from its own topbar. */
.mode-head-activity .indicator { width: 46px; height: 44px; }
.mode-head-activity .indicator-disc { width: 40px; height: 40px; }
.mode-head-activity .orbit {
    --orbit-size: 34px; --orbit-dot: 7px; --orbit-lead: 8px;
}
/* Qualified by `.mode-head` and not by the page alone: the group room's
   exit is also a `.mode-back` inside `#page-multi`, and it is sized for its
   own topbar. */
.page-solo .mode-head,
.page-multi .mode-head { margin-bottom: 6px; }
.page-solo .mode-head .mode-back::before,
.page-solo .mode-head .mode-help-btn::before,
.page-multi .mode-head .mode-back::before,
.page-multi .mode-head .mode-help-btn::before { inset: 2px; }
.page-solo .mode-head .mode-back,
.page-multi .mode-head .mode-back { font-size: 27px; padding-bottom: 3px; }
.page-solo .mode-head .mode-help-btn,
.page-multi .mode-head .mode-help-btn { font-size: 20px; }
.mode-help-btn {
    position: relative;
    isolation: isolate;
    margin-left: auto;
    flex: 0 0 auto;
    width: 44px; height: 44px;
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 15px;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
}

/* ── The live stage's status card (Solo page): every mode page is live
      from entry, and this is the line that says what the mic is doing. ── */
.mode-status {
    display: flex; align-items: center; gap: 12px;
    min-height: 48px;
    padding: 9px 13px;
    margin-bottom: 10px;
    border-radius: var(--radius-standard, 14px);
    background: rgba(13, 20, 36, 0.55);
    border: 1px solid var(--hairline, #2a3550);
    color: var(--text-primary);
    font-size: 14px; font-weight: 600;
}
.mode-status.is-error {
    border-color: rgba(255, 111, 145, 0.55);
    color: #ffd5df;
}
/* D156 — the microphone-quality readout folded into this card, and its
   amber came with it: a line asking the listener to move the mic has to be
   seen from across a lecture hall, which was the whole point of the dot.
   Since D157 the reading lives on its own line inside the frame, and this
   is the frame agreeing with it. */
.mode-status.is-caution {
    border-color: rgba(255, 189, 95, 0.55);
}
.mode-status canvas { flex: 0 0 auto; }
.mode-status.is-restarting canvas { opacity: 0.35; }
/* The two lines, not every span inside the card: since D164 the 2-way page's
   widget wears this frame too, and it carries an incoming caption and a
   one-time lesson below the lines. Dimming "every span" reached into those
   and greyed a live translation because the room was reconnecting. */
.mode-status.is-restarting .mode-status-lines > * {
    color: var(--text-secondary);
    font-weight: 500;
}

/* Follow's two-column instrument. Gemini detects the input language, so its
   former picker cell is the compact echo switch: Start/No echo on the left,
   output language/Mute on the right. Four equal cells make the relationship
   obvious and keep every control within the first thumbful of the page. */
.follow-desk {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
    column-gap: 8px; row-gap: 8px;
    align-items: center;
}
.follow-desk-icon { display: block; align-self: center; }
/* The middle icon sat in the gap with 8 px on either side, which left it
   heading neither column. A wider seam before it is what says it belongs to
   the one on its right — the same thing the page margin says for the one on
   the far left. */
.follow-desk > [data-desk-icon="listen"] { margin-left: 10px; }
.follow-desk > [data-desk-icon="speak"] { grid-column: 1; grid-row: 1 / span 2; }
.follow-desk > [data-desk-cell="speak-key"] { grid-column: 2; grid-row: 1; }
.follow-desk > [data-desk-cell="speak-aec"] { grid-column: 2; grid-row: 2; }
.follow-desk > [data-desk-icon="listen"] { grid-column: 3; grid-row: 1 / span 2; }
.follow-desk > [data-desk-cell="listen-lang"] { grid-column: 4; grid-row: 1; }
.follow-desk > [data-desk-cell="listen-key"] { grid-column: 4; grid-row: 2; }
.follow-desk .picker-value { font-size: 15px; }
@media (max-width: 360px) {
    .follow-desk { column-gap: 6px; }
    .follow-desk .picker-value { font-size: 14px; }
}

/* Follow's two keys since D145: Start/Stop and Mute/Unmute. Start wears the
   microphone treatment (pink, the "this is recording" colour the whole app
   uses) and Mute wears the playback one.

   D158 took their glyphs off and their height down to the picker's 52 px.
   The glyph is drawn once at the head of the column now, so repeating it on
   the key said nothing the column had not already said — and a word alone
   is what makes the four cells one group instead of two kinds of control
   that happen to be adjacent. */
.follow-key {
    height: 52px;
    padding: 0 10px;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-secondary);
    --lift-radius: 14px;
    --lift-g0: #2b354c;
    --lift-g1: #1e283c;
    --lift-g2: #131a2a;
    --lift-border: #465674;
    --lift-highlight: rgba(255, 255, 255, 0.18);
    --lift-inset: 18px;
}
.follow-key:disabled { color: #66728c; }
.follow-key-verb {
    font-size: 15px; font-weight: 700;
    min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.follow-key.is-selected { color: var(--text-primary); }
.follow-key.is-selected[data-follow-key="session"] {
    --lift-g0: #814155;
    --lift-g1: #5d2d40;
    --lift-g2: #391c2a;
    --lift-border: rgba(255, 111, 145, 0.78);
    --lift-highlight: rgba(255, 255, 255, 0.25);
}
.follow-key.is-selected[data-follow-key="output"] {
    --lift-g0: #2c6b62;
    --lift-g1: #1d5049;
    --lift-g2: #11332f;
    --lift-border: rgba(88, 222, 200, 0.78);
    --lift-highlight: rgba(255, 255, 255, 0.25);
}

/* Follow capture setup. This is operational state, not a Settings form: a
   profile or microphone change immediately restarts the active translation
   configuration — which is why D145 gave it a popup of its own rather than
   folding it into the Account tab. Echo cancellation remains operational too,
   but now stays visible on the main Follow page. */
.follow-audio-setup {
    flex: 0 1 auto;
    margin: 0 var(--panel-padding);
    padding: 13px;
    overflow-y: auto;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-standard, 14px);
    background: rgba(13, 20, 36, 0.48);
}
/* A bottom sheet, not a full page: two controls do not fill a phone, and
   a full-height surface for them reads as a route rather than a popup. */
#follow-setup-sheet .sheet {
    top: auto; bottom: 0;
    height: auto;
    max-height: 100%;
    border-radius: var(--radius-modal) var(--radius-modal) 0 0;
}
#follow-setup-sheet .mode-help-done {
    width: auto;
    margin: 14px var(--panel-padding) calc(var(--panel-padding) + 4px);
}
.follow-profile-switch {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
}
.follow-profile-switch button {
    min-width: 0; min-height: 58px;
    display: flex; flex-direction: column; align-items: flex-start;
    justify-content: center; gap: 3px;
    padding: 8px 11px;
    border: 1px solid var(--hairline);
    border-radius: 13px;
    background: #11182b;
    color: var(--text-primary);
    font: inherit;
    font-size: 14px; font-weight: 600;
    text-align: left;
    cursor: pointer;
}
.follow-profile-switch button small {
    max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--text-secondary);
    font-size: 11px; font-weight: 500;
}
.follow-profile-switch button[aria-pressed="true"] {
    border-color: rgba(101, 224, 201, 0.58);
    background: #123029;
    color: #b9f4e8;
    box-shadow: inset 0 0 0 1px rgba(101, 224, 201, 0.08);
}
.follow-profile-switch button[aria-pressed="true"] small { color: #83cdbf; }
.follow-microphone-row {
    display: flex; align-items: center; gap: 12px;
    margin-top: 10px;
    color: var(--text-secondary);
    font-size: 13px; font-weight: 600;
}
.follow-microphone-row > span { flex: 0 0 auto; }
.follow-microphone-row select {
    flex: 1 1 auto; min-width: 0;
    height: 44px;
    padding: 0 10px;
    border: 1px solid #2b5a52;
    border-radius: 12px;
    background: #101f24;
    color: #b9f4e8;
    font: inherit;
    font-size: 16px;
    cursor: pointer;
}
.follow-microphone-row select:disabled { opacity: 0.55; cursor: default; }
.follow-aec-key {
    width: 100%; height: 52px;
    display: flex; align-items: center; justify-content: space-between;
    gap: 8px;
    padding: 0 10px;
    color: var(--text-secondary);
    font: inherit;
    cursor: pointer;
    --lift-g0: #202a40;
    --lift-g1: #172036;
    --lift-g2: #0f1729;
    --lift-border: #3a4966;
    --lift-highlight: rgba(255, 255, 255, 0.17);
}
/* D229 — words now, not an acronym, so the acronym's tracking went; a
   narrow phone may break "No echo" onto two lines inside the key. */
.follow-aec-label {
    flex: 1 1 auto;
    min-width: 0;
    text-align: center;
    font-size: 14px;
    font-weight: 750;
    line-height: 1.15;
}
.follow-echo-switch {
    position: relative;
    flex: 0 0 42px;
    width: 42px; height: 24px;
    border: 1px solid #52617d;
    border-radius: 13px;
    background: #101626;
}
.follow-echo-switch i {
    position: absolute;
    top: 3px; left: 3px;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: #8794af;
    transition: transform var(--state-duration) var(--ease-out),
                background var(--state-duration) var(--ease-out);
}
.follow-aec-key[aria-pressed="true"] {
    color: #b9f4e8;
    --lift-g0: #246257;
    --lift-g1: #19483f;
    --lift-g2: #102f2b;
    --lift-border: rgba(88, 222, 200, 0.78);
    --lift-highlight: rgba(255, 255, 255, 0.22);
}
.follow-aec-key[aria-pressed="true"] .follow-echo-switch {
    border-color: #2b5a52;
    background: #16483f;
}
.follow-aec-key[aria-pressed="true"] .follow-echo-switch i {
    background: #78ead7;
    transform: translateX(18px);
}
.follow-aec-key:focus-visible {
    outline: 2px solid var(--accent-utility);
    outline-offset: 2px;
}
.follow-profile-help {
    margin: 9px 1px 0;
    color: var(--text-secondary);
    font-size: 12px; line-height: 1.4;
}

@media (max-width: 430px) {
    .follow-profile-switch button { padding-inline: 9px; }
    .follow-microphone-row { align-items: flex-start; flex-direction: column; gap: 6px; }
    .follow-microphone-row select { width: 100%; flex-basis: 44px; }
}

/* The status line takes the width so the meter can sit on the far edge of
   the same card rather than under it. */
.mode-status-lines { flex: 1 1 auto; min-width: 0; }

/* D157 — the second line. Meta-sized and secondary, because it answers a
   smaller question than the line above it and must not compete with it for
   the glance. Amber when it is asking the listener to move the phone: at
   that size the colour is doing most of the work, and the card's border
   turns with it so the whole thing is visible from a lecture seat. */
.mode-status-lines {
    display: flex; flex-direction: column; justify-content: center;
    gap: 2px;
}
.mode-status-mic {
    color: var(--text-secondary);
    font-size: var(--type-meta); font-weight: 500;
    line-height: 1.35;
}
.mode-status-mic[hidden] { display: none; }
.mode-status-mic.is-caution { color: #ffd79c; font-weight: 600; }

/* ── The turn meter ──
   Seconds banked in this message against the cap it is heading for. It is
   the transport's retained readout moved to the line that already says what
   the microphone is doing, which is where a speaker looks — the transport
   keeps the number only for follow-along, where it means lag instead.

   Three states, and each one is a fact about the microphone: pink while the
   VAD hears a voice, dim while a translation plays over a paused mic, and
   warm at the cap, where the oldest audio starts being trimmed. */
.mode-meter {
    flex: 0 0 auto;
    font-size: var(--type-body);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--text-secondary);
    white-space: nowrap;
    transition: color var(--state-duration) var(--ease-out);
}
.mode-meter[hidden] { display: none; }
.mode-meter.is-voice { color: var(--accent-solo); }
.mode-meter.is-paused { color: var(--text-disabled); }
.mode-meter.is-near-cap { color: var(--warning); }

/* ── The glanceable tail ──
   Two or three lines of what was just said, below the transport. It is not
   the transcript and must not grow into one: the Transcript tab holds the
   record, and this holds only enough to look up from a conversation and
   catch the sentence you missed. Older rows fade so the newest reads first
   without a label saying which is newest. */
.mode-recent {
    list-style: none;
    /* Below the transport on both pages since D108, so the gap it needs is
       above it; anything that follows (the transcript disclosure) brings its
       own margin-top. */
    margin: 12px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.mode-recent[hidden] { display: none; }
.recent-row {
    padding: 11px var(--card-padding);
    border-radius: var(--radius-card);
    background: var(--surface);
    border: 1px solid var(--hairline);
}
.mode-recent .recent-row:nth-last-child(2) { opacity: 0.78; }
.mode-recent .recent-row:nth-last-child(3) { opacity: 0.58; }
.recent-who {
    font-size: var(--type-meta);
    color: var(--text-secondary);
    margin-bottom: 4px;
}
.recent-src {
    font-size: var(--type-meta);
    color: var(--text-disabled);
    margin-bottom: 3px;
}
.recent-dst {
    font-size: var(--type-body);
    color: var(--text-primary);
}
/* ── D147, the caption surface ──
   Follow Along reads this rather than glancing at it: with the ear muted it
   is the whole of the mode, so the rows stop being three cards of record
   and become one block of running text. The non-rollup 2-way tail keeps the
   older-row fade; Follow Along's fixed line items override it below. */
.mode-recent.is-captions {
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    padding: 12px var(--card-padding);
    gap: 7px;
}
.mode-recent.is-captions .recent-row {
    padding: 0;
    border: 0;
    background: none;
    border-radius: 0;
}
.mode-recent.is-captions .recent-dst { line-height: 1.5; }

/* Follow Along uses LiveLink Lite's roll-up caption shape: a fixed-height
   viewport containing independently positioned, single-line text items.
   Only transform changes during overflow, so retained lines never re-wrap or
   jump sideways when the oldest line leaves. */
.mode-recent.is-captions.is-rollup {
    position: relative;
    display: block;
    overflow: hidden;
    /* Six lines (D213) at the 1.5 line pitch, plus the vertical padding. */
    height: var(--caption-window-height, calc(9em + 26px));
    gap: 0;
    /* D213 — two sizes up from the body type, on the host as well as the
       lines: the roller takes its line pitch from the host's font-size
       (CaptionRoller.layout), and the measuring probe reads the same
       variable, so text, pitch and wrap width agree. */
    --caption-font-size: calc((var(--type-body) + 2px) * var(--caption-scale, 1));
    font-size: var(--caption-font-size);
    /* D229 — and the size is never in flight when the roller reads it.
       Reduce Motion gives every element a 1 ms transition (tokens.css) and
       `all` is the initial transition-property, so an A−/A+ tap animated
       font-size for a millisecond: the roller read the old size, kept its
       old pitch and window, and clipped the larger text's right edge. The
       host animates nothing of its own; its lines keep their roll. */
    transition-property: none;
}
.mode-recent.is-rollup .recent-dst { font-size: var(--caption-font-size); }
/* D229 — the listener's size (A−/A+ above the card): one step below the
   default and three above it, about 15 to 25 px. The window keeps its six
   lines at every size, because its height is measured from this same
   font-size: a larger caption holds fewer words a line, never fewer lines. */
.mode-recent.is-rollup[data-caption-size="small"] { --caption-scale: 0.9; }
.mode-recent.is-rollup[data-caption-size="large"] { --caption-scale: 1.15; }
.mode-recent.is-rollup[data-caption-size="larger"] { --caption-scale: 1.3; }
.mode-recent.is-rollup[data-caption-size="largest"] { --caption-scale: 1.45; }
.mode-recent.is-rollup .recent-caption-line {
    position: absolute;
    top: var(--caption-content-top, 12px);
    left: var(--card-padding);
    right: var(--card-padding);
    height: var(--caption-line-height, 1.5em);
    overflow: hidden;
    opacity: 1;
    will-change: transform;
    transition: transform 480ms cubic-bezier(0.37, 0, 0.63, 1);
}
.mode-recent.is-rollup .recent-caption-line .recent-dst {
    height: var(--caption-line-height, 1.5em);
    line-height: var(--caption-line-height, 1.5em);
    white-space: nowrap;
    overflow: hidden;
}
.recent-caption-measure {
    position: absolute;
    visibility: hidden;
    white-space: nowrap;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .mode-recent.is-rollup .recent-caption-line { transition: none; }
}

/* D229 — the caption size keys. Low: two small faces at the card's
   top-right corner, in the card's own surface and ink rather than a desk
   key's lift, because they are touched once a talk. The face is 32 px; a
   transparent band above and below makes the target the app's 44 px. */
.follow-caption-size {
    display: flex;
    justify-content: flex-end;
    gap: 6px;
    margin-top: 10px;
}
.follow-caption-size:not([hidden]) + .mode-recent { margin-top: 6px; }
.follow-caption-size button {
    position: relative;
    width: 44px; height: 32px;
    padding: 0;
    border: 1px solid var(--hairline);
    border-radius: 9px;
    background: var(--surface);
    color: var(--text-secondary);
    font: inherit;
    font-size: 13px; font-weight: 700;
    cursor: pointer;
}
.follow-caption-size button::before {
    content: "";
    position: absolute;
    inset: -6px 0;
}
.follow-caption-size button:disabled { opacity: 0.4; cursor: default; }
.follow-caption-size button:focus-visible {
    outline: 2px solid var(--accent-utility);
    outline-offset: 2px;
}

/* ── The 2-way invite, inline ──
   Same QR card and the same two buttons the panel's sheet draws, on the
   page column instead of over it. A host waiting for a guest has exactly
   one job, and this is it. */
.duo-invite {
    display: flex; flex-direction: column; align-items: center;
    gap: 14px;
    margin-bottom: 12px;
    padding: var(--panel-padding);
    border-radius: var(--radius-card);
    background: var(--surface);
    border: 1px solid var(--hairline);
}
.duo-invite[hidden] { display: none; }
.duo-invite-title {
    align-self: stretch;
    font-size: var(--type-card); font-weight: 650;
    color: var(--text-primary);
}
/* D237 — the QR takes what the column leaves (fitDuoInvite measures it);
   before the first measurement it is the shared card's own size. */
.duo-invite .qr-card { width: var(--duo-qr-size, clamp(200px, 100%, 288px)); }
@media (max-height: 700px) {
    .duo-invite { gap: 8px; padding-block: 10px; margin-bottom: 6px; }
}
.duo-invite .invite-row { align-self: stretch; }
.duo-invite .invite-button { flex: 1 1 0; }
.duo-invite-waiting {
    align-self: stretch;
    font-size: var(--type-body);
    color: var(--text-secondary);
}

/* The peer's language: the second line of the cell that names them, and the
   fact that decides whether this call translates at all. It steps aside for
   the presence line whenever the answer to "can they hear me" stops being
   yes — see renderConnectPanel. */
.peer-language {
    font-size: 11.5px;
    font-weight: 600;
    color: var(--text-secondary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.peer-language[hidden] { display: none; }

/* ── Mode guide popup: ~95% of the screen, scrollable body, two ways out
      (X up top, Close pinned at the bottom). ── */
.mode-help {
    position: absolute;
    inset: 2.5vh 2.5vw;
    display: flex;
    flex-direction: column;
    padding: 16px 20px 18px;
    border-radius: 18px;
    border: 1px solid var(--hairline, #2a3550);
    background: linear-gradient(180deg, #1a2238 0%, #10182b 100%);
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
}
.mode-help-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 4px;
}
.mode-help-title {
    margin: 0;
    font-size: 22px;
    font-weight: 650;
    color: var(--text-primary);
}
.mode-help-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding-right: 4px;
}
.mode-help-body p {
    margin: 10px 0;
    color: var(--text-primary);
    font-size: 15px;
    line-height: 1.55;
}
.mode-help-body h3 {
    margin: 20px 0 6px;
    font-size: 16px;
    font-weight: 650;
    color: var(--text-primary);
}
.mode-help-body ol,
.mode-help-body ul {
    margin: 8px 0;
    padding-left: 22px;
}
.mode-help-body li {
    margin: 7px 0;
    color: var(--text-primary);
    font-size: 15px;
    line-height: 1.5;
}
.mode-help-done {
    flex: 0 0 auto;
    margin-top: 14px;
    width: 100%;
    height: 48px;
    border: none;
    cursor: pointer;
    border-radius: var(--radius-standard, 14px);
    background: #1f7d70;
    color: #eafffb;
    font-size: 16px;
    font-weight: 600;
    font-family: inherit;
}
.mode-help-done:active { background: #1a6a60; }

/* ── The Transcript tab pins one panel open with the full layout: no
      disclosure header, no phone sheet — the page is the sheet. ── */
.transcript-page-column { display: flex; flex-direction: column; }
.transcript-panel.is-pinned .transcript-header { display: none; }
.transcript-panel.is-pinned {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}
.transcript-panel.is-pinned .transcript-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    display: flex;
    flex-direction: column;
    overflow: visible;
    transition: none;
}
.transcript-panel.is-pinned .transcript-scroll {
    flex: 1 1 auto;
    min-height: 120px;
    max-height: none;
}

/* ── A live session keeps running while the user visits the Transcript,
      Settings or Account tabs (D076 — leaving Speak no longer stops the
      microphone); the Speak tab carries a dot so the ongoing session
      stays visible from every tab. ── */
/* Group unread count on the Speak tab (D090): while messages wait
   behind another tab the dot becomes a number. */
.tab .tab-count {
    display: inline-block;
    min-width: 18px;
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: 9px;
    background: #ff5470;
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 15px;
    text-align: center;
}
body.has-live-session .tab[data-tab="speak"].has-unread .tab-label::after {
    display: none;
}
body.has-live-session .tab[data-tab="speak"] .tab-label::after {
    content: "";
    display: inline-block;
    width: 7px; height: 7px;
    margin-left: 6px;
    border-radius: 50%;
    background: #ff5470;
    vertical-align: 1px;
}

/* ═══════════════════════════════════════════════════════════════════════
   SERVICE COUNTER — a two-seat, host-controlled room profile.

   It travels through Group room infrastructure, so the counter class has
   to win over Group Chat's shell and header navigation in the same frame.
   The live Counter shell is pinned: Conversation, Word Hints, and Host-only
   Settings are its only navigation while a microphone may be active.
   ═══════════════════════════════════════════════════════════════════════ */
#page-multi.is-counter-room-open > .page-scroll,
#page-multi.is-counter-room-open > .group-room-shell {
    display: none !important;
}
#workspace.is-counter-room > .app-header > .group-room-tabs {
    display: none !important;
}
#workspace.is-counter-room > .app-header > .tab-bar { display: none; }
/* Counter owns its own operational top bar. Keeping the now-empty application
   header spent 66px of a landscape tablet on blank chrome. */
#workspace.is-counter-room > .app-header { display: none; }

.counter-room-shell {
    position: absolute;
    z-index: 1;
    inset: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    color: var(--text-primary);
    background: linear-gradient(180deg, #10252c 0%, #0d1b27 46%, #0b1421 100%);
    /* Translated Counter messages previously defaulted to 15px. The
       conversation now uses one language at a four-pixel-larger base. */
    --counter-message-size: 19px;
}
.counter-room-shell[hidden] { display: none; }
.counter-room-shell.is-guest #counter-room-status { display: none; }
/* The edge cue is deliberately tied to actual Guest capture readiness in JS,
   not merely to a floor assignment. It never intercepts a control. */
.counter-room-shell::after {
    content: "";
    position: absolute;
    z-index: 12;
    inset: max(3px, var(--safe-top)) max(3px, var(--safe-right))
           max(3px, var(--safe-bottom)) max(3px, var(--safe-left));
    border: 5px solid transparent;
    border-radius: 16px;
    pointer-events: none;
}
.counter-room-shell.is-guest-speak-ready::after {
    border-color: #45ef79;
    box-shadow: inset 0 0 28px rgba(69, 239, 121, 0.32),
                0 0 18px rgba(69, 239, 121, 0.5);
    animation: counter-speak-glow 1500ms ease-in-out infinite alternate;
}
@keyframes counter-speak-glow {
    from { opacity: 0.66; box-shadow: inset 0 0 18px rgba(69, 239, 121, 0.24), 0 0 10px rgba(69, 239, 121, 0.36); }
    to { opacity: 1; box-shadow: inset 0 0 34px rgba(69, 239, 121, 0.42), 0 0 22px rgba(69, 239, 121, 0.58); }
}
.counter-room-shell[data-counter-text-size="large"] {
    --counter-message-size: 23.75px;
}
.counter-room-shell[data-counter-text-size="larger"] {
    --counter-message-size: 28.5px;
}
.counter-room-shell[data-counter-text-size="largest"] {
    --counter-message-size: 33.25px;
}

.counter-room-topbar {
    flex: 0 0 calc(58px + var(--safe-top));
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: var(--safe-top) calc(12px + var(--safe-right)) 0
             calc(12px + var(--safe-left));
    border-bottom: 1px solid var(--hairline);
    background: rgba(8, 15, 27, 0.9);
}
.counter-room-back { flex: 0 0 42px; }
/* The mode pages' "?" wearing the topbar's own measurements: the hairline
   disc and the letter come from .mode-help-btn, only the 44 px box shrinks
   to the 42 px this row uses for the back chevron. */
.counter-room-help {
    flex: 0 0 42px;
    width: 42px;
    height: 42px;
}
.counter-room-heading {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.counter-room-heading strong {
    overflow: hidden;
    font-size: 16px;
    line-height: 1.25;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.counter-room-heading span {
    overflow: hidden;
    color: var(--text-secondary);
    font-size: 11.5px;
    line-height: 1.25;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.counter-stt-toggle {
    flex: 0 0 auto;
    min-height: 38px;
    max-width: 154px;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: center;
    gap: 1px;
    padding: 5px 11px;
    border: 1px solid #3a5362;
    border-radius: 11px;
    background: #172b35;
    color: var(--text-secondary);
    font: inherit;
    cursor: pointer;
}
.counter-stt-toggle > span {
    font-size: 10.5px;
    white-space: nowrap;
}
.counter-stt-toggle > strong {
    color: #a7b7c9;
    font-size: 12px;
}
.counter-stt-toggle[aria-checked="true"] {
    border-color: #52bfae;
    background: #163d3e;
}
.counter-stt-toggle[aria-checked="true"] > strong { color: #78e0cd; }
.counter-stt-toggle:disabled { opacity: 0.48; cursor: default; }

.counter-room-tabs {
    flex: 0 0 auto;
    display: flex;
    align-self: stretch;
    padding: 0;
    background: transparent;
}
.counter-room-tabs[hidden] { display: none; }
.counter-room-tab {
    position: relative;
    min-width: 82px;
    min-height: 44px;
    padding: 0 14px;
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
}
.counter-room-tab::after {
    content: "";
    position: absolute;
    right: 13px; bottom: 0; left: 13px;
    height: 2px;
    border-radius: 2px 2px 0 0;
    background: #58dec8;
    opacity: 0;
}
.counter-room-tab.is-active { color: var(--text-primary); }
.counter-room-tab.is-active::after { opacity: 1; }
.counter-room-tab[hidden] { display: none; }

.counter-room-stage {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
}
.counter-room-view {
    position: absolute;
    inset: 0;
    display: none;
}
.counter-room-view.is-active { display: block; }
.counter-room-view[hidden] { display: none; }
.counter-room-scroll {
    position: absolute;
    inset: 0;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
}
.counter-room-column {
    width: min(100%, 760px);
    min-height: 100%;
    margin: 0 auto;
    padding: 16px max(16px, calc(16px + var(--safe-right)))
             max(24px, calc(16px + var(--safe-bottom)))
             max(16px, calc(16px + var(--safe-left)));
}

.counter-privacy,
.counter-hints-privacy {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 0 0 12px;
    padding: 11px 13px;
    border: 1px solid rgba(242, 184, 92, 0.34);
    border-radius: 12px;
    background: rgba(82, 57, 25, 0.28);
    color: #d9c9ae;
    font-size: 12px;
    line-height: 1.4;
}
.counter-privacy strong { color: #f1d49f; font-size: 12.5px; }

.counter-state {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 66px;
    padding: 12px 14px;
    border: 1px solid #31515a;
    border-radius: var(--radius-card);
    background: rgba(20, 47, 54, 0.78);
}
.counter-state-signal {
    flex: 0 0 12px;
    width: 12px; height: 12px;
    border: 3px solid rgba(88, 222, 200, 0.22);
    border-radius: 50%;
    background: #58dec8;
    background-clip: padding-box;
}
.counter-state-copy {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 1 1 auto;
}
.counter-state strong { font-size: 15px; line-height: 1.3; }
.counter-state small {
    color: var(--text-secondary);
    font-size: 12px;
    line-height: 1.35;
}
.counter-retry-capture {
    flex: 0 0 auto;
    min-height: 38px;
    padding: 0 12px;
    border: 1px solid #4b8279;
    border-radius: 10px;
    background: #173e3c;
    color: var(--text-primary);
    font: inherit;
    font-size: 12px;
    font-weight: 650;
    cursor: pointer;
}
.counter-retry-capture[hidden] { display: none; }

.counter-invite {
    margin-top: 12px;
    padding: 13px 14px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    border: 1px solid #32465f;
    border-radius: var(--radius-card);
    background: var(--surface);
}
.counter-invite[hidden] { display: none; }
.counter-invite-copy {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    gap: 3px;
    text-align: center;
}
.counter-invite strong { font-size: 14px; }
.counter-invite small { color: var(--text-secondary); line-height: 1.35; }
.counter-invite-actions {
    align-self: stretch;
    display: flex;
    gap: 8px;
}
.counter-invite-actions button { flex: 1 1 0; }
.counter-invite button,
.counter-hint-entry button,
.counter-hint-clear {
    min-height: 40px;
    padding: 0 14px;
    border: 1px solid #3e5c69;
    border-radius: 10px;
    background: #18333b;
    color: var(--text-primary);
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.counter-invite button:disabled,
.counter-hint-clear:disabled { opacity: 0.45; cursor: default; }
.counter-qr-card {
    position: relative;
    width: 184px;
    height: 184px;
    min-height: 184px !important;
    padding: 0 !important;
    /* No overflow clip: the viewfinder brackets below sit OUTSIDE the card,
       and the canvas already measures exactly the box inside the border. */
    border: 7px solid #f4f8ff !important;
    border-radius: 12px !important;
    background: #f4f8ff !important;
}
.counter-qr-card canvas {
    display: block;
    width: 170px;
    height: 170px;
}
/* The same four viewfinder brackets .qr-card uses (see there for why they
   are outside the quiet zone and why they are eight gradients). A customer
   tablet is handed over already showing this code, so the "point a camera
   here" shape earns its place more than it does anywhere else. */
.counter-qr-card::after {
    content: "";
    position: absolute; inset: -7px;
    pointer-events: none;
    --bracket: rgba(88, 222, 200, 0.62);
    background-repeat: no-repeat;
    background-image:
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket)),
        linear-gradient(var(--bracket), var(--bracket));
    background-size: 16px 2px, 2px 16px, 16px 2px, 2px 16px,
                     16px 2px, 2px 16px, 16px 2px, 2px 16px;
    background-position:
        left top, left top, right top, right top,
        left bottom, left bottom, right bottom, right bottom;
    transition: opacity var(--press-duration) var(--ease-out);
}
.counter-qr-card:active::after { opacity: 0.45; }

.counter-floor {
    margin-top: 12px;
    overflow: hidden;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: rgba(20, 29, 45, 0.88);
}
/* One label, left-aligned: the summary that used to sit opposite it was a
   second copy of the status card above. */
.counter-floor > header {
    min-height: 40px;
    padding: 9px 14px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid var(--hairline);
}
.counter-floor > header > span {
    color: var(--text-secondary);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
}
.counter-floor-controls {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    padding: 12px;
}
.counter-floor-controls[hidden] { display: none; }
.counter-floor-controls button {
    min-width: 0;
    min-height: 72px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 4px;
    padding: 10px 12px;
    border: 1px solid #344a60;
    border-radius: 12px;
    background: #17283a;
    color: var(--text-primary);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.counter-floor-controls button > strong { font-size: 14px; }
.counter-floor-controls button > span {
    color: var(--text-secondary);
    font-size: 11.5px;
    line-height: 1.3;
}
.counter-floor-controls button[aria-pressed="true"] {
    border-color: #58dec8;
    background: #153c41;
    box-shadow: inset 0 0 0 1px rgba(88, 222, 200, 0.2);
}
.counter-floor-controls button:disabled { cursor: default; }
.counter-floor-controls button:disabled:not([aria-pressed="true"]) {
    opacity: 0.46;
}
.counter-reset-guest {
    width: calc(100% - 24px);
    min-height: 44px;
    margin: 0 12px 12px;
    padding: 0 13px;
    border: 1px solid #805565;
    border-radius: 11px;
    background: rgba(67, 31, 44, 0.45);
    color: #f3c4d0;
    font: inherit;
    font-size: 12.5px;
    font-weight: 650;
    cursor: pointer;
}
.counter-reset-guest[hidden] { display: none; }
.counter-reset-guest:disabled { opacity: 0.45; cursor: default; }

.counter-transcript {
    margin-top: 12px;
    overflow: hidden;
    border: 0;
    border-radius: 16px;
    background: #0d1725;
}
.counter-conversation-actions,
.counter-idle-warning { flex: 0 0 auto; flex-wrap: wrap; }
.counter-secondary,
.counter-message-actions button,
.counter-idle-warning button {
    min-height: 44px;
    padding: 8px 12px;
    border: 1px solid #40546b;
    border-radius: 10px;
    background: #17263a;
    color: var(--text-primary);
    font: inherit;
    font-size: 13px;
    cursor: pointer;
}
.counter-secondary:disabled,
.counter-message-actions button:disabled,
.counter-idle-warning button:disabled { opacity: 0.45; cursor: default; }
.counter-secondary:focus-visible,
.counter-message-actions button:focus-visible,
.counter-idle-warning button:focus-visible { outline: 2px solid #58dec8; outline-offset: 2px; }
.counter-conversation-actions,
.counter-message-actions,
.counter-idle-warning {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 16px;
}
.counter-conversation-actions { border-top: 1px solid rgba(117, 143, 166, 0.14); }
.counter-message-actions { justify-content: flex-end; padding: 8px 0 0; flex-wrap: wrap; }
.counter-message-actions:empty { display: none; }
.counter-delivery { display: block; margin-top: 6px; font-size: 12px; color: var(--text-secondary); }
.counter-idle-warning { background: #3b2b16; color: #ffe2a5; font-size: 13px; line-height: 1.5; }
.counter-idle-warning[hidden],
.counter-conversation-actions[hidden],
.counter-conversation-actions button[hidden],
.counter-idle-warning button[hidden],
.counter-composer[hidden] { display: none !important; }
#counter-jump-latest { margin-inline-start: auto; color: #91f5df; border-color: #347c72; }
#counter-message-send {
    min-width: 76px;
    width: auto;
    max-width: 40%;
    height: auto;
    min-height: 46px;
    padding: 8px 12px;
    line-height: 1.35;
    overflow-wrap: anywhere;
}
.counter-composer-meta { flex-wrap: wrap; }
.counter-text-size {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 5px;
}
.counter-text-size button {
    width: 42px;
    height: 42px;
    padding: 0;
    border: 1px solid #40546b;
    border-radius: 10px;
    background: #17263a;
    color: var(--text-primary);
    font: inherit;
    font-size: 15px;
    font-weight: 700;
    cursor: pointer;
}
.counter-text-size button:disabled { opacity: 0.4; cursor: default; }
.counter-text-size output {
    min-width: 39px;
    color: var(--text-secondary);
    font-size: 11px;
    text-align: center;
}
.counter-transcript-scroll {
    max-height: min(48vh, 430px);
    overflow-y: auto;
    padding: 18px 16px;
    overscroll-behavior-y: contain;
}
.counter-transcript-empty {
    margin: 18px 8px;
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
    text-align: center;
}
.counter-transcript-empty[hidden] { display: none; }
.counter-messages {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.counter-message {
    width: fit-content;
    max-width: min(82%, 680px);
    padding: 11px 15px 13px;
    border: 0;
    border-radius: 17px 17px 17px 5px;
    background: #1a293b;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.14);
}
.counter-message.is-other { align-self: flex-start; }
.counter-message.is-conversation { align-self: stretch; max-width: 100%; width: auto; }
.counter-message-actions summary { cursor: pointer; font-size: 12px; color: #aebdca; }
.counter-message.is-own {
    align-self: flex-end;
    border-radius: 17px 17px 5px 17px;
    background: #174039;
}
.counter-room-shell[dir="rtl"] .counter-message.is-own {
    /* flex-start remains the physical right edge when the cross axis is RTL. */
    align-self: flex-start;
}
.counter-room-shell[dir="rtl"] .counter-message.is-other,
.counter-room-shell[dir="rtl"] .counter-speaking {
    align-self: flex-end;
}
.counter-message-speaker {
    margin-bottom: 4px;
    color: #aebdca;
    font-size: 11.5px;
    font-weight: 650;
}
.counter-message p {
    margin: 0;
    overflow-wrap: anywhere;
    color: #f2f6f8;
    font-size: var(--counter-message-size);
    line-height: 1.42;
}
.counter-message-pending {
    color: #9bb1bd;
    font-size: 12px;
}
/* D225 — a turn is one bubble: the next sentences of one turn (D203), and
   the card waiting for them, join the bubble they follow. */
.counter-message.is-turn-continuation {
    margin-top: -14px;
    padding-top: 4px;
    border-start-start-radius: 0;
    border-start-end-radius: 0;
}
.counter-message.has-continuation {
    padding-bottom: 6px;
    border-end-start-radius: 0;
    border-end-end-radius: 0;
    box-shadow: none;
}
.counter-message.is-turn-continuation::before { top: 0; }
.counter-message.has-continuation::before { bottom: 0; }
/* D225 — the words stay where they are. What a new version added or
   changed fades in over the words the reader already has; the reader's own
   words wait, muted and at the same size, for their translation (D194). */
.counter-text-fresh { animation: counter-text-arrive 0.5s ease-out; }
@keyframes counter-text-arrive {
    from { opacity: 0.3; }
    to { opacity: 1; }
}
.counter-message-text.is-original p { color: #9bb1bd; }
.counter-message-text[hidden] { display: none; }
.counter-message-untranslated {
    margin-top: 3px;
    color: #e8c98f;
    font-size: 12px;
    line-height: 1.4;
}
/* D194 — the in-flight turn card. One bubble-shaped element per turn that
   is not yet a message; it changes stage in place (listening → finishing →
   the message) instead of vanishing and reappearing. */
.counter-turn {
    background: #162536;
    color: #b8c6d0;
    font-size: 13px;
    min-width: 168px;
}
.counter-turn.is-either { border-style: dashed; border-width: 1px; border-color: #2f4658; }
.counter-turn-row {
    display: flex;
    align-items: center;
    gap: 10px;
}
.counter-turn-label { min-width: 0; flex: 1 1 auto; font-size: 13.5px; line-height: 1.35; }
.counter-turn.is-slow .counter-turn-label,
.counter-message.is-slow .counter-message-pending,
.counter-message.is-slow .counter-message-status { color: #f0c97d; }
.counter-turn.is-empty { color: #9bb1bd; }
.counter-turn.is-empty .counter-turn-bars { display: none; }
.counter-turn.is-failed { color: #f0c97d; }
.counter-turn.is-failed .counter-turn-bars { display: none; }
/* Five bars: a sound wave while sound is being heard, a slow breathing
   pulse while the recognizer and the translator work. */
.counter-turn-bars {
    display: inline-flex;
    align-items: flex-end;
    gap: 3px;
    height: 18px;
    flex: 0 0 auto;
}
.counter-turn-bars i {
    display: block;
    width: 4px;
    height: 18px;
    border-radius: 2px;
    background: #72cdbc;
    transform-origin: bottom;
    transform: scaleY(0.3);
    animation: counter-turn-wave 0.9s ease-in-out infinite alternate;
}
.counter-turn-bars i:nth-child(2) { animation-delay: 0.12s; }
.counter-turn-bars i:nth-child(3) { animation-delay: 0.24s; }
.counter-turn-bars i:nth-child(4) { animation-delay: 0.36s; }
.counter-turn-bars i:nth-child(5) { animation-delay: 0.48s; }
.counter-turn.is-finishing .counter-turn-bars i {
    background: #e2b866;
    animation: counter-turn-breathe 1.4s ease-in-out infinite alternate;
}
@keyframes counter-turn-wave {
    from { transform: scaleY(0.28); opacity: 0.55; }
    to { transform: scaleY(1); opacity: 1; }
}
@keyframes counter-turn-breathe {
    from { transform: scaleY(0.4); opacity: 0.45; }
    to { transform: scaleY(0.6); opacity: 0.95; }
}
/* D223 — the other person is typing: three dots where the bars would be. */
.counter-turn.is-typing .counter-turn-bars { align-items: center; gap: 5px; }
.counter-turn.is-typing .counter-turn-bars i {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #72cdbc;
    transform: none;
    animation: counter-typing-dot 1.2s ease-in-out infinite;
}
.counter-turn.is-typing .counter-turn-bars i:nth-child(2) { animation-delay: 0.2s; }
.counter-turn.is-typing .counter-turn-bars i:nth-child(3) { animation-delay: 0.4s; }
@keyframes counter-typing-dot {
    0%, 70%, 100% { opacity: 0.3; }
    35% { opacity: 1; }
}
/* The stage track of a finishing card and a translating bubble. Its
   width is set by script from the elapsed time, paced by what this tablet
   has observed, so a stall is a bar that stopped rather than one that
   filled and lied. An edge at the bubble's foot, out of the flow (D225):
   it comes and goes without moving a word. */
.counter-turn-progress {
    position: absolute;
    inset-inline: 16px;
    bottom: 5px;
    height: 3px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.09);
    overflow: hidden;
    pointer-events: none;
}
.counter-turn-progress i {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 2px;
    background: #e2b866;
    transition: width 0.25s linear;
}
.counter-message.is-slow .counter-turn-progress i,
.counter-turn.is-slow .counter-turn-progress i { background: #f0c97d; }
.counter-message-reading {
    display: block;
    margin-top: 6px;
    color: #8dd3ff;
    font-size: 12px;
}
/* The page edge says what is happening, in colour, for the person who is
   looking at the other person rather than at the bubble. */
.counter-room-shell:not(.is-guest-speak-ready)[data-counter-stage="listening"]::after {
    border-color: rgba(69, 239, 121, 0.7);
    box-shadow: inset 0 0 22px rgba(69, 239, 121, 0.22);
}
.counter-room-shell:not(.is-guest-speak-ready)[data-counter-stage="processing"]::after {
    border-color: rgba(226, 184, 102, 0.75);
    box-shadow: inset 0 0 22px rgba(226, 184, 102, 0.22);
    animation: counter-stage-breathe 1.4s ease-in-out infinite alternate;
}
.counter-room-shell:not(.is-guest-speak-ready)[data-counter-stage="reading"]::after {
    border-color: rgba(122, 201, 255, 0.75);
    box-shadow: inset 0 0 22px rgba(122, 201, 255, 0.22);
}
@keyframes counter-stage-breathe {
    from { opacity: 0.55; }
    to { opacity: 1; }
}

.counter-composer {
    padding: 10px 12px max(10px, var(--safe-bottom));
    border-top: 1px solid var(--hairline);
    background: rgba(8, 16, 28, 0.94);
}
.counter-composer > label {
    display: block;
    margin: 0 2px 6px;
    color: var(--text-secondary);
    font-size: 11.5px;
    font-weight: 650;
}
.counter-composer-row {
    display: flex;
    align-items: flex-end;
    gap: 8px;
}
.counter-composer textarea {
    min-width: 0;
    min-height: 46px;
    max-height: 108px;
    flex: 1 1 auto;
    resize: none;
    padding: 11px 12px;
    overflow-y: auto;
    border: 1px solid #40546b;
    border-radius: 12px;
    outline: 0;
    background: #101a2b;
    color: var(--text-primary);
    font: inherit;
    font-size: 16px;
    line-height: 1.4;
}
.counter-composer textarea:disabled { opacity: 0.55; cursor: default; }
.counter-composer textarea:focus { border-color: #58dec8; }
.counter-composer-row > button {
    flex: 0 0 46px;
    width: 46px;
    height: 46px;
    padding: 0;
    border: 0;
    border-radius: 12px;
    background: #1f7d70;
    color: #effffb;
    font: inherit;
    cursor: pointer;
}
.counter-composer-row > button:active:not(:disabled) { background: #19695f; }
.counter-composer-row > button:disabled { opacity: 0.38; cursor: default; }
.counter-composer-row > button span {
    display: inline-block;
    font-size: 19px;
    transform: translateX(1px) rotate(-5deg);
}
.counter-composer-meta {
    min-height: 17px;
    display: flex;
    justify-content: space-between;
    gap: 10px;
    margin: 5px 2px 0;
    color: var(--text-secondary);
    font-size: 11px;
    line-height: 1.35;
}
#counter-compose-status { min-width: 0; }
#counter-message-count { flex: 0 0 auto; }

.counter-hints-column { max-width: 680px; }
.counter-hints-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
}
.counter-hints-head h2 { margin: 0; font-size: 21px; }
.counter-hints-head span { color: var(--text-secondary); font-size: 12px; }
.counter-hints-intro {
    margin: 8px 0 12px;
    color: var(--text-secondary);
    font-size: 14px;
    line-height: 1.5;
}
.counter-hints-privacy { margin-bottom: 14px; }
.counter-hint-entry {
    padding: 13px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: var(--surface);
}
.counter-hint-entry > label {
    display: block;
    margin-bottom: 7px;
    color: var(--text-secondary);
    font-size: 12px;
    font-weight: 600;
}
.counter-hint-entry > div { display: flex; gap: 8px; }
.counter-hint-entry input {
    min-width: 0;
    min-height: 42px;
    flex: 1 1 auto;
    padding: 0 12px;
    border: 1px solid #40516a;
    border-radius: 10px;
    outline: 0;
    background: #101a2b;
    color: var(--text-primary);
    font: inherit;
    font-size: 15px;
}
.counter-hint-entry input:focus { border-color: #58dec8; }
.counter-hint-feedback {
    min-height: 18px;
    margin: 7px 2px 5px;
    color: #83cdbf;
    font-size: 12px;
}
.counter-hint-empty {
    margin: 24px 0;
    color: var(--text-secondary);
    font-size: 13px;
    text-align: center;
}
.counter-hint-empty[hidden] { display: none; }
.counter-hint-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 7px;
}
.counter-hint-list li {
    min-height: 46px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 7px 8px 7px 13px;
    border: 1px solid #2f4057;
    border-radius: 11px;
    background: #152035;
}
.counter-hint-list li > span { overflow-wrap: anywhere; }
.counter-hint-list button {
    flex: 0 0 36px;
    width: 36px; height: 34px;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--text-secondary);
    font-size: 20px;
    cursor: pointer;
}
.counter-hint-list button:active { background: #26344a; }
.counter-hint-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 9px;
    margin-top: 14px;
}
.counter-hint-clear,
.counter-hint-reset {
    width: 100%;
    margin: 0;
    background: transparent;
}
.counter-hint-reset {
    border-color: #35675f;
    color: #a9ded4;
}
.counter-hint-clear {
    border-color: #5f4651;
    color: #deb9c2;
}
@media (max-width: 520px) {
    .counter-hint-actions { grid-template-columns: 1fr; }
}

.counter-settings-column {
    max-width: 920px;
    gap: 18px;
}
.counter-settings-head h2,
.counter-tts-destinations h3,
.counter-tts-voices h3 { margin: 0; }
.counter-settings-head p,
.counter-tts-voices > p {
    margin: 7px 0 0;
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
}
.counter-tts-destinations,
.counter-tts-voices {
    display: grid;
    gap: 10px;
    padding: 16px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: var(--surface);
}
.counter-setting-toggle {
    width: 100%;
    min-height: 66px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 11px 14px;
    border: 1px solid #34475f;
    border-radius: 12px;
    background: #131f32;
    color: var(--text-primary);
    text-align: left;
    cursor: pointer;
}
.counter-setting-toggle > span { display: grid; gap: 3px; }
.counter-setting-toggle small {
    color: var(--text-secondary);
    font-size: 11px;
    line-height: 1.35;
}
.counter-setting-toggle > b {
    flex: 0 0 auto;
    min-width: 46px;
    padding: 6px 8px;
    border-radius: 999px;
    background: #27364a;
    color: var(--text-secondary);
    font-size: 12px;
    text-align: center;
}
.counter-setting-toggle[aria-checked="true"] {
    border-color: #388f70;
    background: rgba(31, 125, 112, 0.2);
}
.counter-setting-toggle[aria-checked="true"] > b {
    background: #1f7d70;
    color: #effffb;
}
.counter-setting-toggle:disabled { opacity: 0.5; cursor: default; }
.counter-tts-voices {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
.counter-tts-voices > h3,
.counter-tts-voices > p,
.counter-tts-status { grid-column: 1 / -1; }
.counter-voice-card {
    min-width: 0;
    display: grid;
    align-content: start;
    gap: 10px;
    padding: 14px;
    border: 1px solid #34475f;
    border-radius: 13px;
    background: #101a2b;
}
.counter-voice-card > header { display: grid; gap: 3px; }
.counter-voice-card > header small,
.counter-voice-card label > span {
    color: var(--text-secondary);
    font-size: 11px;
}
.counter-voice-card label { display: grid; gap: 5px; }
.counter-voice-card select,
.counter-voice-card input {
    width: 100%;
    min-width: 0;
    min-height: 42px;
    padding: 0 11px;
    border: 1px solid #40516a;
    border-radius: 10px;
    outline: 0;
    background: #152035;
    color: var(--text-primary);
    font: inherit;
}
.counter-voice-gender { display: flex; gap: 7px; }
.counter-voice-gender button {
    min-height: 38px;
    flex: 1 1 0;
    border: 1px solid #40516a;
    border-radius: 9px;
    background: transparent;
    color: var(--text-secondary);
}
.counter-voice-gender button.is-selected {
    border-color: #58dec8;
    background: rgba(31, 125, 112, 0.2);
    color: var(--text-primary);
}
.counter-voice-preview-row { display: flex; gap: 8px; }
.counter-voice-preview-row button {
    flex: 0 0 44px;
    width: 44px;
    min-height: 42px;
    border: 0;
    border-radius: 10px;
    background: #1f7d70;
    color: #effffb;
    cursor: pointer;
}
.counter-voice-preview-row button:disabled { opacity: 0.45; cursor: default; }
.counter-tts-status {
    min-height: 18px;
    margin: 0;
    color: #83cdbf;
    font-size: 12px;
}

.counter-consent-layer,
.counter-filter-layer {
    position: absolute;
    z-index: 20;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: max(18px, calc(18px + var(--safe-top)))
             max(18px, calc(18px + var(--safe-right)))
             max(18px, calc(18px + var(--safe-bottom)))
             max(18px, calc(18px + var(--safe-left)));
    overflow-y: auto;
    background: rgba(5, 11, 20, 0.88);
    backdrop-filter: blur(8px);
}
.counter-consent-layer[hidden] { display: none; }
/* D208: the Guest's filter wait sits above consent; both block the page. */
.counter-filter-layer { z-index: 21; }
.counter-filter-layer[hidden] { display: none; }
.counter-filter-dialog .counter-filter-message {
    margin-top: 16px;
    font-size: 14px;
    color: var(--text-secondary);
}
.counter-filter-dialog .download-progress { margin-top: 9px; }
.counter-consent-dialog,
.counter-filter-dialog {
    width: min(100%, 560px);
    padding: 24px;
    border: 1px solid #3a5d68;
    border-radius: 18px;
    background: #142431;
    box-shadow: 0 22px 60px rgba(0, 0, 0, 0.48);
}
.counter-consent-dialog h2,
.counter-filter-dialog h2 {
    margin: 0 0 12px;
    font-size: 23px;
    line-height: 1.25;
}
.counter-consent-dialog p,
.counter-filter-dialog p {
    margin: 0;
    color: #d3dce8;
    font-size: 16px;
    line-height: 1.55;
}
.counter-consent-dialog .counter-consent-turn {
    margin-top: 12px;
    padding: 11px 13px;
    border: 1px solid rgba(69, 239, 121, 0.4);
    border-radius: 11px;
    background: rgba(28, 92, 51, 0.27);
    color: #dffff0;
}
.counter-consent-language {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 16px;
}
.counter-consent-language > span {
    color: var(--text-secondary);
    font-size: 13px;
    font-weight: 650;
}
.counter-consent-language .counter-language-picker {
    width: 100%;
    flex: 0 0 auto;
    height: 52px;
}
.counter-consent-language small {
    color: var(--text-secondary);
    font-size: 12px;
    line-height: 1.4;
}
.counter-consent-popular { display: none; }
.counter-consent-popular h3 {
    margin: 0 0 9px;
    color: var(--text-secondary);
    font-size: 13px;
    font-weight: 650;
}
.counter-consent-language-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 9px;
}
.counter-consent-language-choice {
    min-width: 0;
    min-height: 54px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 11px 7px 13px;
    border: 1px solid #30405b;
    border-radius: 14px;
    background: #171f34;
    box-shadow: 0 3px 0 #080c18;
    color: var(--text-primary);
    font: inherit;
    font-size: 14px;
    text-align: left;
    cursor: pointer;
    transition: border-color var(--press-duration) var(--ease-out),
                background var(--press-duration) var(--ease-out),
                transform var(--press-duration) var(--ease-out),
                box-shadow var(--press-duration) var(--ease-out);
}
.counter-consent-language-choice:hover { background: #202a42; }
.counter-consent-language-choice:active {
    transform: translateY(2px);
    box-shadow: 0 1px 0 #080c18;
}
.counter-consent-language-choice.is-current {
    border-color: #58dec8;
    background: rgba(39, 121, 112, 0.27);
    box-shadow: 0 3px 0 #0b403b;
    font-weight: 650;
}
.counter-consent-language-choice .row-name {
    overflow: visible;
    white-space: normal;
    line-height: 1.2;
}
.counter-consent-language-choice .row-flags { gap: 4px; }
.counter-consent-language-choice .row-flag { height: 22px; }
.counter-consent-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 20px;
}
.counter-consent-actions button {
    min-height: 46px;
    padding: 0 18px;
    border: 1px solid #476071;
    border-radius: 11px;
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: 14px;
    font-weight: 650;
    cursor: pointer;
}
#counter-consent-accept {
    border-color: #388f70;
    background: #1f7d70;
    color: #effffb;
}
.counter-consent-actions button:disabled { opacity: 0.45; cursor: default; }
.counter-consent-dialog .counter-consent-status {
    margin-top: 14px;
    padding: 10px 12px;
    border-radius: 10px;
    background: rgba(205, 155, 65, 0.16);
    color: #ffe4ac;
}
.counter-consent-status[hidden] { display: none; }

/* The quick grid is a tablet affordance. A phone keeps the compact standard
   picker and exactly the same full language catalog without duplicating a
   long list down the consent sheet. */
@media (min-width: 720px) and (min-height: 520px) {
    .counter-consent-dialog { width: min(100%, 880px); }
    .counter-consent-popular { display: block; margin-top: 15px; }
}

@media (min-width: 980px) and (min-height: 600px) {
    .counter-consent-language-grid {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

.counter-room-shell button:focus-visible,
.counter-room-shell input:focus-visible,
.counter-room-shell textarea:focus-visible,
.counter-room-shell select:focus-visible {
    outline: 2px solid #58dec8;
    outline-offset: 2px;
}

/* Five cards leave one unmatched door in the two-column layout; the
   counter's wider operational description earns the full final row. */
@media (min-width: 560px) {
    .hub-card.is-counter { grid-column: 1 / -1; }
}

@media (max-width: 480px) {
    .counter-room-topbar { gap: 6px; }
    .counter-room-heading span { display: none; }
    .counter-stt-toggle { max-width: 108px; padding-right: 8px; padding-left: 8px; }
    .counter-stt-toggle > span { font-size: 9.5px; }
    .counter-room-tab { min-width: 0; flex: 1 1 0; }
    .counter-invite { align-items: center; }
    .counter-invite-actions { width: 100%; }
    .counter-tts-voices { grid-template-columns: minmax(0, 1fr); }
    .counter-tts-voices > h3,
    .counter-tts-voices > p,
    .counter-tts-status { grid-column: auto; }
}

@media (max-width: 350px) {
    .counter-room-column { padding-right: 11px; padding-left: 11px; }
    .counter-floor-controls { gap: 7px; padding: 9px; }
    .counter-floor-controls button { padding-right: 9px; padding-left: 9px; }
}

@media (orientation: landscape) and (max-height: 500px) {
    .counter-room-topbar { flex-basis: 48px; }
    .counter-room-tabs { flex-basis: auto; }
    .counter-room-column { padding-top: 10px; }
    .counter-transcript-scroll { max-height: 46vh; }
}

/* A software keyboard reduces the viewport. Keep the draft and its actions
   reachable while voice input is paused; closing the composer restores Live. */
@media (max-height: 650px) {
    .counter-room-shell.is-composing #counter-live-language,
    .counter-room-shell.is-composing #counter-request-speak,
    .counter-room-shell.is-composing #counter-floor { display: none; }
    .counter-room-shell.is-composing #counter-state { min-height: 44px; padding-block: 8px; }
    .counter-room-shell.is-composing #counter-state small { display: none; }
    .counter-room-shell.is-composing #counter-conversation .counter-room-scroll { overflow-y: auto; }
    .counter-room-shell.is-composing #counter-conversation .counter-room-column { min-height: 400px; }
    .counter-room-shell.is-composing .counter-composer textarea { max-height: 110px; }
}

/* Service Counter is installed chiefly on landscape tablets. On a wide,
   tall CSS viewport (not the physical-pixel resolution) the station takes
   the whole workspace, Settings keeps a little more air, the invite puts its
   QR code beside its copy and the glossary uses the width. The operations
   rail that used to live here, status, invite and floor beside a
   full-height transcript, went when Live became one column; every one of
   its rules had been overridden and they are gone (D222). */
@media (orientation: landscape) and (min-width: 1100px) and (min-height: 650px) {
    #workspace.is-counter-room {
        left: 0;
        right: 0;
        border-right: 0;
        border-left: 0;
        box-shadow: none;
    }
    .counter-room-scroll { overflow: hidden; }
    .counter-room-column {
        padding-top: 18px;
        padding-bottom: 18px;
    }
    .counter-settings-column { max-width: 1200px; }
    .counter-invite {
        min-height: 150px;
        margin: 0;
        display: grid;
        grid-template-columns: 132px minmax(0, 1fr);
        grid-template-rows: auto auto;
        align-content: center;
        align-items: center;
        column-gap: 14px;
    }
    .counter-invite-copy { grid-column: 2; text-align: left; }
    .counter-qr-card {
        grid-column: 1;
        grid-row: 1 / span 2;
        width: 132px;
        height: 132px;
        min-height: 132px !important;
    }
    .counter-qr-card canvas { width: 118px; height: 118px; }
    .counter-invite-actions { grid-column: 2; }
    .counter-composer { flex: 0 0 auto; }
    .counter-hint-list {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    }
}

@media (prefers-reduced-motion: reduce) {
    /* The page edge keeps its colour and only stops pulsing. It used to
       share a rule with the speaking bars' resting pose, and scaleY(0.6) on
       the frame folded it into a band across the middle of the screen, over
       the messages (D222). */
    .counter-room-shell.is-guest-speak-ready::after,
    .counter-room-shell[data-counter-stage]::after { animation: none; }
    .counter-turn-bars i { animation: none; transform: scaleY(0.6); }
    .counter-turn.is-typing .counter-turn-bars i { animation: none; transform: none; opacity: 0.85; }
    .counter-turn-progress i { transition: none; }
    .counter-text-fresh { animation: none; }
    #counter-reset-live.is-holding::after { animation: none; }
}

@media (forced-colors: active) {
    .counter-room-shell.is-guest-speak-ready::after,
    .counter-room-shell[data-counter-stage]::after {
        border-color: Highlight;
        box-shadow: none;
    }
    .counter-turn-bars i, .counter-turn-progress i { background: Highlight; }
}

/* D181 — desktop frame. The product still uses the whole browser up to the
   chosen width; beyond it, application screens and viewport-style sheets
   stop growing. `max()` resolves both insets to zero on narrower viewports,
   so this is the literal max-width without a phone/tablet layout branch.

   Positioning with equal left/right insets (instead of transform) matters:
   connect/transcript sheets use transform for their swipe-to-close gesture.
   The ambient landing artwork and every modal scrim remain full-bleed; only
   the interactive application surface is framed. */
.screen,
.full-sheet {
    left: max(0px, calc((100vw - var(--app-max-width)) / 2));
    right: max(0px, calc((100vw - var(--app-max-width)) / 2));
}

/* Once the gutters are substantial, quiet edges make the framed app read as
   one surface rather than a page that happened to stop growing. */
@media (min-width: 1100px) {
    #workspace,
    .full-sheet {
        border-left: 1px solid rgba(91, 111, 146, 0.24);
        border-right: 1px solid rgba(91, 111, 146, 0.24);
        box-shadow: 0 0 56px rgba(0, 0, 0, 0.42);
    }

    /* On a desktop, 95vw is no longer a useful definition of a dialog.
       Keep the guide larger than the ordinary 760px content column, but
       clearly modal and easy to scan. */
    .mode-help {
        top: 24px;
        right: auto;
        bottom: 24px;
        left: 50%;
        width: min(800px, calc(100% - 48px));
        transform: translateX(-50%);
    }
}

/* ═══════════════════════════════════════════════════════════════════════
   D138 — the ?rawtap=on diagnostic recorder's save pill. Fixed above
   everything (z 80 > the dialog layer's 70) because a device test must be
   able to save the take from whatever page the session left it on. Plain
   dark chip on purpose: this is instrumentation, not product surface.
   ═══════════════════════════════════════════════════════════════════════ */
.raw-tap-save {
    position: fixed;
    left: 12px;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    z-index: 80;
    padding: 10px 16px;
    border: 1px solid #7183a4;
    border-radius: 999px;
    background: #253149;
    color: #e8edf7;
    font: inherit;
    font-size: 14px;
    cursor: pointer;
}
.raw-tap-save:active { background: #35445f; }

/* ═══════════════════════════════════════════════════════════════════════
   SERVICE COUNTER — simplified live operation.

   The live page has one vertical reading order: contextual status, independent
   microphone controls, then the conversation. Configuration that is
   not needed during a speaking turn lives on the Settings page.
   ═══════════════════════════════════════════════════════════════════════ */
#counter-conversation .counter-room-scroll { overflow: hidden; }
#counter-conversation .counter-room-column {
    width: min(100%, 1040px);
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding-top: 18px;
    padding-bottom: max(18px, calc(12px + var(--safe-bottom)));
}

#counter-conversation .counter-state {
    flex: 0 0 auto;
    min-height: 58px;
    border-color: #34495a;
    background: rgba(17, 34, 45, 0.9);
}
/* D222 — the customer's language picker lives in their top bar, which
   carries nothing else on that tablet, beside their own A−/A+ (D221). The
   status line only speaks while a change is on its way. */
.counter-live-language {
    flex: 0 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 10px;
}
.counter-live-language[hidden] { display: none; }
.counter-language-label { display: flex; align-items: center; gap: 7px; min-width: 0; }
.counter-language-symbol { width: 22px; height: 22px; flex: 0 0 22px; color: var(--multi-accent, #55d8c5); }
.counter-selected-language-flags { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; direction: ltr; }
.counter-selected-language-flags[hidden] { display: none; }
.counter-selected-language-flags img { height: 20px; width: auto; max-width: 32px; object-fit: contain; border-radius: 2px; }
.counter-language-picker .picker-value { min-width: 0; text-align: start; }
.counter-language-picker > canvas { flex-shrink: 0; }
.counter-live-language .counter-language-picker {
    flex: 0 1 auto;
    width: auto;
    min-width: 168px;
    max-width: min(40vw, 320px);
    height: 44px;
}
.counter-live-language .counter-language-symbol { width: 20px; height: 20px; flex: 0 0 20px; }
.counter-live-language > small {
    min-width: 0;
    max-width: 30vw;
    overflow: hidden;
    color: var(--text-secondary);
    font-size: 12px;
    line-height: 1.3;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.counter-guest-text-size[hidden] { display: none; }
.counter-guest-text-size button { width: 44px; height: 44px; }
.counter-request-speak {
    --lift-radius: 16px;
    --lift-g0: #31506a;
    --lift-g1: #233e57;
    --lift-g2: #172b40;
    --lift-border: #5c7892;
    --lift-highlight: rgba(255, 255, 255, 0.22);
    flex: 0 0 auto;
    width: min(100%, 420px);
    min-height: 62px;
    align-self: center;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 11px;
    padding: 0 22px var(--face-reveal);
    color: #f3f8ff;
    font: inherit;
}
.counter-request-speak[hidden] { display: none; }
.counter-request-speak canvas { width: 28px; height: 28px; }
.counter-request-speak strong { font-size: 17px; font-weight: 720; }
.counter-request-speak.is-sent::after {
    border-color: #7ce2d0;
    box-shadow: 0 0 0 4px rgba(88, 222, 200, 0.14);
}
.counter-request-speak:disabled { opacity: 0.48; }
#counter-state[data-state="idle"] .counter-state-signal {
    border-color: rgba(159, 174, 193, 0.2);
    background: #8795a8;
}
#counter-state[data-state="listening"] {
    border-color: rgba(61, 224, 132, 0.66);
    background: rgba(18, 66, 48, 0.72);
}
#counter-state[data-state="listening"] .counter-state-signal {
    border-color: rgba(72, 239, 139, 0.26);
    background: #48ef8b;
}
#counter-state[data-state="processing"] .counter-state-signal {
    border-color: rgba(88, 222, 200, 0.24);
    background: #58dec8;
}
#counter-state[data-state="error"] {
    border-color: rgba(235, 133, 113, 0.6);
    background: rgba(75, 36, 40, 0.72);
}
#counter-state[data-state="error"] .counter-state-signal {
    border-color: rgba(255, 146, 126, 0.24);
    background: #ff927e;
}

/* D222 — one compact row: the two microphone switches, then Reset
   Conversation. The switches stay on for the whole visit, so they no longer
   take a band of the screen: each is a lit dot and its name. */
#counter-conversation .counter-floor {
    flex: 0 0 auto;
    width: 100%;
    align-self: stretch;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 10px;
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: transparent;
}
#counter-conversation .counter-floor[hidden] { display: none; }
/* The switches take nearly all the spare width while Reset shares their
   line; once the row wraps on a phone, Reset alone grows to the full line. */
#counter-conversation .counter-floor-controls {
    flex: 1000 1 360px;
    gap: 10px;
    padding: 0;
}
#counter-conversation .counter-floor-controls button {
    position: relative;
    min-height: 54px;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 8px 14px;
    border-width: 2px;
    border-color: #40536a;
    border-radius: 14px;
    background: linear-gradient(180deg, #192a3d, #142236);
    text-align: center;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
}
#counter-conversation .counter-floor-controls button::before {
    content: "";
    flex: 0 0 12px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #5d6f82;
    box-shadow: 0 0 0 3px rgba(93, 111, 130, 0.22);
}
#counter-conversation .counter-floor-controls button[aria-pressed="true"]::before {
    background: #48ef8b;
    box-shadow: 0 0 0 3px rgba(72, 239, 139, 0.25);
}
#counter-conversation .counter-floor-controls button.is-pending::before {
    background: #e7c36f;
    box-shadow: 0 0 0 3px rgba(231, 195, 111, 0.22);
}
#counter-conversation .counter-floor-controls button > strong {
    font-size: clamp(17px, 1.7vw, 21px);
    line-height: 1.2;
}
#counter-conversation .counter-floor-controls button > span:last-child {
    color: #9cabbc;
    font-size: 13px;
}
#counter-conversation .counter-floor-controls button[aria-pressed="true"] {
    border-color: #48ef8b;
    background: linear-gradient(180deg, #18523c, #123b31);
    box-shadow: 0 0 0 3px rgba(72, 239, 139, 0.13),
                0 12px 28px rgba(0, 0, 0, 0.24);
}
#counter-conversation .counter-floor-controls button[aria-pressed="true"]
    > span:last-child { color: #baf7d0; }
#counter-conversation .counter-floor-controls button.is-pending {
    border-color: #e7c36f;
    background: linear-gradient(180deg, #4b3d22, #332b1e);
    box-shadow: 0 0 0 3px rgba(231, 195, 111, 0.12),
                0 12px 28px rgba(0, 0, 0, 0.24);
}
#counter-floor-guest.is-speak-requested {
    opacity: 1;
    animation: counter-speak-request-flash 720ms ease-out;
}
@keyframes counter-speak-request-flash {
    0%, 100% {
        border-color: #40536a;
        background: linear-gradient(180deg, #192a3d, #142236);
        box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
    }
    20%, 55% {
        border-color: #f3ce65;
        background: linear-gradient(180deg, #574821, #382f1e);
        box-shadow: 0 0 0 5px rgba(243, 206, 101, 0.24),
                    0 12px 28px rgba(0, 0, 0, 0.24);
    }
}
#counter-conversation .counter-floor-controls button:disabled:not([aria-pressed="true"]) {
    opacity: 0.38;
}

@media (max-width: 480px) {
    .counter-room-shell.is-guest .counter-room-heading { display: none; }
    .counter-live-language { flex: 1 1 auto; }
    .counter-live-language .counter-language-picker { min-width: 0; max-width: none; flex: 1 1 auto; }
    .counter-live-language > small { display: none; }
}

#counter-conversation .counter-transcript {
    flex: 1 1 auto;
    min-height: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    background: #0d1725;
}
#counter-conversation .counter-transcript-scroll {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    display: flex;
    flex-direction: column;
}
#counter-conversation .counter-transcript-empty { margin: auto 8px; }
#counter-conversation .counter-messages {
    width: min(100%, 820px);
    margin-inline: auto;
}
.counter-message.is-live {
    box-shadow: inset -3px 0 0 rgba(88, 222, 200, 0.72),
                0 1px 2px rgba(0, 0, 0, 0.14);
}
.counter-message.is-live .counter-message-pending,
.counter-message.is-live .counter-message-status { color: #8fe2d3; }
.counter-composer[hidden] { display: none; }

/* All non-operational controls share the independently scrollable Settings
   page. Sections stay visually distinct without turning into more tabs. */
#counter-settings .counter-room-scroll { overflow-y: auto; }
#counter-settings .counter-settings-column {
    width: min(100%, 1040px);
    height: auto;
    min-height: 100%;
    display: flex;
    flex-direction: column;
    gap: 16px;
    overflow: visible;
}
/* Flex/grid children must be allowed to shrink before their text wraps.
   In particular, the STT switch also inherits an old compact, nowrap style. */
#counter-settings .counter-settings-column > *,
#counter-settings .counter-settings-column :is(section, article, header, label, button, p, strong, small, span, div) {
    min-width: 0;
    max-width: 100%;
    overflow-wrap: anywhere;
}
#counter-settings :is(.counter-tts-destinations, .counter-tts-voices) {
    align-self: stretch;
    width: 100%;
}
#counter-settings :is(.counter-settings-basics, .counter-tts-destinations, .counter-voice-card) {
    grid-template-columns: minmax(0, 1fr);
}
#counter-settings .counter-setting-toggle > span {
    flex: 1 1 auto;
    white-space: normal;
    line-height: 1.4;
}
#counter-settings .counter-settings-basics > p,
#counter-guest-connection {
    margin: 0;
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
}
#counter-settings .counter-voice-actions { display: flex; flex-wrap: wrap; gap: 8px; }
#counter-settings .counter-voice-actions .counter-voice-button { margin: 0; }
#counter-settings .counter-voice-preview-row input { flex: 1 1 0; }
#counter-settings .counter-voice-gender button { min-height: 44px; }
#counter-settings :is(.counter-voice-button, .counter-hint-reset, .counter-hint-clear) {
    height: auto;
    min-height: 44px;
    white-space: normal;
}
#counter-settings-locale { width: 100%; }
#counter-settings-locale .locale-option {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 0;
    gap: 8px;
    min-height: 44px;
}
#counter-settings-locale .locale-flag { flex: 0 0 auto; }
.counter-settings-head h2,
.counter-settings-section-head h2 { margin: 0; }
#counter-settings .counter-settings-head { order: -3; }
.counter-settings-head p,
.counter-settings-section-head p {
    margin: 7px 0 0;
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.5;
}
.counter-settings-basics,
.counter-settings-visit,
.counter-settings-language,
.counter-settings-hints {
    display: grid;
    gap: 11px;
    padding: 16px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    background: var(--surface);
}
.counter-settings-language {
    order: -2;
    min-height: 78px;
    grid-template-columns: minmax(0, 1fr) minmax(220px, 360px);
    align-items: center;
    gap: 14px;
}
.counter-settings-language-copy {
    min-width: 0;
    display: grid;
    gap: 4px;
}
.counter-settings-language-copy small {
    color: var(--text-secondary);
    font-size: 12px;
    line-height: 1.4;
}
.counter-settings-language .counter-language-picker {
    width: 100%;
    height: 50px;
}
.counter-settings-visit {
    display: flex;
    flex-direction: column;
}
.counter-settings-visit > * { grid-area: auto; }
.counter-settings-basics > h3,
.counter-settings-visit > h3,
.counter-settings-hints h3 { margin: 0; }
.counter-settings-hints { display: block; }
.counter-hints-head {
    min-height: 32px;
    align-items: center;
    cursor: pointer;
    list-style: none;
}
.counter-hints-head::-webkit-details-marker { display: none; }
.counter-hints-head > strong { font-size: 16px; }
.counter-hints-head::after {
    content: "";
    flex: 0 0 9px;
    width: 9px;
    height: 9px;
    margin-left: -4px;
    border-right: 2px solid var(--text-secondary);
    border-bottom: 2px solid var(--text-secondary);
    transform: rotate(45deg) translateY(-2px);
    transition: transform 160ms ease;
}
.counter-settings-hints[open] .counter-hints-head::after {
    transform: rotate(225deg) translate(-2px, -2px);
}
.counter-hints-content {
    display: grid;
    gap: 11px;
    margin-top: 12px;
    padding-top: 14px;
    border-top: 1px solid var(--hairline);
}
#counter-settings .counter-stt-toggle {
    max-width: none;
    min-height: 66px;
    flex-direction: row;
    align-items: center;
    padding: 11px 14px;
}
#counter-settings .counter-stt-toggle > span {
    align-items: start;
    font-size: inherit;
}
.counter-text-size-setting {
    min-height: 62px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    border: 1px solid #34475f;
    border-radius: 12px;
    background: #131f32;
    font-size: 13px;
    font-weight: 650;
}
#counter-settings .counter-privacy { margin: 0; }
#counter-settings .counter-invite {
    margin: 0;
    background: #111d2e;
}
/* Before the second tablet has a live media socket, Settings is the setup
   screen rather than an alternate view. Put its only required action first;
   the Live tab is withheld by the view until this connection exists. */
.counter-room-shell.is-awaiting-guest .counter-settings-visit { order: -1; }
.counter-room-shell.is-awaiting-guest .counter-settings-visit {
    border-color: #397468;
    background: rgba(17, 48, 43, 0.55);
}
#counter-settings .counter-reset-guest {
    width: 100%;
    margin: 0;
}
#counter-settings .counter-hints-intro { margin-top: 0; }
#counter-settings .counter-hints-privacy { margin-bottom: 0; }

@media (max-width: 560px) {
    #counter-conversation .counter-room-column { gap: 10px; padding-top: 10px; }
    #counter-conversation .counter-floor-controls { gap: 9px; }
    #counter-conversation .counter-floor-controls button {
        min-height: 52px;
        padding: 8px;
        border-radius: 13px;
    }
    #counter-conversation .counter-floor-controls button > strong {
        font-size: 16px;
    }
    #counter-conversation .counter-floor-controls button > span:last-child {
        font-size: 10.5px;
    }
    .counter-text-size-setting { align-items: flex-start; flex-direction: column; }
    .counter-settings-language { grid-template-columns: 1fr; }
}

@media (orientation: landscape) and (max-height: 500px) {
    #counter-conversation .counter-room-column { gap: 8px; padding-top: 8px; }
    #counter-conversation .counter-state { min-height: 50px; padding-block: 8px; }
    #counter-conversation .counter-floor-controls button { min-height: 48px; }
}

/* Group conversation controls remain available while browsing People or Account. */
#workspace.is-group-room > .app-header { flex-basis: auto; display: flex; flex-direction: column; }
#workspace.is-group-room > .app-header > .group-room-tabs { height: 62px; flex: 0 0 62px; }
.group-room-audio-controls { flex: 0 0 auto; width: 100%; padding: 5px 12px; border-top: 1px solid var(--hairline); background: #0b1420; }
.group-room-audio-controls[hidden], #workspace:not(.is-group-room) .group-room-audio-controls,
#workspace.is-counter-room .group-room-audio-controls { display: none; }
.group-room-audio-buttons { display: flex; gap: 8px; max-width: 760px; margin: 0 auto; }
.group-room-audio-buttons > button { flex: 1 1 0; width: auto; height: auto; min-height: 44px; padding: 5px 10px; gap: 8px; justify-content: center; border-radius: 12px; white-space: normal; font-size: var(--type-meta); }
.group-room-audio-buttons > button canvas { flex: 0 0 auto; }
.group-room-audio-buttons > button span { white-space: normal; }
.group-room-voice-status-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; max-width: 760px; margin: 4px auto 0; font-size: var(--type-meta); color: var(--text-muted); }
.group-room-voice-status-row button, .group-room-missed-audio button, .group-room-listen-action, .is-live-draft button { min-height: 44px; padding: 6px 12px; border: 1px solid #315a54; border-radius: 10px; background: #12312c; color: #a5f0e3; cursor: pointer; }
.group-room-voice-status-row button[hidden] { display: none; }
.group-room-audio-controls button:focus-visible { outline: 2px solid #7ee6d2; outline-offset: 2px; }
.group-room-missed-audio { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 6px 12px; background: #342b18; color: #f4d694; font-size: var(--type-meta); }
.group-room-missed-audio[hidden] { display: none; }
.group-room-missed-audio > span { flex: 1 1 150px; }
.group-room-missed-message { display: inline-block; margin: 4px 8px; color: #f4d694; }
.group-room-listen-action { margin-top: 6px; font-size: var(--type-meta); }
.group-room-message.is-live-draft { border-style: dashed; opacity: .9; }
.counter-message-text.is-replayable { cursor: pointer; border-radius: 10px; }
.counter-message-text.is-replayable:focus-visible { outline: 2px solid #7ac9ff; outline-offset: 5px; }
.counter-message.is-reading { box-shadow: inset 3px 0 #7ac9ff; }
.counter-message .is-reading-word { background: #ffe29a; color: #17212b; border-radius: 3px; box-shadow: 0 0 0 2px #ffe29a; }
.counter-read-hint { display: block; margin-top: 9px; font-size: 12px; line-height: 1.4; opacity: .72; }
.counter-peer-tts { display: inline-flex; align-items: center; gap: 6px; margin-inline-start: auto; color: #8dd3ff; font-size: 12px; line-height: 1.4; }
.counter-peer-tts:not([hidden]) + .counter-mic-status { margin-inline-start: 14px; }
.counter-peer-tts[hidden] { display: none; }
.counter-peer-tts svg { flex: 0 0 auto; }
.counter-mic-status { display: inline-flex; align-items: center; gap: 7px; margin-inline-start: auto; padding: 5px 2px; color: #adb8c8; font-size: 12px; line-height: 1.3; }
.counter-mic-status svg { flex: 0 0 auto; width: 29px; height: 29px; padding: 4px; border: 1px solid transparent; border-radius: 50%; box-sizing: border-box; }
.counter-mic-status.is-active { color: #73ddbd; }
.counter-mic-status.is-active .counter-mic-slash { display: none; }
.counter-mic-status.is-detecting svg { background: #244c40; border-color: #9af3c8; color: #caffdf; }
.counter-message.is-reading .counter-read-hint { opacity: 1; color: #8dd3ff; }

/* Counter Live: concise controls, delivery metadata and one message action menu. */
#counter-conversation #counter-state[hidden],
#counter-state-detail[hidden], #counter-live-language-status[hidden] { display: none; }
.counter-room-shell.is-guest #counter-state {
    min-height: 48px;
    padding: 10px 14px;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    text-align: start;
}
.counter-room-shell.is-guest #counter-state .counter-state-copy { flex: 0 1 auto; align-items: flex-start; }
.counter-room-shell.is-guest #counter-state .counter-state-signal { flex-basis: 10px; width: 10px; height: 10px; margin: 0; }
.counter-room-shell.is-guest #counter-state strong { font-size: 15px; }
.counter-room-shell.is-guest #counter-state small { font-size: 12px; }
#counter-reset-live.counter-reset-conversation {
    position: relative;
    flex: 1 0 auto;
    min-height: 54px;
    min-width: 180px;
    padding: 10px 20px;
    overflow: hidden;
    border-color: #5b748e;
    border-radius: 14px;
    background: #23384d;
    font-size: 16px;
    font-weight: 700;
}
/* D224 — five seconds of Undo: amber, with a bar draining along the bottom
   edge, while the conversation behind it dims. Nothing has been sent yet. */
#counter-reset-live.is-holding {
    border-color: #e7c36f;
    background: #4b3d22;
    color: #ffe2a5;
}
#counter-reset-live.is-holding::after {
    content: "";
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 4px;
    background: #e7c36f;
    transform-origin: left center;
    animation: counter-reset-hold 5s linear forwards;
}
@keyframes counter-reset-hold {
    from { transform: scaleX(1); }
    to { transform: scaleX(0); }
}
.counter-room-shell.is-reset-holding #counter-transcript-scroll {
    opacity: 0.35;
    transition: opacity 0.2s ease;
}
/* D221 — one column. The ⋯ button left the start of every bubble for its
   trailing corner, out of the flow: the eye meets the words first, and no
   part of a message can land in a narrow track any more (the D210 column
   of letters, where Korean's `overflow-wrap: anywhere` stood "Listening…"
   up one character per line in the 40 px button column). */
.counter-message {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
}
.counter-message > * { grid-column: 1; min-width: 0; }
/* D225 — the ⋯ has its corner in every state, so words never re-wrap when
   it appears: the header row keeps clear of it, and a continuing block's
   first line flows around a spacer of its size. The header is one line;
   what a bubble waits for shortens before it wraps. */
.counter-message-meta,
.counter-turn-row { min-height: 28px; padding-inline-end: 40px; }
.counter-message-meta[hidden] { display: none; }
.counter-message.is-turn-continuation .counter-message-text::before {
    content: "";
    float: right;
    width: 40px;
    height: 24px;
}
.counter-room-shell[dir="rtl"] .counter-message.is-turn-continuation .counter-message-text::before { float: left; }
.counter-message.is-turn-continuation .counter-message-more { top: 2px; height: 30px; }
.counter-turn.is-conversation { min-width: 0; background: #1a293b; }
/* Who first, then when: the speaker in the reader's own terms, and the
   time in hours and minutes. */
.counter-message-meta {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0 8px;
    margin-bottom: 5px;
    color: #8497a9;
    font-size: 12px;
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
}
.counter-message-meta .counter-message-speaker {
    margin: 0;
    color: #c3d0dc;
    font-size: 13.5px;
    font-weight: 700;
}
.counter-message-meta .counter-message-speaker + time::before { content: "·"; margin-inline-end: 8px; }
.counter-message-meta { flex-wrap: nowrap; white-space: nowrap; }
.counter-message-meta > * { flex: none; }
.counter-message-meta .counter-message-status {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    color: #9bb1bd;
}
.counter-message-meta > * + .counter-message-status::before { content: "·"; margin-inline-end: 8px; }
/* A thin edge says who spoke: bright for the other person, quiet for the
   reader's own words, dashed grey while the speaker is not known. */
.counter-message::before {
    content: "";
    position: absolute;
    top: 10px;
    bottom: 10px;
    inset-inline-start: 0;
    width: 3px;
    border-start-end-radius: 3px;
    border-end-end-radius: 3px;
    background: transparent;
    pointer-events: none;
}
.counter-message.is-speaker-peer::before { background: #58dec8; }
.counter-message.is-speaker-self::before { background: #4f667b; }
.counter-message.is-speaker-unknown::before {
    background: repeating-linear-gradient(180deg, #74879a 0 6px, transparent 6px 11px);
}
.counter-message.is-speaker-peer .counter-message-speaker { color: #8ff0dc; }
.counter-message.is-speaker-unknown .counter-message-speaker { color: #a9b6c2; font-weight: 600; }
/* The other person's newest turn stands out a little. */
.counter-message.is-latest-peer { background: #1f3449; }
.counter-message.is-latest-peer::before { width: 4px; background: #7ff3dd; }
.counter-message.is-reading::before { background: #7ac9ff; }
.counter-message-more {
    position: absolute;
    top: 6px;
    inset-inline-end: 6px;
    width: 40px;
    height: 38px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 10px;
    background: transparent;
    color: #6f8497;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}
.counter-message-more:hover, .counter-message-more:focus-visible {
    background: #2c4052;
    border-color: #526c83;
    color: #d6e2ec;
}
.counter-message-menu {
    width: min(360px, calc(100vw - 32px));
    max-height: calc(100dvh - 48px);
    box-sizing: border-box;
    margin: auto;
    padding: 14px;
    overflow-y: auto;
    border: 1px solid #536b81;
    border-radius: 20px;
    background: #132638;
    color: #f2f6f8;
    box-shadow: 0 20px 80px #0008;
}
.counter-message-menu::backdrop { background: #030b14a8; }
.counter-message-menu > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; padding-inline-start: 8px; }
.counter-message-menu > header > button {
    width: 44px; height: 44px; border: 0; border-radius: 10px;
    background: transparent; color: #b9ccdc; font-size: 26px; cursor: pointer;
}
.counter-message-menu-actions { display: grid; gap: 6px; }
.counter-message-menu-actions > button {
    min-height: 48px; padding: 12px; border: 1px solid transparent; border-radius: 10px;
    background: #20364a; color: #f2f6f8; text-align: start; font: inherit; font-size: 14px; cursor: pointer;
}
.counter-message-menu button:hover { background: #2d4961; }
.counter-message-menu button:disabled { opacity: .45; cursor: default; }
.counter-message-menu button:focus-visible { outline: 2px solid #83dcce; outline-offset: 2px; }
.counter-guest-health { display: flex; align-items: center; gap: 14px; padding: 12px 16px; border: 1px solid #7a653e; border-radius: 14px; background: #332d22; color: #f1d7a2; font-size: 13px; }
.counter-guest-health[hidden] { display: none; }
.counter-guest-health > div { flex: 1; min-width: 0; }
.counter-guest-health p { margin: 5px 0 0; line-height: 1.45; }
.counter-guest-health button { flex: 0 0 auto; }
@media (max-width: 480px) {
    .counter-room-topbar .counter-room-tab { padding-inline: 8px; min-width: max-content; white-space: nowrap; }
    .counter-room-topbar .counter-room-tabs { flex: 0 0 auto; }
    #counter-reset-live.counter-reset-conversation { padding-inline: 12px; font-size: 14px; }
    .counter-message { max-width: 100%; padding-inline: 9px; column-gap: 5px; }
}
@media (max-height: 600px) {
 .group-room-voice-status-row { font-size: 11px; }
 .group-room-audio-controls { padding-block: 3px; }
}
