Global styles

Tailwind entry css: dark-mode variant, base resets, component animations, and table styling. Imports theme.css and typography.css.

Installation

npx shadcn@latest add https://boardcn.dev/r/globals.json
npx shadcn@latest add https://boardcn.dev/r/globals.json

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

styles/globals.css
@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 <html> (a theme provider like `next-themes` can do this later).
 *
 *   <html class="dark">  →  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 *));

/* 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;
  }
}

@keyframes image-generation-pulse {
  0%,
  100% {
    opacity: 0.16;
    scale: 0.62;
  }

  50% {
    opacity: 0.95;
    scale: 1.18;
  }
}

.image-generation-pulse-dot {
  animation: image-generation-pulse 1.8s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .image-generation-pulse-dot {
    animation: none;
    opacity: 0.58;
    scale: 1;
  }
}

::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 BoardCN 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;
}

/* TanStack ships here as a transparent black raster mark. Normalizing the
 * visible pixels to black before inversion keeps its antialiased alpha edges
 * intact while producing a clean white logo on dark surfaces. */
.tanstack-logo {
  filter: none;
}
.dark .tanstack-logo {
  filter: brightness(0) invert(1);
}

/* 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);
}

/* Component documentation uses a quieter structural border than interactive
 * controls. Scope it to container elements so buttons, inputs, and menu
 * triggers retain their own border-state tokens. */
.component-detail :where(div, section, article, ul, ol, table, fieldset).border-border-button-default {
  border-color: var(--color-border-component-detail-container);
}

/* 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;
}

@media (prefers-reduced-motion: reduce) {
  .animate-number-fade {
    animation: none !important;
  }
}

/* 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:focus,
.recharts-wrapper:focus-visible,
.recharts-wrapper :focus,
.recharts-wrapper :focus-visible {
  outline: none;
}

/* Landing hero widget marquee. The track holds TWO copies of the card set,
 * so translating exactly -50% and looping restarts on identical pixels —
 * the seam is invisible as long as every card carries the same 20px
 * right-margin. `translate`, not `transform`, per the codebase convention
 * (Tailwind v4 utilities write the `translate` property). */
@keyframes landing-marquee {
  to {
    translate: -50% 0;
  }
}
/* 72s, not 60s: the track grew from 5 cards to 6, and the duration covers
 * the whole -50% sweep — holding it constant would have sped the row up. */
.animate-landing-marquee {
  animation: landing-marquee 72s linear infinite;
}

@keyframes landing-file-drop {
  0% {
    opacity: 0;
    transform: translate3d(-92px, 64px, 0) rotate(-10deg) scale(0.76);
  }
  12% {
    opacity: 1;
    transform: translate3d(-76px, 52px, 0) rotate(-7deg) scale(0.9);
  }
  88% {
    opacity: 1;
    transform: translate3d(52px, -38px, 0) rotate(6deg) scale(0.82);
  }
  100% {
    opacity: 0;
    transform: translate3d(68px, -50px, 0) rotate(8deg) scale(0.1);
  }
}

.animate-landing-file-drop {
  backface-visibility: hidden;
  transform-origin: center;
  will-change: transform, opacity;
  animation: landing-file-drop 1300ms linear both;
}

/* Block cursor on the landing install line. `steps(1)` — a terminal caret
 * snaps between on and off, it never fades. */
@keyframes terminal-caret {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}
.animate-terminal-caret {
  animation: terminal-caret 1060ms steps(1, end) infinite;
}

/* Phosphor bloom around the caret: a tight halo all round plus a wider pool
 * cast below it, so the glow pools under the line the way a lit cursor does
 * on a CRT. Sized in em so it scales with the install line's 12px → 18px
 * responsive step. */
.landing-terminal-caret {
  box-shadow:
    0 0 0.3em color-mix(in srgb, var(--color-purple-500) 60%, transparent),
    0 0.22em 0.75em color-mix(in srgb, var(--color-purple-500) 55%, transparent),
    0 0.4em 1.1em color-mix(in srgb, var(--color-purple-500) 30%, transparent);
}

