atlas-iac/dockerfiles/hermes-webui-atlas-voice.css
jenkins 181a7517c2
Some checks failed
Tests / Declarative: Post Actions failed: 49, skipped: 72, passed: 3724
hermes(voice): interim-ack tail, natural fillers, unified audio + output picker
Three conversation-mode fixes in one pass:
- Interim-acknowledgement truncation: when an interim ack folds into the
  hidden worklog segment mid-speech, the retained unspoken tail is now
  flushed and spoken in full before the Thinking transition, and a
  distinct follow-up message is chunked from its own start and queued
  after the interim drains (no more 'stops after the first clause, rest
  resurfaces with the next message').
- Natural thinking fillers: brief per-language interjections (Umm/Hmm/
  One sec; Mmm/A ver; Хм/Секунду) on genuine >1.9s thinking gaps only,
  non-repeating, answer-preempting, mute-aware.
- One unified audio sink for every spoken output (reply, cues, fillers,
  WAV fallback) - fixes cues playing the loudspeaker while the reply
  used a different output - plus a tidy corner output-device selector
  (enumerateDevices + setSinkId, feature-detected, session-only) styled
  like the language selector. Also realigns two STT-server decode-param
  assertions to the dict form from the STT tuning commit. 284 voice tests.

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

890 lines
24 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));
}
/* Output-device selector: a twin of the language chooser, seated just left of
it in the same corner. Feature-detected and hidden entirely when the browser
cannot enumerate/route outputs, so it never appears as a dead control. */
.voice-conversation-out {
position: absolute;
top: calc(16px + env(safe-area-inset-top, 0px));
right: calc(64px + env(safe-area-inset-right, 0px));
z-index: 2;
}
.voice-conversation-out-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-out-btn:hover {
color: rgba(240, 248, 255, 0.95);
background: rgba(255, 255, 255, 0.12);
}
.voice-conversation-out-btn:focus-visible {
outline: 2px solid rgb(var(--voice-accent));
outline-offset: 2px;
}
/* A non-default output device is marked with the same accent dot. */
.voice-conversation-out-btn.is-forced {
color: rgb(var(--voice-accent));
border-color: rgba(var(--voice-accent), 0.7);
}
.voice-conversation-out-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-out-menu {
position: absolute;
top: 46px;
right: 0;
min-width: 180px;
max-width: 260px;
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-out-menu[hidden] {
display: none;
}
.voice-conversation-out-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;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.voice-conversation-out-item:hover {
background: rgba(255, 255, 255, 0.08);
}
.voice-conversation-out-item:focus-visible {
outline: 2px solid rgb(var(--voice-accent));
outline-offset: -2px;
}
.voice-conversation-out-item[aria-checked="true"] {
color: #fff;
background: rgba(var(--voice-accent), 0.16);
}
.voice-conversation-out-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;
}
}