@import "tailwindcss"; @import "./theme.css"; @import "./typography.css"; /* --------------------------------------------------------------------------- * Dark mode strategy * * We use a class-based dark variant. Toggle dark mode by adding the `.dark` * class to (a theme provider like `next-themes` can do this later). * * → every `dark:*` utility activates * * Components should NOT use `dark:` prefixes directly. Instead they reference * semantic color tokens (e.g. `bg-bg-primary`, `text-text-primary`) that flip * under `.dark` in styles/theme.css. * --------------------------------------------------------------------------- */ @custom-variant dark (&:where(.dark, .dark *)); /* Resolve the nearest direction boundary, including nested LTR regions. * Tailwind's ancestor-based defaults also match inside an opposing region. */ @custom-variant rtl (&:dir(rtl)); @custom-variant ltr (&:dir(ltr)); /* The next theme is revealed by ThemeToggle from the exact interaction point. * Native view-transition crossfades are disabled so only the soft circular * clip-path animation runs; unsupported browsers simply ignore these rules. */ ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } @keyframes ai-chat-text-shimmer { from { background-position: 200% center; } to { background-position: -100% center; } } .agent-progress-loading-text { color: transparent; background-image: linear-gradient( 100deg, var(--color-text-secondary) 16%, var(--color-text-secondary) 38%, var(--color-text-primary) 50%, var(--color-text-secondary) 62%, var(--color-text-secondary) 84% ); background-position: 200% center; background-size: 300% 100%; background-clip: text; -webkit-background-clip: text; animation: ai-chat-text-shimmer 3.4s linear infinite; will-change: background-position; } @media (prefers-reduced-motion: reduce) { .agent-progress-loading-text { color: var(--color-text-secondary); background-image: none; animation: none; } } ::view-transition-old(root) { z-index: 1; } ::view-transition-new(root) { z-index: 2; } /* Components often have their own hover/focus color transitions. Freeze them * while the browser captures the next theme so the expanding circle contains * the fully resolved new palette instead of a frame that is still fading from * the old one. This is what makes the click-origin reveal read clearly. */ .theme-transitioning, .theme-transitioning * { transition: none !important; } /* Reset + base */ html, body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-variant-ligatures: contextual; font-kerning: normal; } html { color-scheme: light; background-color: var(--color-background-full); } html.dark { color-scheme: dark; } body { background-color: var(--color-background-full); color: var(--color-text-primary); } /* Theme-aware monochrome BoardUI mark. Both images keep their intrinsic * dimensions while the root theme class decides which one participates in * layout, avoiding a light-logo flash during hydration. */ .theme-logo-dark, .theme-asset-dark { display: none; } .dark .theme-logo-light, .dark .theme-asset-light { display: none; } .dark .theme-logo-dark, .dark .theme-asset-dark { display: block; } /* Tailwind expands shadow utilities into `--tw-shadow` at build time. Point * those generated utilities back to the live dark elevation tokens so the * class-based theme can adjust opacity without component-level overrides. */ .dark .shadow-2xs { --tw-shadow: var(--shadow-2xs); } .dark .shadow-xs { --tw-shadow: var(--shadow-xs); } .dark .shadow-sm { --tw-shadow: var(--shadow-sm); } .dark .shadow-md { --tw-shadow: var(--shadow-md); } .dark .shadow-lg { --tw-shadow: var(--shadow-lg); } .dark .shadow-xl { --tw-shadow: var(--shadow-xl); } .dark .shadow-card { --tw-shadow: var(--shadow-card); } .dark .shadow-dropdown { --tw-shadow: var(--shadow-dropdown); } .dark .shadow-sidebar { --tw-shadow: var(--shadow-sidebar); } .dark .shadow-waitlist { --tw-shadow: var(--shadow-waitlist); } /* Hide default expand arrow on Safari */ details summary::-webkit-details-marker { display: none; } /* Hide default number-input spinners */ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } input[type="number"] { -moz-appearance: textfield; } /* Hide default search clear button */ input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } /* iOS Safari auto-zooms the page when a focused form field's font-size is * under 16px — our text-body-regular/medium tokens are 14px by design, so * bump just the font-size (not the whole token) on small screens instead of * changing the design system's type scale. */ @media (max-width: 639px) { input, select, textarea { font-size: 16px; } } /* Soft fade applied to an animated number each time its target changes */ @keyframes number-fade { from { opacity: 0.35; } to { opacity: 1; } } .animate-number-fade { animation: number-fade 220ms ease-out; } /* Recharts' accessibility layer makes the chart surface and its shapes * (bars, sectors, dots) keyboard-focusable, and Chrome paints the UA focus * outline around whichever one you click - a blue box around a ring segment * or a bar. Every chart card drives its own focus feedback (hover darkening, * headline swap), so the outline is noise: hide it across every Recharts * chart. Keyboard users still get the cards' own active states. */ .recharts-wrapper { /* SVG axes use physical coordinates. Keep their data and numeric labels in * order while card headers, legends and surrounding controls follow RTL. */ direction: ltr; } .recharts-wrapper:focus, .recharts-wrapper:focus-visible, .recharts-wrapper :focus, .recharts-wrapper :focus-visible { outline: none; } /* Liquid glass (landing nav + hero) — the rim light. * * A fixed conic gradient standing in for a bevel lit from a single direction: * bright along the top edge where the light source is, dark down both sides * where the glass turns away, and a weaker bounce along the bottom. It does * not rotate — a travelling highlight reads as an animation playing on the * control, where a real edge just sits there catching the room. * * Achromatic on purpose: highlights are white, shading is black, nothing in * between carries a hue. * * The two-layer mask (padding-box XOR full box) keeps only the 1.5px ring, * letting the gradient paint the stroke without covering the glass. */ .landing-glass-stroke { pointer-events: none; position: absolute; inset: 0; border-radius: inherit; /* Every value here is set inline by `LiquidGlassSurface` from the shared * glass config; the fallbacks are only for a bare use of the class. */ padding: var(--lg-rim-width, 1.5px); background: conic-gradient( from var(--lg-rim-angle, 0deg), var(--lg-rim-hi, rgb(255 255 255 / 0.9)) 0deg, var(--lg-rim-sh, rgb(0 0 0 / 0.14)) 70deg, var(--lg-rim-dim, rgb(255 255 255 / 0.1)) 140deg, var(--lg-rim-bounce, rgb(255 255 255 / 0.55)) 180deg, var(--lg-rim-dim, rgb(255 255 255 / 0.1)) 220deg, var(--lg-rim-sh, rgb(0 0 0 / 0.14)) 290deg, var(--lg-rim-hi, rgb(255 255 255 / 0.9)) 360deg ); /* Shorthands first, composites last: `-webkit-mask` aliases `mask` in * Chromium, so a shorthand placed after a composite silently resets it to * `add` — and an un-composited mask paints the full conic wash over the * button instead of a ring. */ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; opacity: var(--lg-rim-o, 1); transition: opacity var(--lg-ms, 200ms) ease; } /* Liquid glass hover. * * Resting and hover values arrive together as inline custom properties, and * the swap happens here in CSS rather than in React state — hover then costs * a style recalculation instead of a re-render of everything inside the glass. * `.lg-hover` is the opt-in: a surface without it keeps its resting value. */ .lg-tint { background-color: var(--lg-tint, transparent); transition: background-color var(--lg-ms, 200ms) ease; } .lg-sheen { opacity: var(--lg-sheen, 0.5); transition: opacity var(--lg-ms, 200ms) ease; } .lg-host { box-shadow: var(--lg-shadow, none); transition: box-shadow var(--lg-ms, 200ms) ease, transform var(--lg-ms, 200ms) ease; } .lg-host:hover { box-shadow: var(--lg-shadow-hover, var(--lg-shadow, none)); transform: translateY(var(--lg-lift, 0px)) scale(var(--lg-scale, 1)); } .group:hover .lg-tint.lg-hover { background-color: var(--lg-tint-hover, var(--lg-tint, transparent)); } .group:hover .lg-sheen.lg-hover { opacity: var(--lg-sheen-hover, var(--lg-sheen, 0.5)); } .group:hover .landing-glass-stroke.lg-hover { opacity: var(--lg-rim-o-hover, var(--lg-rim-o, 1)); } @media (prefers-reduced-motion: reduce) { .lg-tint, .lg-sheen, .lg-host, .landing-glass-stroke { transition: none; } .lg-host:hover { transform: none; } } /* Split-flap ("Solari" board) digit, used by the image-generation countdown. * Two leaves hinged on the card's centre seam: * flap-fall the outgoing digit's TOP half swings down over the incoming * bottom half. Eased IN, because a real flap is falling under * gravity — it accelerates — and it darkens as it tips away * from the light. * flap-rise the incoming digit's BOTTOM half swings up from edge-on, * starting exactly when the fall lands (delay = fall duration, * `both` so it waits at 90deg instead of flashing flat). The * small overshoot past 0deg is the slap against the stop — * that bounce is what makes it read mechanical rather than * like a plain rotation. */ @keyframes flap-fall { from { transform: rotateX(0deg); filter: brightness(1); } to { transform: rotateX(-90deg); filter: brightness(0.86); } } @keyframes flap-rise { 0% { transform: rotateX(90deg); filter: brightness(0.86); } 70% { transform: rotateX(-9deg); } 86% { transform: rotateX(4deg); } 100% { transform: rotateX(0deg); filter: brightness(1); } } .animate-flap-fall { transform-origin: bottom center; backface-visibility: hidden; animation: flap-fall 150ms cubic-bezier(0.55, 0, 0.9, 0.35) forwards; } .animate-flap-rise { transform-origin: top center; backface-visibility: hidden; animation: flap-rise 230ms cubic-bezier(0.25, 0.7, 0.35, 1) 150ms both; } @media (prefers-reduced-motion: reduce) { .animate-flap-fall, .animate-flap-rise { animation-duration: 1ms; animation-delay: 0ms; } } /* New-row entrance for the Storage settings table. Three coordinated * pieces (wrapper pairs with `grid grid-rows-[1fr]` + an overflow-hidden * middle layer, content carries the slide): * row-grow-in wrapper's grid track 0fr → 1fr — pushes the rows below * down smoothly. * row-slide-in content translates -100% → 0 with the SAME duration and * easing, so it stays glued to the growing wrapper's bottom * edge — visually the row slides out from underneath the * header row above it. * row-fade-in opacity/blur on a SHORTER clock than the movement — the * row is fully visible before it finishes settling (fades * must complete before movement ends). */ @keyframes row-grow-in { from { grid-template-rows: 0fr; } to { grid-template-rows: 1fr; } } @keyframes row-slide-in { from { translate: 0 -100%; } } @keyframes row-fade-in { from { opacity: 0; filter: blur(2px); } } .animate-row-grow-in { animation: row-grow-in 400ms ease-out; } .animate-row-slide-in { animation: row-slide-in 400ms ease-out, row-fade-in 240ms ease-out; } /* Reverse of the Storage row entrance. The content rises into the clipped * top edge while its grid track collapses, so the following rows move upward * continuously and the deleted row disappears beneath the header surface. */ @keyframes row-collapse-out { from { grid-template-rows: 1fr; } to { grid-template-rows: 0fr; } } @keyframes row-slide-out { to { translate: 0 -100%; } } @keyframes row-fade-out { to { opacity: 0; filter: blur(2px); } } .animate-row-collapse-out { animation: row-collapse-out 225ms ease-in forwards; } .animate-row-slide-out { animation: row-slide-out 225ms ease-in forwards, row-fade-out 160ms ease-in forwards; } @media (prefers-reduced-motion: reduce) { .animate-row-grow-in, .animate-row-slide-in, .animate-row-collapse-out, .animate-row-slide-out { animation-duration: 1ms; } } /* Transitions.dev — "Texts reveal", used by the Storage dropzone's content * swaps (idle copy ↔ file name + status line). * * Entrance (`.is-shown` on the parent): lines rise 12px from below while * un-blurring, 500ms on a soft-landing bezier, staggered 40ms per line so * the eye lands on the first line first. * Exit (`.is-hiding`): a single quiet 200ms opacity fade in place — no Y * return, no blur, no stagger — so the disappearance never reads as a * reverse reveal. * * Deviations from the original snippet: custom props are scoped to * `.t-stagger` instead of :root, and `.t-stagger-line` doesn't force * `display: block` so flex-based lines (the icon disc) keep their layout. */ .t-stagger { --stagger-dur: 500ms; --stagger-distance: 12px; --stagger-stagger: 40ms; --stagger-blur: 3px; --stagger-ease: cubic-bezier(0.22, 1, 0.36, 1); } .t-stagger-line { opacity: 0; transform: translateY(var(--stagger-distance)); filter: blur(var(--stagger-blur)); transition: opacity var(--stagger-dur) var(--stagger-ease), transform var(--stagger-dur) var(--stagger-ease), filter var(--stagger-dur) var(--stagger-ease); will-change: transform, opacity, filter; } .t-stagger-line--2 { transition-delay: var(--stagger-stagger); } .t-stagger-line--3 { transition-delay: calc(var(--stagger-stagger) * 2); } .t-stagger.is-shown .t-stagger-line { opacity: 1; transform: translateY(0); filter: blur(0); } .t-stagger.is-hiding .t-stagger-line { opacity: 0; transform: translateY(0); filter: blur(0); transition: opacity 200ms ease, transform 0s linear, filter 0s linear; transition-delay: 0s; } @media (prefers-reduced-motion: reduce) { .t-stagger-line { transition: none !important; } } /* Rolling-counter label swap: the old value lifts 5px and fades out while the * new one rises in from 5px below and fades in. */ @keyframes label-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-10px); opacity: 0; } } @keyframes label-in { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .animate-label-out { animation: label-out 220ms ease-out forwards; } .animate-label-in { animation: label-in 220ms ease-out; } /* Checkbox tick: draws the stroke on like it's being ticked by hand. * The path sets pathLength=1 so the dash math is resolution-independent. */ @keyframes check-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } } .animate-check-draw { stroke-dasharray: 1; stroke-dashoffset: 1; animation: check-draw 200ms cubic-bezier(0.65, 0, 0.35, 1) forwards; } @media (prefers-reduced-motion: reduce) { .animate-check-draw { animation: none; stroke-dashoffset: 0; } } /* --------------------------------------------------------------------------- * Chart entrance animations (AI profile template & contributions heatmap) * ------------------------------------------------------------------------- */ /* Heatmap cells pop in softly — each cell gets a random-looking per-cell * animation-delay so the grid fills in a scattered order. */ .contributions-grid { --contribution-tier-0: var(--color-chart-neutral); } .contributions-grid[data-accent="emerald"] { --contribution-tier-1: var(--color-emerald-200); --contribution-tier-2: var(--color-emerald-400); --contribution-tier-3: var(--color-emerald-500); --contribution-tier-4: var(--color-emerald-600); --contribution-tier-5: var(--color-emerald-700); } .contributions-grid[data-accent="green"] { --contribution-tier-1: var(--color-green-200); --contribution-tier-2: var(--color-green-400); --contribution-tier-3: var(--color-green-500); --contribution-tier-4: var(--color-green-600); --contribution-tier-5: var(--color-green-700); } .contributions-grid[data-accent="teal"] { --contribution-tier-1: var(--color-teal-200); --contribution-tier-2: var(--color-teal-400); --contribution-tier-3: var(--color-teal-500); --contribution-tier-4: var(--color-teal-600); --contribution-tier-5: var(--color-teal-700); } .contributions-grid[data-accent="cyan"] { --contribution-tier-1: var(--color-cyan-200); --contribution-tier-2: var(--color-cyan-400); --contribution-tier-3: var(--color-cyan-500); --contribution-tier-4: var(--color-cyan-600); --contribution-tier-5: var(--color-cyan-700); } .contributions-grid[data-accent="blue"] { --contribution-tier-1: var(--color-blue-200); --contribution-tier-2: var(--color-blue-400); --contribution-tier-3: var(--color-blue-500); --contribution-tier-4: var(--color-blue-600); --contribution-tier-5: var(--color-blue-700); } .contributions-grid[data-accent="indigo"] { --contribution-tier-1: var(--color-indigo-200); --contribution-tier-2: var(--color-indigo-400); --contribution-tier-3: var(--color-indigo-500); --contribution-tier-4: var(--color-indigo-600); --contribution-tier-5: var(--color-indigo-700); } .contributions-grid[data-accent="violet"] { --contribution-tier-1: var(--color-violet-200); --contribution-tier-2: var(--color-violet-400); --contribution-tier-3: var(--color-violet-500); --contribution-tier-4: var(--color-violet-600); --contribution-tier-5: var(--color-violet-700); } .contributions-grid[data-accent="rose"] { --contribution-tier-1: var(--color-rose-200); --contribution-tier-2: var(--color-rose-400); --contribution-tier-3: var(--color-rose-500); --contribution-tier-4: var(--color-rose-600); --contribution-tier-5: var(--color-rose-700); } .contributions-grid[data-accent="amber"] { --contribution-tier-1: var(--color-amber-200); --contribution-tier-2: var(--color-amber-400); --contribution-tier-3: var(--color-amber-500); --contribution-tier-4: var(--color-amber-600); --contribution-tier-5: var(--color-amber-700); } .dark .contributions-grid[data-accent="emerald"] { --contribution-tier-1: var(--color-emerald-950); --contribution-tier-2: var(--color-emerald-800); --contribution-tier-3: var(--color-emerald-700); --contribution-tier-4: var(--color-emerald-600); --contribution-tier-5: var(--color-emerald-500); } .dark .contributions-grid[data-accent="green"] { --contribution-tier-1: var(--color-green-950); --contribution-tier-2: var(--color-green-800); --contribution-tier-3: var(--color-green-700); --contribution-tier-4: var(--color-green-600); --contribution-tier-5: var(--color-green-500); } .dark .contributions-grid[data-accent="teal"] { --contribution-tier-1: var(--color-teal-950); --contribution-tier-2: var(--color-teal-800); --contribution-tier-3: var(--color-teal-700); --contribution-tier-4: var(--color-teal-600); --contribution-tier-5: var(--color-teal-500); } .dark .contributions-grid[data-accent="cyan"] { --contribution-tier-1: var(--color-cyan-950); --contribution-tier-2: var(--color-cyan-800); --contribution-tier-3: var(--color-cyan-700); --contribution-tier-4: var(--color-cyan-600); --contribution-tier-5: var(--color-cyan-500); } .dark .contributions-grid[data-accent="blue"] { --contribution-tier-1: var(--color-blue-950); --contribution-tier-2: var(--color-blue-800); --contribution-tier-3: var(--color-blue-700); --contribution-tier-4: var(--color-blue-600); --contribution-tier-5: var(--color-blue-500); } .dark .contributions-grid[data-accent="indigo"] { --contribution-tier-1: var(--color-indigo-950); --contribution-tier-2: var(--color-indigo-800); --contribution-tier-3: var(--color-indigo-700); --contribution-tier-4: var(--color-indigo-600); --contribution-tier-5: var(--color-indigo-500); } .dark .contributions-grid[data-accent="violet"] { --contribution-tier-1: var(--color-violet-950); --contribution-tier-2: var(--color-violet-800); --contribution-tier-3: var(--color-violet-700); --contribution-tier-4: var(--color-violet-600); --contribution-tier-5: var(--color-violet-500); } .dark .contributions-grid[data-accent="rose"] { --contribution-tier-1: var(--color-rose-950); --contribution-tier-2: var(--color-rose-800); --contribution-tier-3: var(--color-rose-700); --contribution-tier-4: var(--color-rose-600); --contribution-tier-5: var(--color-rose-500); } .dark .contributions-grid[data-accent="amber"] { --contribution-tier-1: var(--color-amber-950); --contribution-tier-2: var(--color-amber-800); --contribution-tier-3: var(--color-amber-700); --contribution-tier-4: var(--color-amber-600); --contribution-tier-5: var(--color-amber-500); } .contribution-cell[data-tier="0"] { background-color: var(--contribution-tier-0); } .contribution-cell[data-tier="1"] { background-color: var(--contribution-tier-1); } .contribution-cell[data-tier="2"] { background-color: var(--contribution-tier-2); } .contribution-cell[data-tier="3"] { background-color: var(--contribution-tier-3); } .contribution-cell[data-tier="4"] { background-color: var(--contribution-tier-4); } .contribution-cell[data-tier="5"] { background-color: var(--contribution-tier-5); } @keyframes cell-pop { from { opacity: 0; transform: scale(0.5); } to { opacity: 1; transform: scale(1); } } .animate-cell-pop { animation: cell-pop 380ms ease-out both; } /* Bars grow up from the baseline; a per-bar delay staggers them left→right. */ @keyframes bar-rise { from { transform: scaleY(0); } to { transform: scaleY(1); } } .animate-bar-rise { transform-origin: bottom; animation: bar-rise 360ms cubic-bezier(0.22, 1, 0.36, 1) both; } /* Line chart draws in left→right via a clip-path sweep over the whole plot * (line + gradient area together). */ @keyframes chart-reveal { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } } .animate-chart-reveal { animation: chart-reveal 1800ms cubic-bezier(0.33, 0, 0.15, 1) both; } @media (prefers-reduced-motion: reduce) { .animate-cell-pop, .animate-bar-rise, .animate-chart-reveal { animation: none; } } /* --------------------------------------------------------------------------- * Table primitive (components/base/table) * * React Aria's collection components (Column/Row/Cell/…) can't be wrapped in * custom components without losing collection behaviour, so the primitive * re-exports them untouched and styling lives here, scoped to `.bui-table`. * React Aria renders real
| / |
|---|
| elements. * --------------------------------------------------------------------------- */ .bui-table { @apply w-full border-collapse text-start; } /* Header background + border-y live on the cells: with border-collapse the * browser doesn't paint backgrounds/borders set on the row group. * Backgrounds/borders are set with explicit CSS (theme vars) rather than * @apply, which drops border-width/bg on these row-group cells. */ .bui-table th { @apply px-3 py-2.5 text-start align-middle text-body-medium text-text-tertiary whitespace-nowrap outline-none; background-color: var(--color-background-secondary-default); border-top: 1px solid var(--color-separator-border); border-bottom: 1px solid var(--color-separator-border); transition: padding 200ms ease, font-size 200ms ease; } .bui-table td { @apply px-3 py-2.5 align-middle text-body-medium text-text-primary outline-none; transition: padding 200ms ease, font-size 200ms ease; } /* Separators sit *between* rows only: the trailing row's boundary is drawn by * whatever follows the table (the container's bottom edge, or the pagination * footer's own border-top), so a filtered-down table never doubles up lines. */ .bui-table tbody tr:not(:last-child) { border-bottom: 1px solid var(--color-separator-border); } .bui-table tbody tr { @apply transition-colors duration-150; } .bui-table tbody tr[data-focus-visible] { @apply ring-2 ring-inset ring-border-focus-ring; } .bui-table.bui-table-sm th, .bui-table.bui-table-sm td { @apply px-2.5 py-1.5 text-body-2-medium; } /* Agent Thinking (components/application/agent-thinking): the agent thinking state * above a chat composer. The label reuses the ai-chat-text-shimmer travel; * its base/highlight both derive from the tone the component sets via * --bui-agent-thinking-tone, so every tone shimmers consistently. */ .bui-agent-thinking-label { color: transparent; background-image: linear-gradient( 100deg, color-mix(in oklab, var(--bui-agent-thinking-tone) 55%, transparent) 30%, var(--bui-agent-thinking-tone) 50%, color-mix(in oklab, var(--bui-agent-thinking-tone) 55%, transparent) 70% ); background-position: 200% center; background-size: 300% 100%; background-clip: text; -webkit-background-clip: text; animation: ai-chat-text-shimmer 2.6s linear infinite; will-change: background-position; } /* Star sparkle: scale + fade in, settle, and vanish; per-star duration and * stagger come in via inline animation-duration/-delay. */ @keyframes bui-agent-thinking-star { 0%, 100% { transform: scale(0); opacity: 0; } 40% { transform: scale(1); opacity: 1; } 60% { transform: scale(0.8); opacity: 0.9; } } .bui-agent-thinking-star { transform: scale(0); animation-name: bui-agent-thinking-star; animation-timing-function: ease-in-out; animation-iteration-count: infinite; } /* Infinity comet: the dash segment laps the normalized (pathLength=100) * figure-eight once per animation-duration. */ @keyframes bui-agent-thinking-dash { to { stroke-dashoffset: -100; } } .bui-agent-thinking-comet { animation-name: bui-agent-thinking-dash; animation-timing-function: linear; animation-iteration-count: infinite; } @media (prefers-reduced-motion: reduce) { .bui-agent-thinking-label { color: var(--bui-agent-thinking-tone); background-image: none; animation: none; } /* Stars rest visible instead of frozen at scale(0). */ .bui-agent-thinking-star { animation: none; transform: none; opacity: 0.7; } .bui-agent-thinking-comet { animation: none; } } /* Star glyphs read a touch heavier than the dot grids on light backgrounds; * soften only them (not the label) there. Dark mode keeps the full tone. */ .bui-agent-thinking-stars { color: color-mix(in oklab, currentColor 75%, transparent); } .dark .bui-agent-thinking-stars { color: inherit; } /* Composer Loader (components/application/composer-loader): the light band * orbiting the composer while the agent works. Dash segments on a * pathLength-normalized SVG stroke — the offset laps once per duration. */ @keyframes bui-composer-loader-dash { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -100; } } @media (prefers-reduced-motion: reduce) { .bui-composer-loader-rect { animation: none !important; } } /* Composer mic listening state (components/application/ai-chat): equalizer * bars dancing while voice input is active. Per-bar duration/delay come in * inline so the bars drift out of phase like real audio. */ @keyframes bui-composer-mic-bars { 0%, 100% { transform: scaleY(0.4); } 50% { transform: scaleY(1); } } .bui-composer-mic-bar { animation: bui-composer-mic-bars 0.9s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .bui-composer-mic-bar { animation: none; transform: scaleY(0.7); } } /* Shimmer sweep on a primary call to action (the Pro prompt card). */ @keyframes bui-badge-shimmer { 0% { transform: translateX(0) skewX(-12deg); } 40%, 100% { transform: translateX(350%) skewX(-12deg); } } .bui-badge-shimmer { animation: bui-badge-shimmer 2.5s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .bui-badge-shimmer { animation: none; opacity: 0; } } /* Showcase card (docs index, landing, the starter's catalogue). */ .landing-showcase-card { transition: box-shadow 280ms ease; } .landing-showcase-card::after { content: ""; position: absolute; inset: 0; z-index: 60; pointer-events: none; border: 1px solid var(--color-border-button-default); border-radius: inherit; transition: border-color 280ms ease, box-shadow 280ms ease; } .dark .landing-showcase-card::after { border-color: rgb(255 255 255 / 0.08); } @media (min-width: 640px) and (hover: hover) { .landing-showcase-card:hover::after, .landing-showcase-card:focus-within::after { border-color: var(--color-border-button-hover); box-shadow: inset 0 0 0 2px var(--color-border-button-hover); } } |