/* ============================================================
   Sentinel — motion-layer.css
   The motion and render layer's tokens and its few rules (window.SentinelMotion, motion-layer.js). One set of
   durations and curves for the whole interface, CSS and JS alike: motion-layer.js mirrors the :root values below in
   `SentinelMotion.tokens`, and tests/ml-foundation.mjs fails when the two disagree.

   A separate file, not a block at the end of styles.css: the admin console gets the same tokens, and no work package
   has to append to the end of styles.css. It changes NO element's look - colours, sizes, spacing and type stay where
   they are; this file only says how long a change takes, what is drawn over what, and what a view transition does.
============================================================ */

:root {
  /* durations: colour/hover · exits, menus, popovers · entrances · height, drawers, scrims, theme */
  --ml-dur-xs: 120ms;
  --ml-dur-sm: 180ms;
  --ml-dur-md: 260ms;
  --ml-dur-lg: 340ms;
  /* the page FADE-THROUGH: the old view to 0 in 90 ms, THEN the new one in 220 ms (never both on screen); the palette
     opens and closes with opacity only */
  --ml-dur-out: 90ms;
  --ml-dur-in: 220ms;
  --ml-dur-pal-in: 90ms;
  --ml-dur-pal-out: 70ms;
  /* curves: entrances and expands (fast start, long soft landing) · moves, groups, glides · exits only, short */
  --ml-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ml-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ml-ease-in: cubic-bezier(0.4, 0, 1, 1);
  /* distances and scales */
  --ml-rise: 6px;
  --ml-rise-page: 4px;
  --ml-shift-x: 14px;
  --ml-scale-overlay: 0.985;
  --ml-scale-menu: 0.97;
  /* THE Z-INDEX SCALE: today's numbers, named. Nothing here moves a layer; a rule that uses a name draws exactly
     where its number did. */
  --z-sticky: 8;
  --z-handle: 18;
  --z-drawer: 50;
  --z-rail-over-drawer: 51;
  --z-pill: 60;
  --z-modal: 60;
  --z-palette: 80;
  --z-toast: 90;
  --z-tooltip: 95;
}

/* ------------------------------------------------------------
   View Transitions (same document): SentinelMotion.navigate(update, {type}) sets html[data-vt=<type>] for the
   length of one transition. Only the regions named for that type animate; everything else changes at once.
------------------------------------------------------------ */
/* Clicks during the 260 ms reach the live new page, not the pictures over it. */
:root::view-transition {
  pointer-events: none;
}
/* the root is not captured unless the change IS the whole page (the theme) */
html[data-vt]:not([data-vt="theme"]) {
  view-transition-name: none;
}

html[data-vt="page"] #modeBar,
html[data-vt="doc"] #modeBar {
  view-transition-name: ml-bar;
}
html[data-vt="page"] #modeStage,
html[data-vt="doc"] #modeStage,
html[data-vt="view"] #modeStage {
  view-transition-name: ml-stage;
}
html[data-vt="libtab"] #libScroll {
  view-transition-name: ml-libbody;
}
html[data-vt="libtab"] #libFoot {
  view-transition-name: ml-libfoot;
}
html[data-vt="lib"] .library {
  view-transition-name: ml-lib;
}
html[data-vt="lib"] #main {
  view-transition-name: ml-main;
}
/* only one of the two is ever displayed, so the handle glides between its two places */
html[data-vt="lib"] .lib-collapse,
html[data-vt="lib"] .lib-reopen {
  view-transition-name: ml-libhandle;
}
html[data-vt="route"] #dxScroll {
  view-transition-name: ml-page;
}
html[data-vt="route"] #dxTop {
  view-transition-name: ml-top;
}
html[data-vt="tab"] #admBody {
  view-transition-name: ml-admbody;
}
html[data-vt="answer"] #wiAnswer {
  view-transition-name: ml-answer;
}

/* LEAVING OR ENTERING DOCS (v2): the Docs root's columns and the app's columns are ONE left column, ONE bar and ONE
   page, so they fade through together - the library no longer appears at once beside an empty stage (3 blank frames
   measured leaving Docs), and the Docs side nav no longer swaps in before its page. Only one of each pair is ever
   rendered, so a name is never on two elements. The shell (v2) may tag its own regions with data-vt-part. */
html:is([data-vt="page"], [data-vt="forward"], [data-vt="back"]) :is(#library, #dxSide, [data-vt-part="side"]) {
  view-transition-name: ml-side;
}
html:is([data-vt="page"], [data-vt="forward"], [data-vt="back"]) :is(#dxTop, [data-vt-part="bar"]) {
  view-transition-name: ml-bar;
}
html:is([data-vt="page"], [data-vt="forward"], [data-vt="back"]) :is(#dxScroll, [data-vt-part="stage"]) {
  view-transition-name: ml-stage;
}
/* the shared-axis types (a tab-like switch, Back / Forward) */
html:is([data-vt="forward"], [data-vt="back"]) #modeBar {
  view-transition-name: ml-bar;
}
html:is([data-vt="forward"], [data-vt="back"], [data-vt="tab"]) #modeStage {
  view-transition-name: ml-stage;
}

