Spaces:
Running
Running
| @tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| @layer base { | |
| :root { | |
| color-scheme: dark; | |
| } | |
| html, | |
| body, | |
| #root { | |
| height: 100%; | |
| } | |
| body { | |
| /* Off-black, never pure #000. Geist as the base type. */ | |
| @apply bg-zinc-950 text-zinc-100 font-sans antialiased; | |
| font-feature-settings: "cv11", "ss01"; | |
| } | |
| ::selection { | |
| @apply bg-accent/25 text-white; | |
| } | |
| /* | |
| * Motion is opt-out. Anyone who has asked their OS to reduce motion gets the | |
| * layout with none of the movement — nothing here is load-bearing. | |
| */ | |
| @media (prefers-reduced-motion: reduce) { | |
| *, | |
| *::before, | |
| *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| scroll-behavior: auto ; | |
| } | |
| } | |
| } | |
| /* | |
| * Keyframes live outside @layer so they are always emitted, regardless of which | |
| * utilities Tailwind decides to tree-shake. | |
| */ | |
| @keyframes rise-in { | |
| from { | |
| opacity: 0; | |
| transform: translate3d(0, 7px, 0); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: none; | |
| } | |
| } | |
| @keyframes rail-in { | |
| from { | |
| opacity: 0; | |
| transform: translate3d(14px, 0, 0); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: none; | |
| } | |
| } | |
| @keyframes sheen-sweep { | |
| to { | |
| transform: translateX(100%); | |
| } | |
| } | |
| @layer components { | |
| /* Thin, restrained scrollbars. */ | |
| .scrollbar-thin::-webkit-scrollbar { | |
| width: 7px; | |
| height: 7px; | |
| } | |
| .scrollbar-thin::-webkit-scrollbar-thumb { | |
| @apply rounded-full bg-zinc-700/60; | |
| } | |
| .scrollbar-thin::-webkit-scrollbar-thumb:hover { | |
| @apply bg-zinc-600/70; | |
| } | |
| .scrollbar-thin::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| /* For the composer: grows, but never shows a native scrollbar. */ | |
| .scrollbar-none { | |
| scrollbar-width: none; | |
| } | |
| .scrollbar-none::-webkit-scrollbar { | |
| width: 0; | |
| height: 0; | |
| } | |
| /* Hairline separators & surfaces. */ | |
| .hairline { | |
| @apply border-white/[0.07]; | |
| } | |
| /* "Double-bezel" nested cards — outer shell + inner core, concentric radii. */ | |
| .bezel { | |
| @apply rounded-[1.625rem] border border-white/[0.07] bg-white/[0.03] p-1.5; | |
| } | |
| .bezel-core { | |
| @apply rounded-[calc(1.625rem-0.375rem)] border border-white/[0.05] bg-zinc-900/70; | |
| box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); | |
| } | |
| /* Floating glass "island" nav — detached from the top edge. */ | |
| .island { | |
| @apply flex items-center gap-3 rounded-full border border-white/[0.08] bg-zinc-900/60 p-2 pl-4 | |
| shadow-[0_22px_50px_-28px_rgba(0,0,0,0.95)] backdrop-blur-xl; | |
| box-shadow: | |
| 0 22px 50px -28px rgba(0, 0, 0, 0.95), | |
| inset 0 1px 0 rgba(255, 255, 255, 0.07); | |
| } | |
| /* Pill controls (nav tabs, toggles, scope switches). */ | |
| .pill { | |
| @apply flex items-center gap-2 whitespace-nowrap rounded-full border border-transparent px-4 py-2 | |
| text-[13px] text-zinc-400 transition-all duration-[450ms] ease-smooth | |
| hover:bg-white/[0.05] hover:text-zinc-100 active:scale-[0.98]; | |
| } | |
| .pill-active { | |
| @apply border-accent/30 bg-accent/[0.12] text-accent-soft hover:bg-accent/[0.16] hover:text-accent-soft; | |
| } | |
| /* Circular icon button used inside islands and headers. */ | |
| .icon-btn { | |
| @apply grid h-9 w-9 shrink-0 place-items-center rounded-full border border-white/[0.08] bg-white/[0.03] | |
| text-zinc-400 transition-all duration-[450ms] ease-smooth | |
| hover:-translate-y-px hover:bg-white/[0.07] hover:text-zinc-100 active:scale-95; | |
| } | |
| /* Fixed film-grain overlay — breaks digital flatness without repainting on scroll. */ | |
| .grain { | |
| @apply pointer-events-none fixed inset-0 z-[45] opacity-[0.05]; | |
| mix-blend-mode: overlay; | |
| background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='160' height='160' filter='url(%23n)'/></svg>"); | |
| } | |
| /* Range sliders — thin accent track, floating knob, no native chrome. */ | |
| .range-accent { | |
| @apply h-1 w-full cursor-pointer appearance-none rounded-full bg-white/[0.09]; | |
| } | |
| .range-accent::-webkit-slider-thumb { | |
| @apply h-3.5 w-3.5 appearance-none rounded-full border-none bg-accent transition-transform duration-[400ms] ease-smooth; | |
| box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.14); | |
| } | |
| .range-accent:hover::-webkit-slider-thumb { | |
| transform: scale(1.12); | |
| } | |
| .range-accent:active::-webkit-slider-thumb { | |
| transform: scale(0.94); | |
| } | |
| .range-accent::-moz-range-thumb { | |
| @apply h-3.5 w-3.5 rounded-full border-none bg-accent; | |
| box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.14); | |
| } | |
| .range-accent::-moz-range-track { | |
| @apply h-1 rounded-full bg-white/[0.09]; | |
| } | |
| /* All numeric data: mono + tabular figures so columns line up. */ | |
| .num { | |
| @apply font-mono tabular-nums; | |
| } | |
| /* | |
| * Skip link (WCAG 2.4.1): visually hidden until focused, so keyboard users | |
| * can jump past the island nav and the sources rail straight to the thread. | |
| */ | |
| .skip-link { | |
| @apply sr-only; | |
| } | |
| .skip-link:focus { | |
| @apply fixed left-4 top-4 z-[60] rounded-xl bg-accent px-4 py-2.5 | |
| text-sm font-medium text-zinc-950 shadow-lg outline-none ring-2 ring-white/70; | |
| /* Undo sr-only's clipping explicitly. Using `not-sr-only` here would also | |
| emit `position: static`, and which of the two position declarations wins | |
| depends on Tailwind's utility ordering — too fragile for a control that | |
| only keyboard users ever see. */ | |
| width: auto; | |
| height: auto; | |
| margin: 0; | |
| overflow: visible; | |
| clip: auto; | |
| clip-path: none; | |
| white-space: nowrap; | |
| } | |
| /* Micro eyebrow label above section headings. */ | |
| .eyebrow { | |
| @apply text-[10px] uppercase tracking-[0.2em] text-muted; | |
| } | |
| /* | |
| * Staggered entry for lists (results, references, chips). The cascade tells | |
| * you these arrived in rank order — it carries meaning, it is not decoration. | |
| * Pure CSS via :nth-child, so no re-render and no JS timers. The delay caps | |
| * at the 8th child so a 30-hit search never crawls in. | |
| */ | |
| .stagger-in > * { | |
| animation: rise-in 380ms cubic-bezier(0.16, 1, 0.3, 1) backwards; | |
| } | |
| .stagger-in > *:nth-child(1) { | |
| animation-delay: 0ms; | |
| } | |
| .stagger-in > *:nth-child(2) { | |
| animation-delay: 45ms; | |
| } | |
| .stagger-in > *:nth-child(3) { | |
| animation-delay: 90ms; | |
| } | |
| .stagger-in > *:nth-child(4) { | |
| animation-delay: 135ms; | |
| } | |
| .stagger-in > *:nth-child(5) { | |
| animation-delay: 180ms; | |
| } | |
| .stagger-in > *:nth-child(6) { | |
| animation-delay: 225ms; | |
| } | |
| .stagger-in > *:nth-child(7) { | |
| animation-delay: 270ms; | |
| } | |
| .stagger-in > *:nth-child(n + 8) { | |
| animation-delay: 300ms; | |
| } | |
| /* Rail reveal — slides in from its own edge when expanded. */ | |
| .rail-in { | |
| animation: rail-in 420ms cubic-bezier(0.16, 1, 0.3, 1) backwards; | |
| } | |
| /* | |
| * Skeleton sheen: a light sweep across the placeholder instead of a pulsing | |
| * opacity. Reads as "loading", not as "this element is blinking at you". | |
| */ | |
| .sheen { | |
| @apply relative overflow-hidden; | |
| } | |
| .sheen::after { | |
| content: ""; | |
| @apply pointer-events-none absolute inset-0; | |
| transform: translateX(-100%); | |
| background-image: linear-gradient( | |
| 90deg, | |
| transparent 0%, | |
| rgba(255, 255, 255, 0.05) 50%, | |
| transparent 100% | |
| ); | |
| /* Offset per card via --sheen-delay so the sweeps trail each other. */ | |
| animation: sheen-sweep 1.55s cubic-bezier(0.4, 0, 0.2, 1) infinite; | |
| animation-delay: var(--sheen-delay, 0ms); | |
| } | |
| /* Markdown answer/summary rendering. */ | |
| .prose-chat { | |
| @apply text-[15.5px] leading-[1.72] text-zinc-200; | |
| } | |
| .prose-chat p { | |
| @apply my-2.5; | |
| text-wrap: pretty; | |
| } | |
| .prose-chat ul { | |
| @apply my-2 list-disc space-y-1 pl-5 marker:text-subtle; | |
| } | |
| .prose-chat ol { | |
| @apply my-2 list-decimal space-y-1 pl-5 marker:text-subtle; | |
| } | |
| .prose-chat h1, | |
| .prose-chat h2, | |
| .prose-chat h3 { | |
| @apply mt-3 mb-1 font-medium tracking-tight text-white; | |
| } | |
| .prose-chat code { | |
| @apply rounded bg-zinc-800/80 px-1.5 py-0.5 font-mono text-[13px] text-accent-soft; | |
| } | |
| .prose-chat pre { | |
| @apply my-2 overflow-x-auto rounded-xl border border-white/[0.05] bg-zinc-900 p-3 text-[13px]; | |
| } | |
| .prose-chat pre code { | |
| @apply bg-transparent p-0 text-zinc-200; | |
| } | |
| .prose-chat a { | |
| @apply text-accent underline decoration-accent/30 underline-offset-2 transition-colors hover:text-accent-soft; | |
| } | |
| .prose-chat table { | |
| @apply my-2 block w-full overflow-x-auto text-sm; | |
| } | |
| .prose-chat th, | |
| .prose-chat td { | |
| @apply border border-white/[0.07] px-2 py-1; | |
| } | |
| .prose-chat blockquote { | |
| @apply my-2 border-l-2 border-zinc-700 pl-3 text-zinc-400; | |
| } | |
| /* ---- KaTeX ------------------------------------------------------------ */ | |
| /* katex.min.css assumes a light background and hard-codes nothing for | |
| colour, so formulas only need to inherit the surrounding prose colour. */ | |
| .prose-chat .katex { | |
| @apply text-current; | |
| font-size: 1.05em; | |
| } | |
| /* A long derivation is the one thing that can break the chat bubble's width. | |
| Scroll the block itself instead, and keep vertical padding so tall | |
| delimiters and descenders are not clipped by the scroll container. */ | |
| .prose-chat .katex-display { | |
| @apply my-3 overflow-x-auto overflow-y-hidden py-1; | |
| } | |
| /* rehypeKatexA11y gives this element tabindex=0 so the scroll area is | |
| keyboard-reachable; a focusable element needs a visible focus ring | |
| (WCAG 2.4.7), and the browser default is easy to lose on a dark bubble. */ | |
| .prose-chat .katex-display:focus-visible { | |
| @apply rounded outline-none ring-1 ring-accent/50; | |
| } | |
| /* throwOnError is false, so a malformed formula arrives as .katex-error. | |
| Its colour comes from KaTeX's `errorColor` inline style (see Markdown.tsx) | |
| — an inline style outranks a class, so only the framing is set here. */ | |
| .prose-chat .katex-error { | |
| @apply rounded bg-rose-950/30 px-1 font-mono text-[13px]; | |
| } | |
| } | |