/* 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. */
/* Pro badge sheen. Rotating the conic layer reads as light travelling around
 * brushed metal; the badge clips it, so only the sweep is visible. */
@keyframes bui-metal-sweep {
  to {
    transform: rotate(360deg);
  }
}

.bui-metal-sweep {
  animation: bui-metal-sweep 9s linear infinite;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .bui-metal-sweep {
    animation: none;
  }
}

/* Discount-badge glitter: tiny four-point stars around the pill, each fading
 * in and out on its own clock. Peak opacity stays low so it reads as a
 * shimmer, not confetti; per-star size/timing come in via custom properties. */
@keyframes bui-glitter {
  0%,
  100% {
    opacity: 0;
    transform: scale(0.2) rotate(-20deg);
  }
  50% {
    opacity: var(--glitter-peak, 0.6);
    transform: scale(1) rotate(15deg);
  }
}

.bui-glitter {
  animation: bui-glitter var(--glitter-duration, 3s) ease-in-out var(--glitter-delay, 0s)
    infinite;
}

@media (prefers-reduced-motion: reduce) {
  .bui-glitter {
    animation: none;
    opacity: 0;
  }
}

/* Discount-badge shimmer: the band starts a full badge-width left of the pill
 * and crosses it in the first 40% of the cycle, then idles — one pass every
 * 2.5s. Eased per-segment, so the sweep itself accelerates and settles. */
@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;
  }
}

.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;
  }
}

.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);
  }
}

.landing-hero-dither-character {
  backface-visibility: hidden;
  will-change: mask-image, mask-position, opacity, transform, color;
}

