/* CLAUDE 10-18 (design polish B1, analysis/DESIGN-POLISH-PLAN-2026-10-18.md sections 3 and 4.1): the motion system.
   One place for the tokens, the standard patterns (P1 to P19) and the reduced-motion policy, so a surface adopts a
   recipe by a class instead of writing keyframes. Loaded LAST in index.html (after every other stylesheet, a test checks
   it): the neutralising rules below beat the older per-file motion at equal specificity by coming later.

   Rules: only transform / opacity / scale / translate (and one-shot clip-path); exits are ease-out and about 60 % of the
   enter; nothing a finger can grab restarts from zero; hover only where there is a hover; reduced motion is calm, not
   dead: opacity and colour stay, movement, scale and loops go (no global "animation-duration: 0", it deletes feedback).
   What never animates: the playhead, scrubbing, timeline zoom, the preview canvas, keyboard results, typing. */

:root {
  /* the existing curves stay in style.css (--ease-out, --ease-in-out, --ease-sheet). New: a damped spring for the soft
     enter (about 1.5 % overshoot, zeta .8) and a livelier one for momentum and feedback only (about 8 %, zeta .62) */
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --d-fast: 90ms; /* tooltip hide, tiny exits */
  --d-enter: 280ms; /* soft enters: toast, list rise, pane */
  --d-exit: 160ms; /* exits, about 60 % of the enter */
  --d-sheet: 380ms; /* a phone sheet in; out is --d-sheet-out */
  --d-sheet-out: 260ms;
  --d-count: 700ms;
  --d-skel: 1400ms; /* one shimmer pass */
  --d-stagger: 36ms; /* between siblings; ten at most */
  --shift-sm: 4px; /* how far things rise when they enter; 0 under reduced motion */
  --shift-md: 8px;
  --shift-lg: 16px;
  --press: 0.97; /* press scale of buttons, chips, icon buttons; 1 under reduced motion */
  --press-lg: 0.985; /* tiles and cards */
  --pop: 0.97; /* where a menu or dialog starts from; 1 under reduced motion */
  /* depth (4.1): a top-edge highlight is how a floating layer separates from the one under it without a box */
  --hl: inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --e-1: var(--hl), 0 1px 2px rgba(6, 6, 9, 0.45);
  --e-2: var(--hl), 0 8px 20px rgba(6, 6, 9, 0.4), 0 0 0 1px var(--c-line);
  --e-float: var(--hl), 0 12px 32px rgba(6, 6, 9, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.06);
  /* the colour changes every control shares (hover), without the press, which each rule adds itself */
  --m-hover:
    background-color var(--d-hover) ease, border-color var(--d-hover) ease, color var(--d-hover) ease,
    box-shadow var(--d-hover) ease, opacity var(--d-hover) ease;
}
/* the spring curves as real springs where linear() exists (everywhere current); the cubic-beziers above are the fallback */
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --ease-soft: linear(
      0,
      0.051,
      0.168,
      0.312,
      0.458,
      0.592,
      0.706,
      0.799,
      0.871,
      0.923,
      0.961,
      0.986,
      1.001,
      1.01,
      1.014,
      1.015,
      1.014,
      1.013,
      1.01,
      1.008,
      1.006,
      1.004,
      1.003,
      1.002,
      1.001,
      1
    );
    --ease-spring: linear(
      0,
      0.084,
      0.273,
      0.492,
      0.695,
      0.858,
      0.973,
      1.043,
      1.076,
      1.083,
      1.075,
      1.058,
      1.04,
      1.023,
      1.01,
      1.001,
      0.996,
      0.993,
      0.993,
      0.994,
      0.995,
      0.997,
      0.998,
      0.999,
      1,
      1
    );
  }
}

/* ===== the reduced-motion policy (3.5): movement, scale and loops go; opacity and colour stay ===== */
@media (prefers-reduced-motion: reduce) {
  :root {
    --shift-sm: 0px;
    --shift-md: 0px;
    --shift-lg: 0px;
    --press: 1;
    --press-lg: 1;
    --pop: 1;
  }
  .m-skel::after {
    animation: none;
  }
  .m-spin {
    animation: m-spin 2.4s linear infinite; /* a frozen spinner reads as a hang: it keeps turning, slowly */
  }
  .m-tabs.m-js::after,
  .m-vtabs.m-js::after {
    transition: opacity var(--d-hover) ease; /* the indicator hops */
  }
  .m-img {
    transition: opacity var(--d-ui) ease;
  }
}

