5521 lines
174 KiB
CSS
5521 lines
174 KiB
CSS
:root {
|
||
--bg: #07080d;
|
||
--bg-elev: rgba(22, 25, 37, 0.55);
|
||
--bg-elev-2: rgba(31, 35, 52, 0.55);
|
||
--border: rgba(255, 255, 255, 0.08);
|
||
--border-strong: rgba(255, 255, 255, 0.16);
|
||
--glass: rgba(18, 20, 30, 0.45);
|
||
--glass-strong: rgba(18, 20, 30, 0.62);
|
||
--text: #f5f6fa;
|
||
--text-dim: rgba(245, 246, 250, 0.70);
|
||
--text-faint: rgba(245, 246, 250, 0.48);
|
||
|
||
--brand: #8b5cf6;
|
||
--brand-2: #ec4899;
|
||
--brand-3: #22d3ee;
|
||
--accent: #a78bfa;
|
||
--accent-2: #22d3ee;
|
||
--listening: #22d3ee;
|
||
--speaking: #a78bfa;
|
||
--processing: #fbbf24;
|
||
--error: #ff6a75;
|
||
--success: #34d399;
|
||
|
||
--brand-gradient: linear-gradient(135deg, #8b5cf6 0%, #ec4899 50%, #22d3ee 100%);
|
||
--brand-soft-gradient: linear-gradient(135deg, color-mix(in srgb, #8b5cf6 55%, transparent),
|
||
color-mix(in srgb, #ec4899 48%, transparent) 55%,
|
||
color-mix(in srgb, #22d3ee 55%, transparent));
|
||
|
||
/* Mono is the "machine voice": reserved for system text the app emits —
|
||
* the orb's status caption, tool calls, the transport tag. Body/UI stays
|
||
* Inter. */
|
||
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
||
--font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
|
||
/* Thesis: color belongs to the voice. The chrome is monochrome; saturated
|
||
* hue appears only on the orb and on these tiny role echoes, which mirror
|
||
* the orb's own state colors (you listening = cyan, assistant = violet,
|
||
* tool = amber) so the transcript reads in the same color language. */
|
||
--voice-user: var(--accent-2);
|
||
--voice-assistant: var(--accent);
|
||
--voice-tool: var(--processing);
|
||
|
||
/* Smoothed mic RMS in [0..1], updated every frame from JS while a session
|
||
* is active. Used by audio-reactive circle states. */
|
||
--audio-level: 0;
|
||
/* Five log-spaced frequency bands extracted from the mic analyser;
|
||
* drive each bar's height independently for a real "spectrum" feel. */
|
||
--bar0: 0;
|
||
--bar1: 0;
|
||
--bar2: 0;
|
||
--bar3: 0;
|
||
--bar4: 0;
|
||
|
||
--radius-sm: 10px;
|
||
--radius-md: 16px;
|
||
--radius-lg: 26px;
|
||
--radius-xl: 34px;
|
||
|
||
--shadow-soft: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||
--shadow-glow-brand: 0 0 40px -10px color-mix(in srgb, var(--brand) 60%, transparent),
|
||
0 0 80px -30px color-mix(in srgb, var(--brand-2) 55%, transparent);
|
||
--shadow-glass: 0 8px 40px rgba(0, 0, 0, 0.32), 0 2px 8px rgba(0, 0, 0, 0.18);
|
||
|
||
color-scheme: dark;
|
||
}
|
||
|
||
* {
|
||
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%;
|
||
/* ─── iOS / notch / rounded-screen safe-area insets ───
|
||
The user reported the header brand card is slightly clipped (avatar AV
|
||
touches the top-right viewport edge). Mobile OSes use a non-rectangular
|
||
bezel/notch and an iOS Home Indicator bar at the bottom. We reserve
|
||
padding at document scope so NOTHING (topbar orb chat footer) is ever
|
||
painted into the OS reserved bezels. The `max(10px, env(...))` fallback
|
||
gives a minimum 10px breathing even on desktop / devices where env()
|
||
resolves to 0. */
|
||
padding-top: max(10px, env(safe-area-inset-top , 0px));
|
||
padding-right: max(10px, env(safe-area-inset-right , 0px));
|
||
padding-left: max(10px, env(safe-area-inset-left , 0px));
|
||
/* 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.30) 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.10), 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: .9; }
|
||
50% { transform: translate3d(3%, -2%, 0) scale(1.08); opacity: 1; }
|
||
100% { transform: translate3d(-2%, 2%, 0) scale(1.02); opacity: .85; }
|
||
}
|
||
|
||
/* ─── Overlay unlocker TALK mode (S2S_START_SESSION_MODE=TALK) ───
|
||
Capture the first tap/click anywhere on the page because browsers block
|
||
getUserMedia() outside a user gesture. Without this layer, doStart() from
|
||
setTimeout failed and the greeting was silent. Above all content except
|
||
<dialog> modals, whose top layer takes priority. */
|
||
.talk-unlocker {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(180deg, rgba(14,16,25,0.35) 0%, rgba(14,16,25,0.55) 60%, rgba(14,16,25,0.75) 100%);
|
||
backdrop-filter: blur(6px);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
cursor: pointer;
|
||
animation: talkUnlockFade 260ms ease-out both;
|
||
}
|
||
.talk-unlocker[hidden] {
|
||
display: none !important;
|
||
}
|
||
@keyframes talkUnlockFade {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
.talk-unlocker-hint {
|
||
width: min(86vw, 420px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 28px 22px 24px;
|
||
border-radius: 22px;
|
||
background: rgba(22, 25, 40, 0.78);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
box-shadow:
|
||
0 20px 50px rgba(0, 0, 0, 0.45),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||
text-align: center;
|
||
animation: talkUnlockPop 380ms cubic-bezier(.2,.9,.3,1.2) 80ms both;
|
||
}
|
||
@keyframes talkUnlockPop {
|
||
from { opacity: 0; transform: translateY(14px) scale(0.96); }
|
||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||
}
|
||
.talk-unlocker-spinner {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 50%;
|
||
border: 3px solid rgba(255,255,255,0.1);
|
||
border-top-color: #8b5cf6;
|
||
border-right-color: #ec4899;
|
||
animation: talkSpinnerSpin 900ms linear infinite;
|
||
}
|
||
@keyframes talkSpinnerSpin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
.talk-unlocker-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.01em;
|
||
color: var(--text, #f3f4f6);
|
||
}
|
||
.talk-unlocker-sub {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: rgba(226, 232, 240, 0.68);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
#app {
|
||
/* Simple flex column layout: header → stage → chat-panel.
|
||
At least fills viewport, grows if contents taller. The footer below is a
|
||
sibling position:sticky bottom=0 (NOT a child of #app) so to prevent the
|
||
sticky footer from COVERING the last line of the chat history (which
|
||
would otherwise be painted under the ~32-44px of glass footer) we reserve
|
||
a bottom padding matching the real footer height + breathing margin +
|
||
iOS Home Indicator (safe-area-inset-bottom). */
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
min-height: 100svh;
|
||
min-height: 100dvh;
|
||
/* 120px minimum = glass footer bar (32-44) + large breathing (76-88) + any
|
||
iOS home indicator safe inset. We intentionally OVER-provision here: the
|
||
last assistant bubble in the screenshot was clipped because the total
|
||
reserved room (56 + chat-inner 96) was smaller than the real floating
|
||
footer + text-wrap delta + OS safe area. */
|
||
padding-bottom: max(120px, calc(env(safe-area-inset-bottom, 0px) + 120px));
|
||
padding-right: 0; /* side safe areas already on <body>; don't double-add */
|
||
padding-left: 0;
|
||
padding-top: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
/* ─── Footer ─────────────────────────────────────────────────────────── */
|
||
/* #app uses flex column layout so footer sits as the last child.
|
||
We make it STICKY at the very bottom of the viewport so:
|
||
- if page fits within viewport → footer is at bottom, visible.
|
||
- if page is taller and we scroll document → footer STAYS pinned at
|
||
viewport bottom until the very end where it joins the end of content.
|
||
Glass tinted bg prevents footer text becoming unreadable when overlaying
|
||
the orb / aurora gradients during mid-scroll. */
|
||
.app-footer {
|
||
position: sticky;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
flex: 0 0 auto; /* never shrink or grow the footer */
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/* On iOS the floating "home indicator" pill is drawn at the very bottom of
|
||
the screen inside env(safe-area-inset-bottom) and obscures content. We
|
||
extend the footer glass padding to sit ABOVE the pill. Desktop browsers
|
||
resolve env() to 0, so clamp(max(12px,...)) is 12–20px there. */
|
||
padding:
|
||
clamp(8px, 1.6vw, 12px)
|
||
clamp(16px, 4vw, 24px)
|
||
max(clamp(12px, 2.4vw, 20px), env(safe-area-inset-bottom, 0px));
|
||
user-select: none;
|
||
pointer-events: none;
|
||
z-index: 3; /* above aurora bg & stage pedestal, below modals (z 200+) */
|
||
box-sizing: border-box;
|
||
backdrop-filter: blur(10px) saturate(140%);
|
||
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
||
background:
|
||
linear-gradient(180deg,
|
||
transparent 0%,
|
||
color-mix(in srgb, var(--bg) 72%, transparent) 40%,
|
||
color-mix(in srgb, var(--bg) 90%, transparent) 100%);
|
||
}
|
||
.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,.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. <body> 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;
|
||
}
|
||
|
||
.feedback-link {
|
||
align-self: flex-end;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 44px;
|
||
margin: 12px 16px 0;
|
||
padding: 10px 18px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
.feedback-link:hover {
|
||
background: #1d4ed8;
|
||
}
|
||
.feedback-link:focus-visible {
|
||
outline: 2px solid var(--text);
|
||
outline-offset: 3px;
|
||
}
|
||
|
||
.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(.2,.7,.2,1), filter .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,.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 .3s cubic-bezier(.2,.7,.2,1), filter .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: default;
|
||
transition:
|
||
transform .22s cubic-bezier(.2,.7,.2,1),
|
||
box-shadow .3s ease,
|
||
border-color .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: .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 <button> 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);
|
||
}
|
||
@keyframes pulseDot {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: .78; transform: scale(.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;
|
||
}
|
||
/* Keep the account control compact on phones: avatar-only chip, shorter pill. */
|
||
.account-chip {
|
||
height: 36px;
|
||
padding: 0 6px;
|
||
}
|
||
.account-handle {
|
||
display: none;
|
||
}
|
||
.signin-pill {
|
||
height: 36px;
|
||
padding: 0 12px;
|
||
}
|
||
/* ── Footer (narrow viewport): reduce tracking to avoid awkward wrapping;
|
||
* use compact padding and a taller line height. */
|
||
.app-footer {
|
||
padding: 8px 14px 14px;
|
||
}
|
||
.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;
|
||
}
|
||
|
||
/* The HF user pill: compact avatar + handle. Rendered as a flex row so
|
||
* the avatar stays aligned with the text baseline even when the text
|
||
* wraps on narrow viewports. */
|
||
.hf-user {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
padding: 4px 10px 4px 4px;
|
||
border-radius: 999px;
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
line-height: 1;
|
||
}
|
||
|
||
.hf-avatar {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
background: color-mix(in srgb, var(--accent) 25%, var(--bg-elev-2));
|
||
/* Hidden until an actual URL loads (see `setHfAvatar` in main.ts).
|
||
* Keeps the initial login flash from showing a broken image icon. */
|
||
opacity: 0;
|
||
transition: opacity 0.25s ease;
|
||
flex: none;
|
||
}
|
||
.hf-avatar.loaded {
|
||
opacity: 1;
|
||
}
|
||
|
||
.hf-user-name {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ─── 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;
|
||
}
|
||
|
||
/* ─── Account (HF login chip + sign-in pill + popover) ─────────────────────── */
|
||
.account {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
/* Signed-out: a compact pill that reads as the primary affordance. */
|
||
.signin-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
height: 40px;
|
||
padding: 0 14px;
|
||
border-radius: var(--radius-sm);
|
||
border: 1px solid var(--border-strong);
|
||
background: var(--bg-elev);
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
transition: background 0.15s, border-color 0.15s;
|
||
}
|
||
.signin-pill svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
flex: none;
|
||
}
|
||
.signin-pill:hover {
|
||
background: var(--bg-elev-2);
|
||
border-color: var(--text);
|
||
}
|
||
/* Narrow viewports: collapse the pill to an icon-only square. */
|
||
@media (max-width: 800px) {
|
||
.signin-pill {
|
||
gap: 0;
|
||
width: 40px;
|
||
padding: 0;
|
||
justify-content: center;
|
||
}
|
||
.signin-pill span {
|
||
display: none;
|
||
}
|
||
}
|
||
/* Signed-in: avatar + handle chip. */
|
||
.account-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
height: 40px;
|
||
padding: 0 10px 0 6px;
|
||
border-radius: var(--radius-sm);
|
||
border: 1px solid var(--border);
|
||
background: var(--bg-elev);
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||
}
|
||
.account-chip:hover {
|
||
background: var(--bg-elev-2);
|
||
color: var(--text);
|
||
border-color: var(--border-strong);
|
||
}
|
||
.account-avatar {
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
flex: none;
|
||
}
|
||
.account-avatar-fallback {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--bg-elev-2);
|
||
color: var(--text);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
.account-handle {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
max-width: 12ch;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.account-pro {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
padding: 2px 5px;
|
||
border-radius: 5px;
|
||
background: var(--text);
|
||
color: var(--bg);
|
||
}
|
||
/* Org "Team" badge: same pill as PRO, accent-coloured so it reads as
|
||
* unlimited without claiming a paid PRO subscription. */
|
||
.account-team {
|
||
background: var(--accent);
|
||
color: var(--bg);
|
||
}
|
||
.account-pop {
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
right: 0;
|
||
min-width: 200px;
|
||
padding: 6px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-strong);
|
||
background: var(--bg-elev);
|
||
box-shadow: var(--shadow-soft);
|
||
z-index: 50;
|
||
}
|
||
.account-pop[hidden] {
|
||
display: none;
|
||
}
|
||
.account-pop-row {
|
||
padding: 8px 10px;
|
||
}
|
||
.account-pop-name {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
.account-pop-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding-top: 0;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
.account-tier {
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.account-pop-link {
|
||
display: block;
|
||
padding: 8px 10px;
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
text-decoration: none;
|
||
transition: background 0.15s, color 0.15s;
|
||
}
|
||
.account-pop-link:hover {
|
||
background: var(--bg-elev-2);
|
||
color: var(--text);
|
||
}
|
||
.account-signout {
|
||
border-top: 1px solid var(--border);
|
||
margin-top: 4px;
|
||
padding-top: 10px;
|
||
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
||
}
|
||
|
||
/* ─── Daily-limit modal ────────────────────────────────────────────────────── */
|
||
.limit-modal {
|
||
width: min(400px, 92vw);
|
||
}
|
||
.limit-card {
|
||
position: relative;
|
||
align-items: center;
|
||
text-align: center;
|
||
gap: 14px;
|
||
padding: 34px 28px 26px;
|
||
}
|
||
.limit-close {
|
||
position: absolute;
|
||
top: 12px;
|
||
right: 12px;
|
||
}
|
||
/* HF smiling face (brand yellow) on a neutral badge — friendly, not a stop sign.
|
||
* The logo is the single pop of colour, so the badge itself stays quiet. */
|
||
.limit-badge {
|
||
width: 66px;
|
||
height: 66px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
margin: 2px auto 2px;
|
||
background: var(--bg-elev-2);
|
||
border: 1px solid var(--border-strong);
|
||
box-shadow: 0 0 0 6px rgba(255, 210, 30, 0.06);
|
||
}
|
||
.limit-badge .hf-logo {
|
||
width: 46px;
|
||
height: auto;
|
||
}
|
||
.limit-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
letter-spacing: 0;
|
||
}
|
||
.limit-msg {
|
||
color: var(--text-dim);
|
||
font-size: 14px;
|
||
line-height: 1.55;
|
||
margin: 0;
|
||
max-width: 30ch;
|
||
}
|
||
.limit-cta {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
}
|
||
.limit-cta .hf-logo {
|
||
width: 20px;
|
||
height: auto;
|
||
flex: none;
|
||
}
|
||
.limit-note {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--text-faint);
|
||
}
|
||
#limit-cta[hidden] {
|
||
display: none;
|
||
}
|
||
|
||
/* ─── 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: .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: .55; }
|
||
50% { transform: translate(-50%, -50%) scale(1.06); opacity: .75; }
|
||
100% { transform: translate(-50%, -50%) scale(.98); opacity: .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: .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: .42; transform: scale(.985); filter: blur(30px); }
|
||
50% { opacity: .72; transform: scale(1.04); filter: blur(40px); }
|
||
100% { opacity: .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: .4; transform: translate(-50%,-50%) scale(1); }
|
||
50% { opacity: .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);
|
||
}
|
||
/* While the webcam preview sits bottom-left, push the credit to the
|
||
* bottom-right so the two don't collide. */
|
||
body.cam-on .footer {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* ─── 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) ─ */
|
||
/* Old rule: position:fixed right-side overlay.
|
||
New rule: direct flex-col child of #app, 100% width, grows if content is
|
||
taller up to a max of ~40% viewport height, scrolls the history inside.
|
||
Keeps all the "ChatView" internals intact (bubble history renderer, etc). */
|
||
.chat-panel {
|
||
position: static;
|
||
inset: auto;
|
||
z-index: 1;
|
||
pointer-events: auto;
|
||
width: 100%;
|
||
/* Stage now stays compact above us; chat-panel absorbs any remaining vertical
|
||
space so it sits IMMEDIATELY below the orb caption and extends down to the
|
||
sticky footer — while keeping a sensible max so it never squishes stage. */
|
||
flex: 1 1 auto;
|
||
min-height: 160px;
|
||
max-height: 65svh;
|
||
max-height: 65dvh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* Horizontal margins match the 920px inner max-width line-up + side rails.
|
||
BOTTOM PADDING (critical 2026-09-04 bump): was clamp(40px, 9vh, 96px).
|
||
The last assistant bubble line was STILL clipped against the sticky
|
||
footer because (a) the footer is rendered outside #app as a floating
|
||
overlay so it covers 32-56px of the chat area regardless of history
|
||
internal height and (b) when a bubble wraps to >5 lines on narrow mobile
|
||
its rendered height is larger than the 96px breathing. New minimum
|
||
60/160 (12vh = 160 at ~1333 svh but max 160px) adds another 64px. */
|
||
padding: clamp(8px, 1.4vh, 14px) clamp(16px, 3vw, 24px) clamp(60px, 12vh, 160px);
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
/* Inner wrapper: SEAMLESS (no glass card / no border / no shadow) per user
|
||
request 2026-09-04 — the chat area visually melts into the aurora page
|
||
background, no chrome box or visible frame around the history. We keep the
|
||
layout sizing + z-ordering but neutralise all decoration. */
|
||
.chat-panel-inner {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
margin: 0 auto;
|
||
max-width: 920px;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* Decoration removed — seamless transparent on aurora bg */
|
||
background: transparent;
|
||
border: 0 solid transparent;
|
||
border-radius: 0;
|
||
overflow: hidden;
|
||
box-shadow: none;
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
.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 {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
/* Padding sides 14px so "CONVERSACIÓN" lines up with the 14px content insets
|
||
of the bubbles below and leaves a clear breathing column near the right
|
||
edge so the assistant bubble never touches the screen border. */
|
||
padding: 0 14px 6px;
|
||
border-bottom: 0 solid transparent;
|
||
flex: none;
|
||
}
|
||
.chat-panel-header h3 {
|
||
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: .92;
|
||
}
|
||
|
||
/* Chat history: flex column of user/assistant bubbles — scrolled independently
|
||
while header + input row stay pinned. Transparent seamless on page aurora (no own bg).
|
||
Padding 14px sides for alignment with the header.
|
||
NOTE: the critical "not attached to screen border" rule is enforced by
|
||
`margin-right:22px` on `.hist-msg.assistant` and `margin-left:6px` on `.hist-msg.user`
|
||
(hard rules on the bubbles themselves so nothing can hide them).
|
||
Bottom padding BUMP 2026-09-04: 96 → 160px. The screenshot showed the last
|
||
assistant line asking for more details cut cleanly by the footer. 160px
|
||
inside the scroll container is 5× the real footer glass height; after we
|
||
snap scrollTop to the absolute max, the last bubble terminates at the TOP
|
||
of a 160px-high transparent region → fully visible, fully above the floating
|
||
sticky footer even on very short (800px svh) mobile viewports. */
|
||
.chat-history {
|
||
flex: 1 1 auto;
|
||
overflow-y: auto;
|
||
/* 160px bottom padding inside the scrollbox means scrollHeight grows to include
|
||
those 160px. When we snap scrollTop = max, the last bubble ends at the top
|
||
of that 160px void → visible and well above the floating footer line. */
|
||
padding: 4px 14px 160px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
scrollbar-width: thin;
|
||
/* Disable Chrome's "scroll anchoring" which sometimes fights our snap-to-bottom by
|
||
re-anchoring the scroll pos whenever a new token is appended. We truly want
|
||
the tail, always, no exceptions. */
|
||
overflow-anchor: none;
|
||
/* Instant snap (never smooth) — the default was causing perceived "scroll
|
||
scroll does not go far enough" because smooth animates while new content
|
||
outpaces the animation frame budget on long streams). */
|
||
scroll-behavior: auto !important;
|
||
/* gutter removed: spacing is guaranteed by per-bubble margin rules instead */
|
||
}
|
||
.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;
|
||
}
|
||
|
||
/* ─── Chat input row (bottom of chat panel, above the button) ──────── */
|
||
/* NOTE: currently HIDDEN in index.html. Kept seamless here so if/when we
|
||
re-enable it the form melts visually into the page bg, no separator frame. */
|
||
.chat-form {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 8px;
|
||
padding: 8px 2px 0;
|
||
border-top: 0 solid transparent;
|
||
background: transparent;
|
||
flex: none;
|
||
}
|
||
.chat-input {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
height: 40px;
|
||
padding: 0 14px;
|
||
border-radius: 12px;
|
||
background: color-mix(in srgb, var(--bg) 70%, rgba(255,255,255,0.02));
|
||
border: 1px solid color-mix(in srgb, var(--brand) 18%, var(--border));
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
font-family: var(--font-sans);
|
||
line-height: 1;
|
||
transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
|
||
outline: none;
|
||
box-shadow: 0 0 0 0 transparent;
|
||
}
|
||
.chat-input::placeholder {
|
||
color: color-mix(in srgb, var(--text-dim) 72%, transparent);
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.chat-input:hover {
|
||
border-color: color-mix(in srgb, var(--brand-2) 44%, var(--border));
|
||
}
|
||
.chat-input:focus {
|
||
border-color: color-mix(in srgb, var(--brand-3) 60%, transparent);
|
||
background: color-mix(in srgb, var(--bg) 82%, rgba(255,255,255,0.04));
|
||
box-shadow:
|
||
0 0 0 3px color-mix(in srgb, var(--brand-3) 22%, transparent),
|
||
0 0 26px -8px color-mix(in srgb, var(--brand-3) 70%, transparent);
|
||
}
|
||
.chat-send-btn {
|
||
flex: none;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 12px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
color: #fff;
|
||
background:
|
||
radial-gradient(120% 120% at 30% 18%, rgba(255,255,255,0.18), transparent 58%),
|
||
linear-gradient(135deg,
|
||
color-mix(in srgb, var(--brand) 82%, transparent),
|
||
color-mix(in srgb, var(--brand-2) 70%, transparent) 55%,
|
||
color-mix(in srgb, var(--brand-3) 70%, transparent));
|
||
border: 1px solid color-mix(in srgb, var(--brand-2) 50%, var(--border));
|
||
box-shadow:
|
||
0 1px 0 rgba(255,255,255,0.1) inset,
|
||
0 6px 18px rgba(0,0,0,0.30),
|
||
0 0 24px -8px color-mix(in srgb, var(--brand-2) 70%, transparent);
|
||
cursor: pointer;
|
||
transition: transform .22s ease, box-shadow .3s ease, filter .3s ease;
|
||
}
|
||
.chat-send-btn:hover {
|
||
transform: translateY(-1px);
|
||
filter: brightness(1.05);
|
||
box-shadow:
|
||
0 1px 0 rgba(255,255,255,0.12) inset,
|
||
0 10px 22px rgba(0,0,0,0.34),
|
||
0 0 34px -6px color-mix(in srgb, var(--brand-2) 80%, transparent);
|
||
}
|
||
.chat-send-btn:active { transform: translateY(0) scale(0.98); }
|
||
.chat-send-btn:focus-visible {
|
||
outline: none;
|
||
box-shadow:
|
||
0 0 0 3px color-mix(in srgb, var(--brand-3) 40%, transparent),
|
||
0 6px 18px rgba(0,0,0,0.30);
|
||
}
|
||
|
||
/* ─── Reports sidebar (LEFT) ───────────────────────────────────────── */
|
||
/* Behaviour:
|
||
- desktop (>= 1024px): FIXED to the left edge, 320px, always visible.
|
||
Stage/header/footer leave room via #app padding-left 320px so the orb
|
||
is not covered.
|
||
- mobile / tablet: OFF-CANVAS slide-in from the left (like chat panel on
|
||
right), only shown on toggle, closed via backdrop / close button / ESC.
|
||
backdrop is shared between both breakpoints (only visible when open on
|
||
mobile; desktop uses no backdrop). */
|
||
.reports-sidebar {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 190; /* below chat-panel (z 200), above stage/header (z 1-2) */
|
||
pointer-events: none; /* backdrop + inner opt-in to clicks separately */
|
||
}
|
||
.reports-sidebar-backdrop {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(8, 9, 13, 0.42);
|
||
backdrop-filter: blur(5px);
|
||
-webkit-backdrop-filter: blur(5px);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.25s ease;
|
||
}
|
||
.reports-sidebar-inner {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: min(320px, 88vw);
|
||
background:
|
||
linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)),
|
||
var(--bg-elev);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transform: translateX(-105%);
|
||
transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
||
pointer-events: auto;
|
||
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.35);
|
||
overflow: hidden;
|
||
}
|
||
.reports-sidebar.open .reports-sidebar-backdrop {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
.reports-sidebar.open .reports-sidebar-inner {
|
||
transform: translateX(0);
|
||
}
|
||
/* Header — same pattern as chat panel header but tinted brand gradient line */
|
||
.reports-sidebar-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
flex: none;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.reports-sidebar-header::after {
|
||
content: "";
|
||
position: absolute;
|
||
left: 16px;
|
||
right: 16px;
|
||
bottom: -1px;
|
||
height: 1px;
|
||
background: linear-gradient(90deg,
|
||
transparent,
|
||
color-mix(in srgb, var(--brand) 60%, transparent) 45%,
|
||
color-mix(in srgb, var(--brand-2) 55%, transparent) 65%,
|
||
transparent);
|
||
opacity: .85;
|
||
}
|
||
.reports-sidebar-header h3 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
background: var(--brand-gradient);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
/* Hint block lives inside the sidebar now. Reset any "topbar" flavour of the
|
||
card: no max-height cap here (the sidebar scrolls the full panel instead).
|
||
Keep full-height so reports-list chips can breathe. */
|
||
.reports-sidebar .reports-hint {
|
||
margin: 12px 14px 4px 14px;
|
||
padding: 12px 12px 10px 12px;
|
||
max-height: none;
|
||
max-width: none;
|
||
width: auto;
|
||
flex: none;
|
||
box-shadow:
|
||
var(--shadow-glass),
|
||
0 0 0 1px color-mix(in srgb, var(--brand) 16%, transparent) inset,
|
||
0 0 24px -14px color-mix(in srgb, var(--brand) 62%, transparent);
|
||
}
|
||
.reports-sidebar .reports-hint .reports-list {
|
||
gap: 6px 8px;
|
||
}
|
||
/* Scrollbar of the sidebar (NOT reports-hint — that one still has its cap
|
||
when embedded in a smaller view) */
|
||
.reports-sidebar-inner {
|
||
scrollbar-width: thin;
|
||
scrollbar-gutter: stable;
|
||
overflow-y: auto;
|
||
}
|
||
.reports-sidebar-inner::-webkit-scrollbar { width: 6px; }
|
||
.reports-sidebar-inner::-webkit-scrollbar-track { background: transparent; }
|
||
.reports-sidebar-inner::-webkit-scrollbar-thumb {
|
||
background: color-mix(in srgb, var(--brand) 32%, transparent);
|
||
border-radius: 999px;
|
||
border: 1px solid transparent;
|
||
background-clip: content-box;
|
||
}
|
||
/* ── Toggle button in the topbar control cluster ─────────────────── */
|
||
button[data-reports-sidebar-toggle] {
|
||
/* Visible ONLY on mobile/tablet by default (desktop has sidebar always on) */
|
||
display: inline-flex;
|
||
}
|
||
/* 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,.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) {
|
||
/* Leave room for sidebar at left edge of the app */
|
||
#app {
|
||
padding-left: 320px;
|
||
box-sizing: border-box;
|
||
}
|
||
.reports-sidebar {
|
||
pointer-events: auto;
|
||
}
|
||
.reports-sidebar-backdrop {
|
||
display: none; /* never darken the page on desktop */
|
||
}
|
||
.reports-sidebar-inner {
|
||
transform: translateX(0) !important; /* keep always in */
|
||
width: 320px;
|
||
box-shadow: none; /* embedded sidebar, no floating shadow */
|
||
border-right: 1px solid color-mix(in srgb, var(--brand) 22%, var(--border));
|
||
background:
|
||
linear-gradient(180deg,
|
||
rgba(255,255,255,0.02),
|
||
rgba(255,255,255,0) 14%),
|
||
var(--bg);
|
||
}
|
||
/* Hide the toggle button in the topbar control cluster (sidebar always on) */
|
||
button[data-reports-sidebar-toggle] { display: none !important; }
|
||
/* 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);
|
||
}
|
||
}
|
||
/* Mid-range tablet 768..1023 : sidebar is off-canvas (toggle shown) but we
|
||
can still show the toggle and use 320px sliding panel. */
|
||
@media (min-width: 768px) and (max-width: 1023px) {
|
||
.reports-sidebar-inner { width: min(360px, 78vw); }
|
||
button[data-reports-sidebar-toggle] { display: inline-flex; }
|
||
}
|
||
/* Very narrow (<=767): same off-canvas, narrower panel. */
|
||
@media (max-width: 767px) {
|
||
.reports-sidebar-inner { width: min(320px, 86vw); }
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* ─── History messages ───────────────────────────────────────────────── */
|
||
|
||
.hist-msg {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
/* 76% leaves a clear 24% lateral column: at 14px chat side padding + 6px
|
||
scrollbar-gutter this guarantees the ASSISTANT bubble (align-self flex-end)
|
||
never touches the screen border, even with its outer glass shadow. */
|
||
max-width: 76%;
|
||
}
|
||
.hist-msg.user {
|
||
align-self: flex-start;
|
||
/* Hard safety margin-left: guarantees the left bubble NEVER touches the
|
||
viewport border, regardless of parent padding / cache drift. */
|
||
margin-left: 6px;
|
||
margin-right: auto;
|
||
}
|
||
.hist-msg.assistant {
|
||
align-self: flex-end;
|
||
/* Hard safety margin-right: 22px GUARANTEED between the rounded corner of
|
||
the assistant bubble and the right viewport/screen border. Always visible
|
||
even when no scrollbar is rendered (scrollbar-gutter was creating
|
||
inconsistent spacing between empty history and scrolled history). */
|
||
margin-right: 22px;
|
||
margin-left: auto;
|
||
}
|
||
.hist-msg.tool { align-self: flex-start; max-width: 100%; margin-right: 6px; margin-left: 6px; }
|
||
|
||
.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; }
|
||
|
||
/* ─── 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; }
|
||
|
||
/* Full-screen conversation on phones — feels more deliberate than a
|
||
* narrow slide-over. It already spans top-to-bottom (inset 0); make it
|
||
* span edge-to-edge too and drop the now-pointless border/shadow. */
|
||
.chat-panel-inner {
|
||
width: 100vw;
|
||
border-left: none;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
|
||
/* ─── About panel ─────────────────────────────────────────────────────────
|
||
* Opened from the (i) by the wordmark. Reuses the <dialog> .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: .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: .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: .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,.16);
|
||
transition:
|
||
transform .2s cubic-bezier(.2,.7,.2,1),
|
||
box-shadow .25s ease,
|
||
border-color .25s ease,
|
||
background .25s ease,
|
||
color .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,.55)), rgba(20,22,32,.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,.55)), rgba(20,22,32,.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,.55)), rgba(20,22,32,.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,.55)), rgba(20,22,32,.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,.55)), rgba(20,22,32,.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,.55)), rgba(20,22,32,.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,.55)), rgba(20,22,32,.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,.3),
|
||
0 0 14px -4px color-mix(in srgb, var(--brand) 55%, transparent),
|
||
inset 0 1px 0 rgba(255,255,255,.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: .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 .2s ease, color .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;
|
||
}
|
||
.hf-credit {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
/* Brand marks keep their own color — a deliberate exception to the
|
||
* monochrome rule, for the HF and Cerebras logos only. */
|
||
.hf-mark {
|
||
width: 14px;
|
||
height: 14px;
|
||
flex: none;
|
||
color: #ffd21e;
|
||
}
|
||
.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;
|
||
}
|
||
|
||
/* Captured webcam frame shown in the transcript (camera tool result). */
|
||
.hist-image {
|
||
display: block;
|
||
width: 100%;
|
||
max-width: 240px;
|
||
border-radius: var(--radius-sm);
|
||
border: 1px solid var(--border);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* ─── 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;
|
||
}
|
||
|
||
/* ─── Webcam preview (camera tool) — CURRENTLY HIDDEN BY DEFAULT ─────────────
|
||
* Floating self-view, bottom-left. Mirrored for the user; the frame sent to
|
||
* the model is drawn un-mirrored. Markup is retained for future reactivation.
|
||
* To enable it, remove hidden from cam-pip in index.html and restore the main.js listener. */
|
||
.cam-pip {
|
||
position: fixed;
|
||
left: 20px;
|
||
bottom: 20px;
|
||
width: 280px;
|
||
aspect-ratio: 4 / 3;
|
||
border-radius: var(--radius-md);
|
||
overflow: hidden;
|
||
border: 1px solid var(--border-strong);
|
||
background: var(--bg-elev);
|
||
box-shadow: var(--shadow-soft);
|
||
z-index: 90;
|
||
opacity: 0;
|
||
transform: translateY(8px) scale(0.96);
|
||
pointer-events: none;
|
||
transition: opacity 0.22s ease, transform 0.22s ease;
|
||
}
|
||
.cam-pip.visible {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
.cam-video {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transform: scaleX(-1); /* mirror the self-view only */
|
||
background: var(--bg);
|
||
}
|
||
.cam-label {
|
||
position: absolute;
|
||
left: 8px;
|
||
bottom: 6px;
|
||
font-family: var(--font-mono);
|
||
font-size: 9.5px;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--text);
|
||
opacity: 0.8;
|
||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
||
}
|
||
.cam-flash {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: #fff;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.cam-pip.flash .cam-flash {
|
||
animation: cam-flash 0.4s ease;
|
||
}
|
||
@keyframes cam-flash {
|
||
0% { opacity: 0; }
|
||
12% { opacity: 0.85; }
|
||
100% { opacity: 0; }
|
||
}
|
||
@media (max-width: 600px) {
|
||
/* Bottom-centred on phones. Auto margins centre it without touching the
|
||
* transform, so the slide/scale-in animation still works. */
|
||
.cam-pip {
|
||
left: 0;
|
||
right: 0;
|
||
margin-inline: auto;
|
||
bottom: 16px;
|
||
width: min(188px, 52vw);
|
||
}
|
||
/* The credit would sit under the centred preview, so drop it while the
|
||
* camera is on. */
|
||
body.cam-on .footer {
|
||
display: none;
|
||
}
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.cam-pip {
|
||
transition: opacity 0.22s ease;
|
||
transform: none;
|
||
}
|
||
.cam-pip.flash .cam-flash {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
/* ─── 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) {
|
||
.cam-label { font-size: 10.5px; }
|
||
|
||
.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 h3,
|
||
.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; }
|
||
.app-footer { padding: 8px 24px 10px; }
|
||
.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 { padding: 3px 12px 5px; }
|
||
.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 fetchConfig() resolves, <html> 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.
|
||
As soon as fetchConfig runs (main.js fetchConfig → root.setAttribute)
|
||
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,
|
||
html[data-theme="white"] .reports-sidebar-inner,
|
||
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;
|
||
}
|
||
/* In white theme, app column must not leave a 320px gutter for the desktop
|
||
reports-sidebar (which was the dark-theme default layout). Reset paddings. */
|
||
html[data-theme="white"] #app,
|
||
html[data-theme="white"] .app-column {
|
||
padding-left: 16px !important;
|
||
padding-right: 16px !important;
|
||
margin-left: 0 !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 <html data-theme="white"> (main.js sets the attr 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.
|
||
═══════════════════════════════════════════════════════════════════════ */
|
||
|
||
html[data-theme="white"] {
|
||
color-scheme: light;
|
||
/* ── Token overrides ─────────────────────────────────────────────── */
|
||
--bg: #ffffff;
|
||
--bg-elev: #f8fafc;
|
||
--bg-elev-2: #f1f5f9;
|
||
--border: rgba(15, 23, 42, 0.08);
|
||
--border-strong: rgba(15, 23, 42, 0.16);
|
||
--glass: rgba(255, 255, 255, 0.85);
|
||
--glass-strong: rgba(248, 250, 252, 0.92);
|
||
--text: #0f172a; /* slate-900 */
|
||
--text-dim: rgba(15, 23, 42, 0.72);
|
||
--text-faint: rgba(15, 23, 42, 0.55);
|
||
|
||
--brand: #1f6feb; /* deep corporate blue */
|
||
--brand-2: #3b82f6; /* medium blue */
|
||
--brand-3: #60a5fa; /* light blue */
|
||
--accent: #2563eb; /* primary blue */
|
||
--accent-2: #3b82f6; /* call-to-action blue */
|
||
--listening: #2563eb;
|
||
--speaking: #1d4ed8;
|
||
--processing: #d97706;
|
||
--error: #dc2626;
|
||
--success: #16a34a;
|
||
|
||
--brand-gradient: linear-gradient(135deg, #2563eb 0%, #3b82f6 55%, #60a5fa 100%);
|
||
--brand-soft-gradient: linear-gradient(135deg, rgba(37, 99, 235, 0.18),
|
||
rgba(59, 130, 246, 0.14) 55%,
|
||
rgba(96, 165, 250, 0.18));
|
||
|
||
--voice-user: #2563eb;
|
||
--voice-assistant: #64748b;
|
||
--voice-tool: #d97706;
|
||
|
||
--shadow-soft: 0 6px 20px rgba(15, 23, 42, 0.06);
|
||
--shadow-glow-brand: 0 0 28px -6px rgba(37, 99, 235, 0.45),
|
||
0 0 60px -18px rgba(59, 130, 246, 0.40);
|
||
--shadow-glass: 0 2px 12px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
|
||
|
||
--radius-sm: 10px;
|
||
--radius-md: 16px;
|
||
--radius-lg: 22px;
|
||
--radius-xl: 28px;
|
||
}
|
||
|
||
/* ── 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;
|
||
}
|
||
|
||
/* ── App column: slightly reduce reserved bottom room (footer is thinner in white theme) */
|
||
html[data-theme="white"] #app {
|
||
padding-bottom: max(100px, calc(env(safe-area-inset-bottom, 0px) + 100px));
|
||
}
|
||
|
||
/* ── Talk-unlocker (S2S_START_SESSION_MODE=TALK) — restyle for light bg */
|
||
html[data-theme="white"] .talk-unlocker {
|
||
background: linear-gradient(180deg, rgba(255,255,255,0.70) 0%, rgba(255,255,255,0.88) 100%);
|
||
}
|
||
html[data-theme="white"] .talk-unlocker-hint {
|
||
background: #ffffff;
|
||
border-color: rgba(15, 23, 42, 0.08);
|
||
color: #0f172a;
|
||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.10), inset 0 1px 0 rgba(255,255,255,1);
|
||
}
|
||
html[data-theme="white"] .talk-unlocker-spinner {
|
||
border-color: rgba(37,99,235,0.10);
|
||
border-top-color: #2563eb;
|
||
border-right-color: #3b82f6;
|
||
}
|
||
html[data-theme="white"] .talk-unlocker-title { color: #0f172a; }
|
||
html[data-theme="white"] .talk-unlocker-sub { color: rgba(15,23,42,0.65); }
|
||
|
||
/* ── Topbar — flat, no card, center brand, hamburger left, avatar right ── */
|
||
html[data-theme="white"] .topbar {
|
||
padding: 18px 18px 6px 18px;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
html[data-theme="white"] .menu-btn {
|
||
color: #0f172a;
|
||
width: 44px;
|
||
height: 44px;
|
||
padding: 0;
|
||
border-radius: 12px;
|
||
display: inline-flex !important;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0.9;
|
||
}
|
||
html[data-theme="white"] .menu-btn:hover {
|
||
background: rgba(15, 23, 42, 0.05);
|
||
opacity: 1;
|
||
}
|
||
/* ── Topbar layout (theme=white): 3-column grid ──────────────────────────
|
||
Col1 = hamburger 52 px · Col2 = brand centered · Col3 = user avatar 52 px
|
||
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;
|
||
}
|
||
html[data-theme="white"] .menu-btn {
|
||
grid-column: 1 / 2;
|
||
grid-row: 1 / 2;
|
||
justify-self: start;
|
||
align-self: center;
|
||
margin: 0 0 0 2px;
|
||
}
|
||
/* 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, #3b82f6 0%, #60a5fa 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);
|
||
}
|
||
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.60);
|
||
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;
|
||
}
|
||
|
||
/* ── Status card: ready to converse, reports and analysis ─────────────
|
||
User request 2026-09-10: hide the ready-to-converse box. Hide the
|
||
entire status card in white theme (the grey rounded banner with the
|
||
chart icon and arrow between topbar and mic stage, which the user
|
||
circled with a red X). Previously we hid only the right chevron; now
|
||
we kill the full container with the same double-guarantee pattern
|
||
used for the sidebar / bubble-stack. ──────────────────────────────── */
|
||
html[data-theme="white"] .status-card,
|
||
html[data-theme="white"] .status-card-icon,
|
||
html[data-theme="white"] .status-card-text,
|
||
html[data-theme="white"] .status-card-title,
|
||
html[data-theme="white"] .status-card-sub,
|
||
html[data-theme="white"] .status-card-arrow {
|
||
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;
|
||
}
|
||
|
||
/* ── 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(.2,.7,.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.10) 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 {
|
||
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: talkSpinnerSpin 850ms linear infinite;
|
||
margin: 4px !important;
|
||
padding: 0 !important;
|
||
background: transparent !important;
|
||
box-sizing: border-box;
|
||
}
|
||
html[data-theme="white"] .white-mic-circle.state-ai-speaking > svg.wmc-icon.wmc-stop,
|
||
html[data-theme="white"] .white-mic-circle.state-ai-speaking .wmc-icon.wmc-stop {
|
||
display: inline-flex !important;
|
||
width: 34px !important;
|
||
height: 34px !important;
|
||
margin: 7px !important;
|
||
border-radius: 4px !important;
|
||
}
|
||
/* 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: 0.0s; }
|
||
html[data-theme="white"] .white-waveform-right .wb-1 { animation-delay: -0.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; }
|
||
|
||
/* ── "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;
|
||
}
|
||
|
||
/* ── Quick actions — 3 pills (summary / data lookup / response draft).
|
||
User request 2026-09-10 "nascondi le cose in rosso": all 3 quick action
|
||
pills are circled in red → hide the entire quick-actions container. */
|
||
html[data-theme="white"] .quick-actions {
|
||
display: none !important;
|
||
}
|
||
html[data-theme="white"] .quick-action-btn {
|
||
display: none !important;
|
||
}
|
||
|
||
/* ── Chat panel: white rounded conversation card + online status pill.
|
||
CRITICAL SCROLL FIX 2026-09-10: the chat box was not scrolling.
|
||
The previous white-theme override set `flex: 0 0 auto` + `max-height: none`
|
||
which removed every vertical constraint on the chat panel → it grew beyond
|
||
the viewport edge → overflow on the *page* body instead of the chat
|
||
history scrollbox. Instead we preserve the dark-theme flex-col panel
|
||
contract (chat-panel grows into the free space, chat-panel-inner and
|
||
chat-history are flex items with min-height:0 to enable shrinking) and
|
||
we cap the whole card at 58dvh so the topbar + status + mic stage + quick
|
||
actions are always visible above it. ─────────────────────────────────── */
|
||
html[data-theme="white"] .chat-panel {
|
||
margin: 6px 18px 0;
|
||
padding: 0;
|
||
background: #ffffff;
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
border-radius: 22px;
|
||
box-shadow: var(--shadow-soft);
|
||
width: auto;
|
||
max-width: none;
|
||
/* Layout contract for scrollable chat: absorb remaining vertical room,
|
||
shrink as low as needed, cap the card for a sensible upper bound,
|
||
and hide any panel-level overflow (chat-history will own the scroll). */
|
||
flex: 1 1 auto !important;
|
||
min-height: 180px;
|
||
max-height: 58dvh !important;
|
||
max-height: 58svh !important;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
html[data-theme="white"] .chat-panel-inner {
|
||
padding: 14px 14px 12px;
|
||
/* Critical: pass the chat-panel's fixed height down into the inner so
|
||
chat-history can later compute its own flex shrink/scroll correctly. */
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
height: 100% !important;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
html[data-theme="white"] .chat-panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 2px 4px 12px;
|
||
margin: 0 0 4px;
|
||
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||
/* Header never shrinks out of existence; keep it pinned. */
|
||
flex: none;
|
||
}
|
||
html[data-theme="white"] .chat-panel-header h3 {
|
||
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;
|
||
}
|
||
/* Also force the header <hr>-like bottom border to be a thin divider (not a
|
||
thick blue underline visually sitting right below "CONVERSACIÓN" — which is
|
||
what the user's screenshot is showing as the blue underline effect). */
|
||
html[data-theme="white"] .chat-panel-header {
|
||
border-bottom-style: solid !important;
|
||
border-bottom-color: rgba(15, 23, 42, 0.06) !important;
|
||
border-bottom-width: 1px !important;
|
||
text-decoration: none !important;
|
||
}
|
||
/* Online status pill — green dot + text */
|
||
html[data-theme="white"] .chat-status-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
padding: 5px 11px 5px 8px;
|
||
background: rgba(22, 163, 74, 0.08);
|
||
border-radius: 999px;
|
||
color: #16a34a;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
flex: none;
|
||
}
|
||
html[data-theme="white"] .chat-status-dot {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 999px;
|
||
background: #16a34a;
|
||
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.14);
|
||
display: block;
|
||
flex: none;
|
||
animation: chatStatusDotPulse 2s ease-in-out infinite;
|
||
}
|
||
@keyframes chatStatusDotPulse {
|
||
0%, 100% { box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.14); }
|
||
50% { box-shadow: 0 0 0 5px rgba(22, 163, 74, 0.08); }
|
||
}
|
||
|
||
/* ── 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.10);
|
||
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;
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────
|
||
THE SCROLLABLE HISTORY BOX (root cause of the scrolling failure)
|
||
Before: max-height:none. Now it's a flex item with flex:1 1 auto inside
|
||
.chat-panel-inner, which has a bounded 100% height of its parent.
|
||
min-height:0 is MANDATORY — without it, flex column children never shrink
|
||
below their content size, and overflow-y:auto never activates. */
|
||
html[data-theme="white"] .chat-history {
|
||
padding: 12px 4px 40px !important; /* 40 px bottom air inside scroll */
|
||
gap: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1 1 auto !important;
|
||
min-height: 0 !important; /* MAGIC: enable shrink for scroll */
|
||
max-height: 100% !important;
|
||
overflow-y: auto !important;
|
||
overflow-x: hidden !important;
|
||
scrollbar-width: thin;
|
||
-webkit-overflow-scrolling: touch;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
/* ── Chat history bubbles (white theme rewrite).
|
||
The real renderer (ui/chat.js _buildMessageEl) builds rows as:
|
||
<div class="hist-msg user|assistant">
|
||
<div class="hist-role">You / Assistant</div> ← HIDE in white theme
|
||
<div class="hist-body">…message text…</div> ← restyle as flat bubble
|
||
We target those classes (not the .bubble class, which is for the floating
|
||
ephemeral stack) because the persistent conversation rows live here. ─── */
|
||
|
||
/* 1) Kill the old "You / Assistant / Tool call" uppercase mono eyebrow. The
|
||
Vocero mobile design replaces it with a name+timestamp meta line above
|
||
each bubble instead. */
|
||
html[data-theme="white"] .chat-history .hist-role,
|
||
html[data-theme="white"] .chat-history .bubble-role {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 2) Layout: each row is a flex column with room for the 36 px avatar circle
|
||
sitting outside the bubble on the role side. */
|
||
html[data-theme="white"] .chat-history .hist-msg,
|
||
html[data-theme="white"] #bubble-stack .bubble {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
max-width: calc(100% - 56px);
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
margin-top: 8px;
|
||
padding-top: 22px; /* 22 px reserved for the "Vocero 16:14" meta label above */
|
||
}
|
||
/* Anchor hist-body/bubble-body so absolute-positioned floating meta labels
|
||
(painted via ::before on the body) use the bubble itself as the reference
|
||
box instead of climbing up to chat-history. This is the ROOT FIX of the
|
||
"16:14 timestamp floats in the middle of the page" bug from the previous
|
||
screenshot. */
|
||
html[data-theme="white"] .chat-history .hist-msg .hist-body,
|
||
html[data-theme="white"] #bubble-stack .bubble .bubble-body {
|
||
position: relative;
|
||
}
|
||
|
||
/* 3) FLIP the default dark-theme alignment. Screenshot reference:
|
||
ASSISTANT = left side (blue bot avatar left of bubble, grey bubble)
|
||
USER = right side (EM/AV initials circle on RIGHT, light-blue bubble)
|
||
The default dark purple theme stores them the opposite way, so we flip
|
||
align-self AND the auto margin here. */
|
||
html[data-theme="white"] .chat-history .hist-msg.assistant,
|
||
html[data-theme="white"] #bubble-stack .bubble.assistant {
|
||
align-self: flex-start !important;
|
||
margin-right: auto !important;
|
||
margin-left: 52px !important; /* 36 avatar + 16 gap */
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.user,
|
||
html[data-theme="white"] #bubble-stack .bubble.user {
|
||
align-self: flex-end !important;
|
||
margin-left: auto !important;
|
||
margin-right: 52px !important;
|
||
}
|
||
|
||
/* 4) The actual speech bubble body.
|
||
Assistant (grey flat 200-level): #F3F4F6 rounded 18 except bottom-left corner 6px
|
||
User (light-blue 100): #DBEAFE rounded 18 except bottom-right corner 6px
|
||
Drop every original dark-theme decoration: glass backdrop, 28px deep
|
||
drop shadow, gradient borders. Keep it perfectly flat. */
|
||
html[data-theme="white"] .chat-history .hist-msg .hist-body,
|
||
html[data-theme="white"] #bubble-stack .bubble .bubble-body {
|
||
padding: 12px 15px !important;
|
||
border-radius: 18px !important;
|
||
font-size: clamp(14.5px, 3.3vw, 16.5px) !important;
|
||
line-height: 1.45 !important;
|
||
letter-spacing: -0.005em !important;
|
||
color: #0f172a !important;
|
||
background: #ffffff;
|
||
border: 1px solid transparent !important;
|
||
box-shadow: none !important;
|
||
backdrop-filter: none !important;
|
||
-webkit-backdrop-filter: none !important;
|
||
word-break: break-word;
|
||
white-space: pre-wrap;
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.assistant .hist-body,
|
||
html[data-theme="white"] #bubble-stack .bubble.assistant .bubble-body {
|
||
background: #f3f4f6 !important;
|
||
border-bottom-left-radius: 6px !important;
|
||
color: #0f172a !important;
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.user .hist-body,
|
||
html[data-theme="white"] #bubble-stack .bubble.user .bubble-body {
|
||
background: #dbeafe !important;
|
||
border-bottom-right-radius: 6px !important;
|
||
color: #0f172a !important;
|
||
}
|
||
/* 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;
|
||
}
|
||
|
||
/* 5) AVATAR circles outside each bubble (painted on the row wrapper ::before)
|
||
Assistant: blue circle + inline robot SVG, LEFT, outside bubble
|
||
User : light-blue "AV"/"EM" initials, RIGHT, outside bubble
|
||
Initials placeholder for user is pulled from data-user-initials attribute
|
||
if the chat renderer writes it; fallback is the letters "AV" (matches the
|
||
Alessandro V… user shown in the customer screenshot).
|
||
|
||
NOTE 2026-09-10: we replaced the 🤖 emoji with a tiny hand-crafted SVG
|
||
robot icon painted via background-image. The emoji rendered as a
|
||
crown-head robot on some Android versions; the SVG is pixel-perfect
|
||
cross-platform and matches the deep-blue assistant avatar. */
|
||
html[data-theme="white"] .chat-history .hist-msg::before,
|
||
html[data-theme="white"] #bubble-stack .bubble::before {
|
||
position: absolute !important;
|
||
top: 22px !important; /* line up with bubble body, below the meta line */
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 999px;
|
||
display: inline-flex !important;
|
||
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.10);
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.assistant::before,
|
||
html[data-theme="white"] #bubble-stack .bubble.assistant::before {
|
||
left: -52px;
|
||
background-color: #2563eb !important;
|
||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='7' width='18' height='12' rx='3'/><path d='M12 2v4'/><circle cx='9' cy='13' r='1.1' fill='white' stroke='none'/><circle cx='15' cy='13' r='1.1' fill='white' stroke='none'/><path d='M9 17h6'/></svg>");
|
||
background-repeat: no-repeat;
|
||
background-position: center center;
|
||
background-size: 20px 20px;
|
||
border: 1px solid rgba(59, 130, 246, 0.25);
|
||
}
|
||
/* Remove the old ::after with the robot emoji that was overlaying the avatar. */
|
||
html[data-theme="white"] .chat-history .hist-msg.assistant::after,
|
||
html[data-theme="white"] #bubble-stack .bubble.assistant::after {
|
||
display: none !important;
|
||
content: none !important;
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.user::before,
|
||
html[data-theme="white"] #bubble-stack .bubble.user::before {
|
||
right: -52px;
|
||
background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
|
||
border: 1px solid rgba(96, 165, 250, 0.35);
|
||
/* Pull initials from a custom data attribute when chat.js sets it, else show
|
||
"AV" as a generic 2-letter default that matches the customer screenshot */
|
||
content: attr(data-user-initials, "AV");
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 6) META label "name HH:MM" on the same single line, ONE pseudo-element.
|
||
──── USER FIX 2026-09-10: remove the hardcoded user name ────
|
||
The previous iteration hardcoded the Spanish names "Vocero" (assistant)
|
||
and "Emilio" (user) plus a fake "16:14" timestamp. Instead we now read
|
||
BOTH values dynamically from data attributes written by ui/chat.js at the
|
||
moment each row is inserted:
|
||
• assistant: data-asst-name + data-timestamp on .hist-body
|
||
• user : data-user-name + data-timestamp on .hist-body
|
||
Names fall back to "Vocero" / "Usuario" (Spanish generic) when chat.js
|
||
hasn't resolved a profile yet; timestamp falls back to "" which leaves
|
||
just the name visible. The concat format uses two non-breaking spaces
|
||
(\A0\A0 = U+00A0 ×2) between name and time, matching the screenshot.
|
||
|
||
──── ROOT CAUSE FIX of the previous "timestamp in the middle of the page" bug ────
|
||
The earlier implementation split the meta across TWO selectors that used
|
||
different relative reference boxes:
|
||
• `> :first-child::before` → targetted .hist-role (hidden, display:none)
|
||
with `position:absolute; top:-20px` which had NO containing block, so
|
||
the browser walked up and used chat-history (full-width) → label
|
||
ended up at left=50% → "16:14" alone in the page centre.
|
||
NEW approach:
|
||
• Use the BUBBLE body itself (.hist-body / .bubble-body) as the single
|
||
relative reference box. Paint both NAME + HH:MM in ONE `content:`
|
||
string via `.hist-body::before`. Same absolute positioning as
|
||
before, but now contained inside the width of each bubble → name and
|
||
timestamp sit *perfectly aligned* above their bubble on every row,
|
||
at any viewport width. */
|
||
html[data-theme="white"] .chat-history .hist-msg.assistant .hist-body::before,
|
||
html[data-theme="white"] #bubble-stack .bubble.assistant .bubble-body::before {
|
||
/* Assistant meta = "<real assistant name> <real HH:MM>" */
|
||
content: attr(data-asst-name, "Vocero") "\A0\A0" attr(data-timestamp, "");
|
||
position: absolute;
|
||
top: -22px;
|
||
left: 0;
|
||
width: auto;
|
||
height: 16px;
|
||
line-height: 16px;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
pointer-events: none;
|
||
display: inline-block !important;
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.user .hist-body::before,
|
||
html[data-theme="white"] #bubble-stack .bubble.user .bubble-body::before {
|
||
/* User meta = "<real user name> <real HH:MM>"
|
||
Falls back to "Usuario" when no caller profile has been injected yet,
|
||
never "Emilio" — user explicitly flagged the hard-coded name as wrong. */
|
||
content: attr(data-user-name, "Usuario") "\A0\A0" attr(data-timestamp, "");
|
||
position: absolute;
|
||
top: -22px;
|
||
right: 0;
|
||
width: auto;
|
||
height: 16px;
|
||
line-height: 16px;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
letter-spacing: -0.01em;
|
||
white-space: nowrap;
|
||
pointer-events: none;
|
||
text-align: right;
|
||
display: inline-block !important;
|
||
}
|
||
/* Tool-call rows: shrink to pale inline card with no avatar */
|
||
html[data-theme="white"] .chat-history .hist-msg.tool,
|
||
html[data-theme="white"] .chat-history .bubble.tool {
|
||
max-width: 100% !important;
|
||
padding-top: 0;
|
||
margin: 6px 0 !important;
|
||
align-self: center !important;
|
||
}
|
||
html[data-theme="white"] .chat-history .hist-msg.tool::before,
|
||
html[data-theme="white"] .chat-history .bubble.tool::before,
|
||
html[data-theme="white"] .chat-history .hist-msg.tool::after,
|
||
html[data-theme="white"] .chat-history .bubble.tool::after {
|
||
display: none !important;
|
||
content: none;
|
||
}
|
||
/* Also strip the floating meta from tool rows (they don't have a human role). */
|
||
html[data-theme="white"] .chat-history .hist-msg.tool .hist-body::before {
|
||
content: none !important;
|
||
display: none !important;
|
||
}
|
||
|
||
/* ── Chat form (input footer bar): clip · rounded input · blue send ────── */
|
||
html[data-theme="white"] .chat-form {
|
||
display: flex !important;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 6px 6px 6px 6px;
|
||
background: #ffffff;
|
||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||
border-radius: 999px;
|
||
margin: 4px 0 0;
|
||
box-shadow: var(--shadow-soft);
|
||
/* Keep the input row pinned at the bottom of the chat card header; never
|
||
let the history (flex:1) squeeze the form out of the panel. */
|
||
flex: none !important;
|
||
}
|
||
html[data-theme="white"] .chat-form[hidden],
|
||
html[data-theme="white"] .chat-form[style*="display:none"] {
|
||
display: flex !important;
|
||
}
|
||
html[data-theme="white"] .chat-attach-btn {
|
||
flex: none;
|
||
width: 44px;
|
||
height: 44px;
|
||
padding: 0;
|
||
border-radius: 999px;
|
||
color: #64748b;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
html[data-theme="white"] .chat-attach-btn:hover {
|
||
color: #2563eb;
|
||
background: rgba(37, 99, 235, 0.08);
|
||
}
|
||
html[data-theme="white"] .chat-input {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
height: 44px;
|
||
padding: 0 10px 0 6px;
|
||
margin: 0;
|
||
background: transparent !important;
|
||
border: none !important;
|
||
color: #0f172a;
|
||
font-size: clamp(14px, 3.2vw, 16px);
|
||
border-radius: 0;
|
||
box-shadow: none !important;
|
||
outline: none !important;
|
||
}
|
||
html[data-theme="white"] .chat-input::placeholder {
|
||
color: rgba(15, 23, 42, 0.48);
|
||
}
|
||
html[data-theme="white"] .chat-input:focus {
|
||
border-color: transparent;
|
||
box-shadow: none;
|
||
outline: none;
|
||
}
|
||
/* Hide original square send button in white theme */
|
||
html[data-theme="white"] .chat-form .chat-send-btn {
|
||
display: none !important;
|
||
}
|
||
/* New big round blue send button with paper airplane */
|
||
html[data-theme="white"] .chat-send-btn-round {
|
||
flex: none;
|
||
width: 48px;
|
||
height: 48px;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 999px;
|
||
background: linear-gradient(135deg, #2563eb, #3b82f6);
|
||
color: #ffffff;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||
transition: transform 140ms ease, filter 140ms ease;
|
||
}
|
||
html[data-theme="white"] .chat-send-btn-round:hover {
|
||
transform: translateY(-1px);
|
||
filter: brightness(1.05);
|
||
}
|
||
html[data-theme="white"] .chat-send-btn-round svg {
|
||
width: 22px;
|
||
height: 22px;
|
||
stroke-width: 2.2;
|
||
fill: #ffffff;
|
||
stroke: #ffffff;
|
||
}
|
||
|
||
/* ── App footer: thin white bar, just a subtle separator + brand line ─── */
|
||
html[data-theme="white"] .app-footer {
|
||
background: #ffffff;
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
padding-top: 10px;
|
||
padding-bottom: max(10px, env(safe-area-inset-bottom, 0px));
|
||
}
|
||
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.60);
|
||
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.10);
|
||
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.10);
|
||
}
|
||
html[data-theme="white"] .btn.secondary:hover {
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
/* ── Reports sidebar restyle (the off-canvas panel + left sidebar) ───── */
|
||
html[data-theme="white"] .reports-sidebar,
|
||
html[data-theme="white"] .reports-sidebar-inner {
|
||
background: #ffffff;
|
||
color: #0f172a;
|
||
border-color: rgba(15, 23, 42, 0.08);
|
||
}
|
||
html[data-theme="white"] .reports-sidebar-header {
|
||
border-bottom-color: rgba(15, 23, 42, 0.08);
|
||
color: #0f172a;
|
||
}
|
||
html[data-theme="white"] .reports-sidebar-header h3 { color: #0f172a; }
|
||
|
||
/* ── Limit modal (budget) ───────────────────────────────────────────── */
|
||
html[data-theme="white"] .limit-card {
|
||
background: #ffffff;
|
||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||
}
|
||
html[data-theme="white"] .limit-title,
|
||
html[data-theme="white"] .limit-msg { color: #0f172a; }
|
||
html[data-theme="white"] .limit-note { color: rgba(15, 23, 42, 0.62); }
|
||
|
||
/* ── 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.20);
|
||
}
|
||
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"] .status-card { margin: 10px 14px 4px; }
|
||
html[data-theme="white"] .chat-panel { margin: 4px 14px 0; }
|
||
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"] .status-card { padding: 10px; }
|
||
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-mic-circle.state-ai-speaking .wmc-stop { width: 30px; height: 30px; }
|
||
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); }
|
||
}
|