@media (prefers-reduced-motion: reduce) {
  .animate-landing-marquee {
    animation: none;
  }

  /* Keep the caret, drop the blink. */
  .animate-terminal-caret {
    animation: 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 <table>/<thead>/<th>/<tr>/<td> elements.
 * --------------------------------------------------------------------------- */
.bui-table {
  @apply w-full border-collapse text-left;
}
/* Header background + border-y live on the cells: with border-collapse the
 * browser doesn't paint backgrounds/borders set on the <thead> 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-left 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;
}
.bui-table tbody tr {
  @apply transition-colors duration-150;
  border-bottom: 1px solid var(--color-separator-border);
}
.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;
}

/* Beam travelling the FAQ card's border.
 *
 * A conic gradient rotated by an `@property`-typed angle, clipped to a thin
 * ring by the mask-xor trick the glass stroke uses (see `.landing-glass-stroke`
 * for why the shorthand has to precede the composite). The glow is the same
 * ring, wider and blurred, underneath.
 *
 * Both layers are masked. An unmasked glow paints the whole conic wash across
 * the card's interior, which reads as a coloured blob in the middle rather
 * than light on the edge.
 *
 * The angle is a registered property so it can be animated at all: a plain
 * custom property is an unanimatable string, and the gradient would jump
 * between keyframes instead of sweeping. */
@property --bui-beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@keyframes bui-beam-travel {
  to {
    --bui-beam-angle: 360deg;
  }
}

.bui-beam,
.bui-beam-glow,
.bui-beam-aura {
  pointer-events: none;
  position: absolute;
  /* -1px, not 0: `inset: 0` lands on the padding box, which puts the ring
   * just inside the card's own border instead of on top of it. */
  inset: -1px;
  /* Not `inherit`. Each layer sits further out than the card, and a box that
   * is bigger with the same radius has a *tighter* corner — the ring then cuts
   * across the card's own corner and leaves a notch in the border. The radius
   * has to grow by exactly the outset. The host sets --bui-beam-radius to its
   * own corner. */
  border-radius: calc(var(--bui-beam-radius, 26px) + 1px);
  padding: 1.5px;
  /* The sweep runs to 360deg, so the stop at 360 is the beam's leading tip and
   * the ramp behind it is the tail. Brightest at the tip: it is the front of
   * the travelling light, and the blurred copy underneath turns that peak into
   * the glow. Anything dimmer at 360 puts the highlight mid-beam instead. */
  background: conic-gradient(
    from var(--bui-beam-angle),
    transparent 0deg,
    transparent 272deg,
    rgb(104 136 251 / 0.22) 305deg,
    rgb(104 136 251 / 0.8) 340deg,
    rgb(150 176 255 / 0.95) 353deg,
    rgb(219 230 255 / 1) 360deg
  );
  -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;
  /* Two laps, then it stays gone: `forwards` holds the final keyframe so the
   * gradient does not snap back to its start as the fade-out runs. The host
   * mounts these only once the card is on screen. */
  animation: bui-beam-travel 4.5s linear 2 forwards;
  transition: opacity 700ms ease;
}

/* The same ring, blurred. Identical inset and padding to the crisp one on
 * purpose: a wider band here drew a second, softer line beside the first and
 * the border read as double. Same geometry, blurred, is one line with a glow —
 * strongest at the tip, where the gradient peaks. */
.bui-beam-glow {
  filter: blur(4px);
  opacity: 0.85;
}

/* The aura the head carries with it.
 *
 * Its own gradient starts far later than the beam's, so the light gathers
 * around the leading tip instead of tracing the whole tail — that is what
 * makes it read as a glow travelling with the beam rather than a second,
 * fatter border. Wide band, heavy blur, low opacity: enough to spill a little
 * past the stroke, not enough to become an object of its own. The card must
 * leave its overflow visible or this is the layer that gets cut off. */
.bui-beam-aura {
  inset: -4px;
  border-radius: calc(var(--bui-beam-radius, 26px) + 4px);
  padding: 4px;
  background: conic-gradient(
    from var(--bui-beam-angle),
    transparent 0deg,
    transparent 322deg,
    rgb(104 136 251 / 0.4) 348deg,
    rgb(150 176 255 / 0.75) 360deg
  );
  filter: blur(8px);
  opacity: 0.5;
}

/* Set by the host once both laps are done. */
.bui-beam-spent {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .bui-beam,
  .bui-beam-glow,
  .bui-beam-aura {
    animation: none;
    opacity: 0;
  }
}

/* Agent Thinking (components/blocks/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/blocks/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/blocks/composer): 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);
  }
}

/* Composer effort slider (Ultra): the rail is under load. A one-shot kick
 * then a tiny ongoing tremor, while a purple fire jet trails left off the
 * thumb (see EffortFlame in composer.tsx). Reduced-motion keeps a static
 * taper instead of the shader. */
@keyframes bui-effort-ultra-kick {
  0%,
  100% {
    transform: translate3d(0, 0, 0);
  }
  18% {
    transform: translate3d(-1.6px, 0.8px, 0);
  }
  38% {
    transform: translate3d(1.4px, -0.7px, 0);
  }
  58% {
    transform: translate3d(-1.1px, -0.9px, 0);
  }
  76% {
    transform: translate3d(0.9px, 0.5px, 0);
  }
}

@keyframes bui-effort-ultra-tremor {
  0%,
  100% {
    transform: translate3d(0, 0, 0);
  }
  25% {
    transform: translate3d(-0.9px, 0.55px, 0);
  }
  50% {
    transform: translate3d(0.8px, -0.45px, 0);
  }
  75% {
    transform: translate3d(-0.65px, -0.6px, 0);
  }
}

.bui-effort-ultra {
  animation:
    bui-effort-ultra-kick 420ms ease-out 1,
    bui-effort-ultra-tremor 180ms linear 420ms infinite;
  will-change: transform;
}

.bui-effort-ultra-rail {
  background: color-mix(in oklab, var(--color-background-secondary-default) 70%, #160814);
}

.bui-effort-ultra-flame-static {
  pointer-events: none;
  position: absolute;
  inset: 0;
  background: linear-gradient(
    270deg,
    rgb(255 242 255 / 0.88) 0%,
    rgb(196 92 255 / 0.72) 16%,
    rgb(124 42 186 / 0.4) 46%,
    transparent 78%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 6%,
    black 32%,
    black 68%,
    transparent 94%
  );
}

@media (prefers-reduced-motion: reduce) {
  .bui-effort-ultra {
    animation: none;
    will-change: auto;
  }
}
@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 <html> (a theme provider like `next-themes` can do this later).
 *
 *   <html class="dark">  →  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 *));

/* 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;
  }
}

@keyframes image-generation-pulse {
  0%,
  100% {
    opacity: 0.16;
    scale: 0.62;
  }

  50% {
    opacity: 0.95;
    scale: 1.18;
  }
}

.image-generation-pulse-dot {
  animation: image-generation-pulse 1.8s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .image-generation-pulse-dot {
    animation: none;
    opacity: 0.58;
    scale: 1;
  }
}

::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 BoardCN 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;
}

/* TanStack ships here as a transparent black raster mark. Normalizing the
 * visible pixels to black before inversion keeps its antialiased alpha edges
 * intact while producing a clean white logo on dark surfaces. */
.tanstack-logo {
  filter: none;
}
.dark .tanstack-logo {
  filter: brightness(0) invert(1);
}

/* 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);
}

/* Component documentation uses a quieter structural border than interactive
 * controls. Scope it to container elements so buttons, inputs, and menu
 * triggers retain their own border-state tokens. */
.component-detail :where(div, section, article, ul, ol, table, fieldset).border-border-button-default {
  border-color: var(--color-border-component-detail-container);
}

/* 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;
}

@media (prefers-reduced-motion: reduce) {
  .animate-number-fade {
    animation: none !important;
  }
}

/* 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:focus,
.recharts-wrapper:focus-visible,
.recharts-wrapper :focus,
.recharts-wrapper :focus-visible {
  outline: none;
}

/* Landing hero widget marquee. The track holds TWO copies of the card set,
 * so translating exactly -50% and looping restarts on identical pixels —
 * the seam is invisible as long as every card carries the same 20px
 * right-margin. `translate`, not `transform`, per the codebase convention
 * (Tailwind v4 utilities write the `translate` property). */
@keyframes landing-marquee {
  to {
    translate: -50% 0;
  }
}
/* 72s, not 60s: the track grew from 5 cards to 6, and the duration covers
 * the whole -50% sweep — holding it constant would have sped the row up. */
.animate-landing-marquee {
  animation: landing-marquee 72s linear infinite;
}

@keyframes landing-file-drop {
  0% {
    opacity: 0;
    transform: translate3d(-92px, 64px, 0) rotate(-10deg) scale(0.76);
  }
  12% {
    opacity: 1;
    transform: translate3d(-76px, 52px, 0) rotate(-7deg) scale(0.9);
  }
  88% {
    opacity: 1;
    transform: translate3d(52px, -38px, 0) rotate(6deg) scale(0.82);
  }
  100% {
    opacity: 0;
    transform: translate3d(68px, -50px, 0) rotate(8deg) scale(0.1);
  }
}

.animate-landing-file-drop {
  backface-visibility: hidden;
  transform-origin: center;
  will-change: transform, opacity;
  animation: landing-file-drop 1300ms linear both;
}

/* Block cursor on the landing install line. `steps(1)` — a terminal caret
 * snaps between on and off, it never fades. */
@keyframes terminal-caret {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}
.animate-terminal-caret {
  animation: terminal-caret 1060ms steps(1, end) infinite;
}

/* Phosphor bloom around the caret: a tight halo all round plus a wider pool
 * cast below it, so the glow pools under the line the way a lit cursor does
 * on a CRT. Sized in em so it scales with the install line's 12px → 18px
 * responsive step. */
.landing-terminal-caret {
  box-shadow:
    0 0 0.3em color-mix(in srgb, var(--color-purple-500) 60%, transparent),
    0 0.22em 0.75em color-mix(in srgb, var(--color-purple-500) 55%, transparent),
    0 0.4em 1.1em color-mix(in srgb, var(--color-purple-500) 30%, transparent);
}

/* 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. */
/* Pro badge sheen. Rotating the conic layer reads as light travelling around
 * brushed metal; the badge clips it, so only the sweep is visible. */
@keyframes bui-metal-sweep {
  to {
    transform: rotate(360deg);
  }
}

.bui-metal-sweep {
  animation: bui-metal-sweep 9s linear infinite;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .bui-metal-sweep {
    animation: none;
  }
}

/* Discount-badge glitter: tiny four-point stars around the pill, each fading
 * in and out on its own clock. Peak opacity stays low so it reads as a
 * shimmer, not confetti; per-star size/timing come in via custom properties. */
@keyframes bui-glitter {
  0%,
  100% {
    opacity: 0;
    transform: scale(0.2) rotate(-20deg);
  }
  50% {
    opacity: var(--glitter-peak, 0.6);
    transform: scale(1) rotate(15deg);
  }
}

.bui-glitter {
  animation: bui-glitter var(--glitter-duration, 3s) ease-in-out var(--glitter-delay, 0s)
    infinite;
}

@media (prefers-reduced-motion: reduce) {
  .bui-glitter {
    animation: none;
    opacity: 0;
  }
}

/* Discount-badge shimmer: the band starts a full badge-width left of the pill
 * and crosses it in the first 40% of the cycle, then idles — one pass every
 * 2.5s. Eased per-segment, so the sweep itself accelerates and settles. */
@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;
  }
}