/* ===== base: type finish, disabled cursor ===== */
html {
  -webkit-font-smoothing: antialiased;
  font-optical-sizing: auto;
}
:is(h1, h2, h3, .m-title) {
  text-wrap: balance;
}
.m-num {
  font-variant-numeric: tabular-nums;
}
:disabled,
[aria-disabled='true'] {
  cursor: not-allowed;
}
[aria-busy='true'] {
  cursor: progress;
}

/* ===== P1 press + P2 hover: one definition (replaces style.css and the editor.css blanket of 09-27) ===== */
button,
a,
.lib-card,
.proj-tile,
.db-card,
.recent-card,
.coll-tile,
select,
input,
textarea {
  transition:
    var(--m-hover),
    scale 220ms var(--ease-spring);
}
.tl-block {
  transition:
    box-shadow 120ms ease,
    opacity 120ms ease; /* a timeline block never transitions its position: a drag is 1:1 */
}
button:not(:disabled):active,
[role='button']:not([aria-disabled='true']):active,
summary:active,
a:is(.primary, .m-btn):active {
  transform: none; /* editor.css and style.css once pressed with transform, which clobbers a component's own */
  scale: var(--press);
  transition:
    var(--m-hover),
    scale 100ms var(--ease-out);
}
.tile:active,
.lib-card:active,
.proj-tile:active,
.db-card:active,
.recent-card:active,
.coll-tile:active {
  scale: var(--press-lg);
}
.recent-card:hover {
  transform: none; /* Graphite: no lift (G3 below); editor.css lifted it 2 px */
}

