1026 lines
32 KiB
CSS
1026 lines
32 KiB
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 *));
|
||
|
|
|
||
|
|
/* 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.
|
||
|
|
*
|
||
|
|
* Each override is part of its utility (`@utility`), so Tailwind emits it in
|
||
|
|
* the utilities layer right after the base utilities and before every variant:
|
||
|
|
* a later variant such as `disabled:shadow-none` wins in dark as it does in
|
||
|
|
* light. (Written as plain rules outside the layers, they outranked every
|
||
|
|
* variant, and a disabled button kept its shadow in dark.) The `:where()`
|
||
|
|
* keeps the utility's own specificity and matches only an element that has
|
||
|
|
* the base class inside `.dark`, so a variant on its own (`hover:shadow-xs`)
|
||
|
|
* keeps its value as before. One change in dark: a shadow variant on an
|
||
|
|
* element that also has a base shadow class (`shadow-sm hover:shadow-md`) now
|
||
|
|
* takes effect, with its light value, where the base class's dark value used
|
||
|
|
* to win in every state. */
|
||
|
|
@utility shadow-2xs {
|
||
|
|
&:where(.dark .shadow-2xs) {
|
||
|
|
--tw-shadow: var(--shadow-2xs);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-xs {
|
||
|
|
&:where(.dark .shadow-xs) {
|
||
|
|
--tw-shadow: var(--shadow-xs);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-sm {
|
||
|
|
&:where(.dark .shadow-sm) {
|
||
|
|
--tw-shadow: var(--shadow-sm);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-md {
|
||
|
|
&:where(.dark .shadow-md) {
|
||
|
|
--tw-shadow: var(--shadow-md);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-lg {
|
||
|
|
&:where(.dark .shadow-lg) {
|
||
|
|
--tw-shadow: var(--shadow-lg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-xl {
|
||
|
|
&:where(.dark .shadow-xl) {
|
||
|
|
--tw-shadow: var(--shadow-xl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-card {
|
||
|
|
&:where(.dark .shadow-card) {
|
||
|
|
--tw-shadow: var(--shadow-card);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-dropdown {
|
||
|
|
&:where(.dark .shadow-dropdown) {
|
||
|
|
--tw-shadow: var(--shadow-dropdown);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-sidebar {
|
||
|
|
&:where(.dark .shadow-sidebar) {
|
||
|
|
--tw-shadow: var(--shadow-sidebar);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
@utility shadow-waitlist {
|
||
|
|
&:where(.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 <table>/<thead>/<th>/<tr>/<td> 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 <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-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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Progress (components/base/progress): the indeterminate bar's band of
|
||
|
|
* light, 40% of the track wide, sweeping from fully off the start edge to
|
||
|
|
* fully off the end. RTL runs it in reverse. */
|
||
|
|
@keyframes bui-progress-sweep {
|
||
|
|
from {
|
||
|
|
translate: -100% 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
to {
|
||
|
|
translate: 250% 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Skeleton (components/base/skeleton): a sheen sweeping each placeholder,
|
||
|
|
* drawn in layers like the loaders. A crisp 2px line leads, a tight glow
|
||
|
|
* trails it and a wide bloom trails that, all anchored to the sheen's
|
||
|
|
* leading edge so nothing soft runs ahead of the line. The sweep takes
|
||
|
|
* 60% of the cycle and rests off the block for the rest. RTL mirrors it.
|
||
|
|
* Position comes from the component's classes (relative or absolute): an
|
||
|
|
* unlayered position here would beat Tailwind's layered utilities. */
|
||
|
|
.bui-skeleton {
|
||
|
|
--bui-skeleton-line: rgb(255 255 255 / 0.9);
|
||
|
|
--bui-skeleton-glow: rgb(255 255 255 / 0.55);
|
||
|
|
--bui-skeleton-bloom: rgb(255 255 255 / 0.35);
|
||
|
|
overflow: hidden;
|
||
|
|
isolation: isolate;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dark .bui-skeleton {
|
||
|
|
--bui-skeleton-line: rgb(255 255 255 / 0.2);
|
||
|
|
--bui-skeleton-glow: rgb(255 255 255 / 0.08);
|
||
|
|
--bui-skeleton-bloom: rgb(255 255 255 / 0.04);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bui-skeleton::after {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
inset: 0;
|
||
|
|
background:
|
||
|
|
linear-gradient(90deg, transparent, var(--bui-skeleton-line)) right / 2px 100% no-repeat,
|
||
|
|
linear-gradient(90deg, transparent, var(--bui-skeleton-glow)) right / 72px 100% no-repeat,
|
||
|
|
linear-gradient(90deg, transparent, var(--bui-skeleton-bloom)) right / 240px 100% no-repeat;
|
||
|
|
translate: -100% 0;
|
||
|
|
animation: bui-skeleton-sheen 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bui-skeleton:dir(rtl)::after {
|
||
|
|
scale: -1 1;
|
||
|
|
animation-direction: reverse;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes bui-skeleton-sheen {
|
||
|
|
0% {
|
||
|
|
translate: -100% 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
60%,
|
||
|
|
100% {
|
||
|
|
translate: 100% 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes bui-skeleton-pulse {
|
||
|
|
50% {
|
||
|
|
opacity: 0.55;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* No sweep under reduced motion; the block breathes instead. */
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
.bui-skeleton::after {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bui-skeleton {
|
||
|
|
animation: bui-skeleton-pulse 2s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 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);
|
||
|
|
}
|
||
|
|
}
|