Radha / frontend /style.css
Coderadi's picture
first commit
521f25e
/* ================================================================
N.Y.R.A FRONTEND β€” Dark Glass UI
================================================================
DESIGN SYSTEM OVERVIEW
----------------------
This stylesheet powers a single-page AI chat assistant with a
futuristic, dark "glass-morphism" aesthetic. Key design pillars:
1. DARK THEME β€” Near-black background (#050510) with layered
semi-transparent surfaces. All colour is delivered through
translucent whites and a purple/teal accent palette.
2. GLASS-MORPHISM β€” Panels use `backdrop-filter: blur()` to
create a frosted-glass look, letting a decorative animated
"orb" glow through from behind.
3. CSS CUSTOM PROPERTIES β€” Every shared colour, radius, timing
function, and font is stored in :root variables so the entire
theme can be adjusted from one place.
4. LAYOUT β€” A full-viewport flex column: Header β†’ Chat β†’ Input.
The animated orb sits behind everything with `position: fixed`.
5. RESPONSIVE β€” Two breakpoints (768 px tablets, 480 px phones)
progressively hide decorative elements and tighten spacing
while preserving usability. iOS safe-area insets are honoured.
FILE STRUCTURE (top β†’ bottom):
β€’ CSS Custom Properties (:root)
β€’ Reset / Base
β€’ Glass Panel utility class
β€’ App Layout shell
β€’ Orb (animated background decoration)
β€’ Header (logo, mode switch, status badge, new-chat button)
β€’ Chat Area (message list, welcome screen, message bubbles,
typing indicator, streaming cursor)
β€’ Input Bar (textarea, action buttons β€” mic, TTS, send)
β€’ Scrollbar customisation
β€’ Keyframe Animations
β€’ Responsive Breakpoints
================================================================ */
/* ================================================================
CSS CUSTOM PROPERTIES (Design Tokens)
================================================================
Everything that might be reused or tweaked lives here.
Changing a single variable updates the whole UI consistently.
================================================================ */
:root {
/* ---- Backgrounds ---- */
--bg: #050510; /* Page-level dark background */
--glass-bg: rgba(10, 10, 28, 0.72); /* Semi-transparent fill for glass panels (header, input bar) */
--glass-border: rgba(255, 255, 255, 0.06); /* Subtle white border that outlines glass panels */
--glass-hover: rgba(255, 255, 255, 0.10); /* Slightly brighter fill on hover */
/* ---- Accent colours ---- */
--accent: #7c6aef; /* Primary purple accent β€” buttons, highlights, glows */
--accent-glow: rgba(124, 106, 239, 0.35); /* Soft purple used for box-shadows / focus rings */
--accent-secondary: #4ecdc4; /* Teal complement β€” used in gradients alongside --accent */
/* ---- Text ---- */
--text: rgba(255, 255, 255, 0.93); /* Primary readable text β€” near-white */
--text-dim: rgba(255, 255, 255, 0.50); /* Secondary / de-emphasised text */
--text-muted: rgba(255, 255, 255, 0.28); /* Tertiary β€” labels, meta info, placeholders */
/* ---- Semantic colours ---- */
--danger: #ff6b6b; /* Destructive / recording state (mic listening) */
--success: #51cf66; /* Online status, success feedback */
/* ---- Border radii ---- */
--radius: 16px; /* Large radius β€” panels, bubbles */
--radius-sm: 10px; /* Medium radius β€” buttons, avatars */
--radius-xs: 6px; /* Small radius β€” notched bubble corners */
/* ---- Layout ---- */
--header-h: 60px; /* Fixed header height β€” used to reserve space */
/* ---- Motion ---- */
--transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
/* Shared easing curve (Material "standard" ease) for all micro-interactions.
Starts slow, accelerates, then decelerates for a natural feel. */
/* ---- Typography ---- */
--font: 'Poppins', -apple-system, BlinkMacSystemFont, sans-serif;
/* Poppins as primary; system fonts as fallback for fast initial render. */
}
/* ================================================================
RESET & BASE STYLES
================================================================
A minimal "universal reset" that strips browser defaults so
every element starts from zero. `box-sizing: border-box` makes
padding/border count inside the declared width/height β€” the most
intuitive model for layout work.
================================================================ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
/* Full viewport height; overflow hidden because the chat area
manages its own scrolling internally. */
html, body { height: 100%; overflow: hidden; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased; /* Smoother font rendering on macOS/iOS WebKit */
-webkit-tap-highlight-color: transparent; /* Removes the blue tap flash on mobile WebKit */
}
/* Reset native button / textarea styling so we control everything */
button { font-family: var(--font); cursor: pointer; border: none; background: none; color: inherit; }
textarea { font-family: var(--font); color: var(--text); }
/* ================================================================
GLASS PANEL β€” Reusable Utility Class
================================================================
The signature "frosted glass" look. Applied to the header and
input bar (any element that needs a translucent panel).
HOW IT WORKS:
β€’ `background` β€” a dark, semi-transparent fill (72 % opacity).
β€’ `backdrop-filter: blur(32px) saturate(1.2)` β€” blurs whatever
is *behind* the element (the orb glow, the chat) and slightly
boosts colour saturation for a richer look.
β€’ `-webkit-backdrop-filter` β€” Safari still needs the prefix.
β€’ `border` β€” a faint 6 %-white hairline that catches light at
the edges, reinforcing the glass illusion.
================================================================ */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(32px) saturate(1.2);
-webkit-backdrop-filter: blur(32px) saturate(1.2);
border: 1px solid var(--glass-border);
}
/* ================================================================
APP LAYOUT SHELL
================================================================
The top-level `.app` container is a vertical flex column that
fills the entire viewport: Header (fixed) β†’ Chat (grows) β†’ Input
(fixed).
`100dvh` (dynamic viewport height) is the modern replacement for
`100vh` on mobile browsers β€” it accounts for the URL bar sliding
in and out. The plain `100vh` above it is a fallback for older
browsers that don't understand `dvh`.
================================================================ */
.app {
position: relative;
display: flex;
flex-direction: column;
height: 100vh; /* Fallback for browsers without dvh support */
height: 100dvh; /* Preferred: adjusts for mobile browser chrome */
overflow: hidden;
}
/* ================================================================
ORB BACKGROUND β€” Animated Decorative Element
================================================================
The "orb" is a large, softly-glowing circle (rendered by JS /
canvas inside #orb-container) that sits dead-centre behind all
content. It provides ambient motion and reacts to AI state.
POSITIONING:
β€’ `position: fixed` + `top/left 50%` + `translate -50% -50%`
centres it in the viewport regardless of scroll.
β€’ `min(600px, 80vw)` β€” caps the orb at 600 px but lets it shrink
on small screens so it never overflows.
β€’ `z-index: 0` β€” behind everything; content layers sit above.
β€’ `pointer-events: none` β€” clicks pass straight through.
β€’ `opacity: 0.35` β€” subtle by default; it brightens on activity.
================================================================ */
#orb-container {
position: fixed;
top: 50%;
left: 50%;
translate: -50% -50%;
width: min(600px, 80vw);
height: min(600px, 80vw);
z-index: 0;
pointer-events: none;
opacity: 0.35;
transition: opacity 0.5s ease, transform 0.5s ease;
}
/* ORB ACTIVE STATES
When the AI is actively processing (.active) or speaking aloud
(.speaking), the orb ramps to full opacity and plays a gentle
breathing scale animation (orbPulse) so the user sees the AI
is "alive". */
#orb-container.active,
#orb-container.speaking {
opacity: 1;
animation: orbPulse 1.6s ease-in-out infinite;
}
/* No overlay/scrim on the orb β€” the orb is the only background effect.
Previously a radial gradient darkened the edges; removed so only the
central orb remains visible without circular shades. */
/* ================================================================
HEADER
================================================================
A horizontal flex row pinned to the top of the app.
LAYOUT:
β€’ `justify-content: space-between` pushes left group (logo) and
right group (status / new-chat) to opposite edges; the mode
switch sits in the centre via the gap.
β€’ `z-index: 10` ensures the header floats above the chat area
and the orb scrim.
β€’ Bottom border-radius rounds only the lower corners, creating
a "floating shelf" look that separates it from chat content.
β€’ `flex-shrink: 0` prevents the header from collapsing when the
chat area needs space.
================================================================ */
.header {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
height: var(--header-h);
padding: 0 20px;
border-radius: 0 0 var(--radius) var(--radius);
border-top: none;
flex-shrink: 0;
}
/* HEADER LEFT β€” Logo + Tagline
`align-items: baseline` aligns the tall logo text and the
smaller tagline along their text baselines. */
.header-left { display: flex; align-items: baseline; gap: 10px; }
/* LOGO
Gradient text effect: a linear gradient is painted as the
background, then `background-clip: text` masks it to only show
through the letter shapes. `-webkit-text-fill-color: transparent`
makes the original text colour invisible so the gradient shows. */
.logo {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: 3px;
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* TAGLINE β€” small muted descriptor beneath / beside the logo */
.tagline {
font-size: 0.68rem;
font-weight: 300;
color: var(--text-muted);
letter-spacing: 0.5px;
}
/* ----------------------------------------------------------------
MODE SWITCH β€” Chat / Voice Toggle
----------------------------------------------------------------
A pill-shaped toggle with two buttons and a sliding highlight.
STRUCTURE:
β€’ `.mode-switch` β€” the outer pill (flex row, dark bg, rounded).
β€’ `.mode-slider` β€” an absolutely-positioned coloured rectangle
that slides left↔right to indicate the active mode.
β€’ `.mode-btn` β€” individual clickable labels ("Chat", "Voice").
The slider width is `calc(50% - 4px)` β€” half the pill minus
the padding β€” so it exactly covers one button. When `.right` is
added (by JS), `translateX(calc(100% + 2px))` shifts it over
to highlight the second button.
---------------------------------------------------------------- */
.mode-switch {
position: relative;
display: flex;
background: rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 3px;
gap: 2px;
}
.mode-slider {
position: absolute;
top: 3px;
left: 3px;
width: calc(50% - 4px); /* Exactly covers one button */
height: calc(100% - 6px); /* Full height minus top+bottom padding */
background: var(--accent);
border-radius: 10px;
transition: transform var(--transition);
opacity: 0.18; /* Tinted, not solid β€” keeps it subtle */
}
.mode-slider.right {
transform: translateX(calc(100% + 2px)); /* Slide to the second button */
}
.mode-btn {
position: relative;
z-index: 1; /* Above the slider background */
display: flex;
align-items: center;
gap: 6px;
padding: 7px 16px;
font-size: 0.76rem;
font-weight: 500;
border-radius: 10px;
color: var(--text-dim);
transition: color var(--transition);
white-space: nowrap; /* Prevents label from wrapping at narrow widths */
}
.mode-btn.active { color: var(--text); } /* Active mode gets full-white text */
.mode-btn svg { opacity: 0.7; } /* Dim icon by default */
.mode-btn.active svg { opacity: 1; } /* Full opacity when active */
/* ----------------------------------------------------------------
HEADER RIGHT β€” Status Badge & Utility Buttons
---------------------------------------------------------------- */
.header-right { display: flex; align-items: center; gap: 10px; }
/* STATUS BADGE β€” shows a coloured dot + "Online" / "Offline" label */
.status-badge {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
font-weight: 400;
color: var(--text-dim);
}
/* STATUS DOT
A small circle with a coloured glow (box-shadow). The `pulse-dot`
animation fades it in and out to convey a "heartbeat" while online. */
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 6px var(--success);
animation: pulse-dot 2s ease-in-out infinite;
}
/* When the server is unreachable, switch to red and stop pulsing */
.status-dot.offline {
background: var(--danger);
box-shadow: 0 0 6px var(--danger);
animation: none;
}
/* ICON BUTTON β€” generic small square button (e.g. "New Chat").
`display: grid; place-items: center` is the quickest way to
perfectly centre a single child (the SVG icon). */
.btn-icon {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
transition: background var(--transition), border-color var(--transition);
}
.btn-icon:hover {
background: var(--glass-hover);
border-color: rgba(255, 255, 255, 0.14);
}
/* ================================================================
CHAT AREA
================================================================
The scrollable middle section between header and input bar.
`flex: 1` makes it absorb all remaining vertical space.
The inner `.chat-messages` div does the actual scrolling
(`overflow-y: auto`) so the header and input bar stay fixed.
`scroll-behavior: smooth` gives programmatic scrollTo() calls
a gentle animation.
================================================================ */
.chat-area {
position: relative;
z-index: 5;
flex: 1;
overflow: hidden; /* Outer container clips; inner scrolls */
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow-y: auto; /* Vertical scroll when messages overflow */
overflow-x: hidden;
padding: 20px 20px;
display: flex;
flex-direction: column; /* Messages stack top→bottom */
gap: 6px; /* Consistent spacing between messages */
scroll-behavior: smooth;
}
/* ----------------------------------------------------------------
WELCOME SCREEN
----------------------------------------------------------------
Shown when the conversation is empty. A vertically & horizontally
centred splash with a title, subtitle, and suggestion chips.
`flex: 1` + centering fills the entire chat area.
`fadeIn` animation slides it up gently on first load.
---------------------------------------------------------------- */
.welcome-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
flex: 1;
gap: 12px;
padding: 40px 20px;
animation: fadeIn 0.6s ease;
}
.welcome-icon {
color: var(--accent);
opacity: 0.5;
margin-bottom: 6px;
}
/* Same gradient-text technique as the logo */
.welcome-title {
font-size: 1.7rem;
font-weight: 600;
background: linear-gradient(135deg, var(--text), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.welcome-sub {
font-size: 0.9rem;
color: var(--text-dim);
font-weight: 300;
}
/* SUGGESTION CHIPS β€” quick-tap prompts */
.welcome-chips {
display: flex;
flex-wrap: wrap; /* Wraps to multiple rows on narrow screens */
justify-content: center;
gap: 8px;
margin-top: 18px;
}
.chip {
padding: 8px 18px;
font-size: 0.76rem;
font-weight: 400;
border-radius: 20px; /* Fully rounded pill shape */
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
color: var(--text-dim);
transition: all var(--transition);
}
.chip:hover {
background: var(--accent);
color: #fff;
border-color: var(--accent);
transform: translateY(-1px); /* Subtle "lift" effect on hover */
}
/* ================================================================
MESSAGE BUBBLES
================================================================
Each message is a horizontal flex row: avatar + body.
`max-width: 760px` + `margin: 0 auto` centres the conversation
in a readable column on wide screens.
User vs. Assistant differentiation:
β€’ `.message.user` reverses the flex direction so the avatar
appears on the right.
β€’ Background colours differ: assistant is neutral white-tint,
user is purple-tinted (matching --accent).
β€’ One corner of each bubble is given a smaller radius to create
a "speech bubble notch" that points toward the avatar.
================================================================ */
.message {
display: flex;
gap: 10px;
max-width: 760px;
width: 100%;
margin: 0 auto;
animation: msgIn 0.3s ease; /* Slide-up entrance for each new message */
}
.message.user { flex-direction: row-reverse; } /* Avatar on the right for user */
/* MESSAGE AVATAR β€” small icon square beside each bubble */
.msg-avatar {
width: 30px;
height: 30px;
border-radius: 10px;
display: grid;
place-items: center;
font-size: 0.7rem;
font-weight: 600;
flex-shrink: 0; /* Never let the avatar shrink */
margin-top: 4px; /* Align with the first line of text */
}
/* SVG icon inside avatar β€” sized to fit the circle, inherits color from parent */
.msg-avatar .msg-avatar-icon {
width: 18px;
height: 18px;
}
/* Assistant avatar: purple→teal gradient to match the brand */
.message.assistant .msg-avatar {
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
color: #fff;
}
/* User avatar: neutral dark chip */
.message.user .msg-avatar {
background: rgba(255, 255, 255, 0.08);
color: var(--text-dim);
}
/* MSG-BODY β€” column wrapper for label + content bubble.
`min-width: 0` is a flex-child fix that allows long words to
trigger `word-wrap: break-word` instead of overflowing. */
.msg-body {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
/* MSG-CONTENT β€” the actual text bubble */
.msg-content {
padding: 11px 15px;
border-radius: var(--radius);
font-size: 0.87rem;
line-height: 1.65; /* Generous line-height for readability */
font-weight: 400;
word-wrap: break-word;
white-space: pre-wrap; /* Preserves newlines from the AI response */
}
/* Assistant bubble: neutral grey-white tint, notch top-left */
.message.assistant .msg-content {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.07);
border-top-left-radius: var(--radius-xs); /* Notch pointing toward avatar */
}
/* User bubble: purple-tinted, notch top-right */
.message.user .msg-content {
background: rgba(124, 106, 239, 0.13);
border: 1px solid rgba(124, 106, 239, 0.16);
border-top-right-radius: var(--radius-xs); /* Notch pointing toward avatar */
}
/* MSG-LABEL β€” tiny "RADHA" / "You" text above the bubble */
.msg-label {
font-size: 0.66rem;
font-weight: 500;
color: var(--text-muted);
padding: 0 4px;
}
.message.user .msg-label { text-align: right; } /* Right-align label for user */
/* ----------------------------------------------------------------
TYPING INDICATOR β€” Three Bouncing Dots
----------------------------------------------------------------
Displayed in an assistant message while waiting for a response.
Three <span> dots animate with staggered delays (0 β†’ 0.15 β†’ 0.3s)
to create a wave-like bounce.
---------------------------------------------------------------- */
.typing-dots {
display: inline-flex;
gap: 4px;
padding: 4px 0;
}
.typing-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-dim);
animation: dotBounce 1.2s ease-in-out infinite;
}
.typing-dots span:nth-child(2) { animation-delay: 0.15s; } /* Second dot lags slightly */
.typing-dots span:nth-child(3) { animation-delay: 0.3s; } /* Third dot lags more */
/* STREAMING CURSOR β€” blinking pipe character appended while the AI
streams its response token-by-token. */
.stream-cursor {
animation: blink 0.8s step-end infinite;
color: var(--accent);
margin-left: 1px;
}
/* ================================================================
INPUT BAR
================================================================
Pinned to the bottom of the app. Like the header, it uses the
glass-panel class for the frosted look.
iOS SAFE-AREA HANDLING:
`padding-bottom: max(10px, env(safe-area-inset-bottom, 10px))`
ensures the input never hides behind the iPhone home-indicator
bar. `env(safe-area-inset-bottom)` is a CSS environment variable
injected by WebKit on notched iPhones; the `max()` guarantees
at least 10 px even on devices without a home bar.
`flex-shrink: 0` prevents the input bar from being squished when
the chat area grows.
================================================================ */
.input-bar {
position: relative;
z-index: 10;
padding: 10px 20px 10px;
padding-bottom: max(10px, env(safe-area-inset-bottom, 10px));
border-radius: var(--radius) var(--radius) 0 0; /* Top corners rounded */
border-bottom: none;
flex-shrink: 0;
}
/* INPUT WRAPPER β€” the rounded pill that holds textarea + buttons.
`align-items: flex-end` keeps action buttons bottom-aligned when
the textarea grows taller (multi-line input). */
.input-wrapper {
display: flex;
align-items: flex-end;
gap: 6px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 5px 5px 5px 14px;
transition: border-color var(--transition), box-shadow var(--transition);
}
/* Focus ring: purple border + subtle outer glow when typing */
.input-wrapper:focus-within {
border-color: rgba(124, 106, 239, 0.35);
box-shadow: 0 0 0 3px rgba(124, 106, 239, 0.08);
}
/* TEXTAREA β€” auto-growing text input (height controlled by JS).
`resize: none` disables the browser's drag-to-resize handle.
`max-height: 120px` caps growth so it doesn't consume the screen. */
.input-wrapper textarea {
flex: 1;
background: none;
border: none;
outline: none;
resize: none;
font-size: 0.87rem;
line-height: 1.5;
padding: 8px 0;
max-height: 120px;
color: var(--text);
}
.input-wrapper textarea::placeholder { color: var(--text-muted); }
/* ACTION BUTTONS ROW β€” sits to the right of the textarea */
.input-actions {
display: flex;
gap: 6px;
padding-bottom: 2px; /* Micro-nudge to visually centre with one-line textarea */
flex-shrink: 0;
}
/* ----------------------------------------------------------------
ACTION BUTTON β€” Base Style (Mic, TTS, Send)
----------------------------------------------------------------
All three input buttons share this base: a fixed-size square
with rounded corners and a subtle background. `display: grid;
place-items: center` perfectly centres the SVG icon.
---------------------------------------------------------------- */
.action-btn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
min-width: 38px; /* Prevents flex from shrinking the button */
border-radius: 10px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all var(--transition);
color: var(--text-dim);
flex-shrink: 0;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.16);
color: var(--text);
transform: translateY(-1px); /* Lift effect */
}
.action-btn:active {
transform: translateY(0); /* Press-down snap back */
}
/* ----------------------------------------------------------------
SEND BUTTON β€” Accent-Coloured Call-to-Action
----------------------------------------------------------------
Uses `!important` to override the generic `.action-btn` styles
because both selectors have the same specificity. This is the
only button that's always visually prominent (purple fill).
---------------------------------------------------------------- */
.send-btn {
background: var(--accent) !important;
border-color: var(--accent) !important;
color: #fff !important;
box-shadow: 0 2px 8px rgba(124, 106, 239, 0.25); /* Purple underglow */
}
.send-btn:hover {
background: #6a58e0 !important; /* Slightly darker purple on hover */
border-color: #6a58e0 !important;
box-shadow: 0 4px 14px rgba(124, 106, 239, 0.35); /* Stronger glow */
}
/* Disabled state: greyed out, no glow, no cursor, no lift */
.send-btn:disabled {
opacity: 0.4;
cursor: default;
box-shadow: none;
transform: none;
}
/* ----------------------------------------------------------------
MIC BUTTON β€” Default + Listening States
----------------------------------------------------------------
Two SVG icons live inside the button; only one is visible at a
time via `display: none` toggling.
DEFAULT: muted grey square (inherits .action-btn).
LISTENING (.listening): red-tinted background + border + danger
colour text, plus a pulsing red ring animation (micPulse) to
convey "recording in progress".
---------------------------------------------------------------- */
.mic-btn .mic-icon-active { display: none; } /* Hidden when NOT listening */
.mic-btn.listening .mic-icon { display: none; } /* Hide default icon */
.mic-btn.listening .mic-icon-active { display: block; } /* Show active icon */
.mic-btn.listening {
background: rgba(255, 107, 107, 0.18); /* Red-tinted fill */
border-color: rgba(255, 107, 107, 0.3);
color: var(--danger);
animation: micPulse 1.5s ease-in-out infinite; /* Expanding red ring */
}
/* ----------------------------------------------------------------
TTS (TEXT-TO-SPEECH) BUTTON β€” Default + Active + Speaking States
----------------------------------------------------------------
Similar icon-swap pattern to the mic button.
DEFAULT: muted grey (inherits .action-btn). Speaker-off icon.
ACTIVE (.tts-active): TTS is enabled β€” purple tint to show it's
toggled on. Speaker-on icon.
SPEAKING (.tts-speaking): TTS is currently playing audio β€”
pulsing purple ring (ttsPulse) for visual feedback.
---------------------------------------------------------------- */
.tts-btn .tts-icon-on { display: none; } /* Hidden when TTS is off */
.tts-btn.tts-active .tts-icon-off { display: none; } /* Hide "off" icon */
.tts-btn.tts-active .tts-icon-on { display: block; } /* Show "on" icon */
.tts-btn.tts-active {
background: rgba(124, 106, 239, 0.18); /* Purple-tinted fill */
border-color: rgba(124, 106, 239, 0.3);
color: var(--accent);
}
.tts-btn.tts-speaking {
animation: ttsPulse 1.5s ease-in-out infinite; /* Expanding purple ring */
}
/* INPUT META β€” small row below the input showing mode label + hints */
.input-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 8px 0;
font-size: 0.66rem;
color: var(--text-muted);
}
.mode-label { font-weight: 500; }
/* ================================================================
SEARCH RESULTS WIDGET (Realtime β€” Tavily data)
================================================================
Fixed panel on the right: query, AI answer, source cards. Themed
scrollbars, responsive width, no overflow or layout bugs.
================================================================ */
.search-results-widget {
position: fixed;
top: 0;
right: 0;
width: min(380px, 95vw);
min-width: 0;
max-height: 100vh;
height: 100%;
z-index: 20;
display: flex;
flex-direction: column;
border-radius: var(--radius) 0 0 var(--radius);
border-right: none;
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.4);
overflow: hidden;
transform: translateX(100%);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-results-widget.open {
transform: translateX(0);
}
.search-results-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--glass-border);
flex-shrink: 0;
}
.search-results-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.search-results-title::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 8px var(--success);
animation: pulse-dot 2s ease-in-out infinite;
flex-shrink: 0;
}
.search-results-close {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border);
color: var(--text-dim);
cursor: pointer;
transition: all var(--transition);
flex-shrink: 0;
}
.search-results-close:hover {
background: rgba(255, 255, 255, 0.12);
color: var(--text);
}
.search-results-query {
padding: 12px 16px;
font-size: 0.75rem;
color: var(--accent);
font-weight: 500;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
flex-shrink: 0;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.search-results-answer {
padding: 14px 16px;
font-size: 0.85rem;
line-height: 1.55;
color: var(--text);
background: rgba(124, 106, 239, 0.08);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
max-height: 200px;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
word-wrap: break-word;
overflow-wrap: break-word;
}
.search-results-list {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px 24px;
display: flex;
flex-direction: column;
gap: 12px;
scroll-behavior: smooth;
}
.search-result-card {
padding: 12px 14px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.07);
transition: background var(--transition), border-color var(--transition);
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.search-result-card:hover {
background: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.1);
}
.search-result-card .card-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--text);
line-height: 1.35;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.search-result-card .card-content {
font-size: 0.76rem;
color: var(--text-dim);
line-height: 1.5;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
.search-result-card .card-url {
font-size: 0.7rem;
color: var(--accent);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.search-result-card .card-url:hover {
text-decoration: underline;
}
.search-result-card .card-score {
font-size: 0.68rem;
color: var(--text-muted);
}
/* Themed scrollbars for search widget (match app dark theme) */
.search-results-answer::-webkit-scrollbar,
.search-results-list::-webkit-scrollbar {
width: 6px;
}
.search-results-answer::-webkit-scrollbar-track,
.search-results-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
}
.search-results-answer::-webkit-scrollbar-thumb,
.search-results-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12);
border-radius: 10px;
}
.search-results-answer::-webkit-scrollbar-thumb:hover,
.search-results-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
@supports (scrollbar-color: rgba(255,255,255,0.12) rgba(255,255,255,0.03)) {
.search-results-answer,
.search-results-list {
scrollbar-color: rgba(255, 255, 255, 0.12) rgba(255, 255, 255, 0.03);
scrollbar-width: thin;
}
}
/* ================================================================
SCROLLBAR CUSTOMISATION (WebKit / Chromium)
================================================================
A nearly-invisible 4 px scrollbar that only reveals itself on
hover. Keeps the glass aesthetic clean without hiding scroll
affordance entirely.
================================================================ */
.chat-messages::-webkit-scrollbar { width: 4px; }
.chat-messages::-webkit-scrollbar-track { background: transparent; }
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
}
.chat-messages::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.14); }
/* ================================================================
KEYFRAME ANIMATIONS
================================================================
All animations are defined here for easy reference and reuse.
fadeIn β€” Welcome screen entrance: fade up from 12 px below.
msgIn β€” New chat message entrance: fade up from 8 px below
(shorter travel than fadeIn for subtlety).
dotBounce β€” Typing-indicator dots: each dot jumps up 5 px then
falls back down. Staggered delays on nth-child
create the wave pattern.
blink β€” Streaming cursor: toggles opacity on/off every
half-cycle. `step-end` makes the transition instant
(no gradual fade), mimicking a real text cursor.
pulse-dot β€” Status dot heartbeat: gently fades to 40 % and back
over 2 s.
micPulse β€” Mic "listening" ring: an expanding, fading box-shadow
ring in danger-red. Grows from 0 to 8 px then fades
to transparent, repeating every 1.5 s.
ttsPulse β€” TTS "speaking" ring: same expanding ring technique
but in accent-purple.
orbPulse β€” Background orb breathing: scales from 1Γ— to 1.10Γ—
while nudging opacity from 0.92 β†’ 1, creating a
gentle "inhale / exhale" effect.
================================================================ */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes msgIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes dotBounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-5px); opacity: 1; }
}
@keyframes blink {
50% { opacity: 0; }
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes micPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.3); }
50% { box-shadow: 0 0 0 8px rgba(255, 107, 107, 0); }
}
@keyframes ttsPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(124, 106, 239, 0.3); }
50% { box-shadow: 0 0 0 8px rgba(124, 106, 239, 0); }
}
@keyframes orbPulse {
0%, 100% { transform: scale(1); opacity: 0.92; }
50% { transform: scale(1.10); opacity: 1; }
}
/* ================================================================
RESPONSIVE BREAKPOINTS
================================================================
TABLET β€” max-width: 768 px
----------------------------------------------------------------
At this size the sidebar (if any) is gone and horizontal space
is tighter. Changes:
β€’ Header padding/gap shrinks; tagline is hidden entirely.
β€’ Logo shrinks from 1.1 rem β†’ 1 rem.
β€’ Mode-switch buttons lose their SVG icons (text-only) and get
tighter padding, so the toggle still fits.
β€’ Status badge hides its text label β€” only the dot remains.
β€’ Chat message padding and font sizes reduce slightly.
β€’ Action buttons go from 38 px β†’ 36 px.
β€’ Avatars shrink from 30 px β†’ 26 px.
β€’ Input bar honours iOS safe-area at the smaller padding value.
================================================================ */
@media (max-width: 768px) {
.header { padding: 0 12px; gap: 8px; }
.tagline { display: none; }
.logo { font-size: 1rem; }
.mode-btn { padding: 6px 10px; font-size: 0.72rem; }
.mode-btn svg { display: none; }
.status-badge .status-text { display: none; }
.chat-messages { padding: 14px 10px; }
.input-bar { padding: 8px 10px 8px; padding-bottom: max(8px, env(safe-area-inset-bottom, 8px)); }
.input-wrapper { padding: 4px 4px 4px 12px; }
.action-btn { width: 36px; height: 36px; min-width: 36px; border-radius: 9px; }
.msg-content { font-size: 0.84rem; padding: 10px 13px; }
.welcome-title { font-size: 1.3rem; }
.message { gap: 8px; }
.msg-avatar { width: 26px; height: 26px; font-size: 0.62rem; }
.msg-avatar .msg-avatar-icon { width: 16px; height: 16px; }
.search-results-widget { width: min(100vw, 360px); }
.search-results-header { padding: 12px 14px; }
.search-results-query,
.search-results-answer { padding: 10px 14px; }
.search-results-list { padding: 10px 14px 20px; gap: 10px; }
.search-result-card { padding: 10px 12px; }
}
/* PHONE β€” max-width: 480 px
----------------------------------------------------------------
The narrowest target. Every pixel counts.
β€’ Mode switch stretches to full width and centres; each button
gets `flex: 1` so they split evenly.
β€’ "New Chat" button is hidden to save space.
β€’ Suggestion chips get smaller padding and font.
β€’ Action buttons shrink further to 34 px; SVG icons scale down.
β€’ Gaps tighten across the board.
---------------------------------------------------------------- */
@media (max-width: 480px) {
.header-center { flex: 1; justify-content: center; display: flex; }
.mode-switch { width: 100%; }
.mode-btn { flex: 1; justify-content: center; }
.new-chat-btn { display: none; }
.welcome-chips { gap: 6px; }
.chip { font-size: 0.72rem; padding: 6px 14px; }
.action-btn { width: 34px; height: 34px; min-width: 34px; border-radius: 8px; }
.action-btn svg { width: 17px; height: 17px; }
.input-actions { gap: 5px; }
.input-wrapper { gap: 4px; }
.search-results-widget { width: 100vw; max-width: 100%; }
.search-results-header { padding: 10px 12px; }
.search-results-query { font-size: 0.72rem; padding: 10px 12px; }
.search-results-answer { font-size: 0.82rem; padding: 10px 12px; max-height: 160px; }
.search-results-list { padding: 8px 12px 16px; gap: 8px; }
.search-result-card { padding: 10px 12px; }
.search-result-card .card-title { font-size: 0.76rem; }
.search-result-card .card-content { font-size: 0.72rem; -webkit-line-clamp: 3; }
}