feat: refactor coach chat to ChatGPT-style Material You design

- Replaced Gemini-style glass/blue aesthetic with clean Material You pink theme
- ChatGPT-style message layout: user right-aligned, coach left-aligned with avatars
- User avatar shows initials, coach avatar shows brain icon
- Chat bubble design: rounded cards matching app theme (no glass/blur effects)
- Clean textarea input with Enter to send, Shift+Enter for newlines
- ChatGPT-style empty state with suggestion chips
- Typing indicator with bouncing dots animation
- Sidebar uses Material You surface/container colors
- Status bar shows model name and ready/thinking state
- Error messages styled as Material You error containers
- Bottom hint text: 'coach can make mistakes'
- All colors use CSS custom properties for accent theme consistency
This commit is contained in:
Ned
2026-05-22 22:01:26 +00:00
parent 084acfa84a
commit d312321ffa
2 changed files with 362 additions and 197 deletions
+75 -41
View File
@@ -1785,14 +1785,14 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
if (!chatKey) { if (!chatKey) {
return ( return (
<section className="coach-gemini-shell coach-locked-shell"> <section className="coach-shell coach-locked-shell">
<div className="coach-empty-state"> <div className="coach-empty-state">
<div className="coach-brand-orb"> <div className="coach-empty-icon">
<Lock size={30} aria-hidden="true" /> <Lock size={28} aria-hidden="true" />
</div> </div>
<h2>unlock encrypted coach chats</h2> <h2>unlock coach</h2>
<p> <p>
messages encrypt in this browser before appwrite stores them. the passphrase is never saved, so use the same one on every device. messages are encrypted before appwrite stores them. your passphrase is never saved use the same one on every device.
</p> </p>
<form className="coach-unlock-card" onSubmit={unlockChats}> <form className="coach-unlock-card" onSubmit={unlockChats}>
<input <input
@@ -1808,27 +1808,32 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
unlock unlock
</button> </button>
</form> </form>
{error && <p className="mt-4 max-w-xl text-sm text-red-100">{error}</p>} {error && <p className="mt-4 max-w-md text-sm" style={{ color: "var(--error)" }}>{error}</p>}
</div> </div>
</section> </section>
); );
} }
const userInitials = user.name
? user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2)
: (user.email?.[0] ?? "U").toUpperCase();
return ( return (
<section className="coach-gemini-shell"> <section className="coach-shell">
<aside className="coach-chat-sidebar"> <div className="coach-layout">
<div className="coach-sidebar-brand"> <aside className="coach-sidebar">
<div className="coach-brand-orb coach-brand-orb-small"> <div className="coach-sidebar-header">
<div className="coach-sidebar-icon">
<Brain size={18} aria-hidden="true" /> <Brain size={18} aria-hidden="true" />
</div> </div>
<div> <div className="coach-sidebar-label">
<p className="font-semibold text-white">coach</p> <p>coach</p>
<p className="text-xs text-slate-400">{chatStorageStatus}</p> <p>{chatStorageStatus}</p>
</div> </div>
</div> </div>
<button className="coach-new-chat" type="button" onClick={startNewChat} disabled={busy}> <button className="coach-new-chat" type="button" onClick={startNewChat} disabled={busy}>
<MessageSquarePlus size={18} aria-hidden="true" /> <Plus size={16} aria-hidden="true" />
new chat new chat
</button> </button>
@@ -1847,8 +1852,8 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
</div> </div>
<div className="coach-context-card"> <div className="coach-context-card">
<p className="text-xs font-semibold uppercase text-slate-500">today</p> <p className="text-xs font-semibold uppercase" style={{ color: "var(--muted)" }}>today</p>
<div className="mt-3 grid gap-2"> <div className="mt-2 grid gap-2">
<WellnessPill label="cans" value={dashboard.todayCans} /> <WellnessPill label="cans" value={dashboard.todayCans} />
<WellnessPill label="caffeine" value={dashboard.todayCaffeine} /> <WellnessPill label="caffeine" value={dashboard.todayCaffeine} />
<WellnessPill label="favourite" value={dashboard.favouriteFlavour} /> <WellnessPill label="favourite" value={dashboard.favouriteFlavour} />
@@ -1856,23 +1861,27 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
</div> </div>
</aside> </aside>
<section className="coach-stage"> <section className="coach-main">
<div className="coach-stage-topbar"> <div className="coach-topbar">
<span>{OLLAMA_MODEL}</span> <span className="coach-topbar-status">
<span>{busy ? "thinking" : "ready"}</span> <span className={`coach-topbar-status-dot ${busy ? "coach-topbar-status-dot-busy" : "coach-topbar-status-dot-ready"}`} />
{busy ? "thinking" : "ready"}
</span>
<span className="coach-topbar-status" style={{ color: "var(--muted)" }}>{OLLAMA_MODEL}</span>
</div> </div>
<div className="coach-stage-messages" aria-live="polite"> <div className="coach-messages" aria-live="polite">
<div className="coach-messages-inner">
{!visibleMessages.length ? ( {!visibleMessages.length ? (
<div className="coach-empty-state"> <div className="coach-empty-state">
<div className="coach-brand-orb"> <div className="coach-empty-icon">
<Sparkles size={32} aria-hidden="true" /> <Sparkles size={28} aria-hidden="true" />
</div> </div>
<h2>ready when you are</h2> <h2>how can I help?</h2>
<p>ask about caffeine pace, sugar, spend, or your flavour pattern. answers stay lower case.</p> <p>ask about caffeine, sugar, spending, or your flavour patterns.</p>
<div className="coach-prompt-grid"> <div className="coach-prompt-grid">
{quickPrompts.map((prompt) => ( {quickPrompts.map((prompt) => (
<button key={prompt} className="suggestion-chip" type="button" disabled={busy} onClick={() => void sendPrompt(prompt)}> <button key={prompt} className="chat-suggestion-chip" type="button" disabled={busy} onClick={() => void sendPrompt(prompt)}>
{prompt} {prompt}
</button> </button>
))} ))}
@@ -1883,6 +1892,7 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
<CoachMessageBubble <CoachMessageBubble
key={message.id} key={message.id}
message={message} message={message}
userInitials={userInitials}
thinkingOpen={openThinkingIds.includes(message.id)} thinkingOpen={openThinkingIds.includes(message.id)}
onToggleThinking={() => toggleThinking(message.id)} onToggleThinking={() => toggleThinking(message.id)}
/> />
@@ -1890,66 +1900,90 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
)} )}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
</div>
{error && ( {error && (
<div className="mx-4 mb-3 rounded-lg border border-red-400/40 bg-red-500/10 px-3 py-2 text-sm text-red-100"> <div className="coach-error">
<div className="coach-error-inner">
{error} {error}
</div> </div>
</div>
)} )}
<form className="coach-composer" onSubmit={submit}> <form className="coach-composer" onSubmit={submit}>
<div className="coach-composer-inner">
<button className="composer-icon-button" type="button" onClick={startNewChat} disabled={busy} aria-label="new chat"> <button className="composer-icon-button" type="button" onClick={startNewChat} disabled={busy} aria-label="new chat">
<Plus size={22} aria-hidden="true" /> <Plus size={18} aria-hidden="true" />
</button> </button>
<input <textarea
className="coach-input" className="coach-input"
value={input} value={input}
onChange={(event) => setInput(event.target.value)} onChange={(event) => setInput(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
void sendPrompt(input);
}
}}
placeholder="ask coach" placeholder="ask coach"
disabled={busy} disabled={busy}
rows={1}
/> />
{busy ? ( {busy ? (
<button className="composer-send-button composer-stop-button" type="button" onClick={stopThinking} aria-label="stop thinking"> <button className="composer-send-button composer-stop-button" type="button" onClick={stopThinking} aria-label="stop thinking">
<Square size={18} aria-hidden="true" /> <Square size={16} aria-hidden="true" />
</button> </button>
) : ( ) : (
<button className="composer-send-button" type="submit" disabled={!input.trim()} aria-label="send coach message"> <button className="composer-send-button" type="submit" disabled={!input.trim()} aria-label="send message">
<Send size={20} aria-hidden="true" /> <Send size={16} aria-hidden="true" />
</button> </button>
)} )}
</div>
<p className="coach-hint">coach can make mistakes. check important info.</p>
</form> </form>
</section> </section>
</div>
</section> </section>
); );
} }
function CoachMessageBubble({ function CoachMessageBubble({
message, message,
userInitials,
thinkingOpen, thinkingOpen,
onToggleThinking, onToggleThinking,
}: { }: {
message: CoachMessage; message: CoachMessage;
userInitials: string;
thinkingOpen: boolean; thinkingOpen: boolean;
onToggleThinking: () => void; onToggleThinking: () => void;
}) { }) {
const isAssistant = message.role === "assistant"; const isAssistant = message.role === "assistant";
const canShowThinking = isAssistant && (message.pending || Boolean(message.thinking)); const canShowThinking = isAssistant && (message.pending || Boolean(message.thinking));
const thinkingLabel = message.stopped ? "stopped thinking" : message.pending ? "thinking" : "thinking"; const thinkingLabel = message.stopped ? "stopped thinking" : message.pending ? "thinking" : "view reasoning";
return ( return (
<article className={`coach-message coach-message-${message.role}`}> <article className={`coach-message ${isAssistant ? "coach-message-assistant" : "coach-message-user"}`}>
{isAssistant ? (
<div className="coach-message-avatar coach-message-avatar-assistant">
<Brain size={16} aria-hidden="true" />
</div>
) : (
<div className="coach-message-avatar coach-message-avatar-user">
{userInitials}
</div>
)}
<div className="coach-message-bubble"> <div className="coach-message-bubble">
<p className="text-xs font-semibold uppercase text-slate-500">{isAssistant ? "coach" : "you"}</p> <div className="coach-bubble-content">
<div className="mt-2 whitespace-pre-wrap text-sm leading-6 text-white"> {message.content || (message.pending ? (
{message.content || (message.pending ? "streaming response..." : "")} <div className="coach-typing-dots"><span /><span /><span /></div>
) : "")}
</div> </div>
{canShowThinking && ( {canShowThinking && (
<div className="mt-3"> <div className="mt-2">
<button className={`thinking-slider ${message.pending ? "thinking-slider-active" : ""}`} type="button" onClick={onToggleThinking}> <button className={`thinking-slider ${message.pending ? "thinking-slider-active" : ""}`} type="button" onClick={onToggleThinking}>
<span className="thinking-slider-track"> {thinkingLabel}
<span>{thinkingLabel} · click to reveal reasoning</span>
</span>
</button> </button>
<AnimatePresence> <AnimatePresence>
{thinkingOpen && ( {thinkingOpen && (
+215 -84
View File
@@ -326,62 +326,84 @@ textarea:focus-visible {
box-shadow: var(--elevation-1); box-shadow: var(--elevation-1);
} }
.coach-gemini-shell { .coach-shell {
@apply grid min-h-[760px] gap-4; @apply flex flex-col;
height: calc(100vh - 200px);
min-height: 480px;
}
@media (min-width: 1024px) {
.coach-shell {
height: calc(100vh - 160px);
}
} }
.coach-locked-shell { .coach-locked-shell {
@apply place-items-center; @apply flex items-center justify-center;
grid-template-columns: 1fr !important;
} }
.coach-chat-sidebar { .coach-layout {
@apply hidden min-h-[760px] flex-col border p-3 xl:flex; @apply relative flex flex-1 gap-4 overflow-hidden;
background: color-mix(in srgb, var(--surface-container-lowest) 80%, transparent);
border-color: color-mix(in srgb, var(--outline-variant) 64%, transparent);
border-radius: 34px;
box-shadow: var(--elevation-1);
} }
.coach-sidebar-brand { .coach-sidebar {
@apply flex items-center gap-3 px-2 py-2; @apply hidden w-72 shrink-0 flex-col border p-3 xl:flex;
background: var(--surface-container);
border-color: var(--outline-variant);
border-radius: 28px;
} }
.coach-brand-orb { .coach-sidebar-header {
@apply grid h-16 w-16 place-items-center rounded-full shadow-sm; @apply flex items-center gap-3 px-1 py-2;
background: radial-gradient(circle at 30% 26%, #ffffff 0 12%, #d7ecff 13% 48%, #7fb6df 72%, #5d8fb3 100%);
color: #163247;
} }
.coach-brand-orb-small { .coach-sidebar-icon {
@apply h-10 w-10; @apply flex h-10 w-10 items-center justify-center rounded-xl;
background: var(--primary-container);
color: var(--on-primary-container);
}
.coach-sidebar-label {
min-width: 0;
}
.coach-sidebar-label p:first-child {
@apply truncate text-sm font-semibold;
color: var(--text);
}
.coach-sidebar-label p:last-child {
@apply truncate text-xs;
color: var(--muted);
} }
.coach-new-chat { .coach-new-chat {
@apply mt-4 inline-flex min-h-12 items-center gap-3 rounded-full px-4 text-sm font-semibold transition disabled:cursor-not-allowed; @apply mt-3 inline-flex min-h-11 items-center gap-2 rounded-xl border px-4 text-sm font-semibold transition disabled:cursor-not-allowed;
background: var(--surface-container-high); background: var(--surface-container-high);
border-color: var(--outline-variant);
color: var(--text); color: var(--text);
} }
.coach-new-chat:hover:not(:disabled) { .coach-new-chat:hover:not(:disabled) {
background: var(--primary-container); background: var(--primary-container);
color: var(--on-primary-container);
} }
.coach-chat-list { .coach-chat-list {
@apply mt-4 grid flex-1 content-start gap-1 overflow-y-auto; @apply mt-3 grid flex-1 content-start gap-1 overflow-y-auto;
} }
.coach-chat-row { .coach-chat-row {
@apply grid grid-cols-[1fr_auto] items-center rounded-3xl transition; @apply grid grid-cols-[1fr_auto] items-center rounded-2xl transition;
color: var(--text); color: var(--text);
} }
.coach-chat-row > button:first-child { .coach-chat-row > button:first-child {
@apply grid min-w-0 gap-1 px-4 py-3 text-left; @apply grid min-w-0 gap-0.5 px-3 py-2.5 text-left;
} }
.coach-chat-row > button:last-child { .coach-chat-row > button:last-child {
@apply mr-2 grid h-8 w-8 place-items-center rounded-full opacity-0 transition; @apply mr-1 grid h-7 w-7 place-items-center rounded-lg opacity-0 transition;
color: var(--muted); color: var(--muted);
} }
@@ -401,95 +423,156 @@ textarea:focus-visible {
.coach-chat-row-active, .coach-chat-row-active,
.coach-chat-row:hover { .coach-chat-row:hover {
background: var(--surface-container-high); background: var(--primary-container);
}
.coach-chat-row-active {
color: var(--on-primary-container);
}
.coach-chat-row-active small {
color: var(--on-primary-container);
} }
.coach-context-card { .coach-context-card {
@apply mt-4 rounded-[28px] border p-4; @apply mt-auto rounded-2xl border p-3;
background: color-mix(in srgb, var(--surface-container-low) 76%, white); background: var(--surface-container-high);
border-color: var(--outline-variant); border-color: var(--outline-variant);
} }
.coach-stage { .coach-main {
@apply relative flex min-h-[760px] flex-col overflow-hidden border; @apply relative flex flex-1 flex-col overflow-hidden border;
background: background: var(--surface-container-lowest);
radial-gradient(circle at 50% 64%, rgba(192, 225, 250, 0.78) 0 18%, transparent 42%), border-color: var(--outline-variant);
linear-gradient(180deg, rgba(255,255,255,0.94), rgba(248,251,255,0.96)); border-radius: 28px;
border-color: color-mix(in srgb, var(--outline-variant) 64%, transparent);
border-radius: 38px;
box-shadow: var(--elevation-1);
} }
.coach-stage-topbar { .coach-topbar {
@apply flex items-center justify-between px-5 py-4 text-xs font-semibold; @apply flex items-center justify-between border-b px-4 py-2;
border-color: var(--outline-variant);
background: var(--surface-container-low);
}
.coach-topbar-status {
@apply inline-flex items-center gap-1.5 text-xs font-medium;
color: var(--muted); color: var(--muted);
} }
.coach-stage-messages { .coach-topbar-status-dot {
@apply flex-1 space-y-5 overflow-y-auto px-4 pb-36 pt-8 sm:px-8 lg:px-16; @apply h-2 w-2 rounded-full;
}
.coach-topbar-status-dot-ready {
background: var(--chart-secondary);
}
.coach-topbar-status-dot-busy {
background: var(--chart-tertiary);
@apply animate-pulse;
}
.coach-messages {
@apply flex-1 overflow-y-auto;
}
.coach-messages-inner {
@apply mx-auto max-w-3xl space-y-4 px-4 pb-32 pt-6;
} }
.coach-empty-state { .coach-empty-state {
@apply mx-auto flex min-h-[520px] max-w-3xl flex-col items-center justify-center text-center; @apply flex min-h-full flex-col items-center justify-center px-6 text-center;
}
.coach-empty-icon {
@apply mb-5 flex h-16 w-16 items-center justify-center rounded-2xl;
background: var(--primary-container);
color: var(--on-primary-container);
} }
.coach-empty-state h2 { .coach-empty-state h2 {
@apply mt-6 text-5xl font-normal tracking-tight sm:text-6xl; @apply text-3xl font-semibold tracking-tight;
color: var(--text); color: var(--text);
} }
.coach-empty-state p { .coach-empty-state p {
@apply mt-4 max-w-xl text-base leading-7; @apply mt-2 max-w-md text-sm leading-6;
color: var(--muted); color: var(--muted);
} }
.coach-prompt-grid { .coach-prompt-grid {
@apply mt-7 grid gap-2 sm:grid-cols-3; @apply mt-6 grid gap-2 sm:grid-cols-1;
max-width: 480px;
} }
.coach-message { .coach-message {
@apply flex; @apply flex gap-3;
} }
.coach-message-user { .coach-message-user {
@apply justify-end; @apply flex-row-reverse;
} }
.coach-message-assistant { .coach-message-avatar {
@apply justify-start; @apply flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold;
}
.coach-message-avatar-assistant {
background: var(--primary-container);
color: var(--on-primary-container);
}
.coach-message-avatar-user {
background: var(--tertiary-container);
color: var(--on-tertiary-container);
} }
.coach-message-bubble { .coach-message-bubble {
@apply max-w-[840px] rounded-[34px] border px-5 py-4 shadow-sm; @apply max-w-[85%] rounded-2xl px-4 py-3;
background: rgba(255, 255, 255, 0.82); }
border-color: color-mix(in srgb, var(--outline-variant) 58%, transparent);
backdrop-filter: blur(18px); .coach-message-assistant .coach-message-bubble {
background: var(--surface-container-high);
color: var(--text);
border-bottom-left-radius: 6px;
} }
.coach-message-user .coach-message-bubble { .coach-message-user .coach-message-bubble {
background: #ececec; background: var(--primary);
border-color: transparent; color: var(--on-primary);
border-bottom-right-radius: 6px;
} }
.coach-message-user .coach-message-bubble, .coach-bubble-label {
.coach-message-user .coach-message-bubble * { @apply mb-1 text-xs font-semibold;
color: var(--text) !important;
}
.thinking-slider {
@apply w-full overflow-hidden rounded-full border px-3 py-2 text-sm font-medium;
background: rgba(255, 255, 255, 0.72);
border-color: transparent;
color: var(--muted); color: var(--muted);
} }
.coach-bubble-content {
@apply whitespace-pre-wrap text-sm leading-relaxed;
}
.coach-message-user .coach-bubble-content {
color: var(--on-primary);
}
.thinking-slider {
@apply mt-2 w-full overflow-hidden rounded-xl border px-3 py-2 text-xs font-medium transition;
background: var(--surface-container);
border-color: var(--outline-variant);
color: var(--muted);
}
.thinking-slider:hover {
background: var(--primary-container);
color: var(--on-primary-container);
}
.thinking-slider-active { .thinking-slider-active {
border-color: color-mix(in srgb, var(--primary) 42%, var(--outline-variant)); border-color: color-mix(in srgb, var(--primary) 40%, var(--outline-variant));
} }
.thinking-slider-track { .thinking-slider-track {
@apply block overflow-hidden whitespace-nowrap; @apply block overflow-hidden whitespace-nowrap;
mask-image: linear-gradient(90deg, transparent, black 18%, black 82%, transparent);
} }
.thinking-slider-track span { .thinking-slider-track span {
@@ -499,47 +582,57 @@ textarea:focus-visible {
} }
.thinking-trace { .thinking-trace {
@apply mt-2 max-h-56 overflow-auto rounded-3xl border p-4 text-xs leading-5 whitespace-pre-wrap; @apply mt-2 max-h-56 overflow-auto rounded-xl border p-3 text-xs leading-5 whitespace-pre-wrap;
background: rgba(255, 255, 255, 0.72); background: var(--surface-container);
border-color: color-mix(in srgb, var(--outline-variant) 58%, transparent); border-color: var(--outline-variant);
color: var(--muted); color: var(--muted);
} }
.coach-composer { .coach-composer {
@apply absolute inset-x-4 bottom-4 z-10 mx-auto flex max-w-4xl items-center gap-3 rounded-full border p-3 sm:bottom-7; @apply absolute inset-x-0 bottom-0 z-10;
background: rgba(255, 255, 255, 0.94); background: linear-gradient(to top, var(--surface-container-lowest) 60%, transparent);
border-color: color-mix(in srgb, var(--outline-variant) 68%, transparent); padding: 0 1rem 1rem;
box-shadow: 0 18px 50px rgba(74, 102, 122, 0.18); }
backdrop-filter: blur(18px);
.coach-composer-inner {
@apply mx-auto flex max-w-3xl items-end gap-2 rounded-2xl border p-2;
background: var(--surface-container-high);
border-color: var(--outline-variant);
box-shadow: var(--elevation-2);
} }
.coach-input { .coach-input {
@apply min-h-12 flex-1 rounded-full border-0 px-2 text-lg shadow-none transition; @apply min-h-11 flex-1 resize-none rounded-xl border-0 px-3 py-2 text-sm;
background: transparent; background: transparent;
color: var(--text); color: var(--text);
field-sizing: content;
max-height: 160px;
} }
.coach-input:focus { .coach-input:focus {
box-shadow: none; box-shadow: none;
} }
.coach-input::placeholder {
color: var(--muted);
}
.composer-icon-button, .composer-icon-button,
.composer-send-button { .composer-send-button {
@apply grid h-12 w-12 shrink-0 place-items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-45; @apply flex h-9 w-9 shrink-0 items-center justify-center rounded-xl transition disabled:cursor-not-allowed disabled:opacity-40;
color: var(--text);
} }
.composer-icon-button:hover { .composer-icon-button:hover {
background: var(--surface-container-high); background: var(--surface-container);
} }
.composer-send-button { .composer-send-button {
background: #97cbf5; background: var(--primary);
color: #10283a; color: var(--on-primary);
} }
.composer-send-button:hover:not(:disabled) { .composer-send-button:hover:not(:disabled) {
filter: brightness(0.98); filter: brightness(1.05);
} }
.composer-stop-button { .composer-stop-button {
@@ -548,15 +641,53 @@ textarea:focus-visible {
} }
.coach-unlock-card { .coach-unlock-card {
@apply mt-8 flex w-full max-w-xl flex-col gap-3 rounded-full border p-3 sm:flex-row; @apply mt-6 flex w-full max-w-md flex-col gap-3;
background: rgba(255, 255, 255, 0.94); }
border-color: color-mix(in srgb, var(--outline-variant) 68%, transparent);
box-shadow: var(--elevation-2); .coach-unlock-card .coach-input {
@apply rounded-xl border px-4 py-3;
background: var(--surface-container-lowest);
border-color: var(--outline-variant);
}
.coach-error {
@apply mx-auto max-w-3xl px-4 pb-2;
}
.coach-error-inner {
@apply rounded-xl border px-3 py-2 text-sm;
border-color: var(--error-container);
background: var(--error-container);
color: var(--on-error-container);
}
.coach-typing-dots {
@apply flex items-center gap-1 py-1;
}
.coach-typing-dots span {
@apply inline-block h-2 w-2 rounded-full;
background: var(--muted);
animation: coach-bounce 1.4s infinite ease-in-out both;
}
.coach-typing-dots span:nth-child(1) { animation-delay: 0s; }
.coach-typing-dots span:nth-child(2) { animation-delay: 0.16s; }
.coach-typing-dots span:nth-child(3) { animation-delay: 0.32s; }
@keyframes coach-bounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
.coach-hint {
@apply mt-1.5 text-center text-xs;
color: var(--muted);
} }
@media (min-width: 1280px) { @media (min-width: 1280px) {
.coach-gemini-shell { .coach-shell {
grid-template-columns: 340px minmax(0, 1fr); /* sidebar visible */
} }
} }