: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.7); --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.3) 0%, transparent 60%), radial-gradient(900px 700px at 110% 10%, rgba(236, 72, 153, 0.22) 0%, transparent 55%), radial-gradient(1000px 800px at 50% 120%, rgba(34, 211, 238, 0.22) 0%, transparent 60%), radial-gradient(ellipse at 50% 50%, #0e1019 0%, var(--bg) 65%); background-attachment: fixed; } body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; background-image: radial-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px), radial-gradient(rgba(139, 92, 246, 0.035) 1px, transparent 1px); background-size: 42px 42px, 42px 42px; background-position: 0 0, 21px 21px; mix-blend-mode: screen; opacity: 0.55; mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.4) 70%, transparent 100%); } body::after { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; background: radial-gradient(600px 600px at 20% 80%, rgba(139, 92, 246, 0.1), transparent 70%), radial-gradient(500px 500px at 80% 20%, rgba(34, 211, 238, 0.08), transparent 70%); animation: auroraShift 22s ease-in-out infinite alternate; } @keyframes auroraShift { 0% { transform: translate3d(0, 0, 0) scale(1); opacity: 0.9; } 50% { transform: translate3d(3%, -2%, 0) scale(1.08); opacity: 1; } 100% { transform: translate3d(-2%, 2%, 0) scale(1.02); opacity: 0.85; } } /* ─── 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 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(0.2, 0.9, 0.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 ; 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, 0.5)); } /* ─── Topbar ──────────────────────────────────────────────────────────── */ /* Topbar is just a floating row of controls over the stage - no * separator line, no background. Same story for the footer. Keeps the * app feeling like one continuous canvas. */ .topbar { display: flex; /* Top-align so the right control cluster sits up with the title instead of * floating to the vertical middle of the tall identity block. */ align-items: flex-start; justify-content: space-between; /* Before 2026-09-04: padding-top 10px. Was clipped on mobile because the rounded bezel of the phone + OS notch/inset sits on top. now reserves safe-area-inset-top (10px minimum on desktop) and we push the topbar itself another 6px down so the brand glass card rounded corners are fully visible, not half-painted behind the screen bezel. */ padding: 16px 16px 0 16px; margin-top: max(6px, calc(env(safe-area-inset-top, 0px) * 0.2)); /* Default stacking (below the conversation panel, z 200) so the panel drawer * covers the controls when it's open. */ position: relative; z-index: 2; gap: 8px; flex-wrap: wrap; flex: 0 0 auto; /* never let the topbar shrink/stretch inside flex col */ /* Prevent brand card edge from touching the right screen bezel on mobile: box-sizing so padding is taken inside the topbar width. */ width: 100%; box-sizing: border-box; } .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(0.2, 0.7, 0.2, 1), filter 0.35s ease; } .brand:hover .brand-logo { transform: translateY(-2px) rotate(-4deg) scale(1.04); filter: drop-shadow(0 4px 20px color-mix(in srgb, var(--brand-2) 70%, transparent)); } .ident-title { font-weight: 700; font-size: 20px; letter-spacing: -0.01em; line-height: 1.1; background: var(--brand-gradient); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.25)); display: block; /* Previously a stacked two-line block had a small bottom spacer; now the * title and avatar share the row → we don't need it any more. */ margin-bottom: 0; transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1), filter 0.3s ease; } .brand:hover .ident-title { filter: drop-shadow(0 2px 10px color-mix(in srgb, var(--brand-2) 45%, transparent)); transform: translateY(-0.5px); } /* ── User avatar circle (replaces the old user-name text) ── * - perfectly round badge, matches the 42px icon-btn size in the topbar * - glass + brand gradient 1px border * - two uppercase initials (Alessandro Valenti → AV), bold white * - hovering shows the full name via HTML title="" (still accessible), and * lifts slightly with a soft brand glow like the topbar icon buttons. */ .ident-user-name { position: relative; flex: none; width: 40px; height: 40px; aspect-ratio: 1 / 1; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 1px solid color-mix(in srgb, var(--brand) 32%, var(--border)); color: #fff; background: radial-gradient(120% 120% at 30% 20%, rgba(255, 255, 255, 0.18), transparent 55%), linear-gradient( 135deg, color-mix(in srgb, var(--brand) 72%, transparent), color-mix(in srgb, var(--brand-2) 60%, transparent) 55%, color-mix(in srgb, var(--brand-3) 55%, transparent) ); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05) inset, 0 1px 0 rgba(255, 255, 255, 0.08) inset, 0 2px 10px rgba(0, 0, 0, 0.24), 0 0 22px -8px color-mix(in srgb, var(--brand) 58%, transparent); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); font-family: var(--font-sans); font-weight: 800; font-size: 14px; line-height: 1; letter-spacing: 0.05em; text-transform: uppercase; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); user-select: none; cursor: pointer; transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.3s ease, border-color 0.3s ease; } .ident-user-name::before { /* Subtle 1px highlight rim on the top-left quadrant. */ content: ""; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 42%); opacity: 0.9; pointer-events: none; mix-blend-mode: screen; } .ident-user-name:hover { transform: translateY(-1.5px) scale(1.02); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.07) inset, 0 1px 0 rgba(255, 255, 255, 0.1) inset, 0 6px 22px rgba(0, 0, 0, 0.32), 0 0 34px -6px color-mix(in srgb, var(--brand-2) 70%, transparent); border-color: color-mix(in srgb, var(--brand-2) 55%, var(--border)); } /* Accessibility: keyboard-focusable style if ever wrapped in a