atlas-iac/dockerfiles/hermes-webui-atlas-voice.css

918 lines
77 KiB
CSS
Raw Normal View History

/* 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 1: the Hermes CHARACTER watermark inside the orb. A pre-processed data
URI — the 512px character art, feathered to a circle so its edges fade to
transparent (the orb appears to CROP it, with no square box) and luminance-
weighted to a warm near-white so the facial features read as light. Painted
with mix-blend-mode: screen so the glyph's darks drop away and only the light
face features glow over the dark orb. It is a static, centred, inset watermark
that scales with the orb (inset-positioned), carries no animation of its own
(reduced-motion safe) and stays subtle across every state tint. Inlined as a
data URI, so there is no separate static asset to serve. */
.voice-conversation-orb-mark {
position: absolute;
inset: 7%;
pointer-events: none;
background-image: url("data:image/png;base64,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
background-position: center;
background-repeat: no-repeat;
background-size: contain;
mix-blend-mode: normal;
opacity: 0.5;
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 energy layers. */
.voice-conversation[data-voice-state="speaking"] .voice-conversation-orb-mark,
.voice-conversation[data-voice-state="thinking"] .voice-conversation-orb-mark {
opacity: 0.72;
}
.voice-conversation.is-muted .voice-conversation-orb-mark {
opacity: 0.4;
}
/* 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);
hermes(voice): round-3 fixes, language routing, session-toast root cause - Captions read message bodies only (the scraper was concatenating avatar, author and worklog chips); multi-segment interim turns are now speakable and drive clean speak-to-thinking-to-speak cycles when playback drains mid-turn. - Dynamic endpointing: complete-looking partials (3+ words or terminal punctuation) endpoint at the base window; the long hold remains only for one-two-word fragments. A stale-busy 10s settle wait on every post-error send is gone. - Both overlay captions are bounded, touch-scrollable regions with follow-tail; caps raised for long turns. - Error envelopes are never spoken or captioned; errored turns run resyncCapture (fresh STT session on the hot mic). - Workspace toggle now lives in the sidebar rail (floating button only below the rail breakpoint). - False 'session unavailable' toast root-caused: the router continuity guard shows it on a 409 that fired when a transient profile-listing failure failed closed into a fake cross-profile mismatch; the patcher now answers from the alias cache and never claims a default-vs-named mismatch while aliases are unconfirmed. - Barge-in sends carry a one-line cut-point marker with the last spoken sentence; visible-history truncation judged infeasible client-side. - Language switching works end-to-end: sticky per-session STT language hint (restarting an unused next session on switch), reply voice from script evidence, STT detection, then stopword heuristic; cues and WAV fallback share the turn language. - Legacy CI guards: node skip for the DOM probe, ffmpeg/codec skips for the container-fallback test. 272 voice-lane tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:58:35 -03:00
min-height: 0;
text-align: center;
}
hermes(voice): round-3 fixes, language routing, session-toast root cause - Captions read message bodies only (the scraper was concatenating avatar, author and worklog chips); multi-segment interim turns are now speakable and drive clean speak-to-thinking-to-speak cycles when playback drains mid-turn. - Dynamic endpointing: complete-looking partials (3+ words or terminal punctuation) endpoint at the base window; the long hold remains only for one-two-word fragments. A stale-busy 10s settle wait on every post-error send is gone. - Both overlay captions are bounded, touch-scrollable regions with follow-tail; caps raised for long turns. - Error envelopes are never spoken or captioned; errored turns run resyncCapture (fresh STT session on the hot mic). - Workspace toggle now lives in the sidebar rail (floating button only below the rail breakpoint). - False 'session unavailable' toast root-caused: the router continuity guard shows it on a 409 that fired when a transient profile-listing failure failed closed into a fake cross-profile mismatch; the patcher now answers from the alias cache and never claims a default-vs-named mismatch while aliases are unconfirmed. - Barge-in sends carry a one-line cut-point marker with the last spoken sentence; visible-history truncation judged infeasible client-side. - Language switching works end-to-end: sticky per-session STT language hint (restarting an unused next session on switch), reply voice from script evidence, STT detection, then stopword heuristic; cues and WAV fallback share the turn language. - Legacy CI guards: node skip for the DOM probe, ffmpeg/codec skips for the container-fallback test. 272 voice-lane tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:58:35 -03:00
/* 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;
hermes(voice): round-3 fixes, language routing, session-toast root cause - Captions read message bodies only (the scraper was concatenating avatar, author and worklog chips); multi-segment interim turns are now speakable and drive clean speak-to-thinking-to-speak cycles when playback drains mid-turn. - Dynamic endpointing: complete-looking partials (3+ words or terminal punctuation) endpoint at the base window; the long hold remains only for one-two-word fragments. A stale-busy 10s settle wait on every post-error send is gone. - Both overlay captions are bounded, touch-scrollable regions with follow-tail; caps raised for long turns. - Error envelopes are never spoken or captioned; errored turns run resyncCapture (fresh STT session on the hot mic). - Workspace toggle now lives in the sidebar rail (floating button only below the rail breakpoint). - False 'session unavailable' toast root-caused: the router continuity guard shows it on a 409 that fired when a transient profile-listing failure failed closed into a fake cross-profile mismatch; the patcher now answers from the alias cache and never claims a default-vs-named mismatch while aliases are unconfirmed. - Barge-in sends carry a one-line cut-point marker with the last spoken sentence; visible-history truncation judged infeasible client-side. - Language switching works end-to-end: sticky per-session STT language hint (restarting an unused next session on switch), reply voice from script evidence, STT detection, then stopword heuristic; cues and WAV fallback share the turn language. - Legacy CI guards: node skip for the DOM probe, ffmpeg/codec skips for the container-fallback test. 272 voice-lane tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:58:35 -03:00
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;
hermes(voice): round-3 fixes, language routing, session-toast root cause - Captions read message bodies only (the scraper was concatenating avatar, author and worklog chips); multi-segment interim turns are now speakable and drive clean speak-to-thinking-to-speak cycles when playback drains mid-turn. - Dynamic endpointing: complete-looking partials (3+ words or terminal punctuation) endpoint at the base window; the long hold remains only for one-two-word fragments. A stale-busy 10s settle wait on every post-error send is gone. - Both overlay captions are bounded, touch-scrollable regions with follow-tail; caps raised for long turns. - Error envelopes are never spoken or captioned; errored turns run resyncCapture (fresh STT session on the hot mic). - Workspace toggle now lives in the sidebar rail (floating button only below the rail breakpoint). - False 'session unavailable' toast root-caused: the router continuity guard shows it on a 409 that fired when a transient profile-listing failure failed closed into a fake cross-profile mismatch; the patcher now answers from the alias cache and never claims a default-vs-named mismatch while aliases are unconfirmed. - Barge-in sends carry a one-line cut-point marker with the last spoken sentence; visible-history truncation judged infeasible client-side. - Language switching works end-to-end: sticky per-session STT language hint (restarting an unused next session on switch), reply voice from script evidence, STT detection, then stopword heuristic; cues and WAV fallback share the turn language. - Legacy CI guards: node skip for the DOM probe, ffmpeg/codec skips for the container-fallback test. 272 voice-lane tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:58:35 -03:00
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 only — the state caption keeps its single
static ellipsis (e.g. "Thinking…") and is never trailed by animated dots, so
the ellipsis stays consistent instead of doubling up. 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;
}
@keyframes voice-conversation-await-shimmer {
0%, 100% { opacity: 0.6; filter: blur(10px); }
50% { opacity: 0.92; filter: blur(13px); }
}
@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; }
hermes(voice): round-3 fixes, language routing, session-toast root cause - Captions read message bodies only (the scraper was concatenating avatar, author and worklog chips); multi-segment interim turns are now speakable and drive clean speak-to-thinking-to-speak cycles when playback drains mid-turn. - Dynamic endpointing: complete-looking partials (3+ words or terminal punctuation) endpoint at the base window; the long hold remains only for one-two-word fragments. A stale-busy 10s settle wait on every post-error send is gone. - Both overlay captions are bounded, touch-scrollable regions with follow-tail; caps raised for long turns. - Error envelopes are never spoken or captioned; errored turns run resyncCapture (fresh STT session on the hot mic). - Workspace toggle now lives in the sidebar rail (floating button only below the rail breakpoint). - False 'session unavailable' toast root-caused: the router continuity guard shows it on a 409 that fired when a transient profile-listing failure failed closed into a fake cross-profile mismatch; the patcher now answers from the alias cache and never claims a default-vs-named mismatch while aliases are unconfirmed. - Barge-in sends carry a one-line cut-point marker with the last spoken sentence; visible-history truncation judged infeasible client-side. - Language switching works end-to-end: sticky per-session STT language hint (restarting an unused next session on switch), reply voice from script evidence, STT detection, then stopword heuristic; cues and WAV fallback share the turn language. - Legacy CI guards: node skip for the DOM probe, ffmpeg/codec skips for the container-fallback test. 272 voice-lane tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 13:58:35 -03:00
.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;
}
}
/* New-session "Start conversation" affordance: a prominent, centred entry point
into conversation mode on the empty new-chat screen. Injected into #emptyState
by atlas-voice.js only when local voice is available, so it inherits the
empty state's own centred flex layout and show/hide. */
.empty-conversation-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
margin: 2px auto 22px;
padding: 12px 22px;
min-height: 48px;
border-radius: 999px;
border: 1px solid var(--accent-bg-strong, rgba(24, 127, 139, 0.17));
background: var(--accent-bg, rgba(24, 127, 139, 0.09));
color: var(--accent-text, #126f7a);
font-family: inherit;
font-size: 14.5px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease,
color 0.15s ease, transform 0.12s ease, box-shadow 0.15s ease;
}
.empty-conversation-btn:hover {
background: var(--accent, #187f8b);
border-color: var(--accent, #187f8b);
color: #fff;
}
.empty-conversation-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--focus-ring, rgba(24, 127, 139, 0.38));
}
.empty-conversation-btn:active {
transform: translateY(1px);
}
.empty-conversation-icon {
display: inline-flex;
align-items: center;
line-height: 1;
}
@media (prefers-reduced-motion: reduce) {
.empty-conversation-btn {
transition: none;
}
.empty-conversation-btn:active {
transform: none;
}
}