/* THE FADE-THROUGH: the old picture is gone (90 ms, ease-in) before the new one starts (220 ms, ease-out, rising
   6 px). The old crossfade (180 ms out while the new one faded in from 40 ms) left both pages on screen for 3-4
   frames - a double exposure measured on every switch. */
::view-transition-old(*) {
  animation: ml-out var(--ml-dur-out) var(--ml-ease-in) both;
}
::view-transition-new(*) {
  animation: ml-in var(--ml-dur-in) var(--ml-ease-out) var(--ml-dur-out) both;
}
/* a view inside the same page fades without travelling */
html[data-vt="view"]::view-transition-new(*) {
  animation-name: ml-fade-in;
}
/* the left column on a switch that stays in the app (the same library before and after): a plain crossfade over
   the same 220 ms, old and new adding up (plus-lighter), so an unchanged library does not dip or blink at all */
html:not([data-vt-from="docs"]):not([data-vt-to="docs"])::view-transition-old(ml-side) {
  animation: ml-out var(--ml-dur-in) linear both;
}
html:not([data-vt-from="docs"]):not([data-vt-to="docs"])::view-transition-new(ml-side) {
  animation: ml-fade-in var(--ml-dur-in) linear both;
}
/* SHARED AXIS (types tab / forward / back): the old view leaves toward the side it is behind, the new one comes in
   from the side it is ahead of - still a fade-through, never both on screen */
html[data-vt-dir="forward"]::view-transition-old(*) {
  animation-name: ml-out-start;
}
html[data-vt-dir="forward"]::view-transition-new(*) {
  animation-name: ml-in-end;
}
html[data-vt-dir="back"]::view-transition-old(*) {
  animation-name: ml-out-end;
}
html[data-vt-dir="back"]::view-transition-new(*) {
  animation-name: ml-in-start;
}
/* the theme changes the whole page: the new colours fade in OVER the old ones (no dip to nothing between them) */
html[data-vt="theme"]::view-transition-old(root) {
  animation: none;
  mix-blend-mode: normal;
}
html[data-vt="theme"]::view-transition-new(root) {
  animation: ml-fade-in var(--ml-dur-in) var(--ml-ease-out) both;
  mix-blend-mode: normal;
}
/* a shared element (SentinelMotion.share) morphs from where it was to where it lands: the group glides, the two
   pictures crossfade inside it */
::view-transition-group(*) {
  animation-duration: var(--ml-dur-md);
  animation-timing-function: var(--ml-ease-in-out);
}
html::view-transition-old(*):only-child,
html::view-transition-new(*):only-child {
  mix-blend-mode: normal;
}
/* the library folds: the same content changes width, so its pictures crossfade (adding up, no dip) while the
   group glides */
html[data-vt="lib"]::view-transition-old(*) {
  animation: ml-out var(--ml-dur-md) linear both;
}
html[data-vt="lib"]::view-transition-new(*) {
  animation: ml-fade-in var(--ml-dur-md) linear both;
}
/* the main column widens as the library folds: its text is not stretched with it */
::view-transition-old(ml-main),
::view-transition-new(ml-main) {
  object-fit: none;
  object-position: left top;
}
/* the left column changes width between the library and the Docs nav: drawn at its own size, never stretched */
::view-transition-old(ml-side),
::view-transition-new(ml-side),
::view-transition-old(ml-stage),
::view-transition-new(ml-stage) {
  height: 100%;
  object-fit: none;
  object-position: left top;
}

@keyframes ml-out {
  to {
    opacity: 0;
  }
}
@keyframes ml-in {
  from {
    opacity: 0;
    transform: translateY(var(--ml-rise));
  }
}
@keyframes ml-out-start {
  to {
    opacity: 0;
    transform: translateX(calc(-1 * var(--ml-shift-x)));
  }
}
@keyframes ml-out-end {
  to {
    opacity: 0;
    transform: translateX(var(--ml-shift-x));
  }
}
@keyframes ml-in-end {
  from {
    opacity: 0;
    transform: translateX(var(--ml-shift-x));
  }
}
@keyframes ml-in-start {
  from {
    opacity: 0;
    transform: translateX(calc(-1 * var(--ml-shift-x)));
  }
}
@keyframes ml-fade-in {
  from {
    opacity: 0;
  }
}

/* ------------------------------------------------------------
   View Transitions across documents: a rail link between the app and Docs crossfades, the rail held still, and
   with theme-boot.js the next page's first frame is already in the reader's theme (N3, N15). For every reader -
   motion is always on; a reload is never one.
------------------------------------------------------------ */
@view-transition {
  navigation: auto;
}
@media (min-width: 761px) {
  html:not([data-vt]) .rail {
    view-transition-name: ml-rail;
  }
}