/* ===== the primary button: one per area, off-white, a faint gradient and a lit top edge (G4) ===== */
.primary,
.accent {
  background: linear-gradient(180deg, #f6f4ef, #ebe8e1);
  box-shadow:
    var(--hl),
    inset 0 -1px 0 rgba(0, 0, 0, 0.12);
}
.primary:hover:not(:disabled),
.accent:hover:not(:disabled) {
  background: linear-gradient(180deg, #ffffff, #f3f1ec);
}

/* ===== P3 selection ===== */
/* a selected list row: the 2 px bar grows from the centre */
.m-rowsel {
  position: relative;
}
.m-rowsel::before {
  content: '';
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 2px;
  border-radius: 2px;
  background: var(--c-fg);
  transform: scaleY(0);
  transition: transform var(--d-ui) var(--ease-out);
}
.m-rowsel:is(.selected, .active, [aria-selected='true'], [aria-current])::before {
  transform: scaleY(1);
}

/* ===== the sidebar: one bar that slides between pages (motion.js tabs() adds .m-vtabs / .m-js) ===== */
.home-tabs {
  position: relative;
}
.home-tabs.m-vtabs.m-js a.active::before {
  content: none; /* the per-link bar of studio.css is replaced by the sliding one while the script is running */
}
.home-tabs.m-vtabs.m-js::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 1px;
  border-radius: 1px;
  background: var(--c-fg);
  transform-origin: 0 0;
  transform: translateY(var(--tab-y, 0px)) scaleY(var(--tab-h, 1));
  opacity: var(--tab-on, 0);
  transition:
    transform 200ms var(--ease-out),
    opacity var(--d-hover) ease;
  pointer-events: none;
}
.m-tabs-init::after,
.m-tabs-init::before {
  transition: none !important; /* the first placement is not a slide */
}

/* ===== P5 collapse: grid rows 0fr to 1fr, the content clipped ===== */
.m-collapse {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition:
    grid-template-rows var(--d-ui) var(--ease-out),
    opacity var(--d-ui) var(--ease-out);
}
.m-collapse > * {
  min-height: 0;
  overflow: hidden;
}
.m-collapse.open,
.m-collapse[open],
details[open] > .m-collapse {
  grid-template-rows: 1fr;
  opacity: 1;
}

/* ===== P6 menu / popover (anchored to the trigger: set --origin, or data-origin top / bottom / left / right) ===== */
.m-menu {
  --origin: top;
  background: var(--c-overlay);
  border-radius: var(--r-md);
  box-shadow: var(--e-2);
  transform-origin: var(--origin);
  opacity: 1;
  scale: 1;
  transition:
    opacity var(--d-ui) var(--ease-out),
    scale var(--d-ui) var(--ease-out),
    display var(--d-ui) allow-discrete;
}
.m-menu[data-origin='top'] {
  --origin: top;
}
.m-menu[data-origin='bottom'] {
  --origin: bottom;
}
.m-menu[data-origin='left'] {
  --origin: left;
}
.m-menu[data-origin='right'] {
  --origin: right;
}
.m-menu[hidden] {
  opacity: 0;
  scale: var(--pop);
  transition-duration: 110ms; /* out: 110 ms */
}
@starting-style {
  .m-menu:not([hidden]) {
    opacity: 0;
    scale: var(--pop);
  }
}
.m-menu[data-instant] {
  transition-duration: 0s; /* never animate a menu opened from the keyboard */
}

/* the account menu (interface.css) is the same recipe */
.account-dropdown {
  animation: none;
  box-shadow: var(--e-2);
  transform-origin: top right;
  opacity: 1;
  scale: 1;
  transition:
    opacity var(--d-ui) var(--ease-out),
    scale var(--d-ui) var(--ease-out),
    display var(--d-ui) allow-discrete;
}
.account-dropdown[hidden] {
  opacity: 0;
  scale: var(--pop);
  transition-duration: 110ms;
}
@starting-style {
  .account-dropdown:not([hidden]) {
    opacity: 0;
    scale: var(--pop);
  }
}
.account-dropdown[data-instant] {
  transition-duration: 0s;
}

/* the rich tooltip (tip.js; markup and look in editor-chrome.css): 125 ms, from its edge; the next one is instant */
.rip-tip {
  border: 0;
  background: var(--c-overlay);
  box-shadow: var(--e-2);
  transform: none;
  translate: 0 var(--shift-sm);
  scale: var(--pop);
  transform-origin: 50% 100%;
  transition:
    opacity 125ms var(--ease-out),
    translate 125ms var(--ease-out),
    scale 125ms var(--ease-out);
}
.rip-tip[data-side='below'] {
  transform: none;
  translate: 0 calc(var(--shift-sm) * -1);
  transform-origin: 50% 0;
}
.rip-tip.on {
  transform: none;
  translate: 0 0;
  scale: 1;
}
.rip-tip[data-instant] {
  transition-duration: 0s;
}

/* ===== P7 dialog: centred, in 240 ms, out 150 ms; a phone gets the fade only (sheets slide in their own files) ===== */
dialog {
  opacity: 0;
  scale: var(--pop);
  transition:
    opacity 150ms var(--ease-out),
    scale 150ms var(--ease-out),
    overlay 150ms allow-discrete,
    display 150ms allow-discrete;
}
dialog[open] {
  opacity: 1;
  scale: 1;
  animation: none; /* editor.css popped the dialog in with a keyframe and never out */
  transition:
    opacity var(--d-panel) var(--ease-out),
    scale var(--d-panel) var(--ease-out),
    overlay var(--d-panel) allow-discrete,
    display var(--d-panel) allow-discrete;
}
@starting-style {
  dialog[open] {
    opacity: 0;
    scale: var(--pop);
  }
}
dialog::backdrop {
  opacity: 0;
  animation: none;
  background: rgba(0, 0, 0, 0.6);
  transition:
    opacity 120ms ease,
    overlay 120ms allow-discrete,
    display 120ms allow-discrete;
}
dialog[open]::backdrop {
  opacity: 1;
  transition:
    opacity 200ms ease,
    overlay 200ms allow-discrete,
    display 200ms allow-discrete;
}
@starting-style {
  dialog[open]::backdrop {
    opacity: 0;
  }
}
/* the blur: 6 px at most, desktop only, and never over the live preview (the editor is open when body is not at-home) */
@media (min-width: 768px) {
  dialog::backdrop {
    backdrop-filter: blur(6px);
  }
}
@media (max-width: 767.98px) {
  dialog {
    --pop: 1; /* the fade only: a dialog on a phone is often a sheet, and a sheet never scales */
    scale: 1;
  }
  dialog::backdrop {
    backdrop-filter: none;
  }
}
body:not(.at-home) dialog::backdrop {
  backdrop-filter: none;
}
/* the dialogs that are phone sheets and full screens keep their own slide (phone.css, phoneed.css) */
dialog.app-sheet,
dialog.ph-full {
  scale: 1;
  opacity: 1;
  transition: none;
}
@media (prefers-reduced-transparency: reduce) {
  dialog::backdrop {
    backdrop-filter: none;
    background: rgba(8, 8, 10, 0.92);
  }
}

/* ===== P9 toast: Graphite, soft in, out on its own, a swap crossfades (app-base.js toast()) ===== */
body #toast {
  background: var(--c-overlay);
  color: var(--c-fg);
  border: 0;
  border-radius: var(--r-md);
  box-shadow: var(--e-2);
  padding: 10px 16px;
  font-size: var(--t-body);
  line-height: 1.5;
  animation: none; /* the keyframe of editor.css / studio.css */
  opacity: 1;
  translate: 0 0;
  transition:
    opacity var(--d-enter) var(--ease-soft),
    translate var(--d-enter) var(--ease-soft),
    display var(--d-enter) allow-discrete;
}
body #toast[hidden] {
  opacity: 0;
  translate: 0 12px;
  transition-duration: var(--d-exit);
  transition-timing-function: var(--ease-out);
}
@starting-style {
  body #toast:not([hidden]) {
    opacity: 0;
    translate: 0 12px;
  }
}
@media (prefers-reduced-motion: reduce) {
  body #toast,
  body #toast[hidden] {
    translate: 0 0; /* the toast fades */
  }
}