.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;
  }
}

.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);
  }
}

.landing-hero-dither-character {
  backface-visibility: hidden;
  will-change: mask-image, mask-position, opacity, transform, color;
}

@media (prefers-reduced-motion: reduce) {
  .animate-landing-marquee {
    animation: none;
  }

  /* Keep the caret, drop the blink. */
  .animate-terminal-caret {
    animation: 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 <table>/<thead>/<th>/<tr>/<td> elements.
 * --------------------------------------------------------------------------- */
.bui-table {
  @apply w-full border-collapse text-left;
}
/* Header background + border-y live on the cells: with border-collapse the
 * browser doesn't paint backgrounds/borders set on the <thead> 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-left 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;
}
.bui-table tbody tr {
  @apply transition-colors duration-150;
  border-bottom: 1px solid var(--color-separator-border);
}
.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;
}

/* Beam travelling the FAQ card's border.
 *
 * A conic gradient rotated by an `@property`-typed angle, clipped to a thin
 * ring by the mask-xor trick the glass stroke uses (see `.landing-glass-stroke`
 * for why the shorthand has to precede the composite). The glow is the same
 * ring, wider and blurred, underneath.
 *
 * Both layers are masked. An unmasked glow paints the whole conic wash across
 * the card's interior, which reads as a coloured blob in the middle rather
 * than light on the edge.
 *
 * The angle is a registered property so it can be animated at all: a plain
 * custom property is an unanimatable string, and the gradient would jump
 * between keyframes instead of sweeping. */
@property --bui-beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@keyframes bui-beam-travel {
  to {
    --bui-beam-angle: 360deg;
  }
}

.bui-beam,
.bui-beam-glow,
.bui-beam-aura {
  pointer-events: none;
  position: absolute;
  /* -1px, not 0: `inset: 0` lands on the padding box, which puts the ring
   * just inside the card's own border instead of on top of it. */
  inset: -1px;
  /* Not `inherit`. Each layer sits further out than the card, and a box that
   * is bigger with the same radius has a *tighter* corner — the ring then cuts
   * across the card's own corner and leaves a notch in the border. The radius
   * has to grow by exactly the outset. The host sets --bui-beam-radius to its
   * own corner. */
  border-radius: calc(var(--bui-beam-radius, 26px) + 1px);
  padding: 1.5px;
  /* The sweep runs to 360deg, so the stop at 360 is the beam's leading tip and
   * the ramp behind it is the tail. Brightest at the tip: it is the front of
   * the travelling light, and the blurred copy underneath turns that peak into
   * the glow. Anything dimmer at 360 puts the highlight mid-beam instead. */
  background: conic-gradient(
    from var(--bui-beam-angle),
    transparent 0deg,
    transparent 272deg,
    rgb(104 136 251 / 0.22) 305deg,
    rgb(104 136 251 / 0.8) 340deg,
    rgb(150 176 255 / 0.95) 353deg,
    rgb(219 230 255 / 1) 360deg
  );
  -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;
  /* Two laps, then it stays gone: `forwards` holds the final keyframe so the
   * gradient does not snap back to its start as the fade-out runs. The host
   * mounts these only once the card is on screen. */
  animation: bui-beam-travel 4.5s linear 2 forwards;
  transition: opacity 700ms ease;
}

/* The same ring, blurred. Identical inset and padding to the crisp one on
 * purpose: a wider band here drew a second, softer line beside the first and
 * the border read as double. Same geometry, blurred, is one line with a glow —
 * strongest at the tip, where the gradient peaks. */
.bui-beam-glow {
  filter: blur(4px);
  opacity: 0.85;
}

/* The aura the head carries with it.
 *
 * Its own gradient starts far later than the beam's, so the light gathers
 * around the leading tip instead of tracing the whole tail — that is what
 * makes it read as a glow travelling with the beam rather than a second,
 * fatter border. Wide band, heavy blur, low opacity: enough to spill a little
 * past the stroke, not enough to become an object of its own. The card must
 * leave its overflow visible or this is the layer that gets cut off. */
.bui-beam-aura {
  inset: -4px;
  border-radius: calc(var(--bui-beam-radius, 26px) + 4px);
  padding: 4px;
  background: conic-gradient(
    from var(--bui-beam-angle),
    transparent 0deg,
    transparent 322deg,
    rgb(104 136 251 / 0.4) 348deg,
    rgb(150 176 255 / 0.75) 360deg
  );
  filter: blur(8px);
  opacity: 0.5;
}

/* Set by the host once both laps are done. */
.bui-beam-spent {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .bui-beam,
  .bui-beam-glow,
  .bui-beam-aura {
    animation: none;
    opacity: 0;
  }
}

/* Agent Thinking (components/blocks/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/blocks/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/blocks/composer): 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);
  }
}

/* Composer effort slider (Ultra): the rail is under load. A one-shot kick
 * then a tiny ongoing tremor, while a purple fire jet trails left off the
 * thumb (see EffortFlame in composer.tsx). Reduced-motion keeps a static
 * taper instead of the shader. */
@keyframes bui-effort-ultra-kick {
  0%,
  100% {
    transform: translate3d(0, 0, 0);
  }
  18% {
    transform: translate3d(-1.6px, 0.8px, 0);
  }
  38% {
    transform: translate3d(1.4px, -0.7px, 0);
  }
  58% {
    transform: translate3d(-1.1px, -0.9px, 0);
  }
  76% {
    transform: translate3d(0.9px, 0.5px, 0);
  }
}

@keyframes bui-effort-ultra-tremor {
  0%,
  100% {
    transform: translate3d(0, 0, 0);
  }
  25% {
    transform: translate3d(-0.9px, 0.55px, 0);
  }
  50% {
    transform: translate3d(0.8px, -0.45px, 0);
  }
  75% {
    transform: translate3d(-0.65px, -0.6px, 0);
  }
}

.bui-effort-ultra {
  animation:
    bui-effort-ultra-kick 420ms ease-out 1,
    bui-effort-ultra-tremor 180ms linear 420ms infinite;
  will-change: transform;
}

.bui-effort-ultra-rail {
  background: color-mix(in oklab, var(--color-background-secondary-default) 70%, #160814);
}

.bui-effort-ultra-flame-static {
  pointer-events: none;
  position: absolute;
  inset: 0;
  background: linear-gradient(
    270deg,
    rgb(255 242 255 / 0.88) 0%,
    rgb(196 92 255 / 0.72) 16%,
    rgb(124 42 186 / 0.4) 46%,
    transparent 78%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 6%,
    black 32%,
    black 68%,
    transparent 94%
  );
}

@media (prefers-reduced-motion: reduce) {
  .bui-effort-ultra {
    animation: none;
    will-change: auto;
  }
}