/* ------------------------------------------------------------
   A control at work (SentinelMotion.busy): the label steps aside for the interface's small spinner (the .rp-spin
   ring), drawn in the control's own ink, so the control keeps its size and nothing beside it moves.
------------------------------------------------------------ */
.ml-busy {
  color: transparent !important;
  cursor: progress;
  pointer-events: none;
}
.ml-busy.ml-busy-rel {
  position: relative;
}
.ml-busy > * {
  visibility: hidden;
}
.ml-busy::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 13px;
  height: 13px;
  box-sizing: border-box;
  border: 2px solid color-mix(in srgb, var(--ml-busy-ink, currentColor) 30%, transparent);
  border-top-color: var(--ml-busy-ink, currentColor);
  border-radius: 50%;
  animation: ml-spin 0.8s linear infinite;
}
@keyframes ml-spin {
  to {
    transform: rotate(360deg);
  }
}

/* ------------------------------------------------------------
   MOTION IS ALWAYS ON (the owner, 2026-10-15): html[data-motion="full"] always (theme-boot.js), no level to choose,
   no Pause button, and the operating system's reduced-motion setting changes nothing.

   AMBIENT SURFACES (SentinelMotion.ambient.register): the state is on the host, data-ambient="running|paused". A
   CSS loop inside one, marked .ml-loop, runs only while its host runs (off screen, a hidden tab, a view transition
   or its owner's hold pause it - never a reader who stops moving).
------------------------------------------------------------ */
[data-ambient="paused"].ml-loop,
[data-ambient="paused"] .ml-loop {
  animation-play-state: paused !important;
}

/* ------------------------------------------------------------
   OVERLAYS (v2). The palette opens with OPACITY ONLY - 90 ms in, 70 ms out (it used to travel and scale for 260 ms,
   291-333 ms to settle); the Docs palette the same.
------------------------------------------------------------ */
.palette-root .palette,
.palette-root .palette-back {
  transform: none !important;
  transition: opacity var(--ml-dur-pal-out) var(--ml-ease-in) !important;
}
.palette-root.is-open .palette,
.palette-root.is-open .palette-back {
  transition: opacity var(--ml-dur-pal-in) var(--ml-ease-out) !important;
}
.dx .pal-wrap,
.dx .pal-wrap .pal {
  animation: ml-fade-in var(--ml-dur-pal-in) var(--ml-ease-out) both !important;
}
/* menus and popovers that simply appeared (the app's menus on <body>, native popovers) get an entry where the browser
   can start a transition from nothing; the Docs menus keep their own dx-pop entry */
@supports selector(:popover-open) {
  @starting-style {
    body > .menu,
    [popover]:popover-open {
      opacity: 0;
      transform: translateY(-4px) scale(var(--ml-scale-menu));
    }
  }
  body > .menu,
  [popover]:popover-open {
    transition:
      opacity var(--ml-dur-xs) var(--ml-ease-out),
      transform var(--ml-dur-xs) var(--ml-ease-out);
  }
}

/* ------------------------------------------------------------
   LONG PAGES (the Report switch's long task). The page switch paints the whole new page once inside the view
   transition: on the Report that was a 6,997 px, 1,186-node stage laid out and painted in full (a 77-116 ms long
   task). Sections below the fold skip their layout and paint until they near the viewport; their size is held by
   contain-intrinsic-size (auto: the last real size once seen), so the scrollbar and anchors stay right.
   .ml-cv opts any long block in.
------------------------------------------------------------ */
.ml-cv,
.report-layout .rpt-findings > .rpt-card:nth-child(n + 3) {
  content-visibility: auto;
  contain-intrinsic-size: auto 360px;
}
@media print {
  .ml-cv,
  .report-layout .rpt-findings > .rpt-card {
    content-visibility: visible;
  }
}

/* ------------------------------------------------------------
   COUNT-UPS WITHOUT WRITING TEXT (SentinelV2.ambient.countUp): the Russian layer observes every text node, so a
   number is never rewritten per frame. The figure on screen is a CSS counter in ::after, driven by a registered
   <integer> property that transitions once to the real value (--ml-to); the real value is in the element's own
   sr-only text for readers and copy. Money is never counted up (the helper refuses data-money).
------------------------------------------------------------ */
@property --ml-n {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}
.ml-count {
  counter-reset: ml-n var(--ml-to, 0); /* the value itself, with no script at all */
  font-variant-numeric: tabular-nums;
}
.ml-count.ml-armed {
  --ml-n: 0;
  counter-reset: ml-n var(--ml-n);
}
.ml-count.ml-armed.ml-counted {
  --ml-n: var(--ml-to, 0);
  transition: --ml-n 600ms var(--ml-ease-out);
}
.ml-count::after {
  content: counter(ml-n);
  content: counter(ml-n) / ""; /* drawn, not read: the sr-only text is what is read */
}
.ml-count > .ml-count-v {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