/* ===== P10 skeleton, spinner, P14 image fade ===== */
.m-skel {
  position: relative;
  overflow: hidden;
  background: var(--c-raised);
  border-radius: var(--r-sm);
  contain: paint;
}
.m-skel::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
  transform: translateX(-100%);
  animation: m-skel var(--d-skel) linear infinite;
}
.m-skel[data-m-off]::after {
  animation-play-state: paused; /* motion.js pauses a skeleton that is off screen */
}
@keyframes m-skel {
  to {
    transform: translateX(100%);
  }
}
.m-spin {
  display: inline-block;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: m-spin 0.7s linear infinite;
}
@keyframes m-spin {
  to {
    rotate: 360deg;
  }
}
.m-img {
  opacity: 0;
  transition: opacity 200ms ease;
}
.m-img.is-loaded,
.m-img[data-loaded] {
  opacity: 1;
}

/* ===== P12 stagger, P19 enter / fade / leaving ===== */
@keyframes m-rise {
  from {
    opacity: 0;
    translate: 0 var(--shift-md);
  }
}
@keyframes m-enter {
  from {
    opacity: 0;
    translate: 0 var(--shift-sm);
  }
}
@keyframes m-fade {
  from {
    opacity: 0;
  }
}
.m-enter {
  animation: m-enter var(--d-enter) var(--ease-soft) backwards;
}
.m-rise {
  animation: m-rise var(--d-enter) var(--ease-soft) backwards;
}
.m-fade {
  animation: m-fade var(--d-ui) ease backwards;
}
.m-stagger > *:not([data-m-skip]) {
  animation: m-rise var(--d-enter) var(--ease-soft) backwards;
  animation-delay: calc(var(--i, 0) * var(--m-step, var(--d-stagger)));
}
.m-leaving {
  opacity: 0;
  translate: 0 var(--shift-sm);
  transition:
    opacity var(--d-exit) var(--ease-out),
    translate var(--d-exit) var(--ease-out);
  pointer-events: none;
}

/* ===== P13 page and panel change: the page rises in 4 px while it fades; the shell does not move.
   showPage() is synchronous and its callers read the DOM straight after it, so this is the CSS rise (the plan's
   fallback) rather than document.startViewTransition, whose callback runs a frame later. RipMotion.viewTransition()
   is there for a place that can wait. ===== */
@keyframes m-page-in {
  from {
    opacity: 0;
    translate: 0 var(--shift-sm);
  }
}
.home-page:not([hidden]) {
  animation: m-page-in var(--d-panel) var(--ease-out) backwards;
}
body:not(.at-home) main {
  animation: m-page-in var(--d-panel) var(--ease-out) backwards;
}

