/* Shared motion: one small vocabulary every feature uses, its keyframes,
 * loops and utilities, and reduced motion. motion.js is its script half
 * (leave, loops); overlay.css applies it to every menu, popover, tooltip,
 * dialog and sheet; toast.css, skeleton.css and dock.js use it too.
 *
 * The vocabulary (brand.css holds the base --sv-dur-* / --sv-ease-* tokens):
 * - One enter curve, --sv-ease-out (a fast start that settles softly). Exits
 *   use --sv-ease-exit (accelerates away) and are shorter than entrances.
 * - enter --sv-dur-enter (200ms): something arrives, from opacity 0 and a
 *   small offset or scale; exit --sv-dur-exit (120ms): it leaves the same way
 *   back; layout --sv-dur-layout (200ms): a size or place changes.
 * - Overlays (menus, popovers, tooltips) grow from scale(.98) + fade, their
 *   transform-origin at the trigger (--sv-origin; overlay.css, menu.js).
 * - Dialogs: the backdrop fades, the panel lifts 8px; side sheets slide in
 *   24px from their edge; a phone's bottom sheet rises from the bottom.
 * - Height: .sv-expand opens and closes by grid-template-rows 0fr <-> 1fr
 *   plus opacity, never by a jump.
 * - Press: a button scales to .97 while pressed; a disabled one is 64%.
 * - Toasts rise in on --sv-dur-toast (450ms) with the same curve.
 * - Loops: one period per meaning, pulse (1.6s, live work) and ping (2.6s,
 *   needs a person). Breathing loops are stepped (hold, a few steps, hold)
 *   and pause off screen and in a hidden tab (.sv-paused, motion.js loops).
 * - Only new things move: hydrated history, polling and reconnects never
 *   replay motion ([data-hydrating] suppresses entrances during a snapshot
 *   paint, body.stale pauses every loop once nothing confirms the picture).
 * - Every one of them stills under prefers-reduced-motion (the end of this
 *   file, and motion.js checks it before any scripted motion). */

:root {
  --sv-ease-exit: cubic-bezier(0.4, 0, 1, 1);
  --sv-dur-enter: 200ms;
  --sv-dur-exit: 120ms;
  --sv-dur-layout: 200ms;
  --sv-dur-toast: 450ms;
}

/* Breathing: hold, step down, hold, step back (fewer frames than a smooth ramp). */
@keyframes sv-breathe {
  0%, 30% { opacity: 1; animation-timing-function: steps(4, jump-end); }
  50%, 80% { opacity: 0.45; animation-timing-function: steps(4, jump-end); }
}
@keyframes sv-dot-breathe {
  0%, 30% { opacity: 1; transform: none; animation-timing-function: steps(4, jump-end); }
  50%, 80% { opacity: 0.62; transform: scale(0.94); animation-timing-function: steps(4, jump-end); }
}
@keyframes sv-caret { 50% { opacity: 0.35; } }
@keyframes sv-enter-msg { from { opacity: 0; transform: translateY(8px); } }
@keyframes sv-enter-row { from { opacity: 0; transform: translateY(-4px); } }
@keyframes sv-enter-pop { from { opacity: 0; transform: translateY(4px) scale(0.96); } }
@keyframes sv-float { 50% { transform: translateY(-5px); } }
@keyframes sv-sweep {
  from { background-position: 100% 0; }
  to { background-position: 0 0; }
}
@keyframes sv-settle-out { to { opacity: 0; } }
/* Overlays and dialogs (overlay.css): grow from the trigger, lift, fade. */
/* Individual transform properties (scale, translate): they compose with a
 * transform that places the element. */
@keyframes sv-pop-in { from { opacity: 0; scale: 0.98; } }
@keyframes sv-pop-out { to { opacity: 0; scale: 0.98; } }
@keyframes sv-fade-in { from { opacity: 0; } }

/* ---- utilities ---- */

/* An element on its way out (motion.js leave): it plays its exit, takes no
 * clicks, and is removed or hidden when the exit ends. */
.sv-leaving { pointer-events: none; }

/* Height open/close: a grid row from 0fr to 1fr plus opacity. The one child
 * clips while it grows; .open (or [open], aria-expanded on the parent) shows it. */
.sv-expand {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--sv-dur-exit) var(--sv-ease-exit), opacity var(--sv-dur-exit) var(--sv-ease-exit);
}
.sv-expand > * { min-height: 0; overflow: hidden; }
.sv-expand.open {
  grid-template-rows: 1fr;
  opacity: 1;
  transition-duration: var(--sv-dur-layout);
  transition-timing-function: var(--sv-ease-out);
}

/* Press: a button gives under the finger; disabled reads at 64%. */
/* The scale property, not transform: a button placed by a transform keeps it. */
:is(button.primary, button.ghost, .sv-press) { transition-property: opacity, color, background, scale; transition-duration: var(--sv-dur-instant); transition-timing-function: var(--sv-ease-out); }
:is(button.primary, button.ghost, .sv-press):active:not(:disabled) { scale: 0.97; }
button:disabled { opacity: 0.64; }

