atlas-iac/dockerfiles/hermes-webui-atlas-voice.css
jenkins 3fd760de0e hermes(voice): fix HHermesProcessed + first-sentence-stop; orb mark, lang selector
Real root cause (confirmed against the live build-24 DOM): the caption
and TTS extraction fell back to turn.textContent whenever a settle-frame
race left no readable answer segment, scraping the avatar letter,
author name and 'Processed 13s' chip - and that truncated reply made
TTS speak only the first segment then drop to Listening even with the
mic muted (the muted-mic first-sentence-stop). Extraction now prefers
each answer segment's data-raw-text, else the answer .msg-body only
(excluding thinking/tool/worklog/role chrome), and the textContent
fallback is gone; a genuinely mid-flight reply retries briefly so the
whole thing is read before the overlay drains. Also: the app's own
caduceus mark embedded in the conversation orb as a subtle watermark; a
corner language selector (Auto + en/es/ru) that forces both the STT
hint and the reply voice; and a thinking affordance after 2.5s of dead
time. New response probe + extraction test prove caption==body-only and
that every sentence reaches the TTS queue. 278 voice-lane tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 15:46:09 -03:00

776 lines
21 KiB
CSS

/* Private hands-free conversation instrument for the Atlas voice bridge. */
.voice-mode-bar {
--voice-accent: 72, 207, 204;
--voice-accent-secondary: 76, 164, 205;
position: relative;
isolation: isolate;
box-sizing: border-box;
min-height: 106px;
padding: 9px 12px 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
overflow: hidden;
border-bottom: 1px solid rgba(174, 192, 218, 0.1);
background:
radial-gradient(circle at 50% 38%, rgba(var(--voice-accent), 0.075), transparent 46%),
linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(4, 8, 18, 0.035));
}
.voice-mode-bar::before {
content: "";
position: absolute;
z-index: -1;
top: 0;
left: 18%;
right: 18%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(233, 244, 255, 0.16), transparent);
}
:root:not(.dark) .voice-mode-bar {
border-bottom-color: rgba(38, 55, 78, 0.1);
background:
radial-gradient(circle at 50% 38%, rgba(var(--voice-accent), 0.09), transparent 48%),
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(226, 232, 240, 0.12));
}
.voice-mode-indicator {
--voice-ripple-scale: 1.035;
--voice-ripple-opacity: 0.3;
position: relative;
width: 70px;
height: 70px;
flex: 0 0 70px;
border-radius: 50%;
transform: translateZ(0);
}
.voice-mode-indicator.listening {
--voice-accent: 69, 218, 207;
--voice-accent-secondary: 73, 169, 209;
}
.voice-mode-indicator.transcribing {
--voice-accent: 73, 193, 218;
--voice-accent-secondary: 93, 129, 224;
}
.voice-mode-indicator.thinking {
--voice-accent: 135, 111, 244;
--voice-accent-secondary: 81, 62, 190;
}
.voice-mode-indicator.speaking {
--voice-accent: 249, 169, 105;
--voice-accent-secondary: 236, 112, 137;
}
.voice-mode-indicator.error {
--voice-accent: 235, 135, 88;
--voice-accent-secondary: 198, 74, 78;
}
.voice-instrument-halo,
.voice-instrument-ripple,
.voice-instrument-orbit,
.voice-instrument-core {
position: absolute;
display: block;
box-sizing: border-box;
border-radius: 50%;
pointer-events: none;
}
.voice-instrument-halo {
inset: 2px;
opacity: 0;
background: radial-gradient(circle, rgba(var(--voice-accent), 0.2), rgba(var(--voice-accent), 0.05) 43%, transparent 70%);
filter: blur(4px);
}
.voice-instrument-ripple {
inset: 3px;
opacity: 0;
border: 1px solid rgba(var(--voice-accent), 0.56);
box-shadow: 0 0 12px rgba(var(--voice-accent), 0.13);
}
.voice-instrument-orbit {
inset: 2px;
opacity: 0;
border: 1px solid rgba(var(--voice-accent), 0.18);
box-shadow:
inset 0 0 8px rgba(var(--voice-accent), 0.06),
0 0 11px rgba(var(--voice-accent), 0.07);
}
.voice-instrument-orbit::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
border-top: 1.5px solid rgba(var(--voice-accent), 0.88);
border-right: 1px solid rgba(var(--voice-accent-secondary), 0.35);
border-bottom: 1px solid transparent;
border-left: 1px solid transparent;
}
.voice-instrument-orbit::after {
content: "";
position: absolute;
top: -2px;
left: 50%;
width: 5px;
height: 5px;
margin-left: -2.5px;
border-radius: 50%;
background: rgb(var(--voice-accent));
box-shadow:
0 0 5px rgba(var(--voice-accent), 0.92),
0 0 12px rgba(var(--voice-accent), 0.44);
}
.voice-instrument-core {
inset: 12px;
display: grid;
place-items: center;
color: rgba(242, 250, 255, 0.94);
border: 1px solid rgba(238, 247, 255, 0.2);
background:
radial-gradient(circle at 38% 29%, rgba(255, 255, 255, 0.26), transparent 24%),
radial-gradient(circle at 50% 66%, rgba(var(--voice-accent), 0.36), transparent 64%),
linear-gradient(145deg, rgba(36, 45, 61, 0.96), rgba(10, 15, 27, 0.98));
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.22),
inset 0 -9px 18px rgba(0, 0, 0, 0.24),
0 4px 13px rgba(0, 0, 0, 0.25),
0 0 17px rgba(var(--voice-accent), 0.17);
}
:root:not(.dark) .voice-instrument-core {
color: rgba(31, 48, 66, 0.9);
border-color: rgba(255, 255, 255, 0.82);
background:
radial-gradient(circle at 38% 29%, rgba(255, 255, 255, 0.96), transparent 27%),
radial-gradient(circle at 50% 66%, rgba(var(--voice-accent), 0.2), transparent 65%),
linear-gradient(145deg, rgba(248, 250, 251, 0.98), rgba(216, 225, 231, 0.96));
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.94),
inset 0 -8px 17px rgba(64, 84, 100, 0.1),
0 4px 12px rgba(35, 53, 72, 0.15),
0 0 15px rgba(var(--voice-accent), 0.13);
}
.voice-instrument-symbol {
display: block;
width: 22px;
height: 22px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.32));
}
.voice-instrument-symbol svg {
display: block;
width: 100%;
height: 100%;
}
.voice-symbol {
display: none;
}
.voice-mode-indicator.listening .voice-symbol-listening,
.voice-mode-indicator.transcribing .voice-symbol-transcribing,
.voice-mode-indicator.thinking .voice-symbol-thinking,
.voice-mode-indicator.speaking .voice-symbol-speaking,
.voice-mode-indicator.error .voice-symbol-error {
display: inline;
}
.voice-mode-indicator.listening .voice-instrument-halo {
opacity: 0.62;
animation: voice-instrument-breathe 3.8s ease-in-out infinite;
}
.voice-mode-indicator.listening .voice-instrument-ripple {
opacity: var(--voice-ripple-opacity);
transform: scale(var(--voice-ripple-scale));
transition: transform 140ms ease-out, opacity 160ms ease-out;
}
.voice-mode-indicator.transcribing .voice-instrument-orbit {
opacity: 0.92;
animation: voice-instrument-orbit 3.4s cubic-bezier(0.58, 0.12, 0.42, 0.88) infinite;
}
.voice-mode-indicator.thinking .voice-instrument-orbit {
opacity: 0.96;
animation: voice-instrument-orbit 4.6s cubic-bezier(0.58, 0.12, 0.42, 0.88) infinite;
}
.voice-mode-indicator.thinking .voice-instrument-core {
animation: voice-instrument-core-breathe 5.7s ease-in-out infinite;
}
.voice-mode-indicator.speaking .voice-instrument-orbit {
opacity: 0.86;
transform: rotate(38deg);
}
.voice-mode-indicator.speaking.is-playing .voice-instrument-halo {
opacity: 0.66;
animation: voice-instrument-speaking-pulse 2.15s ease-out infinite;
}
.voice-mode-indicator.error .voice-instrument-orbit {
opacity: 0.9;
transform: rotate(-32deg);
border-color: rgba(var(--voice-accent), 0.44);
}
.voice-mode-indicator.error .voice-instrument-core {
color: rgba(255, 232, 219, 0.95);
box-shadow:
inset 0 1px 1px rgba(255, 255, 255, 0.2),
inset 0 -9px 18px rgba(0, 0, 0, 0.24),
0 4px 13px rgba(0, 0, 0, 0.25),
0 0 12px rgba(var(--voice-accent), 0.17);
}
.voice-mode-label {
position: relative;
z-index: 1;
min-height: 16px;
max-width: min(100%, 34rem);
overflow: hidden;
color: var(--text, #f4f6fb);
font-size: 12px;
font-weight: 650;
line-height: 1.35;
letter-spacing: 0.018em;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
}
:root:not(.dark) .voice-mode-label {
color: #263648;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);
}
@keyframes voice-instrument-breathe {
0%, 100% { opacity: 0.38; transform: scale(0.94); }
50% { opacity: 0.72; transform: scale(1.06); }
}
@keyframes voice-instrument-orbit {
0% { transform: rotate(0deg); }
50% { transform: rotate(177deg); }
100% { transform: rotate(360deg); }
}
@keyframes voice-instrument-core-breathe {
0%, 100% { filter: saturate(0.96) brightness(0.98); }
50% { filter: saturate(1.08) brightness(1.06); }
}
@keyframes voice-instrument-speaking-pulse {
0% { opacity: 0.58; transform: scale(0.88); }
58% { opacity: 0.22; transform: scale(1.12); }
100% { opacity: 0; transform: scale(1.2); }
}
@media (max-width: 640px) {
.voice-mode-bar {
min-height: 94px;
padding: 7px 10px 8px;
gap: 4px;
}
.voice-mode-indicator {
width: 62px;
height: 62px;
flex-basis: 62px;
}
.voice-instrument-core {
inset: 11px;
}
.voice-instrument-symbol {
width: 20px;
height: 20px;
}
}
@media (prefers-reduced-motion: reduce) {
.voice-mode-bar *,
.voice-mode-bar *::before,
.voice-mode-bar *::after {
animation: none !important;
transition: none !important;
}
.voice-mode-indicator.listening .voice-instrument-halo {
opacity: 0.45;
transform: none;
}
.voice-mode-indicator.listening .voice-instrument-ripple {
opacity: 0.34;
transform: scale(1.035);
}
.voice-mode-indicator.transcribing .voice-instrument-orbit,
.voice-mode-indicator.thinking .voice-instrument-orbit {
transform: rotate(24deg);
}
.voice-mode-indicator.speaking.is-playing .voice-instrument-halo {
opacity: 0.42;
transform: scale(1.03);
}
}
/* ── Conversation mode overlay ─────────────────────────────────────────── */
/* Full-viewport hands-free conversation surface. Created lazily by
atlas-voice.js on activation and removed on deactivate. The orb breathes in
idle listening, follows live microphone energy through
--conversation-level, and pulses while Hermes speaks (.is-playing, driven
from TTS playback activity). State accents reuse the indicator palette. */
.voice-conversation {
--voice-accent: 69, 218, 207;
--voice-accent-secondary: 73, 169, 209;
--conversation-level: 0;
position: fixed;
inset: 0;
z-index: 420;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 34px 22px calc(28px + env(safe-area-inset-bottom, 0px));
background:
radial-gradient(circle at 50% 30%, rgba(var(--voice-accent), 0.11), transparent 52%),
radial-gradient(circle at 22% 92%, rgba(157, 142, 224, 0.07), transparent 44%),
linear-gradient(160deg, #0b1120, #070a12 55%, #081018);
color: rgba(240, 247, 255, 0.94);
}
.voice-conversation[data-voice-state="transcribing"] {
--voice-accent: 73, 193, 218;
--voice-accent-secondary: 93, 129, 224;
}
.voice-conversation[data-voice-state="thinking"] {
--voice-accent: 135, 111, 244;
--voice-accent-secondary: 81, 62, 190;
}
.voice-conversation[data-voice-state="speaking"] {
--voice-accent: 249, 169, 105;
--voice-accent-secondary: 236, 112, 137;
}
.voice-conversation[data-voice-state="error"] {
--voice-accent: 235, 135, 88;
--voice-accent-secondary: 198, 74, 78;
}
.voice-conversation-orb {
position: relative;
width: min(46vmin, 220px);
height: min(46vmin, 220px);
flex: none;
}
.voice-conversation-orb-halo,
.voice-conversation-orb-core,
.voice-conversation-orb-ring {
position: absolute;
inset: 0;
display: block;
border-radius: 50%;
pointer-events: none;
}
.voice-conversation-orb-halo {
background: radial-gradient(circle, rgba(var(--voice-accent), 0.34), rgba(var(--voice-accent-secondary), 0.1) 52%, transparent 74%);
filter: blur(10px);
opacity: 0.75;
transform: scale(calc(1 + (var(--conversation-level) * 0.3)));
transition: transform 130ms ease-out;
}
.voice-conversation-orb-core {
inset: 12%;
background:
radial-gradient(circle at 36% 28%, rgba(255, 255, 255, 0.34), transparent 34%),
radial-gradient(circle at 52% 68%, rgba(var(--voice-accent), 0.62), rgba(var(--voice-accent-secondary), 0.4) 62%, transparent 86%),
linear-gradient(150deg, rgba(34, 44, 63, 0.95), rgba(9, 13, 24, 0.98));
box-shadow:
inset 0 1px 2px rgba(255, 255, 255, 0.28),
0 10px 42px rgba(0, 0, 0, 0.45),
0 0 44px rgba(var(--voice-accent), 0.3);
transform: scale(calc(1 + (var(--conversation-level) * 0.14)));
transition: transform 110ms ease-out;
}
.voice-conversation-orb-ring {
inset: 3%;
border: 1.5px solid rgba(var(--voice-accent), 0.4);
box-shadow: 0 0 22px rgba(var(--voice-accent), 0.16);
opacity: 0.85;
}
/* FIX 2: the Hermes caduceus mark, a static centred watermark inside the orb.
Sits above the core wash but below no energy layer's motion — it never
animates on its own and never scales past the orb, so the breathing/sweep/
wave animations always read over it. Monochrome via currentColor at low
opacity so it stays legible against every state tint without competing with
the accent colour. Scales with the orb because it is inset-positioned. */
.voice-conversation-orb-mark {
position: absolute;
inset: 24%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
color: rgba(233, 244, 255, 0.9);
opacity: 0.16;
mix-blend-mode: screen;
transition: opacity 200ms ease-out;
}
.voice-conversation-orb-mark svg {
width: 100%;
height: 100%;
display: block;
filter: drop-shadow(0 0 6px rgba(var(--voice-accent), 0.3));
}
/* Speaking/thinking lift the watermark a touch so it feels alive with the turn,
still far below any level that would mask the animation. */
.voice-conversation[data-voice-state="speaking"] .voice-conversation-orb-mark,
.voice-conversation[data-voice-state="thinking"] .voice-conversation-orb-mark {
opacity: 0.2;
}
.voice-conversation.is-muted .voice-conversation-orb-mark {
opacity: 0.1;
}
/* Idle listening: slow breathing. */
.voice-conversation[data-voice-state="listening"] .voice-conversation-orb-halo {
animation: voice-conversation-breathe 4.4s ease-in-out infinite;
}
/* Transcribing/thinking: the ring sweeps. */
.voice-conversation[data-voice-state="transcribing"] .voice-conversation-orb-ring,
.voice-conversation[data-voice-state="thinking"] .voice-conversation-orb-ring {
border-top-color: rgba(var(--voice-accent), 0.95);
border-right-color: rgba(var(--voice-accent-secondary), 0.4);
animation: voice-conversation-sweep 2.6s linear infinite;
}
/* Hermes speaking: the halo waves outward while playback is live. */
.voice-conversation.is-playing .voice-conversation-orb-halo {
animation: voice-conversation-wave 1.9s ease-out infinite;
}
.voice-conversation.is-playing .voice-conversation-orb-core {
animation: voice-conversation-swell 1.9s ease-in-out infinite;
}
.voice-conversation-state {
min-height: 20px;
font-size: 15px;
font-weight: 650;
letter-spacing: 0.02em;
text-align: center;
color: rgba(238, 246, 255, 0.9);
}
.voice-conversation-captions {
display: flex;
flex-direction: column;
gap: 10px;
width: min(100%, 40rem);
min-height: 0;
text-align: center;
}
/* Each caption is its own bounded scroll region: the user transcript and the
reply overflow independently, scroll by touch inside the overlay without
moving the page behind (overscroll-behavior: contain), stay keyboard
scrollable via tabindex, and fade at the top edge so clipped history reads
as scrollable. Auto-follow of the streaming tail lives in atlas-voice.js
(data-follow, per region). */
.voice-conversation-caption-user,
.voice-conversation-caption-assistant {
overflow-y: auto;
touch-action: pan-y;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
scrollbar-width: thin;
-webkit-mask-image: linear-gradient(180deg, transparent 0, #000 18px, #000 100%);
mask-image: linear-gradient(180deg, transparent 0, #000 18px, #000 100%);
}
.voice-conversation-caption-user:focus-visible,
.voice-conversation-caption-assistant:focus-visible {
outline: 2px solid rgb(var(--voice-accent));
outline-offset: 2px;
}
.voice-conversation-caption-user {
margin: 0;
max-height: 22vh;
font-size: 14px;
line-height: 1.5;
color: rgba(178, 196, 214, 0.88);
font-style: italic;
}
.voice-conversation-caption-assistant {
margin: 0;
max-height: 38vh;
font-size: 16px;
line-height: 1.6;
color: rgba(240, 247, 255, 0.95);
}
.voice-conversation-controls {
display: flex;
gap: 14px;
margin-top: 6px;
}
.voice-conversation-controls button {
font: inherit;
font-size: 13px;
font-weight: 600;
color: rgba(233, 243, 252, 0.92);
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(var(--voice-accent), 0.36);
border-radius: 999px;
padding: 10px 18px;
cursor: pointer;
}
.voice-conversation-controls button:hover {
background: rgba(255, 255, 255, 0.12);
}
.voice-conversation-controls button:focus-visible {
outline: 2px solid rgb(var(--voice-accent));
outline-offset: 2px;
}
.voice-conversation.is-muted .voice-conversation-mute {
color: #ffd9c7;
border-color: rgba(235, 135, 88, 0.6);
background: rgba(235, 135, 88, 0.14);
}
/* FIX 3: unobtrusive language chooser in the top-right corner. A small frosted
globe that opens a compact Auto + supported-language menu; selecting forces
both the reply voice and the STT hint for the session. */
.voice-conversation-lang {
position: absolute;
top: calc(16px + env(safe-area-inset-top, 0px));
right: calc(16px + env(safe-area-inset-right, 0px));
z-index: 2;
}
.voice-conversation-lang-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
color: rgba(226, 238, 250, 0.72);
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(var(--voice-accent), 0.28);
border-radius: 50%;
cursor: pointer;
transition: color 160ms ease, background 160ms ease, border-color 160ms ease;
}
.voice-conversation-lang-btn:hover {
color: rgba(240, 248, 255, 0.95);
background: rgba(255, 255, 255, 0.12);
}
.voice-conversation-lang-btn:focus-visible {
outline: 2px solid rgb(var(--voice-accent));
outline-offset: 2px;
}
/* Forced (non-Auto) language: a small accent dot marks the active override. */
.voice-conversation-lang-btn.is-forced {
color: rgb(var(--voice-accent));
border-color: rgba(var(--voice-accent), 0.7);
}
.voice-conversation-lang-btn.is-forced::after {
content: "";
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background: rgb(var(--voice-accent));
box-shadow: 0 0 6px rgba(var(--voice-accent), 0.8);
}
.voice-conversation-lang-menu {
position: absolute;
top: 46px;
right: 0;
min-width: 148px;
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px;
background: rgba(16, 22, 36, 0.96);
border: 1px solid rgba(var(--voice-accent), 0.28);
border-radius: 14px;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
}
.voice-conversation-lang-menu[hidden] {
display: none;
}
.voice-conversation-lang-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font: inherit;
font-size: 14px;
font-weight: 550;
text-align: left;
color: rgba(231, 241, 251, 0.9);
background: transparent;
border: 0;
border-radius: 9px;
padding: 9px 12px;
cursor: pointer;
}
.voice-conversation-lang-item:hover {
background: rgba(255, 255, 255, 0.08);
}
.voice-conversation-lang-item:focus-visible {
outline: 2px solid rgb(var(--voice-accent));
outline-offset: -2px;
}
/* The selected option carries a trailing check. */
.voice-conversation-lang-item[aria-checked="true"] {
color: #fff;
background: rgba(var(--voice-accent), 0.16);
}
.voice-conversation-lang-item[aria-checked="true"]::after {
content: "✓";
font-size: 13px;
color: rgb(var(--voice-accent));
}
.voice-conversation.is-muted .voice-conversation-orb-halo,
.voice-conversation.is-muted .voice-conversation-orb-core {
animation: none;
transform: none;
filter: blur(10px) saturate(0.4);
}
.voice-conversation.is-muted .voice-conversation-orb-core {
filter: saturate(0.35) brightness(0.8);
}
/* FIX 4: "working…" affordance. Armed by atlas-voice.js (.is-awaiting) only
after the model has been Thinking a beat with no reply text yet, so dead time
reads as alive. A slow orb shimmer plus animated dots trailing the state
caption — cheap, CSS-only, and fully suppressed under reduced motion by the
blanket rule below. */
.voice-conversation.is-awaiting .voice-conversation-orb-halo {
animation: voice-conversation-await-shimmer 2.6s ease-in-out infinite;
}
.voice-conversation.is-awaiting .voice-conversation-state::after {
content: "";
display: inline-block;
width: 1.4em;
margin-left: 0.15em;
text-align: left;
vertical-align: bottom;
animation: voice-conversation-await-dots 1.4s steps(4, end) infinite;
}
@keyframes voice-conversation-await-shimmer {
0%, 100% { opacity: 0.6; filter: blur(10px); }
50% { opacity: 0.92; filter: blur(13px); }
}
@keyframes voice-conversation-await-dots {
0% { content: ""; }
25% { content: "·"; }
50% { content: "··"; }
75% { content: "···"; }
100% { content: ""; }
}
@keyframes voice-conversation-breathe {
0%, 100% { opacity: 0.55; transform: scale(calc(0.96 + (var(--conversation-level) * 0.3))); }
50% { opacity: 0.85; transform: scale(calc(1.05 + (var(--conversation-level) * 0.3))); }
}
@keyframes voice-conversation-sweep {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes voice-conversation-wave {
0% { opacity: 0.8; transform: scale(0.96); }
55% { opacity: 0.5; transform: scale(1.12); }
100% { opacity: 0.75; transform: scale(0.98); }
}
@keyframes voice-conversation-swell {
0%, 100% { transform: scale(0.99); }
50% { transform: scale(1.05); }
}
@media (max-width: 640px) {
.voice-conversation { gap: 18px; }
.voice-conversation-caption-user { max-height: 24vh; }
.voice-conversation-caption-assistant { max-height: 34vh; }
}
/* Reduced motion: static orb states — the accent color and the state caption
still communicate every phase without animation or level-driven motion. */
@media (prefers-reduced-motion: reduce) {
.voice-conversation *,
.voice-conversation *::before,
.voice-conversation *::after {
animation: none !important;
transition: none !important;
}
.voice-conversation-orb-halo,
.voice-conversation-orb-core {
transform: none !important;
}
}