atlas-iac/dockerfiles/hermes-webui-atlas-voice.css
jenkins 4d93ef5a0e hermes(voice): workspace nav home, character orb, conversation rename, voice-lang fix
Final conversation-mode polish from mobile testing:
- The Workspace toggle now sits with the chat/Telegram nav at every
  width: nav.rail on desktop, the top app titlebar on mobile. The
  floating pill that pushed the mobile composer's control row (and the
  conversation-mode button) off screen is gone - a fallback exists only
  for headless DOMs and is pinned to a top corner, never over the
  composer.
- The conversation orb watermark is now the Hermes character avatar
  (static/hermes-agent-192.png) instead of the caduceus staff.
- User-facing 'hands-free' copy renamed to 'Conversation mode'.
- Wrong-voice fix: strongReplyLanguage flagged Spanish on a single
  accented char, so an English reply naming European cities (Zürich,
  Málaga) overrode the correct English STT detection and was spoken by
  the Spanish voice. Detection now requires density (Cyrillic >=4 at
  >=50%, or inverted punctuation / >=2 accents corroborated by Spanish
  stopwords); plain English always speaks English, forced language wins,
  accent-free Spanish still routes via trusted STT. 294 voice tests.

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

889 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. */
/* FIX 2: the Hermes CHARACTER avatar (static/hermes-agent-192.png — the same
mark the top app bar shows next to the conversation title), painted as a
centred, low-opacity watermark that scales with the orb. It is a raster mark
already served under /static, so it is referenced by URL rather than inlined.
It carries no animation of its own (reduced-motion safe) and stays subtle
enough to read across every state tint without masking the energy layers. */
.voice-conversation-orb-mark {
position: absolute;
inset: 22%;
pointer-events: none;
background-image: url(hermes-agent-192.png);
background-position: center;
background-repeat: no-repeat;
background-size: contain;
opacity: 0.16;
filter: drop-shadow(0 0 6px rgba(var(--voice-accent), 0.3));
transition: opacity 200ms ease-out;
}
/* 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;
}
}