@tailwind base; @tailwind components; @tailwind utilities; @layer base { /* ──────────────────────────────────────────────────────────────────────── Theme palettes — four families: • Default (.theme-snow) → pure-white neutral greys, blue accent • Cream (:root) and Dark (.dark) → warm family, terracotta accent • Glass (.theme-glass) → translucent purple on near-black Tokens that every theme must publish: --background, --foreground, --card / --popover (+ their -foreground), --primary (+ -foreground), --secondary, --muted (+ -foreground), --accent (+ -foreground), --destructive (+ -foreground), --border, --input, --ring, --overlay → scrim color for modal backdrops / picker shells (replaces hardcoded bg-black/40 in components) ──────────────────────────────────────────────────────────────────────── */ /* Cream — near-white warm canvas. Reads as off-white at first glance; the warm tint only surfaces on filled blocks (selected nav, panels) and against the terracotta primary. Inspired by Claude's chrome: keep bg essentially white, save the visible warmth for accents. */ :root { color-scheme: light; --background: #fdfcf9; --foreground: #1c1816; --card: #ffffff; --card-foreground: #1c1816; --popover: #ffffff; --popover-foreground: #1c1816; --primary: #b0501e; --primary-foreground: #ffffff; --secondary: #f5f2ea; --secondary-foreground: #1c1816; --muted: #f1ede2; --muted-foreground: #6d645a; --accent: #ece7d8; --accent-foreground: #1c1816; --destructive: #c53a2c; --destructive-foreground: #ffffff; --border: #e6decc; --input: #e6decc; --ring: #b0501e; --overlay: rgba(28, 24, 22, 0.42); } /* Dark — warm near-black, same terracotta family as Cream. */ .dark { color-scheme: dark; --background: #1a1918; --foreground: #e8e4de; --card: #242220; --card-foreground: #e8e4de; --popover: #242220; --popover-foreground: #e8e4de; --primary: #d4734b; --primary-foreground: #1a1918; --secondary: #2a2725; --secondary-foreground: #e8e4de; --muted: #2a2725; --muted-foreground: #9b9590; --accent: #302d2a; --accent-foreground: #e8e4de; --destructive: #d44a3c; --destructive-foreground: #ffffff; --border: #3a3634; --input: #3a3634; --ring: #d4734b; --overlay: rgba(0, 0, 0, 0.6); } /* Default — pure-white neutral canvas (theme id "snow" kept for stored preferences). No tint anywhere: flat #fff bg, plain grey surfaces and borders. The only chroma is the blue primary on interactive accents, in the spirit of Codex/ChatGPT chrome. */ .theme-snow { color-scheme: light; --background: #ffffff; --foreground: #0d0d0d; --card: #ffffff; --card-foreground: #0d0d0d; --popover: #ffffff; --popover-foreground: #0d0d0d; --primary: #2563eb; --primary-foreground: #ffffff; --secondary: #f7f7f7; --secondary-foreground: #0d0d0d; --muted: #f2f2f2; --muted-foreground: #6e6e6e; --accent: #ececec; --accent-foreground: #0d0d0d; --destructive: #d92d20; --destructive-foreground: #ffffff; --border: #e5e5e5; --input: #e5e5e5; --ring: #2563eb; --overlay: rgba(0, 0, 0, 0.4); } /* Glass — translucent purple panels on near-black. Slightly brighter primary so the accent reads against the radial gradient. */ .theme-glass { color-scheme: dark; --background: #0e0d1a; --foreground: #ffffff; --card: rgba(255, 255, 255, 0.06); --card-foreground: #ffffff; --popover: rgba(24, 22, 36, 0.92); --popover-foreground: #ffffff; --primary: #a855f7; --primary-foreground: #ffffff; --secondary: rgba(255, 255, 255, 0.04); --secondary-foreground: #eeeeee; --muted: rgba(255, 255, 255, 0.06); --muted-foreground: #b5b5c7; --accent: rgba(255, 255, 255, 0.12); --accent-foreground: #ffffff; --border: rgba(255, 255, 255, 0.12); --input: rgba(255, 255, 255, 0.06); --ring: #a855f7; --overlay: rgba(0, 0, 0, 0.55); } .theme-glass body { background: radial-gradient(circle at 50% 50%, #1a1a2e 0%, #0d0d1a 100%); background-attachment: fixed; } .theme-glass .surface-card { backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); } @media (min-width: 768px) { .theme-glass .surface-card { backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } } .theme-glass .btn-primary { background: linear-gradient(45deg, #9d50bb, #6e48aa); box-shadow: 0 0 15px rgba(157, 80, 187, 0.4); border: none; transition: all 0.3s ease; } .theme-glass .btn-primary:hover { box-shadow: 0 0 25px rgba(157, 80, 187, 0.6); transform: translateY(-1px); } * { @apply border-[color:var(--border)]; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } html, body { height: 100%; overflow: hidden; } html { -webkit-font-smoothing: antialiased; } html[data-scroll-behavior="smooth"] { scroll-behavior: smooth; } body { @apply antialiased; background: var(--background); color: var(--foreground); } button, input, select, textarea { font: inherit; } summary::-webkit-details-marker { display: none; } } @layer components { .surface-card { @apply rounded-2xl border bg-[var(--card)] shadow-sm; } .muted-chip { @apply rounded-full bg-[var(--muted)] px-3 py-1 text-xs text-[var(--muted-foreground)]; } } @layer utilities { .hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } .hide-scrollbar::-webkit-scrollbar { display: none; } } /* ─── Scrollbar ─── */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); } /* ─── Streaming surfaces ─── Every scroll container that wraps an LLM-streaming surface (chat, trace card, quiz follow-up, book chat, …) opts into this contract by carrying ``data-chat-scroll-root="true"``. ``overflow-anchor: none`` Disables the browser's automatic scroll anchoring. Anchoring tries to keep an in-viewport element fixed in screen space when content above it grows; for a chat surface this fights ``useChatAutoScroll`` every time a code block / KaTeX block / dynamic viewer mounts and reflows the message list. Without this rule the page visibly jitters during fast streams. ``scroll-behavior: auto`` The hook intentionally never animates ``scrollTop``. If a global ``scroll-behavior: smooth`` ever lands (theme, base reset, browser experiment), a smooth animation racing against the next-frame pin reintroduces the exact stutter this rule is here to prevent. */ [data-chat-scroll-root="true"] { overflow-anchor: none; scroll-behavior: auto; } /* ─── Prose: base ─── */ .prose { @apply font-serif text-[16px]; } .prose pre { @apply rounded-lg p-4 overflow-x-auto my-4; background: #292524; color: #d6d3d1; } .prose code:not(.md-code-block__code):not(.md-inline-code) { @apply px-1.5 py-0.5 rounded text-sm font-mono; background: var(--muted); color: var(--foreground); } .prose pre code { @apply bg-transparent text-inherit p-0; } .md-renderer .md-inline-code { @apply px-1.5 py-0.5 rounded text-sm font-mono; background: var(--muted); color: var(--foreground); } .md-renderer .md-code-block { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); } /* Force light text on dark code blocks regardless of theme. The wrapper .md-code-block always uses a fixed dark background (#1f2937 / #292524), so we need to override .prose pre (color: #D6D3D1) and any inherited --foreground from the prose wrapper that would otherwise render as near-black in the light theme. Specificity (0,2,1) beats .prose pre. */ .md-renderer .md-code-block, .md-renderer .md-code-block pre, .md-renderer .md-code-block code { color: #e5e7eb; } .md-renderer .md-code-block pre, .md-renderer .md-code-block code { background: transparent !important; } .md-renderer .md-code-block pre { @apply overflow-x-auto; margin: 0 !important; border-radius: 0 !important; padding: 1rem !important; } .md-renderer .md-code-block__code { padding: 0 !important; border-radius: 0 !important; font-size: 0.875rem !important; line-height: 1.7 !important; color: inherit !important; } .md-renderer .md-code-block__code span { background: transparent !important; border: 0 !important; box-shadow: none !important; } /* ─── Headings ─── */ .prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 { @apply font-sans; } .prose h1 { @apply text-2xl font-bold tracking-tight mt-8 mb-4; color: var(--foreground); } .prose h2 { @apply text-xl font-semibold tracking-tight mt-7 mb-3; color: var(--foreground); } .prose h3 { @apply text-lg font-semibold tracking-tight mt-6 mb-2.5; color: var(--foreground); } .prose h4 { @apply text-base font-semibold tracking-tight mt-5 mb-2; color: var(--foreground); } .prose h5 { @apply text-sm font-semibold tracking-tight mt-4 mb-2; color: var(--foreground); } .prose h6 { @apply text-sm font-semibold tracking-tight mt-4 mb-2; color: var(--muted-foreground); } /* ─── Body text ─── */ .prose p { @apply mb-4; line-height: 1.75; color: var(--foreground); } .prose strong { @apply font-semibold; color: var(--foreground); } .prose em { @apply italic; } /* ─── Strikethrough ─── */ .prose del, .prose s { @apply line-through; color: var(--muted-foreground); } /* ─── Highlight / Mark ─── */ .prose mark { @apply rounded px-1 py-0.5; background: #fef08a; color: #422006; } .dark .prose mark { background: #854d0e; color: #fef9c3; } /* ─── Subscript / Superscript ─── */ .prose sub { @apply text-[0.75em]; vertical-align: sub; } .prose sup { @apply text-[0.75em]; vertical-align: super; } /* ─── Abbreviation ─── */ .prose abbr[title] { @apply cursor-help; text-decoration: underline dotted; text-underline-offset: 3px; } /* ─── Keyboard ─── */ .prose kbd { @apply inline-block rounded border px-1.5 py-0.5 font-mono text-[0.8em] font-medium leading-none; background: var(--muted); border-color: var(--border); color: var(--foreground); box-shadow: 0 1px 0 1px var(--border); } /* ─── Links ─── */ .prose a { @apply underline underline-offset-2; color: var(--primary); } /* ─── Lists ─── */ .prose ul { @apply list-disc list-outside ml-6 mb-4; color: var(--foreground); } .prose ol { @apply list-decimal list-outside ml-6 mb-4; color: var(--foreground); } .prose li { @apply mb-1; } .prose li > ul, .prose li > ol { @apply mt-1 mb-0; } /* ─── Task list (GFM) ─── */ .prose ul:has(> li > input[type="checkbox"]) { @apply list-none ml-0 pl-0; } .prose li:has(> input[type="checkbox"]) { @apply flex items-start gap-0; } .prose li > input[type="checkbox"] { @apply mt-1.5 shrink-0; } /* ─── Blockquote ─── */ .prose blockquote { @apply border-l-[3px] pl-4 italic my-4; border-color: var(--muted-foreground); color: var(--muted-foreground); } .prose blockquote blockquote { @apply mt-2; border-color: var(--border); } /* ─── Horizontal rule ─── */ .prose hr { @apply my-6 border-none; height: 1px; background: var(--border); } /* ─── Tables ─── */ .prose table { @apply border-collapse; } .prose th { @apply font-semibold px-4 py-2 border text-left; background: var(--muted); color: var(--foreground); border-color: var(--border); } .prose td { @apply px-4 py-2 border; color: var(--foreground); border-color: var(--border); } /* ─── Images ─── */ .prose img { @apply my-4 max-w-full h-auto rounded-lg; } .prose figure { @apply my-4; } .prose figcaption { @apply mt-2 text-center text-sm; color: var(--muted-foreground); } /* ─── Details / Summary ─── */ .prose details { @apply my-4 rounded-lg border px-4 py-2; background: var(--card); border-color: var(--border); } .prose details:empty, .prose details:not(:has(> :not(summary))), .md-renderer details:empty, .md-renderer details:not(:has(> :not(summary))) { display: none; } .prose > :empty:not(hr):not(br):not(img) { display: none; } .prose summary { @apply cursor-pointer font-medium select-none; color: var(--foreground); } .prose details[open] > summary { @apply mb-2; } /* ─── Definition list ─── */ .prose dl { @apply my-4; } .prose dt { @apply font-semibold mt-3; color: var(--foreground); } .prose dd { @apply ml-6 mt-1; color: var(--muted-foreground); } /* ─── Footnotes (remark-gfm) ─── */ .prose .footnotes { @apply mt-8 border-t pt-4 text-sm; border-color: var(--border); color: var(--muted-foreground); } .prose .footnotes ol { @apply ml-4; } /* ─── md-renderer: scope overrides only inside our component ─── */ .md-renderer .contains-task-list { @apply list-none ml-0 pl-0; } /* Animations */ @keyframes fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in { animation: fade-in 0.35s ease-out forwards; } /* Picker chrome: a soft scrim fade + a small spring-in for the card, so modal pickers feel placed rather than popped. */ @keyframes dt-overlay-in { from { opacity: 0; } to { opacity: 1; } } .animate-overlay-in { animation: dt-overlay-in 160ms ease-out; } @keyframes dt-pop-in { from { opacity: 0; transform: translateY(6px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } .animate-pop-in { animation: dt-pop-in 200ms cubic-bezier(0.22, 1, 0.36, 1); } /* Streaming "breathing" label — used above the trace panel to surface that the assistant is still working. Cycles between full and dim opacity with a slow ease, mirroring Claude desktop's reasoning indicator. */ @keyframes dt-breathing { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } } .dt-breathing-text { animation: dt-breathing 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite; } /* Icon-only stretch/contract on top of the opacity breathing. Slightly different period (2.4s vs 1.8s) so the scale and opacity cycles drift in and out of phase, giving the mark a hand-drawn, "alive" feel rather than a single mechanical pulse. */ @keyframes dt-mark-pulse { 0%, 100% { transform: scale(0.9); } 50% { transform: scale(1.08); } } .dt-mark-pulse { animation: dt-mark-pulse 2.4s cubic-bezier(0.4, 0, 0.6, 1) infinite; transform-origin: center; transform-box: fill-box; will-change: transform; } /* While a fullscreen picker/modal is open, freeze ambient background animations (sidebar "running session" pulse, breathing titles, pulsing marks). They live behind the modal's frosted `backdrop-filter`; their per-frame repaints were being re-sampled by the blur, reading as a constant shimmer behind the scrim. `paused` holds them at their current frame (no jump) and they resume on close. */ body[data-picker-open] .dt-breathing-text, body[data-picker-open] .session-pulse, body[data-picker-open] .dt-session-icon-running, body[data-picker-open] .dt-mark-pulse { animation-play-state: paused !important; } /* Capability menu pop-up — the dropdown blooms out of the chip itself rather than fading in from above the composer. Origin at bottom-left so the menu visually "unfurls" from the chevron corner of the chip. */ @keyframes dt-popup-up { from { opacity: 0; transform: translateY(6px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } } .dt-popup-up { animation: dt-popup-up 180ms cubic-bezier(0.16, 1, 0.3, 1) forwards; transform-origin: bottom left; will-change: opacity, transform; } /* Session status pulse */ @keyframes session-pulse { 0%, 100% { opacity: 0; transform: scale(1); } 50% { opacity: 1; transform: scale(2.2); } } .session-pulse { animation: session-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } /* Session avatar — gentle breathing wiggle when a session is running */ @keyframes dt-session-icon-running { 0%, 100% { transform: translateY(0) rotate(0deg) scale(1); } 25% { transform: translateY(-0.6px) rotate(-6deg) scale(1.05); } 50% { transform: translateY(0) rotate(0deg) scale(1.02); } 75% { transform: translateY(0.6px) rotate(6deg) scale(1.05); } } .dt-session-icon-running { animation: dt-session-icon-running 1.6s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; transform-origin: center; } /* ----- Chat shell ↔ file preview drawer --------------------------------- Squeezes the chat column when the drawer is open. The transition timing here MUST match FilePreviewDrawer's transform transition so the slide-in and the squeeze animate as one motion. Stays a no-op below the lg breakpoint, where the drawer overlays instead. */ .chat-preview-shell { transition: padding-right 220ms ease-out; padding-right: 0; } /* While the viewer is being drag-resized, kill the squeeze transition so the chat column tracks the panel edge frame-for-frame instead of lagging. */ :root[data-viewer-resizing="true"] .chat-preview-shell { transition: none; } @media (min-width: 1024px) { /* The wider composer-side preview drawer (pre-send attachment peek) gets the largest squeeze. */ .chat-preview-shell[data-preview-open="true"] { padding-right: min(560px, 92vw); } /* Viewer panel (tabbed file/web preview, opened from message attachments or assistant links) is next-widest. Wins over the activity panel when both are open. Width is user-resizable, so the squeeze tracks the ``--viewer-width`` var the panel writes (falls back to 620px). */ .chat-preview-shell[data-preview-open="false"][data-viewer-open="true"] { padding-right: min(var(--viewer-width, 620px), 92vw); } } /* Sidebar tooltip */ .dt-tooltip-wrapper { @apply relative inline-flex; } .dt-tooltip-content { @apply absolute z-50 rounded-lg bg-[var(--popover)] px-3 py-1.5 text-xs shadow-md text-[var(--popover-foreground)] pointer-events-none; animation: dt-tooltip-fade-in 150ms ease-out; } .dt-tooltip-label { @apply font-medium truncate; } .dt-tooltip-desc { @apply mt-0.5 text-[11px] text-[var(--muted-foreground)] truncate; } .dt-tooltip-content[data-side="right"] { @apply left-full top-1/2 -translate-y-1/2 ml-2 max-w-[200px]; } .dt-tooltip-content[data-side="bottom"] { @apply bottom-[-40px] left-1/2 -translate-x-1/2 max-w-[180px]; } @keyframes dt-tooltip-fade-in { from { opacity: 0; } to { opacity: 1; } } /* ─── Reduced motion ────────────────────────────────────────────────────── Honor the OS-level prefers-reduced-motion setting. We don't strip motion entirely — interaction-critical transitions (drawer slide, menu reveal) stay visible but are shortened to a near-imperceptible duration so the UI still communicates state changes without vestibular jitter. Loop animations (breathing, pulse, spin) are removed outright. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } .dt-breathing-text, .dt-mark-pulse, .session-pulse, .animate-spin { animation: none !important; } }