)}
{(status === "queued" || status === "your-turn") && (
-
+
{status === "queued" ? (
Posición en la cola: {state.queuePosition}
) : (
<>
-
{
controller.join();
}}
>
Entrar ahora
-
+
>
)}
-
{
void controller.stop();
}}
>
Salir de la cola
-
+
)}
diff --git a/vocero-frontend/src/styles.css b/vocero-frontend/src/styles.css
index ee6db98..c1bc50d 100644
--- a/vocero-frontend/src/styles.css
+++ b/vocero-frontend/src/styles.css
@@ -1,154 +1,9 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "./styles/theme.css";
-@import "./styles/legacy-theme.css" layer(components);
-@import "./styles/layout.css" layer(components);
+@import "./styles/base.css" layer(base);
+@import "./styles/voice-stage.css" layer(components);
+@import "./styles/markdown.css" layer(components);
@import "./styles/surfaces.css" layer(components);
+@import "./styles/range.css" layer(components);
@import "tailwindcss/utilities.css" layer(utilities);
-
-@layer components {
- .chat-empty,
- .chat-empty-hint {
- opacity: 1;
- color: var(--text-dim);
- }
- html[data-theme="white"] .chat-empty,
- html[data-theme="white"] .chat-empty-hint {
- color: #64748b;
- }
- #root {
- display: contents;
- }
- .state-idle .ind-mic {
- opacity: 1;
- transform: scale(1);
- }
- html[data-theme="white"] .topbar-right[data-debug-enabled="true"] {
- display: flex;
- grid-column: 1 / -1;
- grid-row: 2;
- flex-wrap: wrap;
- justify-content: center;
- margin-top: 12px;
- }
- html[data-theme="white"] .white-mic-circle svg.wmc-mic {
- fill: none;
- }
- .loading-screen {
- position: fixed;
- inset: 0;
- display: grid;
- place-items: center;
- background: white;
- color: #334155;
- font: 500 16px system-ui;
- z-index: 1000;
- }
- .startup-error {
- max-width: 32rem;
- padding: 24px;
- text-align: center;
- }
- .app-error {
- display: flex;
- align-items: center;
- gap: 12px;
- margin: 12px auto;
- padding: 10px 16px;
- max-width: 680px;
- border: 1px solid var(--error);
- border-radius: 12px;
- font-size: 13px;
- background: var(--bg-elev);
- }
- .agent-tool-item {
- padding: 12px 0;
- border-bottom: 1px solid var(--border);
- overflow-wrap: anywhere;
- }
- .queue-actions {
- align-items: center;
- flex-wrap: wrap;
- justify-content: center;
- }
- .queue-actions .circle-subcaption {
- flex-basis: 100%;
- }
- .reports-panel {
- position: fixed;
- inset: 0 auto 0 0;
- width: 320px;
- overflow-y: auto;
- box-sizing: border-box;
- padding: 20px;
- background: var(--bg-elev);
- border-right: 1px solid var(--border);
- z-index: 3;
- }
- .reports-panel h2 {
- margin: 0 0 16px;
- font-size: 18px;
- }
- .reports-drawer.modal {
- margin: 0;
- width: min(360px, 92vw);
- max-height: 100dvh;
- height: 100dvh;
- border-radius: 0;
- }
- .reports-content .reports-hint {
- margin: 0;
- max-height: none;
- padding: 12px;
- }
- .wake-word-control {
- display: grid;
- gap: 4px;
- padding: 0 16px 12px;
- text-align: center;
- font-size: 13px;
- }
- .wake-word-control label {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- cursor: pointer;
- }
- .wake-word-control span {
- font-size: 12px;
- color: var(--text-dim);
- }
- html[data-theme="white"] .wake-word-control span {
- color: #64748b;
- }
- .bubble-body {
- max-height: 240px;
- overflow: auto;
- }
- .white-session-controls svg {
- width: 18px;
- height: 18px;
- }
- .mic-gate-arc:focus-visible {
- outline: 2px solid var(--accent-2);
- outline-offset: 4px;
- border-radius: 50%;
- }
- .user-profile-row {
- display: grid;
- grid-template-columns: 90px 1fr;
- gap: 16px;
- padding: 12px 0;
- }
- .user-profile-value {
- margin: 0;
- overflow-wrap: anywhere;
- }
- .user-profile-label {
- color: var(--text-dim);
- }
- .hist-tool-block {
- margin: 0;
- }
-}
diff --git a/vocero-frontend/src/styles/base.css b/vocero-frontend/src/styles/base.css
new file mode 100644
index 0000000..c2a9881
--- /dev/null
+++ b/vocero-frontend/src/styles/base.css
@@ -0,0 +1,128 @@
+/* Minimal element defaults and page background. Preflight is intentionally not enabled. */
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ height: auto;
+ padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) 0 max(8px, env(safe-area-inset-left));
+ font-family: var(--font-sans);
+ color: var(--text);
+ min-height: 100vh;
+ min-height: 100svh;
+ min-height: 100dvh;
+ width: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ position: relative;
+ background:
+ radial-gradient(1200px 800px at 15% -10%, rgba(139, 92, 246, 0.3) 0%, transparent 60%),
+ radial-gradient(900px 700px at 110% 10%, rgba(236, 72, 153, 0.22) 0%, transparent 55%),
+ radial-gradient(1000px 800px at 50% 120%, rgba(34, 211, 238, 0.22) 0%, transparent 60%),
+ radial-gradient(ellipse at 50% 50%, #0e1019 0%, var(--bg) 65%);
+ background-attachment: fixed;
+}
+body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background-image:
+ radial-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
+ radial-gradient(rgba(139, 92, 246, 0.035) 1px, transparent 1px);
+ background-size:
+ 42px 42px,
+ 42px 42px;
+ background-position:
+ 0 0,
+ 21px 21px;
+ mix-blend-mode: screen;
+ opacity: 0.55;
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.4) 70%, transparent 100%);
+}
+body::after {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(600px 600px at 20% 80%, rgba(139, 92, 246, 0.1), transparent 70%),
+ radial-gradient(500px 500px at 80% 20%, rgba(34, 211, 238, 0.08), transparent 70%);
+ animation: auroraShift 22s ease-in-out infinite alternate;
+}
+
+button {
+ font-family: inherit;
+}
+
+a {
+ color: var(--text);
+ text-decoration: none;
+ border-bottom: 1px solid var(--border-strong);
+}
+a:hover {
+ border-bottom-color: var(--text);
+}
+html[data-theme="white"] body {
+ background: var(--bg);
+ color: var(--text);
+ background-image: none;
+}
+html[data-theme="white"] body::before,
+html[data-theme="white"] body::after {
+ display: none;
+ background-image: none;
+ animation: none;
+}
+#root {
+ display: contents;
+}
+@keyframes auroraShift {
+ 0% {
+ transform: translate3d(0, 0, 0) scale(1);
+ opacity: 0.9;
+ }
+ 50% {
+ transform: translate3d(3%, -2%, 0) scale(1.08);
+ opacity: 1;
+ }
+ 100% {
+ transform: translate3d(-2%, 2%, 0) scale(1.02);
+ opacity: 0.85;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ :root {
+ --audio-level: 0 !important;
+ --ai-audio-level: 0 !important;
+ --bar0: 0 !important;
+ --bar1: 0 !important;
+ --bar2: 0 !important;
+ --bar3: 0 !important;
+ --bar4: 0 !important;
+ }
+ *,
+ *::before,
+ *::after {
+ animation: none !important;
+ transition: none !important;
+ scroll-behavior: auto !important;
+ }
+ .circle.state-ai-speaking .bar,
+ .circle.state-user-speaking .bar {
+ transform: none !important;
+ }
+}
diff --git a/vocero-frontend/src/styles/layout.css b/vocero-frontend/src/styles/layout.css
deleted file mode 100644
index ea223b0..0000000
--- a/vocero-frontend/src/styles/layout.css
+++ /dev/null
@@ -1,298 +0,0 @@
-/* Shared layout. Theme colors and voice artwork remain in legacy-theme.css. */
-body {
- display: flex;
- flex-direction: column;
- height: auto;
- min-height: 100dvh;
- padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) 0 max(8px, env(safe-area-inset-left));
-}
-#app {
- display: flex;
- flex: 1;
- flex-direction: column;
- width: 100%;
- min-width: 0;
-}
-.stage,
-html[data-theme="white"] .stage {
- padding: 12px 12px 8px;
- gap: 12px;
- flex: none;
-}
-.chat-panel {
- position: relative;
- z-index: 1;
- display: flex;
- flex: 1 0 auto;
- flex-direction: column;
- height: clamp(240px, 45dvh, 640px);
- min-width: 0;
- margin: 8px 12px 0;
-}
-.circle-caption {
- color: var(--text-dim);
-}
-.chat-panel-inner {
- position: relative;
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- min-height: 0;
- max-width: 920px;
- margin-inline: auto;
- padding: 12px;
- overflow: hidden;
-}
-.chat-panel-header {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- padding: 0 0 10px;
- flex: none;
-}
-.chat-panel-header h2 {
- min-width: 0;
-}
-.chat-header-actions,
-html[data-theme="white"] .chat-header-actions {
- gap: 6px;
-}
-.chat-export-btn {
- width: 40px;
- height: 40px;
-}
-.chat-history {
- display: flex;
- flex-direction: column;
- flex: 1;
- min-height: 0;
- gap: 14px;
- padding: 12px 4px;
- overflow-y: auto;
- overflow-x: hidden;
- overflow-anchor: none;
- overscroll-behavior: contain;
- scrollbar-width: thin;
-}
-.chat-history:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: -2px;
- border-radius: 8px;
-}
-.hist-msg {
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- gap: 6px;
- max-width: 86%;
- min-width: 0;
- position: relative;
-}
-.hist-msg.user {
- align-self: flex-start;
- margin-right: auto;
-}
-.hist-msg.assistant {
- align-self: flex-end;
- margin-left: auto;
-}
-.hist-msg.tool {
- align-self: flex-start;
- max-width: 100%;
-}
-.hist-meta {
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- gap: 4px 10px;
- color: var(--text-dim);
- font-size: 12px;
-}
-.hist-meta span {
- overflow-wrap: anywhere;
- font-weight: 600;
-}
-.hist-meta time {
- white-space: nowrap;
-}
-.hist-body {
- min-width: 0;
-}
-.hist-body:not(.markdown-content) {
- white-space: pre-wrap;
-}
-.jump-to-latest {
- display: inline-flex;
- align-items: center;
- position: absolute;
- bottom: 18px;
- right: 18px;
- z-index: 2;
- gap: 6px;
- box-shadow: var(--shadow-soft);
-}
-html[data-theme="white"] .chat-panel {
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 20px;
- box-shadow: var(--shadow-soft);
-}
-html[data-theme="white"] .chat-panel-header {
- border-bottom: 1px solid var(--border);
-}
-html[data-theme="white"] .chat-history .hist-msg {
- max-width: calc(100% - 40px);
- padding-top: 0;
-}
-html[data-theme="white"] .chat-history .hist-msg.assistant {
- align-self: flex-start;
- margin: 4px auto 4px 40px;
-}
-html[data-theme="white"] .chat-history .hist-msg.user {
- align-self: flex-end;
- margin: 4px 40px 4px auto;
-}
-html[data-theme="white"] .chat-history .hist-msg.tool {
- max-width: 100%;
- margin: 4px 0;
-}
-html[data-theme="white"] .chat-history .hist-msg::before {
- width: 28px;
- height: 28px;
- font-size: 11px;
-}
-html[data-theme="white"] .hist-meta {
- color: #64748b;
-}
-html[data-theme="white"] .hist-msg.user .hist-meta {
- justify-content: flex-end;
-}
-.app-footer {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- padding: 16px 12px max(12px, env(safe-area-inset-bottom));
- flex: none;
- text-align: center;
-}
-.feedback-link {
- font-size: 13px;
- color: var(--text-dim);
- text-decoration: underline;
- text-underline-offset: 3px;
- border: 0;
- padding: 4px;
-}
-.feedback-link:hover {
- color: var(--text);
-}
-.feedback-link:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 3px;
- border-radius: 4px;
-}
-@media (min-width: 1024px) {
- #app.has-reports {
- padding-left: 320px;
- }
- body:has(#app.has-reports) .app-footer {
- margin-left: 320px;
- }
-}
-@media (max-width: 600px) {
- .topbar {
- flex-wrap: wrap;
- gap: 12px;
- }
- .topbar-right {
- flex-wrap: wrap;
- }
- .chat-panel {
- margin-inline: 0;
- }
- .chat-panel-inner {
- padding: 10px;
- }
- html[data-theme="white"] .chat-panel-header h2 {
- font-size: 17px;
- }
- html[data-theme="white"] .chat-status-pill {
- font-size: 11px;
- padding: 5px 7px;
- }
- .orb-wrap {
- flex-direction: row;
- gap: 18px;
- }
- .circle {
- width: clamp(110px, 32vw, 176px);
- }
- .orb-wrap.live .side-btn {
- width: 44px;
- height: 44px;
- }
- .mic-gate-arc {
- width: 62px;
- height: 62px;
- }
- html[data-theme="white"] .white-mic-stage {
- gap: 12px;
- padding-top: 16px;
- }
- html[data-theme="white"] .white-waveform {
- top: 74px;
- }
- html[data-theme="white"] .white-session-controls {
- gap: 8px;
- }
- html[data-theme="white"] .white-mute-btn,
- html[data-theme="white"] .white-stop-btn {
- font-size: 13px;
- padding: 8px 10px;
- min-height: 44px;
- gap: 6px;
- }
-}
-@media (max-height: 640px) {
- .stage,
- html[data-theme="white"] .stage {
- padding-block: 8px;
- }
- html[data-theme="white"] .white-mic-stage {
- padding-top: 8px;
- gap: 8px;
- }
- html[data-theme="white"] .white-waveform {
- top: 57px;
- }
- html[data-theme="white"] .white-mic-caption {
- font-size: 16px;
- }
-}
-@media (prefers-reduced-motion: reduce) {
- :root {
- --audio-level: 0 !important;
- --ai-audio-level: 0 !important;
- --bar0: 0 !important;
- --bar1: 0 !important;
- --bar2: 0 !important;
- --bar3: 0 !important;
- --bar4: 0 !important;
- }
- *,
- *::before,
- *::after {
- animation: none !important;
- transition: none !important;
- scroll-behavior: auto !important;
- }
- .circle.state-ai-speaking .bar,
- .circle.state-user-speaking .bar {
- transform: none !important;
- }
-}
diff --git a/vocero-frontend/src/styles/legacy-theme.css b/vocero-frontend/src/styles/legacy-theme.css
deleted file mode 100644
index 9d5ddc0..0000000
--- a/vocero-frontend/src/styles/legacy-theme.css
+++ /dev/null
@@ -1,4820 +0,0 @@
-* {
- box-sizing: border-box;
-}
-
-html,
-body {
- margin: 0;
- padding: 0;
- height: 100%;
-}
-
-body {
- font-family: var(--font-sans);
- color: var(--text);
- /* Guarantee the page at least fills the viewport; if content is taller we
- * scroll the document naturally (so footer is reachable via scroll in any
- * case). Sticky .app-footer pins it to the BOTTOM of the viewport while
- * scrolling / at rest. */
- min-height: 100vh;
- min-height: 100svh;
- min-height: 100dvh;
- width: 100%;
- /* Bottom handled by #app padding-bottom below (footer bar + home indicator). */
- overflow-x: hidden;
- overflow-y: auto;
- -webkit-font-smoothing: antialiased;
- text-rendering: optimizeLegibility;
- position: relative;
- background:
- radial-gradient(1200px 800px at 15% -10%, rgba(139, 92, 246, 0.3) 0%, transparent 60%),
- radial-gradient(900px 700px at 110% 10%, rgba(236, 72, 153, 0.22) 0%, transparent 55%),
- radial-gradient(1000px 800px at 50% 120%, rgba(34, 211, 238, 0.22) 0%, transparent 60%),
- radial-gradient(ellipse at 50% 50%, #0e1019 0%, var(--bg) 65%);
- background-attachment: fixed;
-}
-body::before {
- content: "";
- position: fixed;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- background-image:
- radial-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
- radial-gradient(rgba(139, 92, 246, 0.035) 1px, transparent 1px);
- background-size:
- 42px 42px,
- 42px 42px;
- background-position:
- 0 0,
- 21px 21px;
- mix-blend-mode: screen;
- opacity: 0.55;
- mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.4) 70%, transparent 100%);
-}
-body::after {
- content: "";
- position: fixed;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- background:
- radial-gradient(600px 600px at 20% 80%, rgba(139, 92, 246, 0.1), transparent 70%),
- radial-gradient(500px 500px at 80% 20%, rgba(34, 211, 238, 0.08), transparent 70%);
- animation: auroraShift 22s ease-in-out infinite alternate;
-}
-@keyframes auroraShift {
- 0% {
- transform: translate3d(0, 0, 0) scale(1);
- opacity: 0.9;
- }
- 50% {
- transform: translate3d(3%, -2%, 0) scale(1.08);
- opacity: 1;
- }
- 100% {
- transform: translate3d(-2%, 2%, 0) scale(1.02);
- opacity: 0.85;
- }
-}
-@keyframes voice-spinner-spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-button {
- font-family: inherit;
-}
-
-a {
- color: var(--text);
- text-decoration: none;
- border-bottom: 1px solid var(--border-strong);
-}
-a:hover {
- border-bottom-color: var(--text);
-}
-
-.hidden {
- display: none !important;
-}
-
-/* ─── Footer ─────────────────────────────────────────────────────────── */
-.app-footer::before {
- content: "";
- position: absolute;
- top: 0;
- left: 50%;
- transform: translateX(-50%);
- width: min(640px, 70vw);
- height: 1px;
- background: linear-gradient(
- 90deg,
- transparent,
- color-mix(in srgb, var(--brand) 40%, transparent) 30%,
- color-mix(in srgb, var(--brand-2) 40%, transparent) 70%,
- transparent
- );
- opacity: 0.7;
-}
-.app-footer-text {
- font-family: var(--font-sans);
- font-weight: 500;
- font-size: clamp(10px, 1.5vw, 11px);
- letter-spacing: 0.18em;
- text-transform: uppercase;
- background: var(--brand-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- color: transparent;
- text-align: center;
- white-space: normal;
- word-break: break-word;
- overflow-wrap: anywhere;
- line-height: 1.35;
- max-width: 100%;
- filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.5));
-}
-
-/* ─── Topbar ──────────────────────────────────────────────────────────── */
-
-/* Topbar is just a floating row of controls over the stage - no
- * separator line, no background. Same story for the footer. Keeps the
- * app feeling like one continuous canvas. */
-.topbar {
- display: flex;
- /* Top-align so the right control cluster sits up with the title instead of
- * floating to the vertical middle of the tall identity block. */
- align-items: flex-start;
- justify-content: space-between;
- /* Before 2026-09-04: padding-top 10px. Was clipped on mobile because the
- rounded bezel of the phone + OS notch/inset sits on top. now
- reserves safe-area-inset-top (10px minimum on desktop) and we push the
- topbar itself another 6px down so the brand glass card rounded corners
- are fully visible, not half-painted behind the screen bezel. */
- padding: 16px 16px 0 16px;
- margin-top: max(6px, calc(env(safe-area-inset-top, 0px) * 0.2));
- /* Default stacking (below the conversation panel, z 200) so the panel drawer
- * covers the controls when it's open. */
- position: relative;
- z-index: 2;
- gap: 8px;
- flex-wrap: wrap;
- flex: 0 0 auto; /* never let the topbar shrink/stretch inside flex col */
- /* Prevent brand card edge from touching the right screen bezel on mobile:
- box-sizing so padding is taken inside the topbar width. */
- width: 100%;
- box-sizing: border-box;
-}
-
-.brand {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- font-weight: 600;
- font-size: 14px;
- letter-spacing: 0.01em;
- color: var(--text-dim);
- flex: 1 1 360px;
- min-width: 0;
- padding: 10px 12px;
- border-radius: var(--radius-md);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), var(--glass);
- border: 1px solid var(--border);
- backdrop-filter: blur(14px) saturate(135%);
- -webkit-backdrop-filter: blur(14px) saturate(135%);
- box-shadow:
- var(--shadow-glass),
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
- position: relative;
- overflow: hidden;
-}
-.brand::before {
- content: "";
- position: absolute;
- inset: -1px;
- border-radius: inherit;
- padding: 1px;
- background: linear-gradient(
- 135deg,
- color-mix(in srgb, var(--brand) 45%, transparent),
- transparent 40%,
- color-mix(in srgb, var(--brand-2) 35%, transparent)
- );
- -webkit-mask:
- linear-gradient(#000, #000) content-box,
- linear-gradient(#000, #000);
- mask:
- linear-gradient(#000, #000) content-box,
- linear-gradient(#000, #000);
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- pointer-events: none;
- opacity: 0.7;
-}
-
-/* Transport tag rides the wordmark as a system label, not prose — mono,
- * dimmed, the only mono in the topbar. */
-.brand-tag {
- font-family: var(--font-mono);
- font-weight: 500;
- font-size: 11px;
- letter-spacing: 0.02em;
- opacity: 0.65;
- color: var(--text-faint);
-}
-
-.brand .brand-logo {
- width: 30px;
- height: 30px;
- object-fit: contain;
- flex: none;
- filter: drop-shadow(0 2px 12px color-mix(in srgb, var(--brand) 60%, transparent));
- transition:
- transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1),
- filter 0.35s ease;
-}
-.brand:hover .brand-logo {
- transform: translateY(-2px) rotate(-4deg) scale(1.04);
- filter: drop-shadow(0 4px 20px color-mix(in srgb, var(--brand-2) 70%, transparent));
-}
-
-.ident-title {
- font-weight: 700;
- font-size: 20px;
- letter-spacing: -0.01em;
- line-height: 1.1;
- background: var(--brand-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- color: transparent;
- filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.25));
- display: block;
- /* Previously a stacked two-line block had a small bottom spacer; now the
- * title and avatar share the row → we don't need it any more. */
- margin-bottom: 0;
- transition:
- transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1),
- filter 0.3s ease;
-}
-.brand:hover .ident-title {
- filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--brand-2) 45%, transparent));
- transform: translateY(-0.5px);
-}
-
-/* ── User avatar circle (replaces the old user-name text) ──
- * - perfectly round badge, matches the 42px icon-btn size in the topbar
- * - glass + brand gradient 1px border
- * - two uppercase initials (Alessandro Valenti → AV), bold white
- * - hovering shows the full name via HTML title="" (still accessible), and
- * lifts slightly with a soft brand glow like the topbar icon buttons. */
-.ident-user-name {
- position: relative;
- flex: none;
- width: 40px;
- height: 40px;
- aspect-ratio: 1 / 1;
- border-radius: 999px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: 1px solid color-mix(in srgb, var(--brand) 32%, var(--border));
- color: #fff;
- background:
- radial-gradient(120% 120% at 30% 20%, rgba(255, 255, 255, 0.18), transparent 55%),
- linear-gradient(
- 135deg,
- color-mix(in srgb, var(--brand) 72%, transparent),
- color-mix(in srgb, var(--brand-2) 60%, transparent) 55%,
- color-mix(in srgb, var(--brand-3) 55%, transparent)
- );
- box-shadow:
- 0 0 0 1px rgba(255, 255, 255, 0.05) inset,
- 0 1px 0 rgba(255, 255, 255, 0.08) inset,
- 0 2px 10px rgba(0, 0, 0, 0.24),
- 0 0 22px -8px color-mix(in srgb, var(--brand) 58%, transparent);
- backdrop-filter: blur(6px);
- -webkit-backdrop-filter: blur(6px);
- font-family: var(--font-sans);
- font-weight: 800;
- font-size: 14px;
- line-height: 1;
- letter-spacing: 0.05em;
- text-transform: uppercase;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
- user-select: none;
- cursor: pointer;
- transition:
- transform 0.22s cubic-bezier(0.2, 0.7, 0.2, 1),
- box-shadow 0.3s ease,
- border-color 0.3s ease;
-}
-.ident-user-name::before {
- /* Subtle 1px highlight rim on the top-left quadrant. */
- content: "";
- position: absolute;
- inset: 0;
- border-radius: inherit;
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 42%);
- opacity: 0.9;
- pointer-events: none;
- mix-blend-mode: screen;
-}
-.ident-user-name:hover {
- transform: translateY(-1.5px) scale(1.02);
- box-shadow:
- 0 0 0 1px rgba(255, 255, 255, 0.07) inset,
- 0 1px 0 rgba(255, 255, 255, 0.1) inset,
- 0 6px 22px rgba(0, 0, 0, 0.32),
- 0 0 34px -6px color-mix(in srgb, var(--brand-2) 70%, transparent);
- border-color: color-mix(in srgb, var(--brand-2) 55%, var(--border));
-}
-/* Accessibility: keyboard-focusable style if ever wrapped in a
or gets
- tabIndex (future-proof, we set :focus-visible for robustness). */
-.ident-user-name:focus-visible {
- outline: none;
- box-shadow:
- 0 0 0 3px color-mix(in srgb, var(--brand-3) 45%, transparent),
- 0 1px 0 rgba(255, 255, 255, 0.08) inset,
- 0 6px 22px rgba(0, 0, 0, 0.28),
- 0 0 34px -6px color-mix(in srgb, var(--brand) 60%, transparent);
-}
-#ident-user-name[hidden],
-#white-session-controls[hidden],
-#white-mute-btn[hidden],
-#white-stop-btn[hidden],
-#white-mic-subcaption[hidden] {
- display: none !important;
-}
-@keyframes pulseDot {
- 0%,
- 100% {
- opacity: 1;
- transform: scale(1);
- }
- 50% {
- opacity: 0.78;
- transform: scale(0.9);
- }
-}
-
-/* Narrow viewports (typically the mobile shell iframe and phone-sized
- browser windows): the full product name pushes the topbar's right
- cluster off-screen. Keep the logo as the brand cue and drop the
- wordmark - the orb already gives enough context. */
-@media (max-width: 600px) {
- /* The full identity stack is too tall for a phone topbar. Show only the
- * title + (i); the popup carries the blurb, credits and pipeline.
- * `.brand` prefix raises specificity so this beats the later base
- * `.ident-meta { display: flex }` rule regardless of source order. */
- .brand .ident-blurb,
- .brand .ident-meta {
- display: none;
- }
- .ident-title {
- font-size: 18px;
- }
- /* (i) moves into the right-hand control cluster on phones, leaving the
- * title alone on the left. */
- .brand .about-btn {
- display: none;
- }
- .about-btn-mobile {
- display: inline-flex;
- }
- /* Keep phone icons at the current compact size — the desktop bump above
- * shouldn't leak into the mobile topbar. */
- .topbar-right .icon-btn {
- width: 36px;
- height: 36px;
- }
- .topbar-right .icon-btn svg {
- width: 18px;
- height: 18px;
- }
- .app-footer::before {
- width: min(320px, 82vw);
- }
- .app-footer-text {
- letter-spacing: 0.12em;
- line-height: 1.5;
- font-size: 10px;
- }
-}
-
-.topbar-right {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-/* Conversation history button: always-visible sibling of .topbar-right.
- Same visual styling, but never gated by the ?debug=true flag. */
-.chat-btn-always {
- display: flex;
- align-items: center;
- gap: 10px;
- /* Keep visual gap consistent whether debug buttons are shown or not. */
- margin-right: 4px;
-}
-.chat-btn-always .icon-btn {
- position: relative;
-}
-/* Safety fallback for any debug-only container: stays hidden unless the JS
- bootstrap explicitly enables it (sets data-debug-enabled="true"). */
-[data-debug-only="true"]:not([data-debug-enabled="true"]) {
- display: none !important;
- visibility: hidden !important;
- opacity: 0 !important;
- pointer-events: none !important;
- width: 0 !important;
- height: 0 !important;
- margin: 0 !important;
- padding: 0 !important;
- overflow: hidden !important;
-}
-
-/* ─── Transport pill ──────────────────────────────────────────────
- * Surface the actual network path WebRTC picked for the robot peer
- * connection (LAN, direct-through-NAT, or TURN-relayed). Lets the
- * user spot at a glance when audio is going through the internet
- * instead of staying on-prem.
- */
-.transport-pill {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- font-weight: 500;
- letter-spacing: 0.02em;
- padding: 5px 10px;
- border-radius: 999px;
- background: var(--bg-elev);
- border: 1px solid var(--border);
- color: var(--text-dim);
- user-select: none;
- transition:
- background 0.2s ease,
- border-color 0.2s ease,
- color 0.2s ease;
-}
-
-.transport-pill .transport-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: currentColor;
- box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 25%, transparent);
- flex: none;
-}
-
-/* Bitrate readout sits after the kind label. Dimmer + monospace so the
- * kind (LAN / Direct / Relayed) stays the primary info and the numbers
- * don't wiggle the layout as digits change. */
-.transport-pill .transport-bitrate {
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 500;
- color: color-mix(in srgb, currentColor 75%, var(--text-dim));
- opacity: 0.85;
- padding-left: 6px;
- border-left: 1px solid color-mix(in srgb, currentColor 25%, transparent);
-}
-.transport-pill .transport-bitrate:empty {
- display: none;
-}
-
-.transport-pill.transport-checking {
- color: var(--text-dim);
-}
-.transport-pill.transport-checking .transport-dot {
- animation: transport-blink 1.2s ease-in-out infinite;
-}
-
-.transport-pill.transport-lan {
- color: #4ade80;
- border-color: color-mix(in srgb, #4ade80 35%, var(--border));
- background: color-mix(in srgb, #4ade80 10%, var(--bg-elev));
-}
-
-.transport-pill.transport-direct {
- color: #60a5fa;
- border-color: color-mix(in srgb, #60a5fa 35%, var(--border));
- background: color-mix(in srgb, #60a5fa 10%, var(--bg-elev));
-}
-
-.transport-pill.transport-relay {
- color: #fbbf24;
- border-color: color-mix(in srgb, #fbbf24 35%, var(--border));
- background: color-mix(in srgb, #fbbf24 10%, var(--bg-elev));
-}
-
-@keyframes transport-blink {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.35;
- }
-}
-
-.icon-btn {
- background: var(--bg-elev);
- border: 1px solid var(--border);
- color: var(--text-dim);
- width: 36px;
- height: 36px;
- border-radius: var(--radius-sm);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- touch-action: manipulation;
- transition:
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
-}
-.icon-btn:hover {
- background: var(--bg-elev-2);
- color: var(--text);
- border-color: var(--border-strong);
-}
-/* Topbar control cluster reads a touch bigger on desktop. Phones keep the
- * compact 36px (reset in the mobile media query). */
-.topbar-right .icon-btn {
- width: 40px;
- height: 40px;
-}
-.topbar-right .icon-btn svg {
- width: 20px;
- height: 20px;
-}
-
-/* ─── Stage ───────────────────────────────────────────────────────────── */
-
-.stage {
- display: flex;
- flex-direction: column;
- align-items: center;
- /* Pack the orb + caption up top and make stage MIN-content (no longer eat
- all remaining vertical space) — so the inline chat panel slides up and
- sits immediately under "TAP OR SAY HOLA VOCERO" with a thin breathing
- gap instead of a huge empty area. */
- justify-content: flex-start;
- padding: clamp(10px, 2vw, 20px) clamp(12px, 3vw, 24px) clamp(6px, 1.2vh, 14px);
- gap: clamp(12px, 2.5vw, 20px);
- padding-top: clamp(14px, 3vh, 36px);
- /* Shrink to content. The chat panel below is what grows to fill slack. */
- flex: 0 1 auto;
- min-height: 0;
- position: relative;
- z-index: 1;
-}
-/* Subtle radial "light pedestal" behind the orb — makes the whole cluster
- feel anchored instead of floating on the dark grid. */
-.stage::before {
- content: "";
- position: absolute;
- top: 46%;
- left: 50%;
- width: clamp(260px, 58vw, 620px);
- height: clamp(260px, 58vw, 620px);
- transform: translate(-50%, -50%);
- background: radial-gradient(
- circle,
- color-mix(in srgb, var(--brand) 22%, transparent) 0%,
- color-mix(in srgb, var(--brand-2) 12%, transparent) 45%,
- transparent 75%
- );
- filter: blur(12px);
- opacity: 0.65;
- pointer-events: none;
- border-radius: 999px;
- animation: pedestalBreathe 8s ease-in-out infinite alternate;
-}
-@keyframes pedestalBreathe {
- 0% {
- transform: translate(-50%, -50%) scale(1);
- opacity: 0.55;
- }
- 50% {
- transform: translate(-50%, -50%) scale(1.06);
- opacity: 0.75;
- }
- 100% {
- transform: translate(-50%, -50%) scale(0.98);
- opacity: 0.5;
- }
-}
-
-/* ─── Central circle ──────────────────────────────────────────────────── */
-
-/* Wraps the orb and its two side controls so they stay aligned on one row. */
-.orb-wrap {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: clamp(18px, 3vw, 32px);
-}
-
-.circle {
- position: relative;
- /* Cap by smallest-dim viewport units — svw/svh = smallest-viewport w/h so the
- URL bar showing/hiding on mobile never makes the orb change size suddenly.
- Since ~30% of the viewport is already header, we use 42svh as max height. */
- width: clamp(120px, min(38svw, 42svh), 300px);
- aspect-ratio: 1 / 1;
- border-radius: 50%;
- border: none;
- background: transparent;
- padding: 0;
- cursor: pointer;
- outline: none;
- display: grid;
- place-items: center;
- color: var(--glow, var(--accent));
- transition:
- transform 0.18s ease,
- filter 0.18s ease;
- -webkit-tap-highlight-color: transparent;
- /* Treat taps as immediate clicks (no 300ms delay / double-tap-zoom). */
- touch-action: manipulation;
-}
-.circle::before {
- content: "";
- position: absolute;
- inset: -8%;
- border-radius: 50%;
- background: conic-gradient(
- from 0deg,
- color-mix(in srgb, var(--brand) 65%, transparent) 0%,
- color-mix(in srgb, var(--brand-2) 55%, transparent) 35%,
- color-mix(in srgb, var(--brand-3) 55%, transparent) 65%,
- color-mix(in srgb, var(--brand) 65%, transparent) 100%
- );
- filter: blur(14px);
- opacity: 0.55;
- z-index: 0;
- pointer-events: none;
- animation: orbConicSpin 18s linear infinite;
-}
-@keyframes orbConicSpin {
- to {
- transform: rotate(360deg);
- }
-}
-.circle:hover {
- filter: brightness(1.08);
-}
-.circle:hover::before {
- opacity: 0.8;
-}
-.circle:active {
- transform: scale(0.97);
- filter: brightness(0.92);
-}
-.circle:focus-visible .circle-core {
- outline: 2px solid var(--glow, var(--accent));
- outline-offset: 6px;
-}
-.circle[disabled] {
- cursor: default;
- opacity: 0.75;
-}
-
-.circle-glow {
- position: absolute;
- inset: -8%;
- border-radius: 50%;
- background: radial-gradient(circle at center, var(--glow, var(--accent)) 0%, transparent 60%);
- filter: blur(34px);
- opacity: 0.55;
- transform: scale(1);
- transition:
- opacity 0.25s,
- background 0.25s,
- transform 1.4s ease-in-out;
- pointer-events: none;
- z-index: 0;
-}
-
-/* Idle-only slow "breathe" pulse for the outer glow — keeps the orb alive
- even when the user is doing nothing. */
-.circle.state-idle .circle-glow,
-.circle.state-idle::before {
- animation:
- orbConicSpin 22s linear infinite,
- orbBreatheGlow 5s ease-in-out infinite alternate;
-}
-@keyframes orbBreatheGlow {
- 0% {
- opacity: 0.42;
- transform: scale(0.985);
- filter: blur(30px);
- }
- 50% {
- opacity: 0.72;
- transform: scale(1.04);
- filter: blur(40px);
- }
- 100% {
- opacity: 0.48;
- transform: scale(1);
- filter: blur(32px);
- }
-}
-
-/* Two nested circular rings: the inner tracks the core edge, the outer
- * expands / fades to convey "audio radiating out" during speaking. */
-.circle-ring,
-.circle-ring-outer {
- position: absolute;
- top: 50%;
- left: 50%;
- border-radius: 50%;
- transform: translate(-50%, -50%);
- pointer-events: none;
- transition:
- opacity 0.4s ease,
- border-color 0.4s ease,
- transform 0.3s ease;
- z-index: 1;
-}
-.circle-ring {
- width: 86%;
- height: 86%;
- border: 1.5px solid transparent;
- background:
- linear-gradient(var(--bg, #07080d), var(--bg, #07080d)) padding-box,
- conic-gradient(
- from 0deg,
- color-mix(in srgb, var(--brand) 80%, transparent),
- color-mix(in srgb, var(--brand-2) 70%, transparent) 40%,
- color-mix(in srgb, var(--brand-3) 75%, transparent) 70%,
- color-mix(in srgb, var(--brand) 80%, transparent)
- )
- border-box;
- border-color: transparent;
- mask:
- linear-gradient(#000, #000) content-box,
- linear-gradient(#000, #000);
- -webkit-mask:
- linear-gradient(#000, #000) content-box,
- linear-gradient(#000, #000);
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- box-shadow: 0 0 20px -6px color-mix(in srgb, var(--brand) 55%, transparent);
- opacity: 0.75;
-}
-.circle-ring-outer {
- width: 98%;
- height: 98%;
- border: 1px dashed color-mix(in srgb, var(--glow, var(--accent)) 30%, transparent);
- opacity: 0.55;
-}
-.circle.state-idle .circle-ring-outer {
- animation: outerRingPulse 6s ease-in-out infinite;
-}
-@keyframes outerRingPulse {
- 0%,
- 100% {
- opacity: 0.4;
- transform: translate(-50%, -50%) scale(1);
- }
- 50% {
- opacity: 0.65;
- transform: translate(-50%, -50%) scale(1.02);
- }
-}
-
-.circle-core {
- position: relative;
- width: 72%;
- height: 72%;
- border-radius: 50%;
- background:
- radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.04) 20%, transparent 45%),
- radial-gradient(
- circle at 35% 28%,
- color-mix(in srgb, var(--glow, var(--accent)) 32%, transparent),
- color-mix(in srgb, var(--glow, var(--accent)) 12%, transparent) 55%,
- color-mix(in srgb, var(--glow, var(--accent)) 5%, transparent)
- );
- border: 1.5px solid color-mix(in srgb, var(--glow, var(--accent)) 48%, transparent);
- display: grid;
- place-items: center;
- box-shadow:
- 0 0 44px -8px color-mix(in srgb, var(--glow, var(--accent)) 42%, transparent),
- inset 0 0 34px color-mix(in srgb, var(--glow, var(--accent)) 22%, transparent),
- inset 0 1px 0 rgba(255, 255, 255, 0.12);
- transition:
- background 0.4s ease,
- border-color 0.4s ease,
- box-shadow 0.4s ease,
- transform 0.4s ease;
- z-index: 2;
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
-}
-.circle.state-idle .circle-core {
- animation: coreBreathe 5s ease-in-out infinite alternate;
-}
-@keyframes coreBreathe {
- 0% {
- transform: scale(0.992);
- box-shadow:
- 0 0 36px -8px color-mix(in srgb, var(--brand) 40%, transparent),
- inset 0 0 26px color-mix(in srgb, var(--brand) 16%, transparent);
- }
- 50% {
- transform: scale(1.01);
- box-shadow:
- 0 0 54px -4px color-mix(in srgb, var(--brand-2) 48%, transparent),
- inset 0 0 38px color-mix(in srgb, var(--brand-2) 22%, transparent);
- }
- 100% {
- transform: scale(1);
- box-shadow:
- 0 0 44px -6px color-mix(in srgb, var(--brand-3) 42%, transparent),
- inset 0 0 32px color-mix(in srgb, var(--brand-3) 20%, transparent);
- }
-}
-
-/* Indicator slot: a single SVG / spinner / bar group is visible at a time,
- * driven by the state class on `.circle`. */
-.circle-indicator {
- position: relative;
- width: 44%;
- height: 44%;
- display: grid;
- place-items: center;
- color: var(--glow, var(--accent));
-}
-.circle-indicator > .ind {
- grid-area: 1 / 1;
- opacity: 0;
- transform: scale(0.85);
- transition:
- opacity 0.25s ease,
- transform 0.25s ease;
- pointer-events: none;
-}
-.circle-indicator > svg.ind {
- width: 60%;
- height: 60%;
-}
-
-/* Spinner: a rotating ring gap, CSS-only.
- *
- * Note: the base `.circle-indicator > .ind` rule forces `transform:
- * scale(.85)` / `scale(1)` on every indicator to drive the show/hide
- * transition. If we only rotate here, the browser has to interpolate
- * between `scale(1)` and `rotate(360deg)` (two different transform
- * functions), which produces a broken, barely-moving animation. So we
- * include the scale explicitly in the keyframes and bump specificity
- * with `!important` so the spinner always wins over the state rule. */
-.ind-spinner {
- width: 48%;
- height: 48%;
- border: 3px solid currentColor;
- border-right-color: transparent;
- border-radius: 50%;
- opacity: 0;
- animation: ind-spin 0.9s linear infinite;
-}
-
-/* Thinking dots: 3 soft pulsing dots while the model is composing a
- * response. Apple-style cadence: each dot scales up and brightens in
- * turn, staggered by ~160 ms. Per-dot animation is on the child, so
- * the parent's scale(.85 → 1) show/hide transform composes cleanly. */
-.ind-thinking {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- width: 60%;
- height: 60%;
-}
-.ind-thinking .dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: currentColor;
- opacity: 0.3;
- animation: thinking-dot 1.25s ease-in-out infinite;
-}
-.ind-thinking .dot:nth-child(1) {
- animation-delay: 0s;
-}
-.ind-thinking .dot:nth-child(2) {
- animation-delay: 0.16s;
-}
-.ind-thinking .dot:nth-child(3) {
- animation-delay: 0.32s;
-}
-
-/* Bars: 5 vertical pills driven by --bar0..--bar4 CSS vars. */
-.ind-bars {
- display: flex;
- align-items: center;
- gap: 5px;
- height: 42%;
-}
-.ind-bars .bar {
- width: 4px;
- min-height: 4px;
- border-radius: 3px;
- background: currentColor;
- opacity: 0.7;
- --h: var(--bar0, 0);
- height: calc(4px + var(--h) * 36px);
- transition:
- height 0.08s ease-out,
- opacity 0.08s ease-out;
-}
-.ind-bars .bar:nth-child(1) {
- --h: var(--bar0);
-}
-.ind-bars .bar:nth-child(2) {
- --h: var(--bar1);
-}
-.ind-bars .bar:nth-child(3) {
- --h: var(--bar2);
-}
-.ind-bars .bar:nth-child(4) {
- --h: var(--bar3);
-}
-.ind-bars .bar:nth-child(5) {
- --h: var(--bar4);
-}
-.ind-bars .bar {
- opacity: calc(0.55 + 0.45 * var(--h));
-}
-
-/* Active indicator per state.
- *
- * Note: `ai-speaking` deliberately has NO indicator here. The orb
- * itself becomes the indicator by pulsing on Reachy's voice (see the
- * `--ai-audio-level` rules further down), which is a lot clearer and
- * less confusing than reusing the mic-bars (which viewers would read
- * as "you are speaking"). */
-.state-signed-out .ind-connect,
-.state-authenticated .ind-mic,
-.state-ready .ind-mic,
-.state-connecting .ind-spinner,
-.state-connected .ind-spinner,
-.state-auto-selecting .ind-spinner,
-.state-starting .ind-spinner,
-.state-queued .ind-spinner,
-.state-processing .ind-thinking,
-.state-listening .ind-bars,
-.state-user-speaking .ind-bars,
-.state-ai-speaking .ind-voice,
-.state-error .ind-error {
- opacity: 1;
- transform: scale(1);
-}
-
-/* AI speaking indicator: speaker + two sound waves.
- *
- * Each wave pulses outward (opacity + stroke grow) with a quarter-
- * beat offset so it reads as sound radiating out. Kept as a pure
- * CSS animation so the icon is always visually alive even between
- * syllables when --ai-audio-level momentarily dips. */
-.ind-voice .wave {
- transform-origin: 50% 50%;
- animation: voice-wave 1.35s ease-out infinite;
-}
-.ind-voice .wave-1 {
- animation-delay: 0s;
-}
-.ind-voice .wave-2 {
- animation-delay: 0.35s;
-}
-
-/* Idle indicators: a chain-link "connect" glyph for the signed-out
- * step (invites the user to authenticate with HF) and a microphone
- * once a session is ready. Both picked up by the generic
- * `.circle-indicator > svg.ind` sizing rule (60% × 60% of the slot)
- * and rely on per-state opacity transitions for show / hide. */
-.ind-connect,
-.ind-mic {
- color: color-mix(in srgb, var(--glow, var(--accent)) 85%, white);
-}
-
-/* ─── Caption below the circle ────────────────────────────────────────── */
-
-/* The caption under the orb is meant to whisper, not shout: micro-label
- * vibe, uppercase, letter-spaced, muted. Only appears for actionable /
- * transitional states (see STATE_VIEWS). During a live conversation the
- * orb alone carries the state so we collapse this row entirely. */
-/* Orbit-caption keeps the "machine voice" position — mono, uppercase,
- * letter-spaced. For idle state we tint it with the brand gradient so it
- * reads as a CTA rather than dead metadata. */
-.circle-caption {
- margin: 0;
- font-family: var(--font-mono);
- font-size: 12px;
- font-weight: 600;
- letter-spacing: 0.24em;
- text-transform: uppercase;
- color: var(--text-faint);
- min-height: 1.3em;
- text-align: center;
- opacity: 0.78;
- transition:
- opacity 0.25s ease,
- color 0.25s ease,
- transform 0.25s ease,
- min-height 0.25s ease,
- background 0.3s ease,
- filter 0.3s ease;
- line-height: 1.2;
-}
-.circle-caption:empty::before {
- content: "\00a0";
-}
-/* Idle CTA styling: brand gradient + bob. We use two ways to ensure
- coverage: the modern :has() selector, plus a .idle-cta class toggled
- from JS as a fallback for older embedded WebViews. */
-.stage:has(.circle.state-idle) .circle-caption,
-.circle-caption.idle-cta {
- background: var(--brand-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- color: transparent;
- opacity: 1;
- filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--brand) 40%, transparent));
- animation: idleCaptionBob 3.6s ease-in-out infinite alternate;
-}
-@keyframes idleCaptionBob {
- 0%,
- 100% {
- transform: translateY(0);
- letter-spacing: 0.24em;
- }
- 50% {
- transform: translateY(-1px);
- letter-spacing: 0.26em;
- }
-}
-.circle-caption.empty {
- opacity: 0;
- min-height: 0;
- transform: translateY(-4px);
- pointer-events: none;
-}
-.circle-caption.muted {
- color: var(--text-faint);
- opacity: 0.65;
-}
-.circle-caption.error {
- color: var(--error);
- opacity: 1;
- letter-spacing: 0.08em;
- background: none;
- -webkit-text-fill-color: var(--error);
- text-fill-color: var(--error);
- filter: drop-shadow(0 0 8px color-mix(in srgb, var(--error) 55%, transparent));
-}
-
-/* Warm, human line under the mono caption — sentence case, only while queued.
- * The caption keeps the terse position; this reassures. */
-.circle-subcaption {
- margin: 8px 0 0;
- max-width: 32ch;
- font-size: 13.5px;
- line-height: 1.5;
- color: var(--text-dim);
- text-align: center;
- text-wrap: balance;
- transition: opacity 0.25s ease;
-}
-.circle-subcaption[hidden] {
- display: none;
-}
-
-/* Queue actions sit under the caption: "Join now" (primary, only when it's your
- * turn) stacked above the quiet "Leave queue" escape hatch. */
-.queue-actions {
- margin-top: 16px;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
-}
-.queue-actions[hidden] {
- display: none;
-}
-
-/* "Join now": the one call to action in the queue flow, so it reads as inviting
- * (filled accent) while everything around it stays quiet. */
-.join-queue-btn {
- padding: 10px 26px;
- font-family: var(--font-mono);
- font-size: 12px;
- font-weight: 600;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: #0b0b10;
- background: var(--accent);
- border: none;
- border-radius: 999px;
- cursor: pointer;
- font-variant-numeric: tabular-nums;
- transition:
- transform 0.12s ease,
- filter 0.2s ease;
-}
-.join-queue-btn:hover {
- filter: brightness(1.08);
-}
-.join-queue-btn:active {
- transform: scale(0.97);
-}
-.join-queue-btn:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 3px;
-}
-.join-queue-btn[hidden] {
- display: none;
-}
-
-/* "Leave queue": a quiet outlined pill, understated so it reads as an escape
- * hatch, not a CTA. */
-.leave-queue-btn {
- padding: 7px 16px;
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: var(--text-faint);
- background: transparent;
- border: 1px solid color-mix(in srgb, var(--text-faint) 35%, transparent);
- border-radius: 999px;
- cursor: pointer;
- transition:
- color 0.2s ease,
- border-color 0.2s ease,
- background 0.2s ease;
-}
-.leave-queue-btn:hover {
- color: var(--text);
- border-color: color-mix(in srgb, var(--text-faint) 60%, transparent);
- background: color-mix(in srgb, var(--text-faint) 8%, transparent);
-}
-.leave-queue-btn:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
-}
-.leave-queue-btn[hidden] {
- display: none;
-}
-
-/* ─── Tool-call toaster ───────────────────────────────────────────────── */
-
-/* A small, non-interactive pill that appears below the circle when the
- * model invokes a tool (move_head, play_move). Sits just under the
- * state caption, collapses to zero height when no toast is active. */
-.tool-toast {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- margin-top: 10px;
- padding: 6px 12px;
- border-radius: 999px;
- border: 1px solid var(--border-strong);
- background: color-mix(in srgb, var(--bg-elev-2) 78%, transparent);
- color: var(--text-dim);
- font-size: 12px;
- font-weight: 500;
- letter-spacing: 0.01em;
- line-height: 1;
- white-space: nowrap;
- max-width: 80vw;
- overflow: hidden;
- text-overflow: ellipsis;
-
- opacity: 0;
- transform: translateY(-4px) scale(0.96);
- pointer-events: none;
- transition:
- opacity 0.22s ease,
- transform 0.22s ease;
-}
-.tool-toast.visible {
- opacity: 1;
- transform: translateY(0) scale(1);
-}
-.tool-toast-icon {
- width: 14px;
- height: 14px;
- flex: none;
- color: color-mix(in srgb, var(--voice-tool) 80%, white);
- animation: tool-toast-spin 3.2s linear infinite;
- animation-play-state: paused;
-}
-.tool-toast.visible .tool-toast-icon {
- animation-play-state: running;
-}
-.tool-toast-text {
- display: inline-block;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-@keyframes tool-toast-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-/* ─── Side controls (mic / stop) ──────────────────────────────────────── */
-
-/* Mic button + its radial noise-gate arc. The wrapper centres the button; the
- arc SVG is an absolute overlay larger than the button, revealed with the
- live session (it has no layout footprint, so the idle collapse is unaffected). */
-.mic-gate {
- position: relative;
- flex: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-.mic-gate-arc {
- position: absolute;
- left: 50%;
- top: 50%;
- width: 80px;
- height: 80px;
- transform: translate(-50%, -50%);
- pointer-events: none; /* only the hit-path below catches drags */
- opacity: 0;
- transition: opacity 0.25s ease;
-}
-.orb-wrap.live .mic-gate-arc {
- opacity: 1;
-}
-.mga-track {
- stroke: rgba(255, 255, 255, 0.1); /* hairline; recedes until needed */
- stroke-width: 1.5;
- stroke-linecap: round;
-}
-.mga-fill {
- stroke: var(--accent-2);
- stroke-width: 2;
- stroke-linecap: round;
- opacity: 0.85;
- transition: stroke-dashoffset 0.06s linear;
-}
-/* Threshold setpoint: a bead riding the ring. White with a thin dark outline so
- it stays legible over the moving fill; it recolors to cyan with a soft glow
- the moment the live level crosses it (the gate opening). */
-.mga-handle {
- fill: var(--text);
- stroke: none;
- transition:
- fill 0.2s ease,
- filter 0.2s ease;
-}
-.mic-gate.gate-open .mga-handle {
- fill: var(--accent-2);
- filter: drop-shadow(0 0 3px var(--accent-2));
-}
-.mga-hit {
- stroke: transparent;
- stroke-width: 16;
- pointer-events: none; /* enabled only while live (below) */
- cursor: pointer;
- touch-action: none;
-}
-/* Only catch drags during a live call; when idle the arc is hidden and must
- not steal clicks near the collapsed mic button / orb. */
-.orb-wrap.live .mga-hit {
- pointer-events: stroke;
-}
-
-.side-btn {
- flex: none;
- width: 52px;
- height: 52px;
- border-radius: 50%;
- border: 1px solid var(--border-strong);
- /* Brighter background so the buttons actually stand out against the
- * deep-blue stage gradient; previous var(--bg-elev) was too close to
- * the page bg to be readable. */
- background: color-mix(in srgb, var(--bg-elev-2) 80%, #2a2e3c);
- color: var(--text);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- touch-action: manipulation;
- box-shadow:
- 0 6px 16px rgba(0, 0, 0, 0.35),
- inset 0 1px 0 rgba(255, 255, 255, 0.06);
- /* Hidden by default: take up no space until the session is live. The
- * `width: 0` collapse keeps the orb centered on the idle screen. */
- opacity: 0;
- transform: scale(0.55);
- width: 0;
- padding: 0;
- pointer-events: none;
- overflow: hidden;
- transition:
- opacity 0.25s ease,
- transform 0.25s ease,
- width 0.25s ease,
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
-}
-.side-btn:hover {
- background: color-mix(in srgb, var(--bg-elev-2) 60%, #323746);
- border-color: var(--text-dim);
-}
-.side-btn svg {
- width: 22px;
- height: 22px;
- flex: none;
-}
-.side-btn .mic-off {
- display: none;
-}
-.side-btn.muted {
- color: #fff;
- border-color: var(--error);
- background: color-mix(in srgb, var(--error) 70%, #1a0e13);
-}
-.side-btn.muted .mic-on {
- display: none;
-}
-.side-btn.muted .mic-off {
- display: block;
-}
-
-/* Stop button: subtle warm tint so "end" reads as destructive. */
-#stop-btn:hover {
- color: #fff;
- border-color: color-mix(in srgb, var(--error) 60%, var(--border-strong));
- background: color-mix(in srgb, var(--error) 25%, var(--bg-elev-2));
-}
-
-/* Reveal when the session is live: buttons flank the orb on a flex row. */
-.orb-wrap.live .side-btn {
- opacity: 1;
- transform: scale(1);
- width: 52px;
- pointer-events: auto;
-}
-
-/* Disable every transition / animation during the first paint so the
- * orb doesn't fade-and-scale in when the page loads. `main.ts` removes
- * the class after one animation frame. */
-body.booting,
-body.booting *,
-body.booting *::before,
-body.booting *::after {
- transition: none !important;
- animation-duration: 0s !important;
- animation-delay: 0s !important;
-}
-
-/* ─── Circle animation keyframes ──────────────────────────────────────── */
-
-/* Slow, subtle breathing for "warm idle" states. */
-@keyframes breathe {
- 0%,
- 100% {
- transform: translate(-50%, -50%) scale(1);
- opacity: 0.4;
- }
- 50% {
- transform: translate(-50%, -50%) scale(1.06);
- opacity: 0.15;
- }
-}
-
-/* Outer ring expanding and fading - conveys "I am producing audio". */
-@keyframes ring-out {
- 0% {
- transform: translate(-50%, -50%) scale(1);
- opacity: 0.35;
- }
- 100% {
- transform: translate(-50%, -50%) scale(1.18);
- opacity: 0;
- }
-}
-
-/* Soft inner scale for the core while talking. */
-@keyframes core-breathe {
- 0%,
- 100% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.04);
- }
-}
-
-/* Subtle glow throb used for "thinking" — dimmer than speaking. */
-@keyframes thinking {
- 0%,
- 100% {
- transform: scale(1);
- opacity: 0.7;
- }
- 50% {
- transform: scale(0.96);
- opacity: 0.45;
- }
-}
-
-/* Individual dot pulse for the 3-dot processing indicator. */
-@keyframes thinking-dot {
- 0%,
- 60%,
- 100% {
- transform: scale(0.7);
- opacity: 0.3;
- }
- 30% {
- transform: scale(1.15);
- opacity: 1;
- }
-}
-
-/* Sound-wave pulse: arc fades in, scales up slightly, fades out.
- * The transform-origin is the speaker's center (roughly x=12), so
- * the waves feel like they're emanating from the cone. */
-@keyframes voice-wave {
- 0% {
- opacity: 0;
- transform: scale(0.7);
- }
- 30% {
- opacity: 1;
- transform: scale(1);
- }
- 70% {
- opacity: 0.2;
- transform: scale(1.12);
- }
- 100% {
- opacity: 0;
- transform: scale(0.7);
- }
-}
-
-@keyframes ind-spin {
- /* Scale kept at 1 so we don't fight with the indicator's base
- * show/hide transform (see `.ind-spinner` for the rationale). */
- from {
- transform: scale(1) rotate(0deg);
- }
- to {
- transform: scale(1) rotate(360deg);
- }
-}
-
-/* ─── State-specific colors ──────────────────────────────────────────── */
-
-.circle.state-signed-out {
- --glow: #8b7dff;
-}
-.circle.state-authenticated,
-.circle.state-ready {
- --glow: #34d399;
-}
-.circle.state-connecting,
-.circle.state-connected,
-.circle.state-auto-selecting,
-.circle.state-starting {
- --glow: #facc15;
-}
-/* Queued: a calm slate glow, distinct from connecting's active yellow — this is
- * waiting, not working. The spinner turns slowly and the ring breathes. */
-.circle.state-queued {
- --glow: #94a3b8;
-}
-.circle.state-queued .ind-spinner {
- animation-duration: 2.4s;
-}
-.circle.state-queued .circle-ring {
- animation: breathe 2.6s ease-in-out infinite;
-}
-/* Your turn: a slot is held for you — the orb warms to the accent and breathes a
- * little quicker, an invitation to join. */
-.circle.state-your-turn {
- --glow: var(--accent);
-}
-.circle.state-your-turn .circle-ring {
- animation: breathe 1.6s ease-in-out infinite;
-}
-.circle.state-listening,
-.circle.state-user-speaking {
- --glow: var(--listening);
-}
-.circle.state-processing {
- --glow: var(--processing);
-}
-.circle.state-ai-speaking {
- --glow: var(--speaking);
-}
-.circle.state-error {
- --glow: var(--error);
-}
-
-/* Idle / ready: gentle breathing of the inner ring. Kept out of the
- * `signed-out` state so the very first paint on page load stays quiet
- * (the orb now shows the Reachy head silhouette, no need to also pulse). */
-.circle.state-authenticated .circle-ring,
-.circle.state-ready .circle-ring {
- animation: breathe 2.4s ease-in-out infinite;
-}
-
-/* Connecting flows: subtle glow throb so the orb feels thoughtful
- * while the session is being negotiated. Kept off `processing` on
- * purpose - the 3 thinking dots already pulse, and layering another
- * throb on top competes with them for attention. */
-.circle.state-connecting .circle-core,
-.circle.state-connected .circle-core,
-.circle.state-auto-selecting .circle-core,
-.circle.state-starting .circle-core {
- animation: thinking 1.4s ease-in-out infinite;
-}
-
-/* User is speaking: the mic RMS drives scale + opacity via --audio-level. */
-.circle.state-user-speaking .circle-ring {
- animation: none;
- opacity: calc(0.25 + 0.55 * var(--audio-level));
- transform: translate(-50%, -50%) scale(calc(1 + 0.08 * var(--audio-level)));
- transition:
- transform 0.08s linear,
- opacity 0.08s linear;
-}
-.circle.state-user-speaking .circle-ring-outer {
- opacity: calc(0.1 + 0.35 * var(--audio-level));
- transform: translate(-50%, -50%) scale(calc(1 + 0.12 * var(--audio-level)));
- transition:
- transform 0.08s linear,
- opacity 0.08s linear;
-}
-.circle.state-listening .circle-ring {
- animation: breathe 2s ease-in-out infinite;
-}
-
-/* Assistant is speaking: the whole orb breathes in sync with Reachy's
- * voice instead of running a fixed timer. `--ai-audio-level` (0-1) is
- * updated at display rate by AiLevelMonitor from the OpenAI output
- * track, so every syllable visibly moves the core + outer ring. This
- * reads instantly as "the orb is the voice" and completely avoids the
- * ambiguity of bars-vs-mic the user flagged. */
-.circle.state-ai-speaking .circle-core {
- animation: none;
- transform: scale(calc(1 + 0.09 * var(--ai-audio-level, 0)));
- transition: transform 0.08s linear;
-}
-.circle.state-ai-speaking .circle-ring {
- animation: none;
- opacity: calc(0.3 + 0.5 * var(--ai-audio-level, 0));
- transform: translate(-50%, -50%) scale(calc(1 + 0.05 * var(--ai-audio-level, 0)));
- transition:
- transform 0.08s linear,
- opacity 0.08s linear;
-}
-.circle.state-ai-speaking .circle-ring-outer {
- animation: none;
- opacity: calc(0.15 + 0.55 * var(--ai-audio-level, 0));
- transform: translate(-50%, -50%) scale(calc(1 + 0.18 * var(--ai-audio-level, 0)));
- transition:
- transform 0.08s linear,
- opacity 0.08s linear;
-}
-.circle.state-ai-speaking .circle-glow {
- opacity: calc(0.35 + 0.45 * var(--ai-audio-level, 0));
- transition: opacity 0.08s linear;
-}
-
-/* ─── Robot picker ────────────────────────────────────────────────────── */
-
-.robot-picker {
- width: min(420px, 92vw);
- background: var(--bg-elev);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 14px 16px;
- box-shadow: var(--shadow-soft);
-}
-
-.picker-title {
- margin: 0 0 10px;
- font-size: 11px;
- font-weight: 600;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: var(--text-faint);
-}
-
-.robot-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.robot-card {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 14px;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- background: var(--bg-elev-2);
- cursor: pointer;
- transition:
- border-color 0.15s,
- background 0.15s;
-}
-.robot-card:hover {
- border-color: var(--border-strong);
-}
-.robot-card.selected {
- border-color: var(--border-strong);
- background: var(--bg-elev);
-}
-
-.robot-card .name {
- font-weight: 600;
- font-size: 14px;
-}
-.robot-card .id {
- font-family: var(--font-mono);
- font-size: 12px;
- color: var(--text-faint);
-}
-
-.robot-empty {
- padding: 14px;
- text-align: center;
- color: var(--text-faint);
- font-size: 13px;
-}
-
-/* ─── Footer ──────────────────────────────────────────────────────────── */
-
-.footer {
- padding: 12px 28px 18px;
- font-size: 11px;
- letter-spacing: 0.02em;
- color: var(--text-faint);
- display: flex;
- justify-content: center;
- opacity: 0.55;
- transition: opacity 0.25s ease;
-}
-.footer:hover {
- opacity: 0.9;
-}
-.footer a {
- color: inherit;
- text-decoration: none;
- border-bottom: 1px dotted currentColor;
-}
-.footer a:hover {
- color: var(--text-dim);
-}
-
-/* ─── Modal ───────────────────────────────────────────────────────────── */
-
-.modal {
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 0;
- background: var(--bg-elev);
- color: var(--text);
- width: min(440px, 92vw);
- box-shadow: var(--shadow-soft);
-}
-.modal::backdrop {
- background: rgba(8, 9, 13, 0.65);
- backdrop-filter: blur(4px);
-}
-
-.modal-content {
- display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 22px 24px 20px;
-}
-
-.modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 4px;
-}
-.modal-header h2 {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- letter-spacing: 0.02em;
-}
-
-.field[hidden] {
- display: none;
-}
-.field {
- display: flex;
- flex-direction: column;
- gap: 6px;
- font-size: 13px;
- font-weight: 500;
- color: var(--text-dim);
-}
-.field > span {
- color: var(--text);
- font-weight: 600;
- font-size: 12px;
- letter-spacing: 0.04em;
- text-transform: uppercase;
-}
-.field input,
-.field select,
-.field textarea {
- font-family: inherit;
- font-size: 14px;
- color: var(--text);
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- padding: 10px 12px;
- outline: none;
- transition: border-color 0.15s;
- resize: vertical;
-}
-.field textarea {
- min-height: 84px;
-}
-.field input:focus,
-.field select:focus,
-.field textarea:focus {
- border-color: var(--text-dim);
-}
-/* Deploy-pinned server URL: shown for transparency, not editable. */
-.field input[readonly] {
- color: var(--text-dim);
- border-style: dashed;
- cursor: default;
-}
-.field input[readonly]:focus {
- border-color: var(--border);
-}
-.field small {
- color: var(--text-faint);
- font-size: 12px;
- line-height: 1.4;
-}
-.field small.error {
- color: var(--error);
-}
-.field small code {
- background: var(--bg);
- padding: 1px 5px;
- border-radius: 4px;
- border: 1px solid var(--border);
-}
-
-/* Noise gate: a header with a live value, a level meter, and a range slider
- that shares the meter's horizontal (dB) axis. */
-.field-head {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
-}
-.field-value {
- font-weight: 500;
- font-size: 12px;
- letter-spacing: 0;
- text-transform: none;
- color: var(--text-dim);
-}
-/* The slider and the level meter are one widget: the range input is overlaid
- on the meter track (its native track is transparent), so the live-level fill
- shows through behind the thumb and the thumb itself is the threshold. */
-.gate {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-.gate-track {
- position: relative;
- height: 14px;
- display: flex;
- align-items: center;
-}
-.gate-track::before {
- /* the visible meter track */
- content: "";
- position: absolute;
- left: 0;
- right: 0;
- height: 8px;
- border-radius: 999px;
- background: var(--bg);
- border: 1px solid var(--border);
-}
-.gate-meter-fill {
- position: absolute;
- left: 1px;
- top: 50%;
- transform: translateY(-50%);
- height: 6px;
- width: 0;
- border-radius: 999px;
- background: var(--accent-2);
- transition: width 0.06s linear;
- pointer-events: none;
-}
-.gate-ends {
- display: flex;
- justify-content: space-between;
- font-size: 11px;
- color: var(--text-faint);
- letter-spacing: 0;
- text-transform: none;
-}
-/* The range input sits transparently on top of the meter track. */
-.gate-track input[type="range"] {
- position: relative;
- z-index: 1;
- width: 100%;
- margin: 0;
- -webkit-appearance: none;
- appearance: none;
- padding: 0;
- border: none;
- background: transparent;
- height: 14px;
- cursor: pointer;
-}
-.gate-track input[type="range"]::-webkit-slider-runnable-track {
- height: 14px;
- background: transparent;
-}
-.gate-track input[type="range"]::-moz-range-track {
- height: 14px;
- background: transparent;
-}
-.gate-track input[type="range"]::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 6px;
- height: 18px;
- border-radius: 3px;
- background: var(--text);
- border: 2px solid var(--bg-elev);
- box-shadow: 0 0 0 1px var(--border-strong);
-}
-.gate-track input[type="range"]::-moz-range-thumb {
- width: 6px;
- height: 18px;
- border-radius: 3px;
- background: var(--text);
- border: 2px solid var(--bg-elev);
- box-shadow: 0 0 0 1px var(--border-strong);
-}
-.gate-track input[type="range"]:focus {
- border: none;
-}
-
-.modal-footer {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- padding-top: 6px;
-}
-
-.btn {
- padding: 10px 16px;
- border-radius: var(--radius-sm);
- border: 1px solid var(--border-strong);
- background: var(--bg-elev-2);
- color: var(--text);
- font-weight: 600;
- font-size: 13px;
- cursor: pointer;
- transition:
- background 0.15s,
- border-color 0.15s,
- transform 0.05s;
-}
-.btn:hover {
- border-color: var(--text);
-}
-.btn:active {
- transform: translateY(1px);
-}
-.btn.primary {
- background: var(--text);
- border-color: var(--text);
- color: var(--bg);
-}
-.btn.primary:hover {
- background: #fff;
- border-color: #fff;
-}
-.btn.ghost {
- background: transparent;
- border-color: var(--border);
- color: var(--text-dim);
-}
-.btn.wide {
- width: 100%;
- padding: 12px 16px;
-}
-.btn[disabled] {
- opacity: 0.45;
- cursor: not-allowed;
-}
-.btn[disabled]:hover {
- border-color: var(--border-strong);
-}
-
-/* ─── Settings tabs ───────────────────────────────────────────────────── */
-
-.tabs {
- display: flex;
- gap: 4px;
- padding: 4px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
-}
-.tab {
- flex: 1;
- padding: 8px 12px;
- border: none;
- background: transparent;
- color: var(--text-dim);
- font-family: inherit;
- font-size: 13px;
- font-weight: 600;
- letter-spacing: 0.02em;
- border-radius: calc(var(--radius-sm) - 3px);
- cursor: pointer;
- transition:
- background 0.15s,
- color 0.15s;
-}
-.tab:hover {
- color: var(--text);
-}
-.tab.active {
- background: var(--bg-elev-2);
- color: var(--text);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.tab-panels {
- display: flex;
- flex-direction: column;
-}
-.tab-panel {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-.tab-panel[hidden] {
- display: none;
-}
-
-/* Horizontal layout for Voice + Model. */
-.field-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
-}
-
-/* ─── Chat button + badge ────────────────────────────────────────────── */
-
-#chat-btn {
- position: relative;
-}
-
-.chat-badge {
- position: absolute;
- top: 6px;
- right: 6px;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: var(--text);
- border: 1.5px solid var(--bg-elev);
- opacity: 0;
- transform: scale(0);
- transition:
- opacity 0.2s ease,
- transform 0.2s ease;
- pointer-events: none;
-}
-.chat-badge.visible {
- opacity: 1;
- transform: scale(1);
-}
-
-/* ─── Ephemeral bubble stack ─────────────────────────────────────────── */
-
-.bubble-stack {
- position: fixed;
- /* Clear the topbar control row so the first bubble doesn't sit at the same
- * height as the buttons. */
- top: 96px;
- right: 28px;
- width: min(300px, calc(100vw - 56px));
- display: flex;
- flex-direction: column;
- gap: 8px;
- pointer-events: none;
- z-index: 100;
-}
-
-.bubble {
- --bubble-dx: 10px;
- pointer-events: auto;
- padding: 10px 14px;
- border-radius: var(--radius-md);
- font-size: 13px;
- line-height: 1.5;
- border: 1px solid var(--border);
- background: var(--bg-elev);
- box-shadow: 0 4px 18px rgba(0, 0, 0, 0.32);
- max-width: 100%;
- word-break: break-word;
- opacity: 0;
- transform: translateX(var(--bubble-dx));
- transition:
- opacity 0.22s ease,
- transform 0.22s ease;
-}
-.bubble.in {
- opacity: 1;
- transform: translateX(0);
-}
-.bubble.out {
- opacity: 0;
- transform: translateX(var(--bubble-dx));
- pointer-events: none;
- transition:
- opacity 0.3s ease,
- transform 0.3s ease;
-}
-
-/* Surfaces stay neutral; the side they sit on plus the mono role label
- * carry the distinction. No tinted fills — color is the orb's job. */
-.bubble.user {
- --bubble-dx: -10px;
- align-self: flex-start;
-}
-.bubble.assistant {
- --bubble-dx: 10px;
- align-self: flex-end;
-}
-.bubble.tool {
- --bubble-dx: -10px;
- align-self: flex-start;
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--text-dim);
- font-family: var(--font-mono);
- font-size: 12px;
-}
-.bubble.tool .bubble-tool-icon {
- width: 13px;
- height: 13px;
- flex: none;
- color: var(--voice-tool);
-}
-
-.bubble-role {
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 500;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- margin-bottom: 4px;
- opacity: 0.7;
-}
-.bubble.user .bubble-role {
- color: var(--voice-user);
-}
-.bubble.assistant .bubble-role {
- color: var(--voice-assistant);
-}
-
-/* ─── Conversation history panel (NOW INLINE in #app, below stage, above footer) ─ */
-.chat-panel-inner::before {
- /* Top aurora fade removed (would add an artificial tint vs the page bg). */
- content: none;
-}
-.chat-panel-inner > * {
- position: relative;
- z-index: 1;
-}
-
-/* The inline chat is ALWAYS shown so we never need .open / backdrop rules. */
-.chat-panel-backdrop {
- display: none;
-}
-.chat-panel-header h2 {
- margin: 0;
- font-size: 13px;
- letter-spacing: 0.04em;
- background: var(--brand-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- font-weight: 800;
- text-transform: uppercase;
- opacity: 0.92;
-}
-.chat-history::-webkit-scrollbar {
- width: 6px;
-}
-.chat-history::-webkit-scrollbar-track {
- background: transparent;
-}
-.chat-history::-webkit-scrollbar-thumb {
- background: color-mix(in srgb, var(--brand) 30%, transparent);
- border-radius: 999px;
- border: 1px solid transparent;
- background-clip: content-box;
-}
-/* Toggle button badge (count of reports scheduled, shown only if >0) */
-#reports-sidebar-badge {
- position: absolute;
- top: -3px;
- right: -3px;
- min-width: 18px;
- height: 18px;
- padding: 0 5px;
- border-radius: 999px;
- background: linear-gradient(135deg, var(--brand-2), var(--brand));
- color: #fff;
- font-size: 10px;
- font-weight: 700;
- letter-spacing: 0.02em;
- line-height: 18px;
- text-align: center;
- display: none;
- box-shadow:
- 0 0 0 2px var(--bg),
- 0 0 10px rgba(0, 0, 0, 0.3);
- pointer-events: none;
-}
-#reports-sidebar-badge[data-count]:not([data-count="0"]):not([data-count=""]) {
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-
-/* ── DESKTOP (>=1024px): sidebar always visible on the LEFT ────────── */
-@media (min-width: 1024px) {
- /* Give the orb some extra breathing room since we stole 320px on the left */
- .stage {
- padding-left: clamp(24px, 4vw, 48px);
- padding-right: clamp(24px, 4vw, 48px);
- }
-}
-
-.chat-empty {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 6px;
- color: var(--text-faint);
- padding: 48px 0;
- opacity: 0.65;
-}
-.chat-empty svg {
- margin-bottom: 6px;
- opacity: 0.8;
-}
-.chat-empty-title {
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.14em;
- text-transform: uppercase;
-}
-.chat-empty-hint {
- font-size: 12px;
- color: var(--text-faint);
- opacity: 0.7;
-}
-
-.hist-role {
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 700;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--text-faint);
- padding: 0 4px;
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.45);
-}
-.hist-msg.user .hist-role {
- color: color-mix(in srgb, var(--voice-user) 92%, white 8%);
-}
-.hist-msg.assistant .hist-role {
- color: color-mix(in srgb, var(--voice-assistant) 92%, white 8%);
-}
-.hist-msg.tool .hist-role {
- color: color-mix(in srgb, var(--voice-tool) 92%, white 8%);
-}
-
-.hist-body {
- padding: 11px 14px;
- border-radius: 14px;
- font-size: 14px;
- line-height: 1.55;
- word-break: break-word;
- color: var(--text);
- /* Glass base — visible against the seamless aurora page background. */
- background: color-mix(in srgb, var(--bg-elev-2) 88%, rgba(255, 255, 255, 0.03));
- border: 1px solid color-mix(in srgb, var(--border) 92%, transparent);
- box-shadow:
- 0 10px 28px rgba(0, 0, 0, 0.28),
- 0 0 0 1px rgba(255, 255, 255, 0.035) inset;
- backdrop-filter: blur(8px) saturate(130%);
- -webkit-backdrop-filter: blur(8px) saturate(130%);
-}
-/* Role-tinted bubbles so you can scan who's talking at a glance even when the
- history is long. User bubble gets a soft cyan/violet rim; assistant gets a
- pink/violet rim. Both stay readable against the page aurora. */
-.hist-msg.user .hist-body {
- background: linear-gradient(
- 180deg,
- color-mix(in srgb, var(--voice-user) 10%, var(--bg-elev-2)),
- color-mix(in srgb, var(--voice-user) 6%, var(--bg-elev-2))
- )
- padding-box;
- border-color: color-mix(in srgb, var(--voice-user) 44%, var(--border));
- box-shadow:
- 0 10px 28px rgba(0, 0, 0, 0.28),
- 0 0 0 1px color-mix(in srgb, var(--voice-user) 16%, rgba(255, 255, 255, 0.05)) inset,
- 0 0 28px -14px color-mix(in srgb, var(--voice-user) 66%, transparent);
-}
-.hist-msg.assistant .hist-body {
- background: linear-gradient(
- 180deg,
- color-mix(in srgb, var(--voice-assistant) 10%, var(--bg-elev-2)),
- color-mix(in srgb, var(--voice-assistant) 5%, var(--bg-elev-2))
- )
- padding-box;
- border-color: color-mix(in srgb, var(--voice-assistant) 42%, var(--border));
- box-shadow:
- 0 10px 28px rgba(0, 0, 0, 0.28),
- 0 0 0 1px color-mix(in srgb, var(--voice-assistant) 14%, rgba(255, 255, 255, 0.05)) inset,
- 0 0 28px -14px color-mix(in srgb, var(--voice-assistant) 62%, transparent);
-}
-.hist-msg.user .hist-body.partial {
- opacity: 0.68;
-}
-
-/* Markdown is confined to assistant message bodies in both themes. */
-.markdown-content {
- min-width: 0;
- max-width: 100%;
- box-sizing: border-box;
- white-space: normal !important;
- overflow-wrap: anywhere;
-}
-.markdown-content > :first-child {
- margin-top: 0;
-}
-.markdown-content > :last-child {
- margin-bottom: 0;
-}
-.markdown-content p {
- margin: 0 0 0.75em;
-}
-.markdown-content strong {
- font-weight: 700;
-}
-.markdown-content :is(h1, h2, h3, h4, h5, h6) {
- margin: 1em 0 0.5em;
- font-size: 1.12em;
- font-weight: 700;
- line-height: 1.35;
-}
-.markdown-content :is(ul, ol) {
- margin: 0.65em 0;
- padding-inline-start: 1.5em;
-}
-.markdown-content ul {
- list-style-type: disc;
-}
-.markdown-content ol {
- list-style-type: decimal;
-}
-.markdown-content li + li {
- margin-top: 0.35em;
-}
-.markdown-content li > p {
- margin: 0.25em 0;
-}
-.markdown-content li :is(ul, ol) {
- margin: 0.3em 0;
-}
-.markdown-content a {
- color: var(--brand-3);
- text-decoration: underline;
- text-underline-offset: 0.15em;
-}
-html[data-theme="white"] .markdown-content a {
- color: #1d4ed8;
-}
-.markdown-content :is(a, pre, .markdown-table):focus-visible {
- outline: 2px solid var(--brand);
- outline-offset: 3px;
-}
-.markdown-content blockquote {
- margin: 0.75em 0;
- padding-inline-start: 12px;
- border-inline-start: 3px solid var(--brand);
- color: var(--text-dim);
-}
-.markdown-content code {
- padding: 0.15em 0.35em;
- border-radius: 4px;
- background: color-mix(in srgb, currentColor 8%, transparent);
- font-family: var(--font-mono);
- font-size: 0.9em;
-}
-.markdown-content pre {
- max-width: 100%;
- margin: 0.75em 0;
- padding: 12px;
- border: 1px solid var(--border-strong);
- border-radius: 8px;
- overflow: auto;
- white-space: pre;
-}
-.markdown-content pre code {
- display: block;
- padding: 0;
- background: transparent;
- white-space: pre;
- overflow-wrap: normal;
- word-break: normal;
-}
-.markdown-content hr {
- margin: 1em 0;
- border: 0;
- border-top: 1px solid var(--border-strong);
-}
-.markdown-content img {
- max-width: 100%;
- height: auto;
-}
-.markdown-content .markdown-table {
- max-width: 100%;
- margin: 0.75em 0;
- overflow-x: auto;
- overscroll-behavior-x: contain;
-}
-.markdown-content table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.92em;
-}
-.markdown-content :is(th, td) {
- padding: 7px 10px;
- border: 1px solid var(--border-strong);
- text-align: start;
- vertical-align: top;
-}
-.markdown-content th {
- font-weight: 700;
-}
-.markdown-content [align="center"] {
- text-align: center;
-}
-.markdown-content [align="right"] {
- text-align: end;
-}
-
-/* ─── Tool call history item ─────────────────────────────────────────── */
-
-.hist-tool-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 12px;
- border-radius: var(--radius-md) var(--radius-md) 0 0;
- background: color-mix(in srgb, var(--processing) 10%, var(--bg-elev-2));
- border: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border));
- border-bottom: 1px solid color-mix(in srgb, var(--processing) 15%, var(--border));
- cursor: pointer;
- color: var(--text);
- font-family: inherit;
- font-size: 13px;
- font-weight: 500;
- width: 100%;
- text-align: left;
- transition: background 0.15s;
-}
-.hist-tool-header:only-child {
- border-radius: var(--radius-md);
- border-bottom: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border));
-}
-.hist-tool-header:hover {
- background: color-mix(in srgb, var(--processing) 16%, var(--bg-elev-2));
-}
-.hist-tool-icon {
- width: 13px;
- height: 13px;
- flex: none;
- color: var(--processing);
-}
-.hist-tool-name {
- font-family: var(--font-mono);
- font-size: 12px;
- color: var(--voice-tool);
- font-weight: 600;
-}
-.hist-tool-chevron {
- margin-left: auto;
- width: 13px;
- height: 13px;
- color: var(--text-faint);
- transition: transform 0.2s ease;
- flex: none;
-}
-.hist-tool-header[aria-expanded="true"] .hist-tool-chevron {
- transform: rotate(180deg);
-}
-.hist-tool-body {
- padding: 10px 12px 12px;
- border-radius: 0 0 var(--radius-md) var(--radius-md);
- background: var(--bg);
- border: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border));
- border-top: none;
- display: none;
-}
-.hist-tool-body.open {
- display: block;
-}
-.hist-tool-label {
- font-family: var(--font-mono);
- font-size: 10px;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--text-faint);
- margin: 8px 0 4px;
-}
-.hist-tool-label:first-child {
- margin-top: 0;
-}
-.hist-tool-block {
- font-family: var(--font-mono);
- font-size: 11px;
- line-height: 1.65;
- color: var(--text-dim);
- white-space: pre-wrap;
- word-break: break-word;
- overflow-x: auto;
-}
-.hist-tool-output {
- color: var(--text);
-}
-
-/* ─── Phone layout ────────────────────────────────────────────────────────
- * On phones the floating bubble stream overlaps the orb and there isn't room
- * for it, so we drop it entirely and rely on the conversation panel (opened
- * from the top-right chat button) as the single place to read the transcript.
- * The badge still pulses there when new messages arrive while it's closed.
- * We also stack the mic / stop controls above and below the orb (instead of
- * left/right) so the wide live row never overflows, and let the panel take
- * the full width. */
-@media (max-width: 600px) {
- .bubble-stack {
- display: none;
- }
-
- .topbar {
- padding: 14px 16px;
- }
-
- .stage {
- padding: 16px 12px 24px;
- }
-
- /* Stack vertically: mic above the orb, stop below it (DOM order is
- * mic → circle → stop). */
- .orb-wrap {
- flex-direction: column;
- gap: 14px;
- }
-
- .circle {
- /* Height-capped like the base rule so a narrow AND short box still fits.
- * Use svh/svw (smallest-viewport units) to avoid URL bar resize jumps. */
- width: clamp(110px, min(56svw, 36svh), 220px);
- }
-
- /* In the column layout the side controls must collapse by HEIGHT, not
- * width, so they take no vertical space until the session is live. */
- .side-btn {
- width: 44px;
- height: 0;
- transition:
- opacity 0.25s ease,
- transform 0.25s ease,
- height 0.25s ease,
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
- }
- .side-btn svg {
- width: 19px;
- height: 19px;
- }
- .orb-wrap.live .side-btn {
- width: 44px;
- height: 44px;
- }
- .mic-gate-arc {
- width: 70px;
- height: 70px;
- }
-}
-
-/* ─── About panel ─────────────────────────────────────────────────────────
- * Opened from the (i) by the wordmark. Reuses the .modal shell.
- * Strictly monochrome per DESIGN.md — the only "color" is the orb, never
- * here. Machine identifiers (model IDs, role tags, usernames) ride Geist
- * Mono; everything human stays Inter. */
-.about-modal {
- /* Roomier on tablet/desktop; the 92vw cap keeps phones full-width. */
- width: min(680px, 92vw);
- max-height: 88vh;
-}
-.about-modal .modal-content {
- max-height: 88vh;
- overflow-y: auto;
- gap: 20px;
- padding: 26px 30px 24px;
-}
-.about-modal .modal-content::-webkit-scrollbar {
- width: 3px;
-}
-.about-modal .modal-content::-webkit-scrollbar-track {
- background: transparent;
-}
-.about-modal .modal-content::-webkit-scrollbar-thumb {
- background: var(--border-strong);
- border-radius: 3px;
-}
-
-/* Links here are quiet: no permanent underline (the global dotted rule is
- * too loud for a dense credit block), brighten + underline on hover only. */
-.about-modal a {
- color: var(--text);
- border-bottom: none;
- text-decoration: none;
- transition: color 0.15s ease;
-}
-.about-modal a:hover {
- color: #fff;
-}
-.about-intro a:hover {
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-.about-modal .ext {
- width: 12px;
- height: 12px;
- flex: none;
- opacity: 0.5;
-}
-
-/* ─── User Profile Modal (opened by clicking the avatar circle) ──────── */
-.user-profile-modal {
- width: min(480px, 90vw);
- max-height: 90vh;
-}
-.user-profile-card {
- position: relative;
- padding: 30px 28px 28px;
- gap: 22px;
- /* Glass + brand-gradient 1px border (same trick used in the .brand card). */
- background:
- linear-gradient(var(--bg-elev), var(--bg-elev)) padding-box,
- linear-gradient(
- 135deg,
- color-mix(in srgb, var(--brand) 55%, transparent),
- color-mix(in srgb, var(--brand-2) 48%, transparent) 55%,
- color-mix(in srgb, var(--brand-3) 55%, transparent)
- )
- border-box;
- border: 1px solid transparent;
- border-radius: 18px;
- box-shadow:
- 0 30px 80px rgba(0, 0, 0, 0.45),
- 0 0 0 1px rgba(255, 255, 255, 0.04) inset,
- 0 0 60px -20px color-mix(in srgb, var(--brand) 60%, transparent);
- backdrop-filter: blur(20px) saturate(150%);
- -webkit-backdrop-filter: blur(20px) saturate(150%);
- overflow: hidden;
-}
-.user-profile-card::before {
- /* Top-edge soft aurora fade so the card feels "lit" from the top-left,
- matching the orb + body aurora palette. */
- content: "";
- position: absolute;
- inset: -1px;
- background:
- radial-gradient(420px 220px at 15% -10%, color-mix(in srgb, var(--brand) 26%, transparent), transparent 60%),
- radial-gradient(380px 220px at 110% 15%, color-mix(in srgb, var(--brand-2) 22%, transparent), transparent 55%);
- pointer-events: none;
- opacity: 0.95;
- z-index: 0;
-}
-.user-profile-card > * {
- position: relative;
- z-index: 1;
-}
-
-/* Close button: absolute top-right, 32px so it doesn't feel cramped near
- the large hero avatar. */
-.user-profile-close {
- position: absolute;
- top: 12px;
- right: 12px;
- width: 32px;
- height: 32px;
- z-index: 5;
- opacity: 0.88;
-}
-
-/* HERO — big glass avatar circle, name, email fallback subtitle. */
-.user-profile-hero {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 14px;
- padding-top: 6px;
- text-align: center;
-}
-.user-profile-avatar {
- width: 88px;
- height: 88px;
- aspect-ratio: 1 / 1;
- border-radius: 999px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: 1px solid color-mix(in srgb, var(--brand) 40%, var(--border));
- color: #fff;
- background:
- radial-gradient(140% 140% at 28% 22%, rgba(255, 255, 255, 0.22), transparent 58%),
- linear-gradient(
- 135deg,
- color-mix(in srgb, var(--brand) 76%, transparent),
- color-mix(in srgb, var(--brand-2) 64%, transparent) 55%,
- color-mix(in srgb, var(--brand-3) 62%, transparent)
- );
- box-shadow:
- 0 0 0 1px rgba(255, 255, 255, 0.05) inset,
- 0 2px 0 rgba(255, 255, 255, 0.08) inset,
- 0 10px 30px rgba(0, 0, 0, 0.32),
- 0 0 48px -10px color-mix(in srgb, var(--brand-2) 72%, transparent);
- font-family: var(--font-sans);
- font-weight: 800;
- font-size: 34px;
- line-height: 1;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
- user-select: none;
- position: relative;
-}
-.user-profile-avatar::after {
- /* subtle specular rim */
- content: "";
- position: absolute;
- inset: 0;
- border-radius: inherit;
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 46%);
- opacity: 0.85;
- pointer-events: none;
- mix-blend-mode: screen;
-}
-.user-profile-title {
- margin: 0;
- font-size: 22px;
- line-height: 1.2;
- font-weight: 800;
- letter-spacing: -0.01em;
- background: var(--brand-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- color: transparent;
- filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.35));
- max-width: 100%;
- overflow-wrap: anywhere;
-}
-.user-profile-subtitle {
- margin: 0;
- font-size: 13px;
- line-height: 1.4;
- color: var(--text-dim);
- max-width: 100%;
- overflow-wrap: anywhere;
-}
-
-/* METADATA — 2-col grid: label (left, dim/caps) + value (right, bright). */
-.user-profile-metadata {
- display: grid;
- grid-template-columns: 1fr 1.35fr;
- row-gap: 12px;
- column-gap: 16px;
- padding: 10px 4px 4px;
- border-top: 1px solid color-mix(in srgb, var(--brand) 18%, var(--border));
- padding-top: 18px;
-}
-.user-profile-metadata:empty {
- display: none;
-}
-
-.user-profile-row {
- display: contents; /* let children participate in the outer grid directly */
-}
-.user-profile-row-label {
- grid-column: 1 / 2;
- align-self: start;
- font-size: 11px;
- line-height: 1.5;
- font-weight: 700;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: color-mix(in srgb, var(--brand-3) 80%, var(--text-dim));
- padding-top: 2px;
-}
-.user-profile-row-value {
- grid-column: 2 / 3;
- font-size: 13.5px;
- line-height: 1.45;
- color: var(--text);
- font-weight: 500;
- overflow-wrap: anywhere;
- word-break: break-word;
-}
-.user-profile-row-value a {
- color: inherit;
- text-decoration: none;
- border-bottom: 1px dashed color-mix(in srgb, var(--brand-2) 50%, transparent);
- padding-bottom: 1px;
-}
-.user-profile-row-value a:hover {
- color: #fff;
- border-bottom-color: color-mix(in srgb, var(--brand-2) 90%, #fff);
-}
-.user-profile-row-value code,
-.user-profile-row-value .mono {
- font-family: var(--font-mono);
- font-size: 12px;
- padding: 2px 7px;
- border-radius: 6px;
- background: color-mix(in srgb, var(--brand) 12%, rgba(255, 255, 255, 0.03));
- border: 1px solid color-mix(in srgb, var(--brand) 20%, var(--border));
- color: color-mix(in srgb, var(--brand-2) 90%, #fff);
- display: inline-block;
- max-width: 100%;
- overflow-wrap: anywhere;
-}
-
-@media (max-width: 480px) {
- .user-profile-card {
- padding: 24px 20px 20px;
- }
- .user-profile-avatar {
- width: 72px;
- height: 72px;
- font-size: 28px;
- }
- .user-profile-title {
- font-size: 19px;
- }
- .user-profile-metadata {
- grid-template-columns: 1fr; /* stack label/value on super-narrow */
- row-gap: 6px;
- padding-top: 16px;
- }
- .user-profile-row-label {
- grid-column: 1 / -1;
- padding-top: 6px;
- }
- .user-profile-row-value {
- grid-column: 1 / -1;
- }
-}
-/* Title row: the demo name and the (i) read as one unit. */
-.ident-head {
- display: flex;
- align-items: center;
- gap: 9px;
-}
-/* (i) trigger sits right after the title. A faint outline keeps it
- * catchable without turning into a card; it fills in on hover. */
-.about-btn {
- flex: none;
- width: 34px;
- height: 34px;
- border-radius: 50%;
- background: transparent;
- border: 1px solid var(--border-strong);
- color: var(--text-dim);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- touch-action: manipulation;
- transition:
- background 0.15s ease,
- color 0.15s ease,
- border-color 0.15s ease;
-}
-.about-btn:hover {
- background: var(--bg-elev);
- border-color: var(--text-dim);
- color: var(--text);
-}
-.about-btn svg {
- width: 20px;
- height: 20px;
-}
-
-/* The mobile twin of (i) lives in the right-hand control cluster and is
- * hidden on desktop (the in-title one shows there instead). */
-.about-btn-mobile {
- display: none;
-}
-
-/* ── Popup intro: a plain paragraph on the project + a repo link ── */
-.about-intro p {
- margin: 0;
- font-size: 14px;
- line-height: 1.6;
- color: var(--text-dim);
-}
-.about-repo {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- margin-top: 12px;
- font-size: 14px;
- font-weight: 500;
-}
-
-/* ── Corner identity (replaces the wordmark) ──
- * A compact stack in the topbar: title, one-line blurb, two meta rows.
- * Monochrome; only the role/name identifiers ride the machine typeface. */
-.ident {
- /* Align the wordmark (left) and the user avatar circle (right) on the SAME
- * row. Vertical-align: both centered relative to each other — so "AV" in the
- * round badge lines up perfectly with the "Vocero" gradient wordmark. */
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- min-width: 0;
- width: 100%;
-}
-/* Reset the global underlined-anchor styling inside the identity block. */
-.ident a {
- border-bottom: none;
- color: inherit;
- transition: color 0.15s ease;
-}
-.ident a:hover {
- color: var(--text);
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-.ident-title {
- font-size: 22px;
- font-weight: 600;
- letter-spacing: 0.005em;
- line-height: 1.1;
- color: var(--text);
-}
-.ident-user-name {
- max-width: 32ch;
- font-size: 13px;
- line-height: 1.35;
- color: var(--text-dim);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.reports-hint {
- max-width: min(720px, 100%);
- /* IMPORTANT: hard cap height so even 20 report chips never grow the topbar
- * beyond ~35% of the viewport. If more space is needed the user scrolls
- * inside the card itself (branded thin scrollbar below). */
- max-height: 34svh;
- max-height: 36dvh;
- overflow-y: auto;
- overflow-x: hidden;
- scrollbar-width: thin;
- scrollbar-gutter: stable;
- font-size: 12px;
- line-height: 1.3;
- color: var(--text-dim);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.028), rgba(255, 255, 255, 0)), var(--glass);
- border: 1px solid color-mix(in srgb, var(--brand) 26%, var(--border));
- border-radius: var(--radius-sm);
- backdrop-filter: blur(12px) saturate(135%);
- -webkit-backdrop-filter: blur(12px) saturate(135%);
- padding: 8px 10px;
- margin-top: 4px;
- box-shadow:
- var(--shadow-glass),
- 0 0 0 1px color-mix(in srgb, var(--brand) 14%, transparent) inset,
- 0 0 20px -10px color-mix(in srgb, var(--brand) 55%, transparent);
- position: relative;
-}
-/* thin scrollbar inside the reports card so chips are still readable. */
-.reports-hint::-webkit-scrollbar {
- width: 6px;
-}
-.reports-hint::-webkit-scrollbar-track {
- background: transparent;
-}
-.reports-hint::-webkit-scrollbar-thumb {
- background: color-mix(in srgb, var(--brand-3) 32%, transparent);
- border-radius: 999px;
- border: 1px solid transparent;
- background-clip: content-box;
-}
-.reports-hint::before {
- content: "";
- position: absolute;
- top: -1px;
- left: -1px;
- right: -1px;
- height: 1px;
- background: linear-gradient(
- 90deg,
- transparent 0%,
- color-mix(in srgb, var(--brand) 70%, transparent) 25%,
- color-mix(in srgb, var(--brand-2) 60%, transparent) 55%,
- color-mix(in srgb, var(--brand-3) 65%, transparent) 80%,
- transparent 100%
- );
- opacity: 0.85;
- pointer-events: none;
-}
-.reports-hint .reports-summary {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- flex-wrap: wrap;
- gap: 4px 8px;
- font-weight: 600;
- color: var(--text);
- margin-bottom: 6px;
- font-size: 11.5px;
- line-height: 1.15;
-}
-.reports-hint .reports-summary::before {
- content: "";
- width: 7px;
- height: 7px;
- border-radius: 999px;
- background: var(--success);
- box-shadow:
- 0 0 0 2.5px color-mix(in srgb, var(--success) 18%, transparent),
- 0 0 10px 1px color-mix(in srgb, var(--success) 68%, transparent);
- animation: pulseDot 2.2s ease-in-out infinite;
- flex: none;
-}
-.reports-hint .reports-summary-badge {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 7px;
- border-radius: 999px;
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 600;
- letter-spacing: 0.03em;
- color: var(--text);
- background: linear-gradient(
- 135deg,
- color-mix(in srgb, var(--brand) 24%, transparent),
- color-mix(in srgb, var(--brand-2) 20%, transparent)
- );
- border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
- white-space: nowrap;
-}
-.reports-hint .reports-summary-badge + .reports-summary-badge {
- background: linear-gradient(
- 135deg,
- color-mix(in srgb, var(--brand-3) 20%, transparent),
- color-mix(in srgb, var(--brand) 18%, transparent)
- );
- border-color: color-mix(in srgb, var(--brand-3) 28%, transparent);
-}
-.reports-hint .reports-list {
- margin: 0;
- padding: 0;
- list-style: none;
- display: flex;
- flex-wrap: wrap;
- gap: 4px 6px;
-}
-.reports-hint .reports-list li {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 3px 8px 3px 6px;
- border-radius: 999px;
- font-size: 11px;
- font-weight: 500;
- line-height: 1.1;
- color: var(--text);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
- position: relative;
- border: 1px solid var(--border);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), rgba(20, 22, 32, 0.52);
- box-shadow:
- 0 1px 0 rgba(255, 255, 255, 0.035) inset,
- 0 1px 4px rgba(0, 0, 0, 0.16);
- transition:
- transform 0.2s cubic-bezier(0.2, 0.7, 0.2, 1),
- box-shadow 0.25s ease,
- border-color 0.25s ease,
- background 0.25s ease,
- color 0.25s ease;
-}
-.reports-hint .reports-list li::before {
- content: "";
- width: 5px;
- height: 5px;
- border-radius: 999px;
- background: var(--brand);
- box-shadow: 0 0 7px 0 color-mix(in srgb, var(--brand) 75%, transparent);
- flex: none;
-}
-/* Per-chip tint cycles (keyword-based, if no data attribute → fallback brand) */
-.reports-hint .reports-list li[data-tint="mortality"] {
- background: linear-gradient(180deg, color-mix(in srgb, #ff6a75 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #ff6a75 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="mortality"]::before {
- background: #ff6a75;
- box-shadow: 0 0 8px 0 #ff6a75aa;
-}
-.reports-hint .reports-list li[data-tint="metrics"] {
- background: linear-gradient(180deg, color-mix(in srgb, #22d3ee 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #22d3ee 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="metrics"]::before {
- background: #22d3ee;
- box-shadow: 0 0 8px 0 #22d3eeaa;
-}
-.reports-hint .reports-list li[data-tint="daily"] {
- background: linear-gradient(180deg, color-mix(in srgb, #8b5cf6 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #8b5cf6 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="daily"]::before {
- background: #8b5cf6;
- box-shadow: 0 0 8px 0 #8b5cf6aa;
-}
-.reports-hint .reports-list li[data-tint="products"] {
- background: linear-gradient(180deg, color-mix(in srgb, #ec4899 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #ec4899 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="products"]::before {
- background: #ec4899;
- box-shadow: 0 0 8px 0 #ec4899aa;
-}
-.reports-hint .reports-list li[data-tint="synapse"] {
- background: linear-gradient(180deg, color-mix(in srgb, #fbbf24 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #fbbf24 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="synapse"]::before {
- background: #fbbf24;
- box-shadow: 0 0 8px 0 #fbbf24aa;
-}
-.reports-hint .reports-list li[data-tint="supply"] {
- background: linear-gradient(180deg, color-mix(in srgb, #34d399 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #34d399 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="supply"]::before {
- background: #34d399;
- box-shadow: 0 0 8px 0 #34d399aa;
-}
-.reports-hint .reports-list li[data-tint="finance"] {
- background: linear-gradient(180deg, color-mix(in srgb, #22c55e 14%, rgba(20, 22, 32, 0.55)), rgba(20, 22, 32, 0.55));
- border-color: color-mix(in srgb, #22c55e 35%, var(--border));
-}
-.reports-hint .reports-list li[data-tint="finance"]::before {
- background: #22c55e;
- box-shadow: 0 0 8px 0 #22c55eaa;
-}
-.reports-hint .reports-list li:hover {
- transform: translateY(-1px) scale(1.01);
- border-color: color-mix(in srgb, var(--brand) 55%, var(--border-strong));
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)), rgba(24, 27, 40, 0.7);
- box-shadow:
- 0 6px 16px rgba(0, 0, 0, 0.3),
- 0 0 14px -4px color-mix(in srgb, var(--brand) 55%, transparent),
- inset 0 1px 0 rgba(255, 255, 255, 0.07);
-}
-.reports-hint .reports-suggestions {
- margin-top: 12px;
- padding-top: 10px;
- border-top: 1px dashed color-mix(in srgb, var(--brand) 30%, transparent);
- position: relative;
-}
-.reports-hint .reports-suggestions::before {
- content: "";
- position: absolute;
- top: -1px;
- left: 10%;
- right: 10%;
- height: 1px;
- background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--brand-2) 55%, transparent) 50%, transparent);
- opacity: 0.7;
-}
-.reports-hint .reports-suggestions-title {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-weight: 600;
- color: var(--text);
- margin-bottom: 6px;
- font-size: 12.5px;
- line-height: 1.2;
-}
-.reports-hint .reports-suggestions-title::before {
- content: "💡";
- filter: drop-shadow(0 0 6px color-mix(in srgb, var(--brand-3) 65%, transparent));
-}
-.reports-hint .reports-suggestions-list {
- margin: 0;
- padding: 0;
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: 5px;
- color: var(--text-dim);
-}
-.reports-hint .reports-suggestions-list li {
- white-space: normal;
- word-break: break-word;
- padding: 5px 8px 5px 26px;
- border-radius: 10px;
- position: relative;
- font-size: 12.5px;
- transition:
- background 0.2s ease,
- color 0.2s ease;
-}
-.reports-hint .reports-suggestions-list li::before {
- content: "→";
- position: absolute;
- left: 8px;
- top: 5px;
- color: var(--brand-2);
- font-weight: 700;
- filter: drop-shadow(0 0 4px color-mix(in srgb, var(--brand-2) 70%, transparent));
-}
-.reports-hint .reports-suggestions-list li:hover {
- background: color-mix(in srgb, var(--brand) 8%, transparent);
- color: var(--text);
-}
-.ident-blurb {
- margin: 0;
- max-width: 48ch;
- font-size: 14.5px;
- line-height: 1.5;
- font-weight: 400;
- color: var(--text-dim);
-}
-.ident-meta {
- display: flex;
- flex-direction: column;
- gap: 7px;
- font-size: 14px;
- font-weight: 400;
- color: var(--text-dim);
-}
-.ident-row {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 4px 8px;
-}
-.ident-label {
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: var(--text-faint);
-}
-
-/* Shared credit bits — now used in the corner identity block. */
-.sep {
- color: var(--text-faint);
- opacity: 0.6;
-}
-.cerebras-credit {
- display: inline-flex;
- align-items: center;
- gap: 5px;
-}
-.cerebras-mark {
- width: 14px;
- height: 14px;
- flex: none;
-}
-/* Usernames are identifiers, so they ride the machine typeface. */
-.handle {
- font-family: var(--font-mono);
- font-size: 13.5px;
-}
-
-/* ── Pipeline (the signal flow) ── */
-.about-pipeline {
- border-top: 1px solid var(--border);
- padding-top: 16px;
-}
-.pipeline-title {
- margin: 0 0 14px;
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--text-faint);
-}
-.pipeline {
- list-style: none;
- margin: 0;
- padding: 0;
- position: relative;
-}
-/* One continuous rail threading every node, dot centers at x=8.5px. */
-.pipeline::before {
- content: "";
- position: absolute;
- left: 8px;
- top: 7px;
- bottom: 7px;
- width: 1px;
- background: var(--border-strong);
-}
-.pipeline > li {
- position: relative;
- padding-left: 30px;
-}
-/* Stages: solid node. Endpoints (you / orb): hollow node. */
-.pipe-stage::before {
- content: "";
- position: absolute;
- left: 5px;
- top: 4px;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- background: var(--text-dim);
-}
-.pipe-endpoint::before {
- content: "";
- position: absolute;
- left: 5px;
- top: 3px;
- width: 7px;
- height: 7px;
- border-radius: 50%;
- border: 1px solid var(--text-faint);
- background: var(--bg-elev);
-}
-.pipe-endpoint {
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 500;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: var(--text-faint);
- padding-bottom: 12px;
-}
-.pipeline > li.pipe-endpoint:last-child {
- padding-bottom: 0;
-}
-.pipe-stage {
- padding-bottom: 16px;
-}
-.pipe-tag {
- font-family: var(--font-mono);
- font-size: 12px;
- font-weight: 600;
- letter-spacing: 0.08em;
- color: var(--text);
- margin-right: 9px;
-}
-.pipe-job {
- font-size: 14px;
- color: var(--text-dim);
-}
-/* Middot between the job and its model link, matching the separators
- * used elsewhere. */
-.pipe-job::after {
- content: "·";
- margin: 0 7px;
- color: var(--text-faint);
-}
-.pipe-note {
- color: var(--text-faint);
-}
-/* The Cerebras link stays as quiet as the note; brightens + underlines on hover. */
-.pipe-note a {
- color: inherit;
-}
-.pipe-note a:hover {
- color: var(--text);
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-.pipe-model {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- margin-top: 4px;
- font-family: var(--font-mono);
- font-size: 12px;
- color: var(--text-dim);
- word-break: break-all;
-}
-.pipe-model:hover {
- color: #fff;
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-.pipe-model .ext {
- width: 11px;
- height: 11px;
-}
-
-/* "Interrupted" tag on an assistant reply the user barged in on. */
-.hist-note {
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 500;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: var(--text-faint);
- padding: 0 2px;
-}
-.hist-msg.assistant .hist-note {
- align-self: flex-end;
-}
-.hist-msg.interrupted .hist-body {
- opacity: 0.7;
-}
-
-/* ─── Tools panel ──────────────────────────────────────────────────────────
- * Reuses the modal/field shell. Switches are monochrome (checked = near-white,
- * the same high-contrast treatment as the primary button): color belongs to
- * the voice, not the chrome. */
-.tools-intro {
- margin: 0;
- font-size: 13px;
- line-height: 1.5;
- color: var(--text-dim);
-}
-.tool-list {
- display: flex;
- flex-direction: column;
-}
-.tool-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 12px 0;
-}
-.tool-row-sep {
- border-top: 1px solid var(--border);
- margin-top: 4px;
-}
-.tool-info {
- display: flex;
- flex-direction: column;
- gap: 3px;
- min-width: 0;
-}
-.tool-name {
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
-}
-.tool-desc {
- font-size: 12.5px;
- color: var(--text-dim);
-}
-.tool-row.disabled .tool-name,
-.tool-row.disabled .tool-desc {
- opacity: 0.5;
-}
-.tool-hint {
- display: block;
- font-size: 12px;
- line-height: 1.4;
- color: var(--text-faint);
-}
-.agent-tools-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-top: 10px;
-}
-.agent-tool-item {
- border: 1px solid var(--border);
- border-radius: 12px;
- background: rgba(255, 255, 255, 0.02);
-}
-.agent-tool-summary {
- list-style: none;
- cursor: pointer;
- padding: 10px 12px;
-}
-.agent-tool-summary::-webkit-details-marker {
- display: none;
-}
-.agent-tool-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- margin-bottom: 4px;
-}
-.agent-tool-badge {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 2px 8px;
- border: 1px solid rgba(116, 167, 255, 0.35);
- border-radius: 999px;
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.03em;
- color: #9cc0ff;
- background: rgba(90, 140, 255, 0.12);
-}
-.agent-tool-badge.suggested {
- border-color: rgba(123, 223, 168, 0.35);
- color: #9fe0bc;
- background: rgba(79, 168, 124, 0.14);
-}
-.agent-tool-match-banner {
- padding: 10px 12px;
- border: 1px solid rgba(123, 223, 168, 0.2);
- border-radius: 12px;
- background: rgba(79, 168, 124, 0.08);
-}
-.agent-tool-match-text {
- margin-top: 6px;
- font-size: 12px;
- line-height: 1.45;
- color: var(--text);
-}
-.agent-tool-body {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 0 12px 12px;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
-}
-.agent-tool-call-label {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-.agent-tool-call-copy {
- font-size: 12px;
- color: var(--text-faint);
-}
-.agent-tool-call-args {
- width: 100%;
- min-height: 88px;
- padding: 10px 12px;
- border: 1px solid var(--border);
- border-radius: 10px;
- background: rgba(255, 255, 255, 0.03);
- color: var(--text);
- font: inherit;
- font-family: "Geist Mono", monospace;
- resize: vertical;
-}
-.agent-tool-call-actions {
- display: flex;
- justify-content: flex-start;
-}
-.agent-tool-call-btn {
- min-width: 128px;
-}
-.agent-tool-call-result {
- padding: 10px 12px;
- border-radius: 10px;
- border: 1px solid rgba(255, 255, 255, 0.08);
- background: rgba(255, 255, 255, 0.03);
-}
-.agent-tool-call-result.success {
- border-color: rgba(123, 223, 168, 0.25);
- background: rgba(79, 168, 124, 0.08);
-}
-.agent-tool-call-result.error {
- border-color: rgba(255, 120, 120, 0.24);
- background: rgba(120, 40, 40, 0.16);
-}
-.agent-tool-call-result.running {
- border-color: rgba(90, 140, 255, 0.24);
- background: rgba(90, 140, 255, 0.08);
-}
-.agent-tool-call-result-title {
- margin-bottom: 8px;
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--text-faint);
-}
-.agent-tool-call-output {
- margin: 0;
- white-space: pre-wrap;
- word-break: break-word;
- max-height: 260px;
- overflow: auto;
- font: inherit;
- font-family: "Geist Mono", monospace;
- font-size: 12px;
- line-height: 1.5;
-}
-.agent-tool-section-title {
- margin-top: 10px;
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--text-faint);
-}
-.agent-param-row {
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 8px 10px;
- border-radius: 10px;
- background: rgba(255, 255, 255, 0.03);
-}
-.agent-param-name {
- font-size: 12.5px;
- font-weight: 600;
- color: var(--text);
-}
-.agent-param-type {
- font-size: 11.5px;
- line-height: 1.4;
- color: var(--text-faint);
-}
-.agent-tool-item .tool-name {
- display: block;
-}
-.agent-tool-empty {
- padding: 10px 12px;
- border: 1px dashed var(--border);
- border-radius: 12px;
- color: var(--text-faint);
- font-size: 12px;
-}
-.agent-tool-empty.small {
- padding: 8px 10px;
- font-size: 11.5px;
-}
-.tools-key {
- margin: 0 0 4px;
-}
-
-/* Toggle switch */
-.switch {
- position: relative;
- display: inline-flex;
- flex: none;
- width: 40px;
- height: 24px;
- cursor: pointer;
-}
-.switch input {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- margin: 0;
- opacity: 0;
- cursor: pointer;
-}
-.switch-track {
- position: absolute;
- inset: 0;
- border-radius: 999px;
- background: var(--bg);
- border: 1px solid var(--border-strong);
- transition:
- background 0.15s,
- border-color 0.15s;
-}
-.switch-track::after {
- content: "";
- position: absolute;
- top: 50%;
- left: 3px;
- transform: translateY(-50%);
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: var(--text-dim);
- transition:
- transform 0.18s ease,
- background 0.15s;
-}
-.switch input:checked + .switch-track {
- background: var(--text);
- border-color: var(--text);
-}
-.switch input:checked + .switch-track::after {
- transform: translate(16px, -50%);
- background: var(--bg);
-}
-.switch input:focus-visible + .switch-track {
- outline: 2px solid var(--text-dim);
- outline-offset: 2px;
-}
-.switch input:disabled {
- cursor: not-allowed;
-}
-.switch input:disabled + .switch-track {
- opacity: 0.5;
-}
-
-/* ─── Desktop type scale ────────────────────────────────────────────────────
- * A uniform step up (~+1px, title +2) for all UI text on larger screens.
- * Scoped to min-width: 601px so it can't reach phones — the ≤600px layout
- * keeps every size exactly as it was. */
-@media (min-width: 601px) {
- .bubble-role,
- .hist-role {
- font-size: 11px;
- }
-
- .circle-caption,
- .footer,
- .chat-empty-title,
- .hist-tool-block,
- .ident-label,
- .pipeline-title,
- .pipe-endpoint {
- font-size: 12px;
- }
-
- .field > span,
- .field small,
- .bubble.tool,
- .chat-empty-hint,
- .hist-tool-name,
- .pipe-tag,
- .pipe-model,
- .tool-hint {
- font-size: 13px;
- }
-
- .tool-desc {
- font-size: 13.5px;
- }
-
- .btn,
- .bubble,
- .field,
- .hist-body,
- .hist-tool-header,
- .tools-intro {
- font-size: 14px;
- }
-
- .handle {
- font-size: 14.5px;
- }
-
- .about-intro p,
- .about-repo,
- .brand,
- .chat-panel-header h2,
- .field textarea,
- .ident-meta,
- .pipe-job,
- .tool-name {
- font-size: 15px;
- }
-
- .ident-blurb {
- font-size: 15.5px;
- }
-
- .modal-header h2 {
- font-size: 17px;
- }
-
- .ident-title {
- font-size: 24px;
- }
-}
-
-/* ─── Short viewports: fit without a vertical scrollbar ─────────────────────
- * The base media queries only react to WIDTH, so a short container (e.g. the
- * app embedded in a small box, or a laptop window once the browser chrome eats
- * into the viewport) used to overflow vertically and get clipped. We KEEP the
- * identity header, orb, caption and credits — we just undo the desktop type
- * bump, tighten the vertical rhythm and let the height-capped orb shrink, so
- * everything fits with comfortable margin. No opt-in needed.
- *
- * The single biggest space eater is the identity block (~180px at the desktop
- * type scale), so this tier compacts it first. */
-@media (max-height: 780px) {
- .topbar {
- padding: 12px 20px 0;
- }
- .ident {
- gap: 5px;
- }
- .ident-title {
- font-size: 20px;
- }
- .ident-user-name {
- font-size: 12px;
- }
- .ident-blurb {
- font-size: 13.5px;
- line-height: 1.4;
- }
- .ident-meta {
- gap: 5px;
- font-size: 13px;
- }
- .stage {
- padding: 10px 18px 12px;
- gap: 12px;
- }
- .reports-hint {
- max-height: 30svh;
- }
- .circle {
- width: clamp(130px, min(36svw, 38svh), 280px);
- }
-}
-
-@media (max-height: 560px) {
- .topbar {
- padding: 6px 14px 0;
- gap: 6px;
- }
- .ident-title {
- font-size: 17px;
- }
- .ident-user-name {
- font-size: 11px;
- }
- /* Below this, room is tight enough that the one-line blurb and the footer
- * (a verbatim duplicate of the header's "Powered by" credit) are dropped so
- * the title + credits still read; the rest of the header stays. */
- .ident-blurb {
- display: none;
- }
- .stage {
- padding: 6px 12px 8px;
- gap: 6px;
- }
- .app-footer-text {
- font-size: 9px;
- letter-spacing: 0.1em;
- line-height: 1.25;
- }
- .reports-hint {
- max-height: 28svh;
- padding: 6px 8px;
- }
- .circle {
- width: clamp(90px, min(40svw, 34svh), 200px);
- }
-}
-
-/* ═══════════════════════════════════════════════════════════════════════
- THEME GATING (shared between purple-dark S2S_THEME="dark" / empty and
- light mobile S2S_THEME="white")
- ═══════════════════════════════════════════════════════════════════════ */
-
-/* Anti-FOUC veil: until configuration resolves, does not carry any
- `data-theme` attribute yet. Keep every parallel UI block tagged with
- `data-theme-only` INVISIBLE in that window so the user never sees the
- purple orb flash-and-disappear on `S2S_THEME=white` deploys, nor the
- light-blue mic flash on dark deploys.
- Once App.tsx applies the resolved theme,
- the html tag receives either data-theme="dark" or data-theme="white"
- → this guard stops applying → the correctly gated branch paints. */
-html:not([data-theme]) [data-theme-only] {
- visibility: hidden !important;
- pointer-events: none !important;
-}
-/* Explicit defaults when data-theme is SET (so stray [hidden] attributes
- left by previous runs / partial hydration do not accidentally suppress
- the visible branch). Rules only re-show elements that belong to the
- currently active theme. */
-html[data-theme="white"] [data-theme-only="white"]:not([data-debug-only]),
-html[data-theme="dark"] [data-theme-only="dark"]:not([data-debug-only]) {
- visibility: visible;
- pointer-events: auto;
-}
-/* Hard guarantee: sidebar reports (desktop: always-on left column) is NEVER
- rendered on the light Vocero mobile theme. Double-safety even if the
- `data-theme-only="dark"` gate is somehow stripped or JS runtime fails. */
-html[data-theme="white"] #reports-sidebar,
-html[data-theme="white"] #reports-sidebar-toggle,
-html[data-theme="white"] #reports-hint,
-html[data-theme="white"] .reports-hint {
- display: none !important;
- width: 0 !important;
- height: 0 !important;
- padding: 0 !important;
- margin: 0 !important;
- transform: translateX(-9999px) !important;
- visibility: hidden !important;
-}
-/* ─────────────────────────────────────────────────────────────────────
- USER FIX 2026-09-10: show the conversation only inside the chat box.
- The dark theme renders a SECOND ephemeral transcript into #bubble-stack
- (floating anchored top-right — the grey/light blue bubbles the user
- circled in red with an X on the last screenshot). Since the white
- mobile theme shows ALL conversation exclusively inside the persistent
- conversation card (chat-history inside .chat-panel bounded 58dvh),
- we kill the parallel floating bubble stack HARD in white theme — both
- the ID and the class, zero width/height/transform so it can't leave
- layout artifacts. */
-html[data-theme="white"] #bubble-stack,
-html[data-theme="white"] .bubble-stack {
- display: none !important;
- width: 0 !important;
- height: 0 !important;
- padding: 0 !important;
- margin: 0 !important;
- transform: translateX(-9999px) !important;
- visibility: hidden !important;
- pointer-events: none !important;
-}
-
-/* ═══════════════════════════════════════════════════════════════════════
- THEME WHITE — S2S_THEME="white"
- Light mobile "Vocero app" theme matching the provided screenshot:
- pure white background, deep blue accents, card-based UI, avatar+timestamp
- per chat bubble, rounded input with clip+send.
- Activated on (App.tsx sets the attribute from
- /api/config response). All selectors below are scoped to that attribute
- so the native dark theme is 100% preserved when the flag is off.
- ═══════════════════════════════════════════════════════════════════════ */
-
-/* ── Page chrome (strip aurora gradients / dot patterns) ────────────── */
-html[data-theme="white"] body {
- background: #ffffff !important;
- color: var(--text);
- background-image: none;
-}
-html[data-theme="white"] body::before,
-html[data-theme="white"] body::after {
- display: none !important;
- background-image: none;
- animation: none;
-}
-
-/* ── Topbar — flat, no card, centered brand and avatar right ── */
-html[data-theme="white"] .topbar {
- padding: 18px 18px 6px 18px;
- align-items: center;
- gap: 10px;
-}
-/* ── Topbar layout (theme=white): 3-column grid ──────────────────────────
- Balanced side gutters keep the brand centered with an optional avatar.
- Using a real grid eliminates absolute-position overlap bugs between the
- avatar circle and the 2-line brand title/subtitle that plagued the previous
- implementation. Every item lives inside its own grid cell → no collision. */
-html[data-theme="white"] .topbar {
- display: grid !important;
- grid-template-columns: 52px minmax(0, 1fr) 52px;
- grid-auto-rows: min-content;
- align-items: start;
- justify-items: center;
- column-gap: 4px;
- row-gap: 0;
- padding: 16px 10px 0;
- position: static !important;
- box-sizing: border-box;
- width: 100%;
- background: transparent;
- border: none;
-}
-/* Brand card → strip glass, becomes a transparent centered title block */
-html[data-theme="white"] .brand {
- grid-column: 2 / 3;
- grid-row: 1 / 2;
- flex: none;
- width: 100%;
- max-width: 100%;
- padding: 0;
- margin: 0;
- background: transparent !important;
- border: none !important;
- backdrop-filter: none;
- -webkit-backdrop-filter: none;
- box-shadow: none;
- display: flex;
- justify-content: center;
- text-align: center;
- border-radius: 0;
- justify-self: center;
-}
-html[data-theme="white"] .brand::before {
- display: none;
-}
-html[data-theme="white"] .ident {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- gap: 2px;
- width: 100%;
- max-width: 100%;
- padding: 0 4px !important;
- box-sizing: border-box;
-}
-html[data-theme="white"] .ident-head {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- text-align: center;
- gap: 2px;
- width: 100%;
- max-width: 100%;
- margin: 0 auto;
- /* Prevent 2-line brand words from ever overflowing on narrow phones */
- word-break: keep-all;
- overflow-wrap: normal;
-}
-html[data-theme="white"] .ident-user-name {
- grid-column: 3 / 4;
- grid-row: 1 / 2;
- position: static !important;
- justify-self: end;
- align-self: center;
- margin: 0 2px 0 0;
- z-index: 2;
- width: 42px;
- height: 42px;
- border-radius: 999px;
- background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%);
- color: #ffffff;
- font-size: 13px;
- font-weight: 700;
- display: inline-flex !important;
- align-items: center;
- justify-content: center;
- border: 2px solid rgba(59, 130, 246, 0.25);
- box-shadow: 0 3px 10px rgba(37, 99, 235, 0.22);
- text-shadow: none;
-}
-html[data-theme="white"] .ident-user-name::before {
- content: none;
-}
-html[data-theme="white"] .ident-title {
- font-size: clamp(24px, 6vw, 32px);
- font-weight: 800;
- letter-spacing: -0.02em;
- color: #0f172a !important;
- background: none !important;
- -webkit-background-clip: initial;
- background-clip: initial;
- -webkit-text-fill-color: initial;
- text-shadow: none;
- filter: none;
- line-height: 1.05;
- white-space: nowrap;
-}
-/* Subtitle "Agente conversacional" below Vocero (theme=white only) */
-html[data-theme="white"] .ident-subtitle {
- margin: 0;
- font-size: clamp(12px, 2.6vw, 14px);
- color: rgba(15, 23, 42, 0.6);
- font-weight: 500;
- letter-spacing: -0.005em;
- display: block;
- line-height: 1.3;
- white-space: nowrap;
-}
-html[data-theme="white"] .topbar-right {
- /* debug-only controls (tools/settings) hidden; keep them out of grid flow */
- display: none;
-}
-
-/* ── Stage: tighten room (purple orb + arc hidden by data-theme-only=dark) */
-html[data-theme="white"] .stage {
- padding: 10px 16px 6px;
- gap: 12px;
- min-height: 0;
-}
-/* Safety: hide orb wrapper even if data-theme-only=dark fails for any reason */
-html[data-theme="white"] .orb-wrap,
-html[data-theme="white"] #mic-gate,
-html[data-theme="white"] #stop-btn {
- display: none !important;
-}
-html[data-theme="white"] #circle-caption {
- display: none !important;
-}
-html[data-theme="white"] #circle-subcaption {
- display: none !important;
-}
-
-/* ── White mic stage (116px blue circle, waveform banks, caption, stop pill) ── */
-html[data-theme="white"] .white-mic-stage {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 16px;
- padding: 22px 0 6px;
- position: relative;
- box-sizing: border-box;
-}
-/* Row: waveform-left · mic-circle · waveform-right (absolute centered row) */
-html[data-theme="white"] .white-mic-circle {
- position: relative;
- z-index: 2;
- width: 116px;
- height: 116px;
- border-radius: 999px;
- border: none;
- padding: 0;
- cursor: pointer;
- background: #ffffff;
- color: #ffffff;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- overflow: visible;
- flex: none;
- transition:
- transform 180ms cubic-bezier(0.2, 0.7, 0.2, 1),
- box-shadow 180ms ease;
-}
-/* Concentric layers of the blue mic button */
-html[data-theme="white"] .white-mic-circle .wmc-halo {
- position: absolute;
- inset: -22px;
- border-radius: 999px;
- background: radial-gradient(closest-side, rgba(59, 130, 246, 0.22), rgba(59, 130, 246, 0.1) 55%, transparent 75%);
- z-index: 0;
- pointer-events: none;
- animation: wmcHaloPulse 3.2s ease-in-out infinite alternate;
-}
-@keyframes wmcHaloPulse {
- 0% {
- transform: scale(0.98);
- opacity: 0.75;
- }
- 100% {
- transform: scale(1.08);
- opacity: 1;
- }
-}
-html[data-theme="white"] .white-mic-circle .wmc-ring {
- position: absolute;
- inset: 0;
- border-radius: 999px;
- padding: 4px;
- background: linear-gradient(135deg, #2563eb 0%, #3b82f6 45%, #60a5fa 100%);
- z-index: 0;
- pointer-events: none;
- box-sizing: border-box;
- display: block;
-}
-/* The ring paints a thin blue 4-px border around a transparent inner circle
- (NOT a full white disc that would occlude the solid blue core behind it). */
-html[data-theme="white"] .white-mic-circle .wmc-ring::before {
- content: "";
- display: block;
- width: 100%;
- height: 100%;
- border-radius: 999px;
- background: transparent;
-}
-/* Solid blue disc filling everything inside the 4-px ring. This sits BEHIND
- the white mic icon so the button looks like a solid blue circle with a
- thin gradient light-blue border — exactly the screenshot look. */
-html[data-theme="white"] .white-mic-circle .wmc-core {
- position: absolute;
- inset: 4px; /* sit flush inside the 4-px wmc-ring padding */
- border-radius: 999px;
- background: linear-gradient(135deg, #2563eb 0%, #3b82f6 55%, #60a5fa 100%);
- box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.35),
- inset 0 -3px 6px rgba(15, 23, 42, 0.12),
- 0 8px 22px rgba(37, 99, 235, 0.22);
- z-index: 1;
- pointer-events: none;
-}
-/* Icon layer: WHITE on top of the solid blue disc, never occluded. */
-html[data-theme="white"] .white-mic-circle .wmc-icon {
- position: relative;
- z-index: 5; /* above halo / ring / core */
- color: #ffffff !important;
- fill: #ffffff;
- stroke: #ffffff;
- width: 48px;
- height: 48px;
- flex: none;
- display: none;
- filter: drop-shadow(0 2px 4px rgba(15, 23, 42, 0.14));
-}
-/* Force visibility of the mic SVG in idle / your-turn / error states — the
- most common customer-reported missing-icon bug comes from a stale state
- where `state-idle` exists but `.wmc-mic` still has `display:none`. The
- rules below are written with high specificity and !important on the
- display property so they cannot be overridden by accidental resets. */
-html[data-theme="white"] .white-mic-circle.state-idle > svg.wmc-icon.wmc-mic,
-html[data-theme="white"] .white-mic-circle.state-idle .wmc-icon.wmc-mic,
-html[data-theme="white"] .white-mic-circle.state-listening .wmc-icon.wmc-mic,
-html[data-theme="white"] .white-mic-circle.state-user-speaking .wmc-icon.wmc-mic,
-html[data-theme="white"] .white-mic-circle.state-ai-speaking .wmc-icon.wmc-mic {
- display: inline-flex !important;
-}
-html[data-theme="white"] .white-mic-circle.state-your-turn > svg.wmc-icon.wmc-mic,
-html[data-theme="white"] .white-mic-circle.state-your-turn .wmc-icon.wmc-mic {
- display: inline-flex !important;
-}
-html[data-theme="white"] .white-mic-circle.state-error > svg.wmc-icon.wmc-mic,
-html[data-theme="white"] .white-mic-circle.state-error .wmc-icon.wmc-mic {
- display: inline-flex !important;
-}
-html[data-theme="white"] .white-mic-circle.state-processing > .wmc-icon.wmc-spinner,
-html[data-theme="white"] .white-mic-circle.state-processing .wmc-icon.wmc-spinner,
-html[data-theme="white"] .white-mic-circle.state-connecting > .wmc-icon.wmc-spinner,
-html[data-theme="white"] .white-mic-circle.state-connecting .wmc-icon.wmc-spinner,
-html[data-theme="white"] .white-mic-circle.state-queued > .wmc-icon.wmc-spinner,
-html[data-theme="white"] .white-mic-circle.state-queued .wmc-icon.wmc-spinner {
- display: block !important;
- width: 40px !important;
- height: 40px !important;
- border-radius: 999px;
- border: 4px solid rgba(255, 255, 255, 0.22) !important;
- border-top-color: #ffffff !important;
- animation: voice-spinner-spin 850ms linear infinite;
- margin: 4px !important;
- padding: 0 !important;
- background: transparent !important;
- box-sizing: border-box;
-}
-html[data-theme="white"] .white-mic-circle:disabled {
- cursor: default;
-}
-html[data-theme="white"] .white-mic-circle:focus-visible,
-html[data-theme="white"] .white-mute-btn:focus-visible,
-html[data-theme="white"] .white-stop-btn:focus-visible {
- outline: 3px solid #1d4ed8;
- outline-offset: 4px;
-}
-/* Subtle ring pulse while listening — uses box-shadow because the ring
- itself is a hard 4-px painted border and transform:scale() would also
- scale the icon. */
-html[data-theme="white"] .white-mic-circle.state-your-turn .wmc-ring {
- animation: wmcListeningPulse 1.6s ease-in-out infinite;
-}
-@keyframes wmcListeningPulse {
- 0%,
- 100% {
- box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
- }
- 50% {
- box-shadow: 0 0 0 18px rgba(59, 130, 246, 0);
- }
-}
-
-/* ── Waveform banks (7 bars each side) ──────────────────────────────── */
-html[data-theme="white"] .white-waveform {
- position: absolute;
- top: calc(22px + 116px / 2);
- transform: translateY(-50%);
- display: flex;
- align-items: center;
- gap: 7px;
- height: 70px;
- pointer-events: none;
- z-index: 1;
-}
-html[data-theme="white"] .white-waveform-left {
- left: calc(50% - 58px - 90px);
-}
-html[data-theme="white"] .white-waveform-right {
- right: calc(50% - 58px - 90px);
-}
-html[data-theme="white"] .white-waveform .w-bar {
- display: block;
- width: 6px;
- border-radius: 999px;
- background: linear-gradient(180deg, #3b82f6, #60a5fa);
- opacity: 0.8;
- transform-origin: center;
- height: calc(var(--bar-h, 0.5) * 70px);
- min-height: 8px;
-}
-html[data-theme="white"] .white-mic-circle.state-your-turn ~ * .w-bar,
-html[data-theme="white"] .white-mic-stage:has(.state-your-turn) .w-bar {
- animation: wBarDance 900ms ease-in-out infinite alternate;
-}
-@keyframes wBarDance {
- from {
- transform: scaleY(0.55);
- }
- to {
- transform: scaleY(1.05);
- }
-}
-html[data-theme="white"] .white-waveform .wb-1 {
- animation-delay: -0.6s;
-}
-html[data-theme="white"] .white-waveform .wb-2 {
- animation-delay: -0.5s;
-}
-html[data-theme="white"] .white-waveform .wb-3 {
- animation-delay: -0.4s;
-}
-html[data-theme="white"] .white-waveform .wb-4 {
- animation-delay: -0.3s;
-}
-html[data-theme="white"] .white-waveform .wb-5 {
- animation-delay: -0.2s;
-}
-html[data-theme="white"] .white-waveform .wb-6 {
- animation-delay: -0.1s;
-}
-html[data-theme="white"] .white-waveform .wb-7 {
- animation-delay: 0s;
-}
-html[data-theme="white"] .white-waveform-right .wb-1 {
- animation-delay: -0s;
-}
-html[data-theme="white"] .white-waveform-right .wb-2 {
- animation-delay: -0.1s;
-}
-html[data-theme="white"] .white-waveform-right .wb-3 {
- animation-delay: -0.2s;
-}
-html[data-theme="white"] .white-waveform-right .wb-4 {
- animation-delay: -0.3s;
-}
-html[data-theme="white"] .white-waveform-right .wb-5 {
- animation-delay: -0.4s;
-}
-html[data-theme="white"] .white-waveform-right .wb-6 {
- animation-delay: -0.5s;
-}
-html[data-theme="white"] .white-waveform-right .wb-7 {
- animation-delay: -0.6s;
-}
-
-/* ── Mic caption "Escuchando... / Hablando..." ───────────────────────── */
-html[data-theme="white"] .white-mic-caption {
- margin: 4px 0 0;
- color: #2563eb;
- font-size: clamp(17px, 4.2vw, 20px);
- font-weight: 600;
- letter-spacing: -0.005em;
- text-align: center;
- line-height: 1.25;
-}
-html[data-theme="white"] .white-mic-caption.muted {
- color: #64748b;
-}
-html[data-theme="white"] .white-mic-caption.error {
- color: #dc2626;
-}
-html[data-theme="white"] .white-mic-subcaption {
- margin: 0;
- max-width: 440px;
- color: #475569;
- font-size: 13px;
- line-height: 1.5;
- text-align: center;
-}
-
-/* ── Active conversation controls ──────────────────────────────────── */
-html[data-theme="white"] .white-session-controls {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- gap: 10px;
- max-width: 100%;
-}
-html[data-theme="white"] .white-mute-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- min-height: 44px;
- padding: 10px 16px;
- border: 1px solid #cbd5e1;
- border-radius: 999px;
- background: #ffffff;
- color: #1d4ed8;
- font: inherit;
- font-size: clamp(14px, 3.4vw, 16px);
- font-weight: 600;
- cursor: pointer;
- transition:
- background 160ms ease,
- border-color 160ms ease;
-}
-html[data-theme="white"] .white-mute-btn:hover:not(:disabled) {
- background: #eff6ff;
- border-color: #93c5fd;
-}
-html[data-theme="white"] .white-mute-btn.muted {
- background: #fffbeb;
- border-color: #fcd34d;
- color: #92400e;
-}
-html[data-theme="white"] .white-mute-btn.muted:hover:not(:disabled) {
- background: #fef3c7;
-}
-html[data-theme="white"] .white-mute-btn:disabled {
- cursor: default;
-}
-html[data-theme="white"] .white-mute-btn svg {
- flex: none;
- width: 18px;
- height: 18px;
-}
-html[data-theme="white"] .white-mute-btn .mic-off,
-html[data-theme="white"] .white-mute-btn.muted .mic-on {
- display: none;
-}
-html[data-theme="white"] .white-mute-btn.muted .mic-off {
- display: block;
-}
-
-/* ── "Detener" pill ─────────────────────────────────────────────────── */
-html[data-theme="white"] .white-stop-btn {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- padding: 10px 22px 10px 16px;
- background: #e2e8f0;
- color: #0f172a;
- border: none;
- border-radius: 999px;
- cursor: pointer;
- font-size: clamp(14px, 3.4vw, 16px);
- font-weight: 600;
- letter-spacing: -0.005em;
- box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
- transition:
- background 160ms ease,
- transform 160ms ease;
-}
-html[data-theme="white"] .white-stop-btn:hover {
- background: #cbd5e1;
- transform: translateY(-1px);
-}
-html[data-theme="white"] .white-stop-btn svg {
- width: 18px;
- height: 18px;
- border-radius: 3px;
- color: #0f172a;
- flex: none;
-}
-html[data-theme="white"] .chat-panel-header h2 {
- margin: 0;
- color: #0f172a !important; /* slate-900 flat, no brand gradient */
- background: none !important;
- -webkit-background-clip: initial !important;
- background-clip: initial !important;
- -webkit-text-fill-color: initial !important;
- text-fill-color: initial !important;
- text-shadow: none;
- filter: none;
- opacity: 1;
- font-size: clamp(18px, 4.3vw, 22px);
- font-weight: 700 !important;
- letter-spacing: -0.01em !important;
- text-transform: none !important; /* TitleCase, no ALLCAPS */
- text-decoration: none !important; /* kill the underline */
- border-bottom: none !important;
- box-shadow: none !important;
-}
-/* Session status uses connection events rather than a permanent online badge. */
-html[data-theme="white"] .chat-status-pill {
- display: inline-flex;
- align-items: center;
- gap: 7px;
- padding: 5px 11px 5px 8px;
- background: #f1f5f9;
- border-radius: 999px;
- color: #475569;
- font-size: 12.5px;
- font-weight: 600;
- flex: none;
-}
-html[data-theme="white"] .chat-status-pill[data-status="online"] {
- background: #ecfdf5;
- color: #15803d;
-}
-html[data-theme="white"] .chat-status-pill[data-status="waiting"] {
- background: #fffbeb;
- color: #92400e;
-}
-html[data-theme="white"] .chat-status-pill[data-status="error"] {
- background: #fef2f2;
- color: #b91c1c;
-}
-html[data-theme="white"] .chat-status-dot {
- width: 9px;
- height: 9px;
- border-radius: 999px;
- background: currentColor;
- display: block;
- flex: none;
-}
-
-/* ── Chat header actions row: online status pill + conversation export button.
- Added 2026-09-23. Horizontal, right-aligned layout with wrapping and gaps
- to avoid overflow on narrow screens. */
-.chat-header-actions {
- display: inline-flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
- flex: 0 0 auto;
- flex-wrap: wrap;
- min-width: 0;
-}
-/* Conversation export button, consistent with other .icon-btn controls. */
-.chat-export-btn {
- position: relative;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 32px;
- padding: 0;
- border-radius: 999px;
- border: 1px solid rgba(255, 255, 255, 0.1);
- background: rgba(255, 255, 255, 0.06);
- color: rgba(255, 255, 255, 0.88);
- cursor: pointer;
- transition:
- transform 0.12s ease,
- background 0.16s ease,
- color 0.16s ease,
- border-color 0.16s ease,
- box-shadow 0.16s ease;
- flex: 0 0 auto;
- overflow: visible;
- -webkit-tap-highlight-color: transparent;
-}
-.chat-export-btn:focus-visible {
- outline: 2px solid var(--brand, #6d63ff);
- outline-offset: 2px;
-}
-.chat-export-btn:hover {
- background: rgba(255, 255, 255, 0.12);
- border-color: rgba(255, 255, 255, 0.18);
- transform: translateY(-0.5px);
-}
-.chat-export-btn:active {
- transform: translateY(0);
- background: rgba(255, 255, 255, 0.08);
-}
-.chat-export-btn svg {
- display: block;
- pointer-events: none;
- flex: 0 0 auto;
-}
-/* Successful export feedback: temporarily green. */
-.chat-export-btn.chat-export-ok {
- background: rgba(22, 163, 74, 0.18) !important;
- border-color: rgba(22, 163, 74, 0.34) !important;
- color: #22c55e !important;
- box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.12);
-}
-/* Empty export feedback: amber pulse when there are no messages. */
-.chat-export-btn.chat-export-empty {
- background: rgba(245, 158, 11, 0.18) !important;
- border-color: rgba(245, 158, 11, 0.34) !important;
- color: #f59e0b !important;
- animation: chatExportEmptyShake 0.42s ease-in-out;
-}
-@keyframes chatExportEmptyShake {
- 0%,
- 100% {
- transform: translateX(0);
- }
- 20% {
- transform: translateX(-2px);
- }
- 40% {
- transform: translateX(2px);
- }
- 60% {
- transform: translateX(-1.5px);
- }
- 80% {
- transform: translateX(1.5px);
- }
-}
-
-/* White-theme variants of the above (matches screenshot chat header). */
-html[data-theme="white"] .chat-header-actions {
- gap: 10px;
-}
-html[data-theme="white"] .chat-export-btn {
- border-color: rgba(15, 23, 42, 0.08);
- background: rgba(15, 23, 42, 0.03);
- color: #0f172a;
-}
-html[data-theme="white"] .chat-export-btn:hover {
- background: rgba(59, 130, 246, 0.08);
- border-color: rgba(59, 130, 246, 0.22);
- color: #1d4ed8;
-}
-html[data-theme="white"] .chat-export-btn:active {
- background: rgba(59, 130, 246, 0.12);
-}
-html[data-theme="white"] .chat-export-btn.chat-export-ok {
- background: rgba(22, 163, 74, 0.12) !important;
- border-color: rgba(22, 163, 74, 0.24) !important;
- color: #15803d !important;
-}
-html[data-theme="white"] .chat-export-btn.chat-export-empty {
- background: rgba(245, 158, 11, 0.12) !important;
- border-color: rgba(245, 158, 11, 0.24) !important;
- color: #b45309 !important;
-}
-
-/* Light transcript appearance; layout and semantic metadata live in layout.css. */
-html[data-theme="white"] .chat-history .hist-msg .hist-body {
- padding: 12px 15px;
- border-radius: 18px;
- font-size: clamp(14.5px, 3.3vw, 16.5px);
- line-height: 1.45;
- letter-spacing: -0.005em;
- color: #0f172a;
- background: #ffffff;
- border: 1px solid transparent;
- box-shadow: none;
- backdrop-filter: none;
- -webkit-backdrop-filter: none;
- word-break: break-word;
- white-space: pre-wrap;
-}
-html[data-theme="white"] .chat-history .hist-msg.assistant .hist-body {
- background: #f3f4f6;
- border-bottom-left-radius: 6px;
-}
-html[data-theme="white"] .chat-history .hist-msg.user .hist-body {
- background: #dbeafe;
- border-bottom-right-radius: 6px;
-}
-/* Partial-streaming bubble (growing AI response) = same shape, slightly lower opacity */
-html[data-theme="white"] .chat-history .hist-body.partial,
-html[data-theme="white"] .bubble-body.partial {
- opacity: 0.92;
-}
-
-/* Decorative avatars accompany the accessible message author text. */
-html[data-theme="white"] .chat-history .hist-msg::before {
- position: absolute;
- top: 22px;
- width: 36px;
- height: 36px;
- border-radius: 999px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: #ffffff;
- font-size: 14px;
- font-weight: 700;
- letter-spacing: -0.01em;
- z-index: 2;
- content: "";
- box-shadow: 0 2px 8px rgba(15, 23, 42, 0.1);
-}
-html[data-theme="white"] .chat-history .hist-msg.assistant::before {
- left: -40px;
- background-color: #2563eb;
- background-image: url("data:image/svg+xml;utf8, ");
- background-repeat: no-repeat;
- background-position: center center;
- background-size: 20px 20px;
- border: 1px solid rgba(59, 130, 246, 0.25);
-}
-html[data-theme="white"] .chat-history .hist-msg.user::before {
- right: -40px;
- background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
- border: 1px solid rgba(96, 165, 250, 0.35);
- content: attr(data-user-initials);
- align-items: center;
- justify-content: center;
-}
-/* Tool rows have no speaker avatar. */
-html[data-theme="white"] .chat-history .hist-msg.tool::before {
- display: none;
- content: none;
-}
-html[data-theme="white"] .app-footer::before {
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(37, 99, 235, 0.28) 30%,
- rgba(59, 130, 246, 0.28) 70%,
- transparent
- );
- opacity: 0.6;
-}
-html[data-theme="white"] .app-footer-text {
- color: rgba(15, 23, 42, 0.6);
- background: none;
- -webkit-text-fill-color: inherit;
- -webkit-background-clip: initial;
- background-clip: initial;
- filter: none;
- letter-spacing: 0.12em;
- font-weight: 600;
-}
-
-/* ── Modals: light theme restyle to match the white palette ─────────── */
-html[data-theme="white"] .modal-content {
- background: #ffffff;
- color: #0f172a;
- border: 1px solid rgba(15, 23, 42, 0.08);
- box-shadow: 0 30px 80px rgba(15, 23, 42, 0.18);
-}
-html[data-theme="white"] .modal-header h2 {
- color: #0f172a;
-}
-html[data-theme="white"] .modal-header {
- border-bottom-color: rgba(15, 23, 42, 0.08);
-}
-html[data-theme="white"] .modal-footer {
- border-top-color: rgba(15, 23, 42, 0.08);
-}
-html[data-theme="white"] .modal input[type="text"],
-html[data-theme="white"] .modal input[type="password"],
-html[data-theme="white"] .modal textarea,
-html[data-theme="white"] .modal select {
- background: #f8fafc;
- border-color: rgba(15, 23, 42, 0.1);
- color: #0f172a;
-}
-html[data-theme="white"] .modal input[type="text"]:focus,
-html[data-theme="white"] .modal input[type="password"]:focus,
-html[data-theme="white"] .modal textarea:focus,
-html[data-theme="white"] .modal select:focus {
- border-color: #3b82f6;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
-}
-html[data-theme="white"] .btn.primary {
- background: linear-gradient(135deg, #2563eb, #3b82f6);
- color: #ffffff;
- border-color: transparent;
-}
-html[data-theme="white"] .btn.primary:disabled {
- opacity: 0.6;
- filter: grayscale(0%);
-}
-html[data-theme="white"] .btn.secondary {
- background: #f1f5f9;
- color: #0f172a;
- border-color: rgba(15, 23, 42, 0.1);
-}
-html[data-theme="white"] .btn.secondary:hover {
- background: #e2e8f0;
-}
-
-/* ── User profile modal ─────────────────────────────────────────────── */
-html[data-theme="white"] .user-profile-card {
- background: #ffffff;
-}
-html[data-theme="white"] .user-profile-avatar {
- background: linear-gradient(135deg, #2563eb, #60a5fa);
- color: #ffffff;
- border-color: rgba(37, 99, 235, 0.2);
-}
-html[data-theme="white"] .user-profile-title {
- color: #0f172a;
-}
-html[data-theme="white"] .user-profile-subtitle {
- color: rgba(15, 23, 42, 0.65);
-}
-
-/* ── Tools modal switches ───────────────────────────────────────────── */
-html[data-theme="white"] .switch-track {
- background: #cbd5e1;
-}
-html[data-theme="white"] .switch input:checked + .switch-track {
- background: #2563eb;
-}
-
-/* ── Mobile: tighten when room is short ─────────────────────────────── */
-@media (max-width: 480px) {
- html[data-theme="white"] .topbar {
- padding: 14px 14px 4px;
- }
- html[data-theme="white"] .white-waveform-left {
- left: calc(50% - 58px - 72px);
- }
- html[data-theme="white"] .white-waveform-right {
- right: calc(50% - 58px - 72px);
- }
- html[data-theme="white"] .white-waveform {
- gap: 5px;
- }
- html[data-theme="white"] .white-waveform .w-bar {
- width: 5px;
- }
-}
-@media (max-height: 640px) {
- html[data-theme="white"] .white-mic-stage {
- padding: 14px 0 4px;
- }
- html[data-theme="white"] .white-mic-circle {
- width: 98px;
- height: 98px;
- }
- html[data-theme="white"] .white-mic-circle .wmc-icon {
- width: 40px;
- height: 40px;
- }
- html[data-theme="white"] .white-mic-circle.state-processing .wmc-spinner,
- html[data-theme="white"] .white-mic-circle.state-connecting .wmc-spinner,
- html[data-theme="white"] .white-mic-circle.state-queued .wmc-spinner {
- width: 34px;
- height: 34px;
- }
- html[data-theme="white"] .white-waveform .w-bar {
- height: calc(var(--bar-h, 0.5) * 56px);
- }
- html[data-theme="white"] .white-waveform {
- height: 56px;
- }
- html[data-theme="white"] .white-waveform-left {
- left: calc(50% - 49px - 76px);
- top: calc(14px + 49px);
- }
- html[data-theme="white"] .white-waveform-right {
- right: calc(50% - 49px - 76px);
- top: calc(14px + 49px);
- }
-}
diff --git a/vocero-frontend/src/styles/markdown.css b/vocero-frontend/src/styles/markdown.css
new file mode 100644
index 0000000..b548746
--- /dev/null
+++ b/vocero-frontend/src/styles/markdown.css
@@ -0,0 +1,123 @@
+/* Scoped styles for sanitized HTML produced by Marked; model content never supplies utility classes. */
+
+.markdown-content {
+ min-width: 0;
+ max-width: 100%;
+ box-sizing: border-box;
+ white-space: normal;
+ overflow-wrap: anywhere;
+}
+.markdown-content > :first-child {
+ margin-top: 0;
+}
+.markdown-content > :last-child {
+ margin-bottom: 0;
+}
+.markdown-content p {
+ margin: 0 0 0.75em;
+}
+.markdown-content strong {
+ font-weight: 700;
+}
+.markdown-content :is(h1, h2, h3, h4, h5, h6) {
+ margin: 1em 0 0.5em;
+ font-size: 1.12em;
+ font-weight: 700;
+ line-height: 1.35;
+}
+.markdown-content :is(ul, ol) {
+ margin: 0.65em 0;
+ padding-inline-start: 1.5em;
+}
+.markdown-content ul {
+ list-style-type: disc;
+}
+.markdown-content ol {
+ list-style-type: decimal;
+}
+.markdown-content li + li {
+ margin-top: 0.35em;
+}
+.markdown-content li > p {
+ margin: 0.25em 0;
+}
+.markdown-content li :is(ul, ol) {
+ margin: 0.3em 0;
+}
+.markdown-content a {
+ color: var(--brand-3);
+ text-decoration: underline;
+ text-underline-offset: 0.15em;
+}
+html[data-theme="white"] .markdown-content a {
+ color: var(--speaking);
+}
+.markdown-content :is(a, pre, .markdown-table):focus-visible {
+ outline: 2px solid var(--brand);
+ outline-offset: 3px;
+}
+.markdown-content blockquote {
+ margin: 0.75em 0;
+ padding-inline-start: 12px;
+ border-inline-start: 3px solid var(--brand);
+ color: var(--text-dim);
+}
+.markdown-content code {
+ padding: 0.15em 0.35em;
+ border-radius: 4px;
+ background: color-mix(in srgb, currentColor 8%, transparent);
+ font-family: var(--font-mono);
+ font-size: 0.9em;
+}
+.markdown-content pre {
+ max-width: 100%;
+ margin: 0.75em 0;
+ padding: 12px;
+ border: 1px solid var(--border-strong);
+ border-radius: 8px;
+ overflow: auto;
+ white-space: pre;
+}
+.markdown-content pre code {
+ display: block;
+ padding: 0;
+ background: transparent;
+ white-space: pre;
+ overflow-wrap: normal;
+ word-break: normal;
+}
+.markdown-content hr {
+ margin: 1em 0;
+ border: 0;
+ border-top: 1px solid var(--border-strong);
+}
+.markdown-content img {
+ max-width: 100%;
+ height: auto;
+}
+.markdown-content .markdown-table {
+ max-width: 100%;
+ margin: 0.75em 0;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+}
+.markdown-content table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.92em;
+}
+.markdown-content :is(th, td) {
+ padding: 7px 10px;
+ border: 1px solid var(--border-strong);
+ text-align: start;
+ vertical-align: top;
+}
+.markdown-content th {
+ font-weight: 700;
+}
+.markdown-content [align="center"] {
+ text-align: center;
+}
+.markdown-content [align="right"] {
+ text-align: end;
+}
diff --git a/vocero-frontend/src/styles/range.css b/vocero-frontend/src/styles/range.css
new file mode 100644
index 0000000..d35789b
--- /dev/null
+++ b/vocero-frontend/src/styles/range.css
@@ -0,0 +1,26 @@
+/* Native range pseudo-elements cannot share a selector across browser engines. */
+.gate-range::-webkit-slider-runnable-track {
+ height: 14px;
+ background: transparent;
+}
+.gate-range::-moz-range-track {
+ height: 14px;
+ background: transparent;
+}
+.gate-range::-webkit-slider-thumb {
+ appearance: none;
+ width: 6px;
+ height: 18px;
+ border-radius: 3px;
+ background: var(--text);
+ border: 2px solid var(--bg-elev);
+ box-shadow: 0 0 0 1px var(--border-strong);
+}
+.gate-range::-moz-range-thumb {
+ width: 6px;
+ height: 18px;
+ border-radius: 3px;
+ background: var(--text);
+ border: 2px solid var(--bg-elev);
+ box-shadow: 0 0 0 1px var(--border-strong);
+}
diff --git a/vocero-frontend/src/styles/theme.css b/vocero-frontend/src/styles/theme.css
index 2486f79..e532c87 100644
--- a/vocero-frontend/src/styles/theme.css
+++ b/vocero-frontend/src/styles/theme.css
@@ -26,9 +26,9 @@
--color-voice-user: var(--voice-user);
--color-voice-assistant: var(--voice-assistant);
--color-voice-tool: var(--voice-tool);
- --radius-control: var(--radius-sm);
- --radius-panel: var(--radius-md);
- --radius-card: var(--radius-lg);
+ --radius-control: var(--app-radius-control);
+ --radius-panel: var(--app-radius-panel);
+ --radius-card: var(--app-radius-card);
--shadow-panel: var(--shadow-soft);
--shadow-brand: var(--shadow-glow-brand);
--shadow-card-glass: var(--shadow-glass);
@@ -73,17 +73,10 @@
--voice-user: var(--accent-2);
--voice-assistant: var(--accent);
--voice-tool: var(--processing);
- --audio-level: 0;
- --bar0: 0;
- --bar1: 0;
- --bar2: 0;
- --bar3: 0;
- --bar4: 0;
- --radius-sm: 10px;
- --radius-md: 16px;
- --radius-lg: 26px;
- --radius-xl: 34px;
+ --app-radius-control: 10px;
+ --app-radius-panel: 16px;
+ --app-radius-card: 26px;
--shadow-soft: 0 10px 40px rgba(0, 0, 0, 0.35);
--shadow-glow-brand:
@@ -161,9 +154,8 @@
--message-user-shadow: none;
--message-assistant-shadow: none;
- --radius-sm: 10px;
- --radius-md: 16px;
- --radius-lg: 22px;
- --radius-xl: 28px;
+ --app-radius-control: 10px;
+ --app-radius-panel: 16px;
+ --app-radius-card: 22px;
}
}
diff --git a/vocero-frontend/src/styles/voice-stage.css b/vocero-frontend/src/styles/voice-stage.css
new file mode 100644
index 0000000..9d48abc
--- /dev/null
+++ b/vocero-frontend/src/styles/voice-stage.css
@@ -0,0 +1,986 @@
+/* Audio-reactive artwork. Runtime CSS variables are written by the audio engine, outside React. */
+:root {
+ --audio-level: 0;
+ --ai-audio-level: 0;
+ --bar0: 0;
+ --bar1: 0;
+ --bar2: 0;
+ --bar3: 0;
+ --bar4: 0;
+}
+
+.stage::before {
+ content: "";
+ position: absolute;
+ top: 46%;
+ left: 50%;
+ width: clamp(260px, 58vw, 620px);
+ height: clamp(260px, 58vw, 620px);
+ transform: translate(-50%, -50%);
+ background: radial-gradient(
+ circle,
+ color-mix(in srgb, var(--brand) 22%, transparent) 0%,
+ color-mix(in srgb, var(--brand-2) 12%, transparent) 45%,
+ transparent 75%
+ );
+ filter: blur(12px);
+ opacity: 0.65;
+ pointer-events: none;
+ border-radius: 999px;
+ animation: pedestalBreathe 8s ease-in-out infinite alternate;
+}
+
+.circle {
+ position: relative;
+ width: clamp(120px, min(38svw, 42svh), 300px);
+ aspect-ratio: 1 / 1;
+ border-radius: 50%;
+ border: none;
+ background: transparent;
+ padding: 0;
+ cursor: pointer;
+ outline: none;
+ display: grid;
+ place-items: center;
+ color: var(--glow, var(--accent));
+ transition:
+ transform 0.18s ease,
+ filter 0.18s ease;
+ -webkit-tap-highlight-color: transparent;
+ touch-action: manipulation;
+}
+.circle::before {
+ content: "";
+ position: absolute;
+ inset: -8%;
+ border-radius: 50%;
+ background: conic-gradient(
+ from 0deg,
+ color-mix(in srgb, var(--brand) 65%, transparent) 0%,
+ color-mix(in srgb, var(--brand-2) 55%, transparent) 35%,
+ color-mix(in srgb, var(--brand-3) 55%, transparent) 65%,
+ color-mix(in srgb, var(--brand) 65%, transparent) 100%
+ );
+ filter: blur(14px);
+ opacity: 0.55;
+ z-index: 0;
+ pointer-events: none;
+ animation: orbConicSpin 18s linear infinite;
+}
+.circle:hover {
+ filter: brightness(1.08);
+}
+.circle:hover::before {
+ opacity: 0.8;
+}
+.circle:active {
+ transform: scale(0.97);
+ filter: brightness(0.92);
+}
+.circle:focus-visible .circle-core {
+ outline: 2px solid var(--glow, var(--accent));
+ outline-offset: 6px;
+}
+.circle[disabled] {
+ cursor: default;
+ opacity: 0.75;
+}
+
+.circle-glow {
+ position: absolute;
+ inset: -8%;
+ border-radius: 50%;
+ background: radial-gradient(circle at center, var(--glow, var(--accent)) 0%, transparent 60%);
+ filter: blur(34px);
+ opacity: 0.55;
+ transform: scale(1);
+ transition:
+ opacity 0.25s,
+ background 0.25s,
+ transform 1.4s ease-in-out;
+ pointer-events: none;
+ z-index: 0;
+}
+.circle.state-idle .circle-glow,
+.circle.state-idle::before {
+ animation:
+ orbConicSpin 22s linear infinite,
+ orbBreatheGlow 5s ease-in-out infinite alternate;
+}
+.circle-ring,
+.circle-ring-outer {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ transition:
+ opacity 0.4s ease,
+ border-color 0.4s ease,
+ transform 0.3s ease;
+ z-index: 1;
+}
+.circle-ring {
+ width: 86%;
+ height: 86%;
+ border: 1.5px solid transparent;
+ background:
+ linear-gradient(var(--bg, #07080d), var(--bg, #07080d)) padding-box,
+ conic-gradient(
+ from 0deg,
+ color-mix(in srgb, var(--brand) 80%, transparent),
+ color-mix(in srgb, var(--brand-2) 70%, transparent) 40%,
+ color-mix(in srgb, var(--brand-3) 75%, transparent) 70%,
+ color-mix(in srgb, var(--brand) 80%, transparent)
+ )
+ border-box;
+ border-color: transparent;
+ mask:
+ linear-gradient(#000, #000) content-box,
+ linear-gradient(#000, #000);
+ -webkit-mask:
+ linear-gradient(#000, #000) content-box,
+ linear-gradient(#000, #000);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ box-shadow: 0 0 20px -6px color-mix(in srgb, var(--brand) 55%, transparent);
+ opacity: 0.75;
+}
+.circle-ring-outer {
+ width: 98%;
+ height: 98%;
+ border: 1px dashed color-mix(in srgb, var(--glow, var(--accent)) 30%, transparent);
+ opacity: 0.55;
+}
+.circle.state-idle .circle-ring-outer {
+ animation: outerRingPulse 6s ease-in-out infinite;
+}
+
+.circle-core {
+ position: relative;
+ width: 72%;
+ height: 72%;
+ border-radius: 50%;
+ background:
+ radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.04) 20%, transparent 45%),
+ radial-gradient(
+ circle at 35% 28%,
+ color-mix(in srgb, var(--glow, var(--accent)) 32%, transparent),
+ color-mix(in srgb, var(--glow, var(--accent)) 12%, transparent) 55%,
+ color-mix(in srgb, var(--glow, var(--accent)) 5%, transparent)
+ );
+ border: 1.5px solid color-mix(in srgb, var(--glow, var(--accent)) 48%, transparent);
+ display: grid;
+ place-items: center;
+ box-shadow:
+ 0 0 44px -8px color-mix(in srgb, var(--glow, var(--accent)) 42%, transparent),
+ inset 0 0 34px color-mix(in srgb, var(--glow, var(--accent)) 22%, transparent),
+ inset 0 1px 0 rgba(255, 255, 255, 0.12);
+ transition:
+ background 0.4s ease,
+ border-color 0.4s ease,
+ box-shadow 0.4s ease,
+ transform 0.4s ease;
+ z-index: 2;
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+}
+.circle.state-idle .circle-core {
+ animation: coreBreathe 5s ease-in-out infinite alternate;
+}
+.circle-indicator {
+ position: relative;
+ width: 44%;
+ height: 44%;
+ display: grid;
+ place-items: center;
+ color: var(--glow, var(--accent));
+}
+.circle-indicator > .ind {
+ grid-area: 1 / 1;
+ opacity: 0;
+ transform: scale(0.85);
+ transition:
+ opacity 0.25s ease,
+ transform 0.25s ease;
+ pointer-events: none;
+}
+.circle-indicator > svg.ind {
+ width: 60%;
+ height: 60%;
+}
+.ind-spinner {
+ width: 48%;
+ height: 48%;
+ border: 3px solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ opacity: 0;
+ animation: ind-spin 0.9s linear infinite;
+}
+.ind-thinking {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 60%;
+ height: 60%;
+}
+.ind-thinking .dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: currentColor;
+ opacity: 0.3;
+ animation: thinking-dot 1.25s ease-in-out infinite;
+}
+.ind-thinking .dot:nth-child(1) {
+ animation-delay: 0s;
+}
+.ind-thinking .dot:nth-child(2) {
+ animation-delay: 0.16s;
+}
+.ind-thinking .dot:nth-child(3) {
+ animation-delay: 0.32s;
+}
+.ind-bars {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ height: 42%;
+}
+.ind-bars .bar {
+ width: 4px;
+ min-height: 4px;
+ border-radius: 3px;
+ background: currentColor;
+ opacity: 0.7;
+ --h: var(--bar0, 0);
+ height: calc(4px + var(--h) * 36px);
+ transition:
+ height 0.08s ease-out,
+ opacity 0.08s ease-out;
+}
+.ind-bars .bar:nth-child(1) {
+ --h: var(--bar0);
+}
+.ind-bars .bar:nth-child(2) {
+ --h: var(--bar1);
+}
+.ind-bars .bar:nth-child(3) {
+ --h: var(--bar2);
+}
+.ind-bars .bar:nth-child(4) {
+ --h: var(--bar3);
+}
+.ind-bars .bar:nth-child(5) {
+ --h: var(--bar4);
+}
+.ind-bars .bar {
+ opacity: calc(0.55 + 0.45 * var(--h));
+}
+.state-connecting .ind-spinner,
+.state-queued .ind-spinner,
+.state-processing .ind-thinking,
+.state-listening .ind-bars,
+.state-user-speaking .ind-bars,
+.state-ai-speaking .ind-voice,
+.state-error .ind-error {
+ opacity: 1;
+ transform: scale(1);
+}
+.ind-mic {
+ color: color-mix(in srgb, var(--glow, var(--accent)) 85%, white);
+}
+.circle-caption {
+ margin: 0;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.24em;
+ text-transform: uppercase;
+ color: var(--text-faint);
+ min-height: 1.3em;
+ text-align: center;
+ opacity: 0.78;
+ transition:
+ opacity 0.25s ease,
+ color 0.25s ease,
+ transform 0.25s ease,
+ min-height 0.25s ease,
+ background 0.3s ease,
+ filter 0.3s ease;
+ line-height: 1.2;
+}
+.circle-caption:empty::before {
+ content: "\00a0";
+}
+.stage:has(.circle.state-idle) .circle-caption {
+ background: var(--brand-gradient);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ color: transparent;
+ opacity: 1;
+ filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--brand) 40%, transparent));
+ animation: idleCaptionBob 3.6s ease-in-out infinite alternate;
+}
+.circle-subcaption {
+ margin: 8px 0 0;
+ max-width: 32ch;
+ font-size: 13.5px;
+ line-height: 1.5;
+ color: var(--text-dim);
+ text-align: center;
+ text-wrap: balance;
+ transition: opacity 0.25s ease;
+}
+.mic-gate {
+ position: relative;
+ flex: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.mic-gate-arc {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 80px;
+ height: 80px;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.25s ease;
+}
+.orb-wrap.live .mic-gate-arc {
+ opacity: 1;
+}
+.mga-track {
+ stroke: rgba(255, 255, 255, 0.1);
+ stroke-width: 1.5;
+ stroke-linecap: round;
+}
+.mga-fill {
+ stroke: var(--accent-2);
+ stroke-width: 2;
+ stroke-linecap: round;
+ opacity: 0.85;
+ transition: stroke-dashoffset 0.06s linear;
+}
+.mga-handle {
+ fill: var(--text);
+ stroke: none;
+ transition:
+ fill 0.2s ease,
+ filter 0.2s ease;
+}
+.mic-gate.gate-open .mga-handle {
+ fill: var(--accent-2);
+ filter: drop-shadow(0 0 3px var(--accent-2));
+}
+.mga-hit {
+ stroke: transparent;
+ stroke-width: 16;
+ pointer-events: none;
+ cursor: pointer;
+ touch-action: none;
+}
+.orb-wrap.live .mga-hit {
+ pointer-events: stroke;
+}
+
+.side-btn {
+ flex: none;
+ width: 52px;
+ height: 52px;
+ border-radius: 50%;
+ border: 1px solid var(--border-strong);
+ background: color-mix(in srgb, var(--bg-elev-2) 80%, #2a2e3c);
+ color: var(--text);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ touch-action: manipulation;
+ box-shadow:
+ 0 6px 16px rgba(0, 0, 0, 0.35),
+ inset 0 1px 0 rgba(255, 255, 255, 0.06);
+ opacity: 0;
+ transform: scale(0.55);
+ width: 0;
+ padding: 0;
+ pointer-events: none;
+ overflow: hidden;
+ transition:
+ opacity 0.25s ease,
+ transform 0.25s ease,
+ width 0.25s ease,
+ background 0.15s,
+ color 0.15s,
+ border-color 0.15s;
+}
+.side-btn:hover {
+ background: color-mix(in srgb, var(--bg-elev-2) 60%, #323746);
+ border-color: var(--text-dim);
+}
+.side-btn svg {
+ width: 22px;
+ height: 22px;
+ flex: none;
+}
+.side-btn.muted {
+ color: #fff;
+ border-color: var(--error);
+ background: color-mix(in srgb, var(--error) 70%, #1a0e13);
+}
+.orb-wrap.live .side-btn {
+ opacity: 1;
+ transform: scale(1);
+ width: 52px;
+ pointer-events: auto;
+}
+.circle.state-connecting {
+ --glow: #facc15;
+}
+.circle.state-queued {
+ --glow: #94a3b8;
+}
+.circle.state-queued .ind-spinner {
+ animation-duration: 2.4s;
+}
+.circle.state-queued .circle-ring {
+ animation: breathe 2.6s ease-in-out infinite;
+}
+.circle.state-your-turn {
+ --glow: var(--accent);
+}
+.circle.state-your-turn .circle-ring {
+ animation: breathe 1.6s ease-in-out infinite;
+}
+.circle.state-listening,
+.circle.state-user-speaking {
+ --glow: var(--listening);
+}
+.circle.state-processing {
+ --glow: var(--processing);
+}
+.circle.state-ai-speaking {
+ --glow: var(--speaking);
+}
+.circle.state-error {
+ --glow: var(--error);
+}
+.circle.state-connecting .circle-core {
+ animation: thinking 1.4s ease-in-out infinite;
+}
+.circle.state-user-speaking .circle-ring {
+ animation: none;
+ opacity: calc(0.25 + 0.55 * var(--audio-level));
+ transform: translate(-50%, -50%) scale(calc(1 + 0.08 * var(--audio-level)));
+ transition:
+ transform 0.08s linear,
+ opacity 0.08s linear;
+}
+.circle.state-user-speaking .circle-ring-outer {
+ opacity: calc(0.1 + 0.35 * var(--audio-level));
+ transform: translate(-50%, -50%) scale(calc(1 + 0.12 * var(--audio-level)));
+ transition:
+ transform 0.08s linear,
+ opacity 0.08s linear;
+}
+.circle.state-listening .circle-ring {
+ animation: breathe 2s ease-in-out infinite;
+}
+.circle.state-ai-speaking .circle-core {
+ animation: none;
+ transform: scale(calc(1 + 0.09 * var(--ai-audio-level, 0)));
+ transition: transform 0.08s linear;
+}
+.circle.state-ai-speaking .circle-ring {
+ animation: none;
+ opacity: calc(0.3 + 0.5 * var(--ai-audio-level, 0));
+ transform: translate(-50%, -50%) scale(calc(1 + 0.05 * var(--ai-audio-level, 0)));
+ transition:
+ transform 0.08s linear,
+ opacity 0.08s linear;
+}
+.circle.state-ai-speaking .circle-ring-outer {
+ animation: none;
+ opacity: calc(0.15 + 0.55 * var(--ai-audio-level, 0));
+ transform: translate(-50%, -50%) scale(calc(1 + 0.18 * var(--ai-audio-level, 0)));
+ transition:
+ transform 0.08s linear,
+ opacity 0.08s linear;
+}
+.circle.state-ai-speaking .circle-glow {
+ opacity: calc(0.35 + 0.45 * var(--ai-audio-level, 0));
+ transition: opacity 0.08s linear;
+}
+@media (max-width: 600px) {
+ .circle {
+ width: clamp(110px, min(56svw, 36svh), 220px);
+ }
+ .side-btn {
+ width: 44px;
+ height: 0;
+ transition:
+ opacity 0.25s ease,
+ transform 0.25s ease,
+ height 0.25s ease,
+ background 0.15s,
+ color 0.15s,
+ border-color 0.15s;
+ }
+ .side-btn svg {
+ width: 19px;
+ height: 19px;
+ }
+ .orb-wrap.live .side-btn {
+ width: 44px;
+ height: 44px;
+ }
+ .mic-gate-arc {
+ width: 70px;
+ height: 70px;
+ }
+}
+@media (min-width: 601px) {
+ .circle-caption {
+ font-size: 12px;
+ }
+}
+@media (max-height: 780px) {
+ .circle {
+ width: clamp(130px, min(36svw, 38svh), 280px);
+ }
+}
+
+@media (max-height: 560px) {
+ .circle {
+ width: clamp(90px, min(40svw, 34svh), 200px);
+ }
+}
+html[data-theme="white"] .white-mic-circle {
+ position: relative;
+ z-index: 2;
+ width: 116px;
+ height: 116px;
+ border-radius: 999px;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ background: #ffffff;
+ color: #ffffff;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ overflow: visible;
+ flex: none;
+ transition:
+ transform 180ms cubic-bezier(0.2, 0.7, 0.2, 1),
+ box-shadow 180ms ease;
+}
+html[data-theme="white"] .white-mic-circle .wmc-halo {
+ position: absolute;
+ inset: -22px;
+ border-radius: 999px;
+ background: radial-gradient(closest-side, rgba(59, 130, 246, 0.22), rgba(59, 130, 246, 0.1) 55%, transparent 75%);
+ z-index: 0;
+ pointer-events: none;
+ animation: wmcHaloPulse 3.2s ease-in-out infinite alternate;
+}
+html[data-theme="white"] .white-mic-circle .wmc-ring {
+ position: absolute;
+ inset: 0;
+ border-radius: 999px;
+ padding: 4px;
+ background: linear-gradient(135deg, #2563eb 0%, #3b82f6 45%, #60a5fa 100%);
+ z-index: 0;
+ pointer-events: none;
+ box-sizing: border-box;
+ display: block;
+}
+html[data-theme="white"] .white-mic-circle .wmc-ring::before {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 100%;
+ border-radius: 999px;
+ background: transparent;
+}
+html[data-theme="white"] .white-mic-circle .wmc-core {
+ position: absolute;
+ inset: 4px;
+ border-radius: 999px;
+ background: linear-gradient(135deg, #2563eb 0%, #3b82f6 55%, #60a5fa 100%);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.35),
+ inset 0 -3px 6px rgba(15, 23, 42, 0.12),
+ 0 8px 22px rgba(37, 99, 235, 0.22);
+ z-index: 1;
+ pointer-events: none;
+}
+html[data-theme="white"] .white-mic-circle .wmc-icon {
+ position: relative;
+ z-index: 5;
+ color: #ffffff;
+ fill: #ffffff;
+ stroke: #ffffff;
+ width: 48px;
+ height: 48px;
+ flex: none;
+ display: none;
+ filter: drop-shadow(0 2px 4px rgba(15, 23, 42, 0.14));
+}
+html[data-theme="white"] .white-mic-circle.state-idle > svg.wmc-icon.wmc-mic,
+html[data-theme="white"] .white-mic-circle.state-idle .wmc-icon.wmc-mic,
+html[data-theme="white"] .white-mic-circle.state-listening .wmc-icon.wmc-mic,
+html[data-theme="white"] .white-mic-circle.state-user-speaking .wmc-icon.wmc-mic,
+html[data-theme="white"] .white-mic-circle.state-ai-speaking .wmc-icon.wmc-mic {
+ display: inline-flex;
+}
+html[data-theme="white"] .white-mic-circle.state-your-turn > svg.wmc-icon.wmc-mic,
+html[data-theme="white"] .white-mic-circle.state-your-turn .wmc-icon.wmc-mic {
+ display: inline-flex;
+}
+html[data-theme="white"] .white-mic-circle.state-error > svg.wmc-icon.wmc-mic,
+html[data-theme="white"] .white-mic-circle.state-error .wmc-icon.wmc-mic {
+ display: inline-flex;
+}
+html[data-theme="white"] .white-mic-circle.state-processing > .wmc-icon.wmc-spinner,
+html[data-theme="white"] .white-mic-circle.state-processing .wmc-icon.wmc-spinner,
+html[data-theme="white"] .white-mic-circle.state-connecting > .wmc-icon.wmc-spinner,
+html[data-theme="white"] .white-mic-circle.state-connecting .wmc-icon.wmc-spinner,
+html[data-theme="white"] .white-mic-circle.state-queued > .wmc-icon.wmc-spinner,
+html[data-theme="white"] .white-mic-circle.state-queued .wmc-icon.wmc-spinner {
+ display: block;
+ width: 40px;
+ height: 40px;
+ border-radius: 999px;
+ border: 4px solid rgba(255, 255, 255, 0.22);
+ border-top-color: #ffffff;
+ animation: voice-spinner-spin 850ms linear infinite;
+ margin: 4px;
+ padding: 0;
+ background: transparent;
+ box-sizing: border-box;
+}
+html[data-theme="white"] .white-mic-circle:disabled {
+ cursor: default;
+}
+html[data-theme="white"] .white-mic-circle:focus-visible {
+ outline: 3px solid #1d4ed8;
+ outline-offset: 4px;
+}
+html[data-theme="white"] .white-mic-circle.state-your-turn .wmc-ring {
+ animation: wmcListeningPulse 1.6s ease-in-out infinite;
+}
+html[data-theme="white"] .white-waveform {
+ position: absolute;
+ top: calc(22px + 116px / 2);
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ height: 70px;
+ pointer-events: none;
+ z-index: 1;
+}
+html[data-theme="white"] .white-waveform-left {
+ left: calc(50% - 58px - 90px);
+}
+html[data-theme="white"] .white-waveform-right {
+ right: calc(50% - 58px - 90px);
+}
+html[data-theme="white"] .white-waveform .w-bar {
+ display: block;
+ width: 6px;
+ border-radius: 999px;
+ background: linear-gradient(180deg, #3b82f6, #60a5fa);
+ opacity: 0.8;
+ transform-origin: center;
+ height: calc(var(--bar-h, 0.5) * 70px);
+ min-height: 8px;
+}
+html[data-theme="white"] .white-mic-circle.state-your-turn ~ * .w-bar,
+html[data-theme="white"] .white-mic-stage:has(.state-your-turn) .w-bar {
+ animation: wBarDance 900ms ease-in-out infinite alternate;
+}
+html[data-theme="white"] .white-waveform .wb-1 {
+ animation-delay: -0.6s;
+}
+html[data-theme="white"] .white-waveform .wb-2 {
+ animation-delay: -0.5s;
+}
+html[data-theme="white"] .white-waveform .wb-3 {
+ animation-delay: -0.4s;
+}
+html[data-theme="white"] .white-waveform .wb-4 {
+ animation-delay: -0.3s;
+}
+html[data-theme="white"] .white-waveform .wb-5 {
+ animation-delay: -0.2s;
+}
+html[data-theme="white"] .white-waveform .wb-6 {
+ animation-delay: -0.1s;
+}
+html[data-theme="white"] .white-waveform .wb-7 {
+ animation-delay: 0s;
+}
+html[data-theme="white"] .white-waveform-right .wb-1 {
+ animation-delay: -0s;
+}
+html[data-theme="white"] .white-waveform-right .wb-2 {
+ animation-delay: -0.1s;
+}
+html[data-theme="white"] .white-waveform-right .wb-3 {
+ animation-delay: -0.2s;
+}
+html[data-theme="white"] .white-waveform-right .wb-4 {
+ animation-delay: -0.3s;
+}
+html[data-theme="white"] .white-waveform-right .wb-5 {
+ animation-delay: -0.4s;
+}
+html[data-theme="white"] .white-waveform-right .wb-6 {
+ animation-delay: -0.5s;
+}
+html[data-theme="white"] .white-waveform-right .wb-7 {
+ animation-delay: -0.6s;
+}
+@media (max-width: 480px) {
+ html[data-theme="white"] .white-waveform-left {
+ left: calc(50% - 58px - 72px);
+ }
+ html[data-theme="white"] .white-waveform-right {
+ right: calc(50% - 58px - 72px);
+ }
+ html[data-theme="white"] .white-waveform {
+ gap: 5px;
+ }
+ html[data-theme="white"] .white-waveform .w-bar {
+ width: 5px;
+ }
+}
+@media (max-height: 640px) {
+ html[data-theme="white"] .white-mic-circle {
+ width: 98px;
+ height: 98px;
+ }
+ html[data-theme="white"] .white-mic-circle .wmc-icon {
+ width: 40px;
+ height: 40px;
+ }
+ html[data-theme="white"] .white-mic-circle.state-processing .wmc-spinner,
+ html[data-theme="white"] .white-mic-circle.state-connecting .wmc-spinner,
+ html[data-theme="white"] .white-mic-circle.state-queued .wmc-spinner {
+ width: 34px;
+ height: 34px;
+ }
+ html[data-theme="white"] .white-waveform .w-bar {
+ height: calc(var(--bar-h, 0.5) * 56px);
+ }
+ html[data-theme="white"] .white-waveform {
+ height: 56px;
+ }
+ html[data-theme="white"] .white-waveform-left {
+ left: calc(50% - 49px - 76px);
+ top: calc(14px + 49px);
+ }
+ html[data-theme="white"] .white-waveform-right {
+ right: calc(50% - 49px - 76px);
+ top: calc(14px + 49px);
+ }
+}
+.circle-caption {
+ color: var(--text-dim);
+}
+@media (max-width: 600px) {
+ .circle {
+ width: clamp(110px, 32vw, 176px);
+ }
+ .orb-wrap.live .side-btn {
+ width: 44px;
+ height: 44px;
+ }
+ .mic-gate-arc {
+ width: 62px;
+ height: 62px;
+ }
+ html[data-theme="white"] .white-waveform {
+ top: 74px;
+ }
+}
+@media (max-height: 640px) {
+ html[data-theme="white"] .white-waveform {
+ top: 57px;
+ }
+}
+.state-idle .ind-mic {
+ opacity: 1;
+ transform: scale(1);
+}
+html[data-theme="white"] .white-mic-circle svg.wmc-mic {
+ fill: none;
+}
+.mic-gate-arc:focus-visible {
+ outline: 2px solid var(--accent-2);
+ outline-offset: 4px;
+ border-radius: 50%;
+}
+@keyframes voice-spinner-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+@keyframes pedestalBreathe {
+ 0% {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 0.55;
+ }
+ 50% {
+ transform: translate(-50%, -50%) scale(1.06);
+ opacity: 0.75;
+ }
+ 100% {
+ transform: translate(-50%, -50%) scale(0.98);
+ opacity: 0.5;
+ }
+}
+@keyframes orbConicSpin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+@keyframes orbBreatheGlow {
+ 0% {
+ opacity: 0.42;
+ transform: scale(0.985);
+ filter: blur(30px);
+ }
+ 50% {
+ opacity: 0.72;
+ transform: scale(1.04);
+ filter: blur(40px);
+ }
+ 100% {
+ opacity: 0.48;
+ transform: scale(1);
+ filter: blur(32px);
+ }
+}
+@keyframes outerRingPulse {
+ 0%,
+ 100% {
+ opacity: 0.4;
+ transform: translate(-50%, -50%) scale(1);
+ }
+ 50% {
+ opacity: 0.65;
+ transform: translate(-50%, -50%) scale(1.02);
+ }
+}
+@keyframes coreBreathe {
+ 0% {
+ transform: scale(0.992);
+ box-shadow:
+ 0 0 36px -8px color-mix(in srgb, var(--brand) 40%, transparent),
+ inset 0 0 26px color-mix(in srgb, var(--brand) 16%, transparent);
+ }
+ 50% {
+ transform: scale(1.01);
+ box-shadow:
+ 0 0 54px -4px color-mix(in srgb, var(--brand-2) 48%, transparent),
+ inset 0 0 38px color-mix(in srgb, var(--brand-2) 22%, transparent);
+ }
+ 100% {
+ transform: scale(1);
+ box-shadow:
+ 0 0 44px -6px color-mix(in srgb, var(--brand-3) 42%, transparent),
+ inset 0 0 32px color-mix(in srgb, var(--brand-3) 20%, transparent);
+ }
+}
+@keyframes idleCaptionBob {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ letter-spacing: 0.24em;
+ }
+ 50% {
+ transform: translateY(-1px);
+ letter-spacing: 0.26em;
+ }
+}
+@keyframes breathe {
+ 0%,
+ 100% {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 0.4;
+ }
+ 50% {
+ transform: translate(-50%, -50%) scale(1.06);
+ opacity: 0.15;
+ }
+}
+@keyframes thinking {
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: 0.7;
+ }
+ 50% {
+ transform: scale(0.96);
+ opacity: 0.45;
+ }
+}
+@keyframes thinking-dot {
+ 0%,
+ 60%,
+ 100% {
+ transform: scale(0.7);
+ opacity: 0.3;
+ }
+ 30% {
+ transform: scale(1.15);
+ opacity: 1;
+ }
+}
+
+@keyframes ind-spin {
+ from {
+ transform: scale(1) rotate(0deg);
+ }
+ to {
+ transform: scale(1) rotate(360deg);
+ }
+}
+@keyframes wmcHaloPulse {
+ 0% {
+ transform: scale(0.98);
+ opacity: 0.75;
+ }
+ 100% {
+ transform: scale(1.08);
+ opacity: 1;
+ }
+}
+@keyframes wmcListeningPulse {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45);
+ }
+ 50% {
+ box-shadow: 0 0 0 18px rgba(59, 130, 246, 0);
+ }
+}
+@keyframes wBarDance {
+ from {
+ transform: scaleY(0.55);
+ }
+ to {
+ transform: scaleY(1.05);
+ }
+}