/* ===== P17 success ===== */
.m-check path {
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  animation: m-draw 360ms var(--ease-out) forwards;
}
@keyframes m-draw {
  to {
    stroke-dashoffset: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .m-check path {
    animation: none;
    stroke-dashoffset: 0;
  }
}

/* ===== reduced motion, part 2: calm, not dead =====
   editor.css (09-27) still ends with `* { animation: none !important; transition: none !important }` under reduced motion,
   which deletes every fade too (measured 10-18: a toast and a dialog simply cut). The policy keeps opacity and colour, so
   the shared patterns turn their fades back on here, with !important at a specificity above `*`. The movement (shift,
   scale, the slide) stays off through the tokens above. When editor.css loses that block (design polish B3a) this is
   harmless and can stay. */
@media (prefers-reduced-motion: reduce) {
  button,
  a,
  select,
  input,
  textarea,
  .lib-card,
  .proj-tile,
  .db-card,
  .recent-card,
  .coll-tile {
    transition: var(--m-hover) !important;
  }
  body #toast,
  body #toast[hidden] {
    transition:
      opacity var(--d-enter) ease,
      display var(--d-enter) allow-discrete !important;
  }
  dialog,
  dialog[open] {
    transition:
      opacity var(--d-panel) ease,
      overlay var(--d-panel) allow-discrete,
      display var(--d-panel) allow-discrete !important;
  }
  dialog::backdrop,
  dialog[open]::backdrop {
    transition:
      opacity 160ms ease,
      overlay 160ms allow-discrete,
      display 160ms allow-discrete !important;
  }
  dialog.app-sheet,
  dialog.ph-full {
    transition: none !important;
  }
  .m-menu,
  .account-dropdown {
    transition:
      opacity var(--d-ui) ease,
      display var(--d-ui) allow-discrete !important;
  }
  .rip-tip {
    transition: opacity 125ms ease !important;
  }
  .m-leaving {
    transition: opacity var(--d-exit) ease !important;
  }
  .m-collapse {
    transition: opacity var(--d-ui) ease !important;
  }
  .m-switch > i {
    transition: background-color var(--d-hover) ease !important;
  }
  .m-tabs.m-js::after,
  .m-vtabs.m-js::after,
  .m-seg.m-js::before {
    transition: opacity var(--d-hover) ease !important;
  }
  .m-img {
    transition: opacity var(--d-ui) ease !important;
  }
  .m-enter,
  .m-rise,
  .m-fade,
  .home-page:not([hidden]),
  body:not(.at-home) main {
    animation: m-fade var(--d-ui) ease backwards !important;
  }
  .ready-ping {
    animation: ready-ping-in var(--d-ui) ease !important; /* interface.css: its shift is 0 here, so it fades */
  }
  .m-stagger > *:not([data-m-skip]) {
    animation: m-fade var(--d-ui) ease backwards !important;
    animation-delay: calc(var(--i, 0) * var(--m-step, var(--d-stagger))) !important;
  }
  .m-spin,
  .m-btn[aria-busy='true']::after {
    animation: m-spin 2.4s linear infinite !important;
  }
}

/* a sideways tab row (motion.js grabScroll, owner 11-03): the edge that has more tabs past it fades (no box, no line);
   with a mouse the row is grabbed and dragged. :where keeps these at one class, so a surface's own fade size or a phone
   sheet's own mask (phonescreen.css) still wins. */
:where([data-m-scroll]) {
  --m-fade-l: 0px;
  --m-fade-r: 0px;
  user-select: none;
  -webkit-user-select: none;
}
:where([data-m-scroll]).more-l {
  --m-fade-l: var(--m-fade, var(--s-5));
}
:where([data-m-scroll]).more-r {
  --m-fade-r: var(--m-fade, var(--s-5));
}
:where([data-m-scroll]):is(.more-l, .more-r) {
  -webkit-mask-image: linear-gradient(
    to right,
    transparent,
    black var(--m-fade-l),
    black calc(100% - var(--m-fade-r)),
    transparent
  );
  mask-image: linear-gradient(
    to right,
    transparent,
    black var(--m-fade-l),
    black calc(100% - var(--m-fade-r)),
    transparent
  );
}
@media (hover: hover) and (pointer: fine) {
  [data-m-scroll]:is(.more-l, .more-r),
  [data-m-scroll]:is(.more-l, .more-r) * {
    cursor: grab;
  }
}
/* while dragged: no snapping or smooth scrolling under the pointer */
[data-m-scroll].m-grabbing {
  scroll-snap-type: none;
  scroll-behavior: auto;
}
[data-m-scroll].m-grabbing,
[data-m-scroll].m-grabbing * {
  cursor: grabbing;
}

/* the Settings line that tells the owner the system reduces motion (motion.js settingsNote) */
.m-note {
  margin: var(--s-1) 0 0;
  color: var(--c-fg-3);
  font-size: var(--t-meta);
}
