:root {
/* transitions.dev motion tokens */
--duration-stagger: 40ms;
--duration-micro: 80ms;
--duration-quick: 150ms;
--duration-fast: 250ms;
--duration-medium: 350ms;
--duration-slow: 400ms;
--duration-very-slow: 500ms;
--ease-smooth-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-out: ease-in-out;
--ease-out: ease-out;
--ease-linear: linear;
--ease-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);
--ease-bounce-strong: cubic-bezier(0.34, 3.85, 0.64, 1);
--distance-micro: 4px;
--distance-small: 6px;
--distance-base: 8px;
--distance-medium: 12px;
--distance-large: 30px;
--scale-large: 0.96;
--scale-medium: 0.97;
--scale-small: 0.98;
--scale-tiny: 0.99;
--blur-small: 2px;
--blur-medium: 3px;
--blur-large: 8px;
/* Per pattern values, from the transitions.dev snippets */
--dropdown-open-dur: var(--duration-fast);
--dropdown-close-dur: var(--duration-quick);
--modal-open-dur: var(--duration-fast);
--modal-close-dur: var(--duration-quick);
--panel-open-dur: var(--duration-slow);
--panel-close-dur: var(--duration-medium);
--panel-distance: 100px;
--tt-in-dur: var(--duration-quick);
--tt-out-dur: 50ms;
--toggle-dur: var(--duration-medium);
--toggle-ease: cubic-bezier(0.34, 1.35, 0.64, 1);
--check-draw: var(--duration-medium);
--check-uncheck: var(--duration-quick);
--toast-open: var(--duration-medium);
--toast-close: var(--duration-fast);
--resize-dur: 300ms;
--resize-ease: var(--ease-smooth-out);
/* Error state shake */
--shake-distance: 6px;
--shake-overshoot: 4px;
--shake-dur-a: 80ms;
--shake-dur-b: 60ms;
--shake-ease: cubic-bezier(0.22, 1, 0.36, 1);
--revert-dur: 280ms;
/* Tabs sliding */
--tabs-dur: var(--duration-fast);
--tabs-ease: var(--ease-smooth-out);
/* Accordion */
--acc-expand: 250ms;
--acc-collapse: 250ms;
--acc-chevron: 250ms;
--acc-ease: var(--ease-smooth-out);
/* Toggle overshoot */
--toggle-ov1: 1px;
--toggle-ov2: 0px;
}
/*
* Error state shake. Any control that turns invalid (aria-invalid="true")
* shakes once and its border eases to the error color. The sound binder sets
* data-shaking, since React would overwrite a class it does not know about.
* Percent stops are the cumulative legs: 80, 60, 80, 60 = 280ms.
*/
:is(
[data-slot="input"],
[data-slot="textarea"],
[data-slot="select-trigger"],
[data-slot="input-group"],
[data-slot="input-otp-slot"],
[data-slot="combobox-input"],
[data-slot="checkbox"],
[data-slot="radio-group-item"],
[data-slot="switch"]
) {
transition-property: color, background-color, border-color, box-shadow;
transition-duration: var(--revert-dur);
transition-timing-function: ease-out;
}
[data-shaking] {
animation: t-input-shake calc(var(--shake-dur-a) * 2 + var(--shake-dur-b) * 2) linear;
will-change: transform;
}
@keyframes t-input-shake {
0% {
transform: translateX(0);
animation-timing-function: var(--shake-ease);
}
28.57% {
transform: translateX(var(--shake-distance));
animation-timing-function: var(--shake-ease);
}
57.14% {
transform: translateX(calc(var(--shake-distance) * -1));
animation-timing-function: var(--shake-ease);
}
78.57% {
transform: translateX(var(--shake-overshoot));
animation-timing-function: var(--shake-ease);
}
100% {
transform: translateX(0);
}
}
/* Error messages fade in under their field. */
@keyframes t-error-msg-in {
from {
opacity: 0;
}
}
[data-slot="field-error"],
[data-slot="form-message"] {
animation: t-error-msg-in var(--revert-dur) ease-out;
}
/* Card resize: tween a container between two sizes. */
.t-resize {
transition:
width var(--resize-dur) var(--resize-ease),
height var(--resize-dur) var(--resize-ease);
will-change: width, height;
}
/* Content that swaps in place fades up through a soft blur. */
@keyframes t-swap-in {
from {
opacity: 0;
filter: blur(var(--blur-small));
}
}
.t-swap-in {
animation: t-swap-in var(--duration-fast) var(--ease-in-out) both;
}
@keyframes t-dropdown-in {
from {
opacity: 0;
transform: scale(var(--scale-medium));
}
}
@keyframes t-dropdown-out {
to {
opacity: 0;
transform: scale(var(--scale-tiny));
}
}
@keyframes t-modal-in {
from {
opacity: 0;
transform: scale(var(--scale-large));
}
}
@keyframes t-modal-out {
to {
opacity: 0;
transform: scale(var(--scale-large));
}
}
@keyframes t-fade-in {
from {
opacity: 0;
}
}
@keyframes t-fade-out {
to {
opacity: 0;
}
}
@keyframes t-tooltip-in {
from {
opacity: 0;
transform: scale(var(--scale-small));
}
}
@keyframes t-panel-in {
from {
opacity: 0;
filter: blur(var(--blur-small));
transform: var(--panel-from);
}
}
@keyframes t-panel-out {
to {
opacity: 0;
filter: blur(var(--blur-small));
transform: var(--panel-from);
}
}
/* Menu dropdown: grows from its trigger. The primitives already set the
transform origin to the side the menu opened from. */
:is(
[data-slot="dropdown-menu-content"],
[data-slot="dropdown-menu-sub-content"],
[data-slot="context-menu-content"],
[data-slot="context-menu-sub-content"],
[data-slot="menubar-content"],
[data-slot="menubar-sub-content"],
[data-slot="popover-content"],
[data-slot="select-content"],
[data-slot="combobox-content"],
[data-slot="hover-card-content"]
):is([data-state="open"], [data-open]) {
animation: t-dropdown-in var(--dropdown-open-dur) var(--ease-smooth-out);
}
:is(
[data-slot="dropdown-menu-content"],
[data-slot="dropdown-menu-sub-content"],
[data-slot="context-menu-content"],
[data-slot="context-menu-sub-content"],
[data-slot="menubar-content"],
[data-slot="menubar-sub-content"],
[data-slot="popover-content"],
[data-slot="select-content"],
[data-slot="combobox-content"],
[data-slot="hover-card-content"]
):is([data-state="closed"], [data-closed]) {
animation: t-dropdown-out var(--dropdown-close-dur) var(--ease-smooth-out) forwards;
}
/* Modal: scales up from 96%, with a softer close. */
:is([data-slot="dialog-content"], [data-slot="alert-dialog-content"], [data-slot="command-dialog"]):is(
[data-state="open"],
[data-open]
) {
animation: t-modal-in var(--modal-open-dur) var(--ease-smooth-out);
}
:is([data-slot="dialog-content"], [data-slot="alert-dialog-content"], [data-slot="command-dialog"]):is(
[data-state="closed"],
[data-closed]
) {
animation: t-modal-out var(--modal-close-dur) var(--ease-smooth-out) forwards;
}
:is(
[data-slot="dialog-overlay"],
[data-slot="alert-dialog-overlay"],
[data-slot="sheet-overlay"]
):is([data-state="open"], [data-open]) {
animation: t-fade-in var(--modal-open-dur) var(--ease-smooth-out);
}
:is(
[data-slot="dialog-overlay"],
[data-slot="alert-dialog-overlay"],
[data-slot="sheet-overlay"]
):is([data-state="closed"], [data-closed]) {
animation: t-fade-out var(--modal-close-dur) var(--ease-smooth-out) forwards;
}
/* Panel reveal: sheets slide in from their side with a cross blur. */
[data-slot="sheet-content"] {
--panel-from: translateX(var(--panel-distance));
}
[data-slot="sheet-content"][data-side="left"] {
--panel-from: translateX(calc(var(--panel-distance) * -1));
}
[data-slot="sheet-content"][data-side="top"] {
--panel-from: translateY(calc(var(--panel-distance) * -1));
}
[data-slot="sheet-content"][data-side="bottom"] {
--panel-from: translateY(var(--panel-distance));
}
[data-slot="sheet-content"]:is([data-state="open"], [data-open]) {
animation: t-panel-in var(--panel-open-dur) var(--ease-smooth-out);
}
[data-slot="sheet-content"]:is([data-state="closed"], [data-closed]) {
animation: t-panel-out var(--panel-close-dur) var(--ease-smooth-out) forwards;
}
/* Tooltip: fade and scale in, out almost at once. */
[data-slot="tooltip-content"]:is([data-state="delayed-open"], [data-state="instant-open"], [data-open]) {
animation: t-tooltip-in var(--tt-in-dur) var(--ease-out);
}
[data-slot="tooltip-content"]:is([data-state="closed"], [data-closed]) {
animation: t-fade-out var(--tt-out-dur) var(--ease-out) forwards;
}
/* Accordion: the height opens on the motion scale while the content fades
in through a blur, and the chevron flips instead of rotating. */
:is([data-slot="accordion-content"], [data-slot="collapsible-content"]) {
animation-duration: var(--acc-expand);
animation-timing-function: var(--acc-ease);
}
@keyframes t-acc-in {
from {
opacity: 0;
filter: blur(var(--blur-small));
}
}
@keyframes t-acc-out {
to {
opacity: 0;
filter: blur(var(--blur-small));
}
}
:is([data-slot="accordion-content"], [data-slot="collapsible-content"]):is([data-state="open"], [data-open]) > * {
animation: t-acc-in var(--acc-expand) var(--acc-ease);
}
:is([data-slot="accordion-content"], [data-slot="collapsible-content"]):is([data-state="closed"], [data-closed]) > * {
animation: t-acc-out var(--acc-collapse) var(--acc-ease) forwards;
}
[data-slot="accordion-trigger"] > svg {
rotate: none !important;
transform: scaleY(1);
transform-origin: center;
transition: transform var(--acc-chevron) var(--acc-ease);
}
[data-slot="accordion-trigger"]:is([data-state="open"], [data-panel-open], [aria-expanded="true"]) > svg {
transform: scaleY(-1);
}
/* Styles that swap a down icon for an up icon keep only the first, so the
one chevron can flip. */
[data-slot="accordion-trigger"] > [data-slot="accordion-trigger-icon"]:first-of-type {
display: inline-block !important;
}
[data-slot="accordion-trigger"] > [data-slot="accordion-trigger-icon"] ~ [data-slot="accordion-trigger-icon"] {
display: none !important;
}
/* Tabs sliding: one pill per tabs list slides to the active tab. The motion
binder adds the pill and writes its position. */
[data-slot="tabs-list"][data-sliding] {
position: relative;
isolation: isolate;
}
[data-tabs-pill] {
position: absolute;
top: 0;
left: 0;
z-index: 0;
pointer-events: none;
background: var(--background);
box-shadow: var(--surface-shadow);
transition:
transform var(--tabs-dur) var(--tabs-ease),
width var(--tabs-dur) var(--tabs-ease),
height var(--tabs-dur) var(--tabs-ease);
will-change: transform, width;
}
.dark [data-tabs-pill] {
background: color-mix(in oklab, var(--input) 45%, var(--background));
}
[data-slot="tabs-list"][data-variant="line"] > [data-tabs-pill] {
background: var(--foreground);
box-shadow: none;
}
[data-slot="tabs-list"][data-sliding] > [data-slot="tabs-trigger"] {
position: relative;
z-index: 1;
background: transparent !important;
box-shadow: none !important;
border-color: transparent !important;
}
[data-slot="tabs-list"][data-sliding] > [data-slot="tabs-trigger"]::after {
opacity: 0 !important;
}
/* Toggle: the thumb travels with a double bounce. It only plays after a
real change (the motion binder marks the switch), never on first paint. */
[data-slot="switch-thumb"] {
transition: background-color var(--duration-quick) var(--ease-smooth-out);
}
[data-slot="switch"] {
transition: background-color var(--duration-quick) var(--ease-smooth-out);
}
/* The first leg eases out instead of using the bouncy curve. Our thumb sits
flush with the track, so the curve's own overshoot pushed it past the edge
and the switch looked like it grew. The keyframes still do the 1px bounce. */
@keyframes t-toggle-on {
0% {
translate: 0 0;
animation-timing-function: var(--ease-smooth-out);
}
55% {
translate: calc(100% - 2px + var(--toggle-ov1)) 0;
}
80% {
translate: calc(100% - 2px - var(--toggle-ov2)) 0;
}
100% {
translate: calc(100% - 2px) 0;
}
}
@keyframes t-toggle-off {
0% {
translate: calc(100% - 2px) 0;
animation-timing-function: var(--ease-smooth-out);
}
55% {
translate: calc(0px - var(--toggle-ov1)) 0;
}
80% {
translate: var(--toggle-ov2) 0;
}
100% {
translate: 0 0;
}
}
[data-slot="switch"][data-toggled]:is([data-state="checked"], [data-checked], [data-selected]) [data-slot="switch-thumb"] {
animation: t-toggle-on var(--toggle-dur) var(--toggle-ease) both;
}
[data-slot="switch"][data-toggled]:is([data-state="unchecked"], [data-unchecked]):not([data-selected]) [data-slot="switch-thumb"] {
animation: t-toggle-off var(--toggle-dur) var(--toggle-ease) both;
}
/* Checkbox check: the box fills, then the check strokes in. The check is
Phosphor's Check drawn as a stroke with pathLength 1. */
[data-slot="checkbox"] {
transition:
background-color var(--duration-quick) var(--ease-smooth-out),
border-color var(--duration-quick) var(--ease-smooth-out),
box-shadow var(--duration-quick) var(--ease-smooth-out);
}
@keyframes t-check-draw {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
[data-slot="checkbox-indicator"] [data-check-path] {
stroke-dasharray: 1;
stroke-dashoffset: 0;
animation: t-check-draw var(--check-draw) var(--ease-smooth-out) var(--duration-micro) both;
}
/* Tabs, toggles and radios settle their state colors instead of snapping. */
:is(
[data-slot="tabs-trigger"],
[data-slot="toggle"],
[data-slot="toggle-group-item"],
[data-slot="radio-group-item"]
) {
transition-property: color, background-color, border-color, box-shadow;
transition-duration: var(--duration-quick);
transition-timing-function: var(--ease-smooth-out);
}
/* Toast: rises with a cross blur, slower in than out. */
[data-sonner-toast] {
transition-duration: var(--toast-open) !important;
transition-timing-function: var(--ease-smooth-out) !important;
}
[data-sonner-toast][data-removed="true"] {
transition-duration: var(--toast-close) !important;
}
@media (prefers-reduced-motion: reduce) {
:is(
[data-slot$="menu-content"],
[data-slot$="menu-sub-content"],
[data-slot="menubar-content"],
[data-slot="menubar-sub-content"],
[data-slot="popover-content"],
[data-slot="select-content"],
[data-slot="combobox-content"],
[data-slot="hover-card-content"],
[data-slot="tooltip-content"],
[data-slot$="dialog-content"],
[data-slot$="dialog-overlay"],
[data-slot="command-dialog"],
[data-slot^="sheet-"],
[data-slot="checkbox-indicator"]
) {
animation: none !important;
}
[data-slot="switch-thumb"],
[data-slot="checkbox"],
[data-sonner-toast],
.t-resize,
[data-tabs-pill],
[data-slot="accordion-trigger"] > svg {
transition: none !important;
}
.t-swap-in,
[data-shaking],
[data-check-path],
[data-slot="switch-thumb"],
[data-slot$="-content"] > *,
[data-slot="field-error"],
[data-slot="form-message"] {
animation: none !important;
}
}
/* Unchecking undraws the check before the indicator leaves. */
@keyframes t-check-erase {
to {
stroke-dashoffset: 1;
}
}
[data-slot="checkbox-indicator"]:is([data-state="unchecked"], [data-unchecked]) {
animation: t-fade-out var(--check-uncheck) var(--ease-smooth-out) forwards;
}
[data-slot="checkbox-indicator"]:is([data-state="unchecked"], [data-unchecked]) [data-check-path] {
animation: t-check-erase var(--check-uncheck) var(--ease-smooth-out) forwards;
}
/*
* Matrix dot loader (the Dot loader component). One pulse shared by every
* cell, offset by a per-cell delay. Dots pulse colour; ASCII cells step
* through a character ramp. Colours follow the theme.
*/
:root {
--matrix-cycle: 1400ms;
--matrix-base: color-mix(in oklab, var(--muted-foreground) 25%, transparent);
--matrix-active: var(--foreground);
--matrix-ease: ease-in-out;
/* ASCII grids fill the same box as the 4 by 4 dot grid. */
--matrix-box: 21px;
}
/* Dots and gaps share the box, so any grid size keeps the same footprint. */
.t-matrix {
--matrix-cell: calc(var(--matrix-box) / (var(--matrix-n, 4) * 2 - 1));
display: inline-grid;
grid-template-columns: repeat(var(--matrix-n, 4), var(--matrix-cell));
grid-auto-rows: var(--matrix-cell);
gap: var(--matrix-cell);
}
.t-matrix[data-size="sm"] {
--matrix-box: 14px;
}
.t-matrix[data-size="lg"] {
--matrix-box: 35px;
}
.t-matrix i {
display: block;
border-radius: 999px;
background: var(--matrix-base);
animation: t-matrix-pulse var(--matrix-cycle) var(--matrix-ease) infinite;
animation-delay: calc(var(--d, 0) * 1ms);
}
.t-matrix i.is-gap {
visibility: hidden;
animation: none;
}
@keyframes t-matrix-pulse {
0%,
45%,
100% {
background-color: var(--matrix-base);
}
15% {
background-color: var(--matrix-active);
}
}
/* ASCII mode: tiny square monospace cells, so a dense grid takes about the
same room as the dot grid at the same size. */
.t-matrix[data-mode="ascii"] {
grid-template-columns: repeat(var(--matrix-n, 4), 0.75em);
grid-auto-rows: 0.75em;
gap: 0;
font-family: var(--font-mono), ui-monospace, monospace;
font-size: calc(var(--matrix-box) / (var(--matrix-n, 4) * 0.75));
line-height: 0.75;
font-weight: 700;
}
.t-matrix[data-mode="ascii"] i {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: none;
border-radius: 0;
font-style: normal;
text-align: center;
color: var(--matrix-base);
animation-name: t-matrix-ink;
}
.t-matrix[data-mode="ascii"] i::before {
content: "\00b7";
animation: t-matrix-glyph var(--matrix-cycle) steps(1, end) infinite;
animation-delay: calc(var(--d, 0) * 1ms);
}
.t-matrix[data-mode="ascii"] i.is-gap::before {
animation: none;
content: " ";
}
@keyframes t-matrix-ink {
0%,
55%,
100% {
color: var(--matrix-base);
}
18%,
26% {
color: var(--matrix-active);
}
}
@keyframes t-matrix-glyph {
0% {
content: "\00b7";
}
8% {
content: ":";
}
14% {
content: "+";
}
20% {
content: "#";
}
26% {
content: "@";
}
33% {
content: "#";
}
40% {
content: "+";
}
48% {
content: ":";
}
56%,
100% {
content: "\00b7";
}
}
@media (prefers-reduced-motion: reduce) {
.t-matrix i,
.t-matrix i::before {
animation: none !important;
}
}
/*
* Like button (transitions.dev), for every star, favourite, bookmark and like
* toggle. The icon fills and pops, and eight particles burst out. Toggles
* whose label names a favourite get it automatically from the motion binder;
* FavoriteButton uses it directly.
*/
:root {
--like-color: #f40051;
--star-color: oklch(79.5% 0.184 86.047);
--thumbs-color: #f97316;
--like-fill: 150ms;
--like-pop: 350ms;
--like-pop-ease: cubic-bezier(0.34, 1.96, 0.64, 1);
--like-particle-dur: 600ms;
--like-particle-size: 2.5px;
--like-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
[data-liked] {
position: relative;
--like-tone: var(--like-color);
}
[data-liked][data-like-kind="star"] {
--like-tone: var(--star-color);
}
[data-liked][data-like-kind="thumbs"] {
--like-tone: var(--thumbs-color);
}
[data-liked] svg {
transition: color var(--like-fill) var(--like-ease);
}
[data-liked="true"] svg {
color: var(--like-tone);
}
/* Duotone icons fill their soft layer completely when liked. */
[data-liked] svg [opacity] {
transition: opacity var(--like-fill) var(--like-ease);
}
[data-liked="true"] svg [opacity] {
opacity: 1;
}
[data-liked="true"][data-bursting] svg {
animation: t-like-pop var(--like-pop) var(--like-pop-ease);
}
@keyframes t-like-pop {
0% {
transform: scale(1);
}
30% {
transform: scale(0.82);
}
100% {
transform: scale(1);
}
}
.t-like-particles {
position: absolute;
left: 50%;
top: 50%;
width: 0;
height: 0;
pointer-events: none;
color: var(--like-tone);
}
.t-like-particles i {
position: absolute;
left: calc(var(--like-particle-size) * var(--psize, 1) / -2);
top: calc(var(--like-particle-size) * var(--psize, 1) / -2);
width: calc(var(--like-particle-size) * var(--psize, 1));
height: calc(var(--like-particle-size) * var(--psize, 1));
border-radius: 50%;
background: currentColor;
opacity: 0;
}
@keyframes t-like-burst {
0% {
opacity: 0;
transform: translate(0, 0) scale(0.4);
}
20% {
opacity: 1;
transform: translate(calc(var(--px) * 0.25), calc(var(--py) * 0.25)) scale(1);
}
100% {
opacity: 0;
transform: translate(var(--px), var(--py)) scale(var(--p-end-scale, 0.6));
}
}
[data-bursting] .t-like-particles i {
animation: t-like-burst var(--pdur, var(--like-particle-dur)) ease-out var(--pdelay, 0ms) forwards;
}
@media (prefers-reduced-motion: reduce) {
[data-liked] svg,
.t-like-particles i {
animation: none !important;
}
}
/* Number pop-in: each digit re-enters with a blurred slide when a number updates. */
:root {
--digit-dur: 500ms;
--digit-distance: 8px;
--digit-stagger: 70ms;
--digit-blur: 2px;
--digit-ease: cubic-bezier(0.34, 1.45, 0.64, 1);
--digit-dir-x: 0;
--digit-dir-y: 1;
}
@keyframes t-digit-pop-in {
0% {
transform: translate(
calc(var(--digit-distance) * var(--digit-dir-x)),
calc(var(--digit-distance) * var(--digit-dir-y))
);
opacity: 0;
filter: blur(var(--digit-blur));
}
100% { transform: translate(0, 0); opacity: 1; filter: blur(0); }
}
.t-digit-group {
display: inline-flex;
align-items: baseline;
}
.t-digit {
display: inline-block;
will-change: transform, opacity, filter;
}
.t-digit-group.is-animating .t-digit {
animation: t-digit-pop-in var(--digit-dur) var(--digit-ease) both;
}
.t-digit-group.is-animating .t-digit[data-stagger="1"] {
animation-delay: var(--digit-stagger);
}
.t-digit-group.is-animating .t-digit[data-stagger="2"] {
animation-delay: calc(var(--digit-stagger) * 2);
}
@media (prefers-reduced-motion: reduce) {
.t-digit-group .t-digit { animation: none !important; }
}
/* Page side-by-side: slide between two pages (list and detail). */
:root {
--page-slide-dur: 250ms;
--page-fade-dur: 250ms;
--page-slide-distance: 8px;
--page-blur: 3px;
--page-stagger: 0ms;
--page-exit-enabled: 1;
--page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
--page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
.t-page-slide {
position: relative;
}
.t-page-slide .t-page[data-page-id="1"] {
--t-page-from-x: calc(var(--page-slide-distance) * -1);
}
.t-page-slide .t-page[data-page-id="2"] {
--t-page-from-x: var(--page-slide-distance);
}
.t-page-slide .t-page {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
transition:
opacity var(--page-fade-dur) var(--page-fade-ease),
transform var(--page-slide-dur) var(--page-slide-ease),
filter var(--page-slide-dur) var(--page-slide-ease);
will-change: opacity, transform, filter;
}
.t-page-slide[data-page="1"] .t-page[data-page-id="1"],
.t-page-slide[data-page="2"] .t-page[data-page-id="2"] {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
filter: blur(0);
transition-delay: var(--page-stagger);
}
@media (prefers-reduced-motion: reduce) {
.t-page-slide .t-page { transition: none !important; }
}
/* Icon swap: two icons cross-fade in the same slot with blur and scale. */
:root {
--icon-swap-dur: 250ms;
--icon-swap-blur: 2px;
--icon-swap-start-scale: 0.25;
--icon-swap-ease: ease-in-out;
}
.t-icon-swap {
position: relative;
display: inline-grid;
}
.t-icon-swap .t-icon {
grid-area: 1 / 1;
transition:
opacity var(--icon-swap-dur) var(--icon-swap-ease),
filter var(--icon-swap-dur) var(--icon-swap-ease),
transform var(--icon-swap-dur) var(--icon-swap-ease);
will-change: opacity, filter, transform;
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="a"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] {
opacity: 1;
filter: blur(0);
transform: scale(1);
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="b"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] {
opacity: 0;
filter: blur(var(--icon-swap-blur));
transform: scale(var(--icon-swap-start-scale));
}
@media (prefers-reduced-motion: reduce) {
.t-icon-swap .t-icon { transition: none !important; }
}
/* Spinning counter: each digit is a clipped reel that spins before landing. */
:root {
--reel-dur: 1400ms;
--reel-cell: 30px;
--reel-spin-blur: 3px;
--reel-stagger: 90ms;
--reel-ease: cubic-bezier(0.16, 1, 0.3, 1);
}
.t-reel { display: inline-flex; align-items: center; height: var(--reel-cell); font-variant-numeric: tabular-nums; }
.t-reel-col {
position: relative; height: var(--reel-cell); overflow: hidden;
/* Soft-fade the window edges instead of hard-cropping. */
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%);
mask-image: linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%);
}
.t-reel-strip { display: flex; flex-direction: column; will-change: transform, filter; }
.t-reel-digit { height: var(--reel-cell); display: flex; align-items: center; justify-content: center; }
/* Separators ($ , .) fold away while the reels show their mask. */
.t-reel-sep {
display: inline-block;
overflow: hidden;
max-width: 1em;
transition:
max-width var(--reel-dur) var(--reel-ease),
opacity var(--duration-fast) var(--ease-smooth-out);
}
.t-reel[data-masked] .t-reel-sep { max-width: 0; opacity: 0; }
@media (prefers-reduced-motion: reduce) {
.t-reel-strip { transition: none !important; filter: none !important; }
.t-reel-sep { transition: none !important; }
}
/* Segmented control: transitions.dev "Tabs sliding". One pill per control
slides and resizes to the chosen segment. JS writes the box, CSS owns the
tween. Timing reads the shared --tabs-dur and --tabs-ease tokens. */
[data-segmented-pill] {
position: absolute;
top: 0;
left: 0;
width: 0;
z-index: 0;
pointer-events: none;
opacity: 0;
border-radius: calc(var(--radius-md, 0.375rem));
background: var(--background);
box-shadow: var(--surface-shadow);
transform: translateX(0);
transition:
transform var(--tabs-dur) var(--tabs-ease),
width var(--tabs-dur) var(--tabs-ease),
height var(--tabs-dur) var(--tabs-ease);
will-change: transform, width;
}
.dark [data-segmented-pill] {
background: color-mix(in oklab, var(--input) 45%, var(--background));
}
[data-segmented-control][data-variant="outline"] > [data-segmented-pill] {
background: var(--muted);
box-shadow: none;
}
.dark [data-segmented-control][data-variant="outline"] > [data-segmented-pill] {
background: var(--muted);
}
[data-slot="segmented-control-item"] {
transition: color var(--tabs-dur) var(--tabs-ease);
}
@media (prefers-reduced-motion: reduce) {
[data-segmented-pill],
[data-slot="segmented-control-item"] {
transition: none !important;
}
}
/*
* Sortable and Kanban drop settle. The item you drop swells a touch and a
* soft ring fades out, so you can see where it landed. Uses the independent
* `scale` property, since dnd-kit owns `transform` on sortable items.
*/
@keyframes t-sortable-drop {
0% {
scale: 1;
box-shadow: 0 0 0 0 color-mix(in oklab, var(--ring) 0%, transparent);
}
35% {
scale: 1.015;
box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 45%, transparent);
}
100% {
scale: 1;
box-shadow: 0 0 0 6px color-mix(in oklab, var(--ring) 0%, transparent);
}
}
[data-slot="sortable-item"][data-dropped] {
animation: t-sortable-drop var(--duration-slow) var(--ease-smooth-out) both;
}
@media (prefers-reduced-motion: reduce) {
[data-slot="sortable-item"][data-dropped] {
animation: none;
}
}
/* Text states swap (transitions.dev), used by CopyButton for "Copy" to "Copied". */
:root {
--text-swap-dur: 150ms;
--text-swap-translate-y: 4px;
--text-swap-blur: 2px;
--text-swap-ease: ease-in-out;
}
.t-text-swap {
display: inline-block;
transform: translateY(0);
filter: blur(0);
opacity: 1;
transition:
transform var(--text-swap-dur) var(--text-swap-ease),
filter var(--text-swap-dur) var(--text-swap-ease),
opacity var(--text-swap-dur) var(--text-swap-ease);
will-change: transform, filter, opacity;
}
.t-text-swap.is-exit {
transform: translateY(calc(var(--text-swap-translate-y) * -1));
filter: blur(var(--text-swap-blur));
opacity: 0;
}
.t-text-swap.is-enter-start {
transform: translateY(var(--text-swap-translate-y));
filter: blur(var(--text-swap-blur));
opacity: 0;
transition: none;
}
@media (prefers-reduced-motion: reduce) {
.t-text-swap { transition: none !important; }
}
/*
* Rating. Each star is a like-button target (data-liked, data-like-kind), so
* setting one fills, pops and bursts like FavoriteButton. The empty star
* underneath keeps a muted duotone look even when the like rule colors it.
*/
[data-slot="rating-star"] [data-slot="rating-icon-empty"] {
color: var(--muted-foreground);
opacity: 0.55;
}
[data-slot="rating-star"] [data-slot="rating-icon-empty"] [opacity] {
opacity: 0.2;
}
[data-slot="rating-star"] [data-slot="rating-icon-fill"] {
color: var(--like-tone);
}
[data-slot="rating-fill"] {
transition: width var(--like-fill) var(--like-ease);
}
@media (prefers-reduced-motion: reduce) {
[data-slot="rating-fill"] { transition: none !important; }
}
/* Banner stacking (transitions.dev 32): new banners rise in, older ones push back. */
:root {
--stack-open: 350ms;
--stack-close: 250ms;
--stack-rise: 60px;
--stack-blur: 2px;
--stack-scale: 0.97;
--stack-peek: 12px;
--stack-spread-gap: 8px;
--stack-depth-scale: 0.06;
--stack-depth-fade: 0.4;
--stack-depth1-blur: 1px;
--stack-depth2-blur: 2px;
--stack-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
.t-stack { position: relative; }
.t-stack-banner {
position: absolute;
left: 0;
bottom: 0;
/* Centre origin so an arriving banner scales like a toast; the
push-back depths switch to the bottom edge, which they can do
freely because the swap happens at identity. */
transform-origin: 50% 50%;
/* Explicit blur(0) rather than an unset filter. blur to none is
not reliably interpolated, and the cross-blur would snap. */
filter: blur(0);
will-change: transform, opacity, filter;
transition:
transform var(--stack-open) var(--stack-ease),
opacity var(--stack-open) var(--stack-ease),
filter var(--stack-open) var(--stack-ease);
}
.t-stack-banner[data-depth="0"] {
z-index: 3;
transform: translateY(0) scale(1);
opacity: 1;
}
.t-stack-banner[data-depth="1"] {
z-index: 2;
transform-origin: 50% 100%;
transform: translateY(calc(var(--stack-peek) * -1))
scale(calc(1 - var(--stack-depth-scale)));
opacity: calc(1 - var(--stack-depth-fade));
filter: blur(var(--stack-depth1-blur));
}
.t-stack-banner[data-depth="2"] {
z-index: 1;
transform-origin: 50% 100%;
transform: translateY(calc(var(--stack-peek) * -2))
scale(calc(1 - var(--stack-depth-scale) * 2));
opacity: calc(1 - var(--stack-depth-fade) * 1.6);
filter: blur(var(--stack-depth2-blur));
}
/* Spread: newest stays put, older ones climb one banner height
each (100% = the banner's own height). Driven by a class, not
:hover. The gaps between spread banners belong to no element. */
.t-stack.is-spread .t-stack-banner[data-depth="1"],
.t-stack.is-spread .t-stack-banner[data-depth="2"] { opacity: 1; filter: blur(0); }
.t-stack.is-spread .t-stack-banner[data-depth="1"] {
transform: translateY(calc((100% + var(--stack-spread-gap)) * -1)) scale(1);
}
.t-stack.is-spread .t-stack-banner[data-depth="2"] {
transform: translateY(calc((100% + var(--stack-spread-gap)) * -2)) scale(1);
}
.t-stack-banner.is-enter {
transition: none;
transform: translateY(var(--stack-rise)) scale(var(--stack-scale));
opacity: 0;
filter: blur(var(--stack-blur));
}
.t-stack-banner.is-leaving {
z-index: 0;
transform-origin: 50% 100%;
transform: translateY(calc(var(--stack-peek) * -3))
scale(calc(1 - var(--stack-depth-scale) * 3));
opacity: 0;
filter: blur(var(--stack-blur));
transition:
transform var(--stack-close) var(--stack-ease),
opacity var(--stack-close) var(--stack-ease),
filter var(--stack-close) var(--stack-ease);
}
@media (prefers-reduced-motion: reduce) {
.t-stack-banner { transition: none !important; transform: none !important; filter: none !important; }
}
/* Banners of different heights: the spread climbs by the measured heights of
the banners in front (set as --stack-offset by BannerStack) instead of the
banner's own height, so tall and short banners never overlap. */
.t-stack.is-spread .t-stack-banner[data-depth][data-stack-offset] {
transform: translateY(calc(var(--stack-offset) * -1)) scale(1);
}
/* Data table: rows reveal once when loading ends. This is the content side
of the transitions.dev skeleton reveal (14), the skeleton rows are already
gone, so the new rows fade in and un-blur over the same duration and ease. */
.t-data-table-body {
--reveal-dur: 400ms;
--reveal-blur: 2px;
--reveal-ease: ease-in-out;
}
.t-data-table-body[data-reveal] > tr {
animation: t-data-table-reveal var(--reveal-dur) var(--reveal-ease) both;
}
@keyframes t-data-table-reveal {
from { opacity: 0; filter: blur(var(--reveal-blur)); }
to { opacity: 1; filter: blur(0); }
}
@media (prefers-reduced-motion: reduce) {
.t-data-table-body[data-reveal] > tr { animation: none !important; }
}
/* transitions.dev texts reveal (18), used by Terminal output lines. */
:root {
--stagger-dur: 500ms;
--stagger-distance: 12px;
--stagger-stagger: 40ms;
--stagger-blur: 3px;
--stagger-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
/* Lines start translated down + blurred + invisible; .is-shown
on the parent flips them to their resting state. The second
line's transition-delay holds it back by --stagger-stagger
so the eye lands on the headline first. */
.t-stagger-line {
display: block;
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.is-shown .t-stagger-line {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
/* Exit decouples from the stagger: same fade for every line,
no Y return, no blur, so the disappearance reads as a
single quiet fade instead of a reverse reveal. */
.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; }
}
/* Terminal: the output reveal is shorter than a headline reveal, and the
cursor blinks in steps like a real terminal. */
[data-slot="terminal"] {
--stagger-dur: var(--duration-medium);
--stagger-distance: var(--distance-small);
--stagger-blur: var(--blur-small);
}
@keyframes t-terminal-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.t-terminal-cursor {
animation: t-terminal-blink 1.06s steps(1, end) infinite;
}
@media (prefers-reduced-motion: reduce) {
.t-terminal-cursor { animation: none; }
}
/*
* Kanban columns tween their width with card resize, but a standing
* will-change layer makes iOS Safari drop the 1px side rings on the cards
* inside while a card is dragged. The width tween runs fine without it.
*/
[data-slot="kanban-column"].t-resize {
will-change: auto;
}
/*
* Data grid scroll fade on both axes, like the data table one in
* globals.css. The top and start stay sharp up to the pinned headers (and
* frozen columns), so cells fade only as they slide under them.
*/
[data-slot="data-grid-frame"] [data-slot="scroll-fade-viewport"] {
--_scroll-fade-size-b: min(12%, calc(var(--spacing) * 10));
--_scroll-fade-size-s: min(12%, calc(var(--spacing) * 8));
--_scroll-fade-size-e: min(12%, calc(var(--spacing) * 10));
--_grid-start: var(--data-grid-sticky-start, 0px);
-webkit-mask-image:
linear-gradient(to bottom, #000 0, #000 calc(100% - var(--scroll-fade-b, 0px)), transparent 100%),
linear-gradient(to right, #000 0, #000 var(--_grid-start), transparent var(--_grid-start), #000 calc(var(--_grid-start) + var(--scroll-fade-s, 0px)), #000 calc(100% - var(--scroll-fade-e, 0px)), transparent 100%);
mask-image:
linear-gradient(to bottom, #000 0, #000 calc(100% - var(--scroll-fade-b, 0px)), transparent 100%),
linear-gradient(to right, #000 0, #000 var(--_grid-start), transparent var(--_grid-start), #000 calc(var(--_grid-start) + var(--scroll-fade-s, 0px)), #000 calc(100% - var(--scroll-fade-e, 0px)), transparent 100%);
-webkit-mask-composite: source-in;
mask-composite: intersect;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
@supports (animation-timeline: scroll()) {
animation:
scroll-fade-reveal-b 1ms ease-in-out,
scroll-fade-reveal-s 1ms ease-in-out,
scroll-fade-reveal-e 1ms ease-in-out;
animation-timeline: scroll(self y), scroll(self x), scroll(self x);
animation-range:
calc(100% - calc(var(--spacing) * 24)) 100%,
0 calc(var(--spacing) * 24),
calc(100% - calc(var(--spacing) * 24)) 100%;
animation-fill-mode: both;
}
@supports not (animation-timeline: scroll()) {
--scroll-fade-b: var(--_scroll-fade-size-b);
--scroll-fade-s: 0px;
--scroll-fade-e: var(--_scroll-fade-size-e);
}
}
/* Data grid suggestions grow from the cell like a dropdown (transitions.dev). */
.t-data-grid-suggest {
transform-origin: top left;
animation: t-dropdown-in var(--dropdown-open-dur) var(--ease-smooth-out);
}
@media (prefers-reduced-motion: reduce) {
.t-data-grid-suggest { animation: none; }
}
/* Notices block. transitions.dev toast open / close (22): a notice rises in
with a fade, cross blur and slight scale, and leaves on the faster clock. */
:root {
--toast-distance: 16px;
--toast-blur: 2px;
--toast-scale: 0.97;
--toast-ease: cubic-bezier(0.22, 1, 0.36, 1);
--learn-shift: 2px;
--learn-spread: 8deg;
--learn-in: 350ms;
--learn-out: 350ms;
--learn-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
.t-toast {
opacity: 0;
transform: translateY(var(--toast-distance)) scale(var(--toast-scale));
filter: blur(var(--toast-blur));
will-change: transform, opacity, filter;
transition:
opacity var(--toast-close) var(--toast-ease),
transform var(--toast-close) var(--toast-ease),
filter var(--toast-close) var(--toast-ease);
}
.t-toast.is-open {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0);
transition:
opacity var(--toast-open) var(--toast-ease),
transform var(--toast-open) var(--toast-ease),
filter var(--toast-open) var(--toast-ease);
}
/* Card resize (01) on a grid row: the row tweens its height between the
content and zero, so the list reflows as a notice comes or goes, and a
tray footer folds away. */
.t-notice-row {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows var(--resize-dur) var(--resize-ease);
}
.t-notice-row[data-state="closed"] {
grid-template-rows: 0fr;
}
.t-notice-row > * {
min-height: 0;
}
/* transitions.dev learn more hover (24): the chevron shifts and its arms open. */
.t-learn-chevron {
display: inline-flex;
transform: translateX(0);
transition: transform var(--learn-out) var(--learn-ease);
}
.t-learn-arm {
transform-box: view-box;
transform-origin: 10px 8px;
vector-effect: non-scaling-stroke;
transition: transform var(--learn-out) var(--learn-ease);
}
.t-learn:hover .t-learn-chevron { transform: translateX(var(--learn-shift)); transition-duration: var(--learn-in); }
.t-learn:hover .t-learn-arm { transition-duration: var(--learn-in); }
.t-learn:hover .t-learn-arm-top { transform: rotate(var(--learn-spread)); }
.t-learn:hover .t-learn-arm-bot { transform: rotate(calc(var(--learn-spread) * -1)); }
@media (prefers-reduced-motion: reduce) {
.t-toast { transition: none !important; }
.t-notice-row { transition: none !important; }
.t-learn-chevron, .t-learn-arm { transition: none !important; }
}
/*
* Notices events: a notice can start as a plain card and grow its tinted
* tray on an event. The footer opens with the transitions.dev accordion
* expand (21), the tint fades in and bleeds out from under the card on the
* same clock, and the footer line rises in with texts reveal (18).
*/
.t-acc-panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows var(--acc-collapse) var(--acc-ease);
}
.t-acc[data-open="true"] .t-acc-panel {
grid-template-rows: 1fr;
transition: grid-template-rows var(--acc-expand) var(--acc-ease);
}
.t-acc-panel-inner {
overflow: hidden;
opacity: 0;
filter: blur(2px);
transition:
opacity var(--acc-collapse) var(--acc-ease),
filter var(--acc-collapse) var(--acc-ease);
}
.t-acc[data-open="true"] .t-acc-panel-inner {
opacity: 1;
filter: blur(0);
transition:
opacity var(--acc-expand) var(--acc-ease),
filter var(--acc-expand) var(--acc-ease);
}
@media (prefers-reduced-motion: reduce) {
.t-acc-panel, .t-acc-panel-inner, .t-acc-chevron {
transition: none !important;
}
}
/* The tray's tint sits under the card. Closed it hugs the card and is
clear; open it bleeds out by --notice-bleed on three sides and down
past the footer. */
[data-slot="notice"] {
--notice-bleed: 4px;
--acc-expand: var(--resize-dur);
--acc-collapse: var(--resize-dur);
--stagger-dur: var(--duration-medium);
--stagger-distance: 6px;
/* Success check (10), tuned down for a 16px icon in a footer line. */
--check-y-amount: 8px;
--check-rotate-from: 60deg;
--check-blur-from: 4px;
}
.t-notice-tint {
position: absolute;
inset: 0;
z-index: -1;
opacity: 0;
pointer-events: none;
transition:
opacity var(--acc-collapse) var(--acc-ease),
top var(--acc-collapse) var(--acc-ease),
right var(--acc-collapse) var(--acc-ease),
left var(--acc-collapse) var(--acc-ease),
background-color var(--duration-medium) var(--ease-smooth-out),
box-shadow var(--duration-medium) var(--ease-smooth-out);
}
.t-acc[data-open="true"] > .t-notice-tint {
opacity: 1;
top: calc(var(--notice-bleed) * -1);
right: calc(var(--notice-bleed) * -1);
left: calc(var(--notice-bleed) * -1);
transition:
opacity var(--acc-expand) var(--acc-ease),
top var(--acc-expand) var(--acc-ease),
right var(--acc-expand) var(--acc-ease),
left var(--acc-expand) var(--acc-ease),
background-color var(--duration-medium) var(--ease-smooth-out),
box-shadow var(--duration-medium) var(--ease-smooth-out);
}
[data-slot="notice-footer"] {
transition: color var(--duration-medium) var(--ease-smooth-out);
}
@media (prefers-reduced-motion: reduce) {
.t-notice-tint, [data-slot="notice-footer"] { transition: none !important; }
}
/* transitions.dev success check (10). The draw keyframe is renamed from
t-check-draw, which the checkbox above already uses. */
:root {
--check-opacity-dur: 500ms;
--check-rotate-dur: 500ms;
--check-rotate-from: 80deg;
--check-bob-dur: 500ms;
--check-y-amount: 40px;
--check-blur-dur: 500ms;
--check-blur-from: 10px;
--check-path-dur: 500ms;
--check-path-delay: 80ms;
--check-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1);
--check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1);
--check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1);
--check-ease-path: cubic-bezier(0.22, 1, 0.36, 1);
}
.t-success-check {
display: inline-block;
transform-origin: center;
opacity: 0;
will-change: transform, opacity, filter;
}
.t-success-check svg { display: block; overflow: visible; }
.t-success-check svg path {
stroke-dasharray: 20;
stroke-dashoffset: 20;
}
.t-success-check[data-state="in"] {
animation:
t-check-fade var(--check-opacity-dur) var(--check-ease-opacity) forwards,
t-check-rotate var(--check-rotate-dur) var(--check-ease-rotate) forwards,
t-check-blur var(--check-blur-dur) var(--check-ease-out) forwards,
t-check-bob var(--check-bob-dur) var(--check-ease-bob) forwards;
}
.t-success-check[data-state="in"] svg path {
animation: t-success-check-draw var(--check-path-dur) var(--check-ease-path) var(--check-path-delay, 0ms) forwards;
}
@keyframes t-check-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes t-check-rotate {
from { transform: rotate(var(--check-rotate-from)); }
to { transform: rotate(0deg); }
}
@keyframes t-check-blur {
from { filter: blur(var(--check-blur-from)); }
to { filter: blur(0); }
}
@keyframes t-check-bob {
from { translate: 0 var(--check-y-amount); }
to { translate: 0 0; }
}
@keyframes t-success-check-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
.t-success-check { animation: none !important; opacity: 1; }
.t-success-check svg path { animation: none !important; stroke-dashoffset: 0 !important; }
}
/* ------------------------------------------------------------------
* Signature (components/ui/signature.tsx)
* Clear and undo: the strokes that leave are copied onto a ghost canvas
* that flies down, blurs and fades, using the transitions.dev input clear
* dissolve text-out values (12px fly, 2px blur, 400ms, smooth out).
* Undo is the same move on the fast duration. The empty hint fades back in.
* ------------------------------------------------------------------ */
:root {
--sig-out-dur: 400ms;
--sig-out-fly: 12px;
--sig-out-blur: 2px;
--sig-out-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes t-sig-out {
from { opacity: 1; transform: translateY(0); filter: blur(0); }
to { opacity: 0; transform: translateY(var(--sig-out-fly)); filter: blur(var(--sig-out-blur)); }
}
[data-slot="signature-ghost"][data-animating] {
animation: t-sig-out var(--sig-out-dur) var(--sig-out-ease) forwards;
}
[data-slot="signature-ghost"][data-animating="undo"] {
animation-duration: var(--duration-fast);
}
[data-slot="signature-placeholder"] {
transition: opacity var(--duration-fast) var(--ease-smooth-out), transform var(--duration-fast) var(--ease-smooth-out);
}
[data-slot="signature-placeholder"][data-hidden] {
opacity: 0;
transform: translateY(-4px);
}
@media (prefers-reduced-motion: reduce) {
[data-slot="signature-ghost"][data-animating] { animation: none; opacity: 0; }
[data-slot="signature-placeholder"] { transition: none; }
}