/* A loop off screen or in a hidden tab holds still (motion.js loops). */
.sv-paused, .sv-paused *, .sv-paused *::before, .sv-paused *::after,
html.sv-tab-hidden *, html.sv-tab-hidden *::before, html.sv-tab-hidden *::after { animation-play-state: paused !important; }


/* A live label sweeps ink through its text; falls back to plain secondary ink. */
.sweep {
  color: var(--sv-ink-secondary);
}
@supports (-webkit-background-clip: text) {
  .sweep {
    background: linear-gradient(90deg, var(--sv-ink-secondary) 0 38%, var(--sv-ink) 50%, var(--sv-ink-secondary) 62% 100%) 100% 0 / 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    animation: sv-sweep var(--pulse) linear infinite;
  }
}

/* Nothing is confirming the live picture: freeze the loops (never the
 * entrances, or anything that arrives meanwhile sits invisible at frame 0),
 * hollow the dots. */
body.stale :is(.sweep, .status-dot, .c-dot.running, .who-mark),
body.stale .status-dot::after,
body.stale .streaming *::after,
body.stale [data-slot="trace-line"][data-status="running"] > .tr-title { animation-play-state: paused !important; }
body.stale .status-dot,
body.stale .chats .c-dot.running { opacity: 0.4; }
body.stale [data-slot="trace-line"][data-status="running"] > .tr-title { opacity: 0.4; }

/* Snapshot paint: history never replays motion. */
[data-hydrating] *, [data-hydrating] *::before, [data-hydrating] *::after { animation: none !important; }

/* ---- the rest of v1's motion vocabulary, one meaning each ----
 * view-enter: a whole surface arrives (chat, board, empty state).
 * halo: the working dot rings outward on the work pulse.
 * tick: a number changed (ctx, spend).
 * arrive: a row joined or moved (rail, board stagger). */
@keyframes sv-view-enter { from { opacity: 0; transform: translateY(6px); } }
@keyframes sv-halo {
  0% { opacity: 0.5; transform: scale(0.6); }
  70%, 100% { opacity: 0; transform: scale(2.4); }
}
@keyframes sv-tick { from { color: var(--sv-ink); } to { color: var(--sv-ink-tertiary); } }

.transcript.enter { animation: sv-view-enter var(--sv-dur-base) var(--sv-ease-out) both; }
.meta-num.tick { animation: sv-tick calc(var(--pulse) / 2) var(--sv-ease-out) backwards; }

/* The status dot wears a halo while work runs; settling pops it to rest. */
.status-dot { position: relative; }
.status-line:not(.attention) .status-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--ws-working);
  animation: sv-halo var(--pulse) var(--sv-ease-out) infinite;
}

/* The brand mark turns in 3D while the answer streams: presence, not progress. */
.turn-agent:has(.text.streaming) .who-mark { animation: sv-who-turn calc(var(--pulse) * 2) var(--sv-ease-inout) infinite; }
@keyframes sv-who-turn {
  from { transform: perspective(60px) rotateY(0deg); }
  50%, to { transform: perspective(60px) rotateY(360deg); }
}

/* Rail rows that are new or just changed group slide in (their glide when
 * reordered and the selected row's fill are rail.css / railmotion.js). */
.chats .c-row.arrive { animation: sv-enter-row var(--sv-dur-fast) var(--sv-ease-out) both; }

/* Board rows stagger on the first paint; refreshes repaint still. */
.transcript > .board:not(.repaint) .board-row { animation: sv-enter-row var(--sv-dur-fast) var(--sv-ease-out) both; }
.transcript > .board:not(.repaint) .board-row:nth-child(2) { animation-delay: 30ms; }
.transcript > .board:not(.repaint) .board-row:nth-child(3) { animation-delay: 60ms; }
.transcript > .board:not(.repaint) .board-row:nth-child(4) { animation-delay: 90ms; }
.transcript > .board:not(.repaint) .board-row:nth-child(5) { animation-delay: 120ms; }
.transcript > .board:not(.repaint) .board-row:nth-child(n + 6) { animation-delay: 150ms; }

/* Attachments pop into the tray; an armed Delete pings for attention. */
.attachment-chip { animation: sv-enter-pop var(--sv-dur-fast) var(--sv-ease-out) both; }
.rail-menu-item.armed { animation: sv-breathe var(--ping) var(--sv-ease-inout) infinite; }

/* The panel column eases open; a theme switch crossfades once. */
.app { transition: grid-template-columns var(--sv-dur-base) var(--sv-ease-out); }
body.side-resizing .app { transition: none; }
html.theme-fade, html.theme-fade *, html.theme-fade *::before, html.theme-fade *::after {
  transition: background-color var(--sv-dur-base) var(--sv-ease-out), color var(--sv-dur-base) var(--sv-ease-out), border-color var(--sv-dur-base) var(--sv-ease-out) !important;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
  :is(button.primary, button.ghost, .sv-press):active:not(:disabled) { scale: none; }
  .sweep {
    background: none;
    -webkit-text-fill-color: currentColor;
    color: var(--sv-ink-secondary);
  }
}
