/* ============================================================================
   nav_motion.css -- the toolbar's current-page marker TRAVELS between tabs.

   Loaded from base_site.html's {% block extrahead %}, which Django's
   admin/base.html places after {% block extrastyle %} -- so this sheet resolves
   after chrome.css and borealis.css. Nothing here relies on that: every rule
   that has to beat one of them carries the `html[data-sas-navmotion="on"]`
   gate, which adds a type + an attribute and wins on specificity alone.

   WHY THIS EXISTS

   The bar already animated. borealis.css's SAS-VT block morphs the pill with
   cross-document View Transitions, and that part is good. What it cannot fix is
   WHEN the motion starts: the old document's snapshot is taken at `pageswap`,
   which does not fire until the new document has been fetched and is ready to
   render. Every admin page is `no-store`, so that is 150-500ms in which the
   click produced NOTHING -- the bar sits still, the page sits still, and then
   everything moves at once. Doctrine rule 5: response to input is immediate,
   and the length belongs in the settle, not in the start.

   So the marker is lifted off the nav item and onto its own element, which JS
   springs to the tab you pressed the instant you press it -- before the
   navigation has even been issued. By the time the new document arrives the
   marker is already where it belongs, and the view transition has nothing left
   to do. Interruptible: press a third tab mid-flight and it retargets from its
   current position at its current velocity rather than restarting.

   THE FALLBACK LADDER -- all three rungs are real, none is a degraded guess:

     no JS          `data-sas-navmotion` is never set, none of this applies,
                    and the nav item keeps the pill chrome.css draws today.
     reduced motion the marker still moves (it is the only "you are here"
                    signal) but arrives instantly, and the press scale is
                    dropped. Travel goes, not just speed.
     no ViewTransitions  Firefox and Safari run the optimistic spring and then
                    do a plain navigation. That is strictly better than today,
                    where they get no motion at all.

   WHAT MUST STAY IN STEP WITH chrome.css

   The marker repaints what SAS-BAR3 draws on the item, so the two are written
   from the SAME TOKENS rather than from copied literals -- fill
   `--sas-red` at 18%, border `--sas-red-line`, ink and rail
   `--sas-chrome-accent-ink`. Theme switches therefore follow on their own.
   Retuning the pill's SHAPE (its 8px radius, its 10px rail inset) still means
   editing both files; work/navmotion/audit_navmotion.py asserts they agree and
   is the cheap way to catch a drift.
   ========================================================================= */

:root {
  /* The spring, as one number in each place it is expressed.

     --sas-navmo-spring is a linear() sampling of the SAME analytic spring
     sas_nav_motion.js integrates -- response 0.30s, bounce 0.15, i.e.
     dampingRatio 0.85, which is SwiftUI's .snappy and the doctrine default for
     a small state change. Regenerate with work/navmotion/gen_spring_linear.py
     if either constant moves; do not hand-edit the samples.

     It is used for exactly one thing: the cross-document handoff, where the
     browser owns the interpolation and JS cannot. Peak overshoot is 0.6% --
     one subtle settle, which is the point. A bezier was the alternative and
     cannot express the settle at all. */
  --sas-navmo-spring: linear(0.0000, 0.0178, 0.0634, 0.1275, 0.2024, 0.2827, 0.3639, 0.4432, 0.5184, 0.5882, 0.6518, 0.7088, 0.7592, 0.8032, 0.8411, 0.8735, 0.9008, 0.9237, 0.9425, 0.9578, 0.9702, 0.9800, 0.9876, 0.9935, 0.9979, 1.0011, 1.0033, 1.0048, 1.0057, 1.0062, 1.0063, 1.0062, 1.0059, 1.0054, 1.0050, 1.0045, 1.0039, 1.0035, 1.0030, 1.0025, 1.0022, 1.0018, 1.0015, 1.0012, 1.0000);

  /* Ink and press are discrete two-state changes that cannot be interrupted
     mid-flight, which is the one case doctrine allows a curve. Both are far
     shorter than the travel: the colour must have resolved before the marker
     lands or the label reads as lagging behind its own pill. */
  --sas-navmo-ink: 190ms;
  --sas-navmo-press: 120ms;
  --sas-navmo-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* quint-out */
}

/* ---------------------------------------------------------------------------
   1. The strip becomes the marker's positioning context

   Applied ungated, and deliberately so: it must be true before JS measures
   anything, because offsetLeft/offsetTop are read against it. It is inert on
   its own -- `position: relative` with no offsets moves nothing.

   z-index 0 makes the strip a stacking context so the marker's z-index: -1
   below is sealed inside it. That is safe here for the reason the strip's own
   comment in chrome.css gives: it holds nothing but static anchors. The nav
   overflow PANEL is `position: fixed` and mounts in #site-name, not in here --
   re-check this if that ever changes, because a popup inside a stacking
   context is the Airspace suggestions bug CLAUDE.md records.
   ------------------------------------------------------------------------ */
body.sas-chrome #header .sas-nav {
  position: relative;
  z-index: 0;
}

/* ---------------------------------------------------------------------------
   2. The marker

   Everything from here down is gated on `data-sas-navmotion="on"`, which
   sas_nav_motion.js sets only after it has created the element AND measured it
   into place. That makes the swap atomic in a single style recalculation:
   the marker appears and the item's own pill disappears in the same frame,
   so there is no flash of two pills and no flash of none.

   z-index: -1 puts it below the items' inline content (icons and labels) but
   above the strip's own background -- CSS painting order, step 2 against step
   5. A `position: absolute` element at `z-index: auto` paints at step 6, ABOVE
   the labels, which would cover every word in the bar with a tinted box.

   Out of flow, so it contributes nothing to the flex line: no phantom column,
   no phantom `gap`, and every width admin_nav_overflow.js has measured is
   unchanged.
   ------------------------------------------------------------------------ */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-navmo-pill {
  position: absolute;
  left: 0;
  top: 0;
  z-index: -1;
  pointer-events: none;

  /* Written from SAS-BAR3's tokens, not from its computed values -- see the
     header. --sas-red-soft is the fallback for engines without color-mix and
     is what borealis.css itself falls back to. */
  background: var(--sas-red-soft);
  background: color-mix(in srgb, var(--sas-red) 18%, transparent);
  box-sizing: border-box;

  /* NO BORDER, AND 9px -- both of these took the audit to get right, and both
     were wrong in the obvious way first.

     borealis.css gives the current item `border-color: var(--sas-red-line)` at
     line 226 and restates it `transparent` at 508; then chrome.css drops the
     border entirely with `border: 0` and moves the radius 8px -> 9px, in a
     rule that also takes the item to 34px tall and 11px of side padding. So
     the pill on screen has no edge and a 9px corner, and reading either sheet
     alone gives a marker that is visibly not the thing it replaces.

     `border: 0` is load-bearing beyond the colour: an absolutely positioned
     ::after offsets from its parent's PADDING box, so a 1px border here would
     push the rail's 10px inset to 11px from the outer edge while the item's
     own sits at 10px. A one-pixel wrong rail on the only mark that survives a
     bright wallpaper.

     audit_navmotion.py resolves both sheets in load order and compares. Run it
     rather than reading either file. */
  border: 0;
  border-radius: 9px;

  /* WIDTH IS ANIMATED, AND THAT IS DELIBERATE.

     The checklist's default is transform-only, and the transform-only version
     here is `scaleX`, which distorts both the 8px corner radius and the 1px
     border into ellipses and a taper -- on the one object in the bar whose
     whole job is to look like the pill it replaced. This element is absolutely
     positioned with `contain`, so a width change reflows and repaints nothing
     but itself; there are no siblings in flow to disturb. `translate3d` still
     carries the position, which is the part that actually travels distance. */
  contain: layout paint style;

  /* Transitions are for the paint only. Geometry is written every frame by the
     integrator, and a transition on transform/width would fight it -- the
     compositor would be interpolating toward a value the spring has already
     left, which is the "fights me" symptom. */
  transition: background-color var(--sas-navmo-ink) var(--sas-navmo-ease-out);
}

/* Promoted only while it is actually moving. A permanent `will-change` holds a
   compositor layer for the life of every admin page to animate a few hundred
   milliseconds an hour. sas_nav_motion.js adds this on the first frame and
   removes it on rest. */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-navmo-pill.is-moving {
  will-change: transform, width;
}

/* The rail, which is the part that survives a bright wallpaper behind 88%
   glass. Geometry copied from SAS-BAR3: inset 10px so it reads as a mark under
   the label rather than as a bottom border, and clear of the corner radius.

   chrome.css writes `background: currentColor` because there it is inside the
   item and inherits the item's ink. Here it is a sibling of the items, so the
   token is named outright -- the SAME token that ink resolves to, which is
   what keeps the audited 4.55:1 worst case (Sand) true. */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-navmo-pill::after {
  content: "";
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: var(--sas-chrome-accent-ink);
}

/* Nothing is current -- a record page, or the current tab has been moved into
   the More menu, where the wrap's own `has-current` styling takes over. Forced
   because the UA rule for [hidden] is (0,0,0) and loses to everything above. */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-navmo-pill[hidden] {
  display: none !important;
}

/* ---------------------------------------------------------------------------
   3. The item stops drawing its own pill

   Suppressed on [aria-current] rather than on the selected item, because
   [aria-current] is the only one the base sheets ever give a pill to. During a
   pending navigation the two are different elements, and neither should paint:
   the marker is the single source of truth for "you are here" while the layer
   is on.
   ------------------------------------------------------------------------ */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[aria-current="page"] {
  background: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
}
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[aria-current="page"]::after {
  display: none;
}

/* The hover surface would paint OVER the marker -- the item's background is
   step 3 of the painting order and the marker is step 2 -- so the selected tab
   would take a grey box on top of its red pill. The marker brightens instead,
   in the rule below, which is what SAS-BAR3's `[aria-current]:hover` did. */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[data-sas-navsel]:hover {
  background: transparent !important;
  border-color: transparent !important;
}

/* `:has()` keeps this in CSS rather than adding a third piece of state for JS
   to keep in sync. An engine without it simply does not brighten on hover,
   which is a rest state losing an emphasis, not a broken control. */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav:has(.sas-nav-item[data-sas-navsel]:hover) .sas-navmo-pill {
  background: var(--sas-red-soft);
  background: color-mix(in srgb, var(--sas-red) 26%, transparent);
}

/* ---------------------------------------------------------------------------
   4. Ink follows the SELECTION, not aria-current

   aria-current stays truthful to the document for the whole navigation -- it
   is an accessibility contract and speculatively moving it would announce a
   page the user has not arrived at. `data-sas-navsel` is the visual channel and
   is the one that moves on press.

   The two are the same element in the steady state, so the reset carries
   `:not([data-sas-navsel])` and the pair becomes mutually exclusive. Splitting
   them on source order instead would work today and break the first time
   anything is appended after this file.
   ------------------------------------------------------------------------ */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[aria-current="page"]:not([data-sas-navsel]) {
  color: var(--sas-chrome-fg-2) !important;
}
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[aria-current="page"]:not([data-sas-navsel]) .sas-nav-i {
  opacity: 0.82;
}
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[data-sas-navsel] {
  color: var(--sas-chrome-accent-ink) !important;
}
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[data-sas-navsel] .sas-nav-i {
  opacity: 1;
}

/* The Borealis item is the exception and it is not a small one: its icon is the
   product's own five-gradient tile, held at opacity 1 by SAS-BLICON precisely
   because compositing it at .82 turns the aurora grey-green and it stops
   reading as the mark. Restate that here so the rule above cannot dim it on the
   way past. */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[href="/borealis/"] .sas-nav-i {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   5. Press

   The one place the interface is touched directly, so it is the one place that
   must not lag. A 4% compression from the item's own centre, held while the
   pointer is down and released on the way out.

   Not applied to the Borealis item: that one opens a menu instead of
   navigating, and sas_nav_menu.js already owns its pressed/expanded state.
   ------------------------------------------------------------------------ */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item {
  transition:
    background-color 130ms ease,
    color var(--sas-navmo-ink) var(--sas-navmo-ease-out),
    box-shadow 130ms ease,
    transform var(--sas-navmo-press) var(--sas-navmo-ease-out);
  transform-origin: 50% 50%;
}
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[data-sas-navpress] {
  transform: scale(0.96);
}

/* ---------------------------------------------------------------------------
   6. The handoff across the navigation

   The marker gets its own view-transition-name, which lifts it out of the
   #header snapshot that borealis.css freezes. That matters in exactly one case
   and it is the case that would otherwise be visible: a FAST navigation, where
   `pageswap` fires while the spring is still in flight. Captured with the
   chrome, the old marker would be occluded by the new one and the pill would
   visibly jump the rest of the way. As its own group, the browser interpolates
   the remainder -- on the same spring, via linear() -- and it simply arrives.

   In the ordinary case the spring has already finished, old and new geometry
   are identical, and this group animates nothing at all.

   sas-vt-navmark is switched OFF while the layer is on. That name is on the
   nav ITEM, and with the pill suppressed the group would morph a bare
   icon-and-label from one side of the bar to the other -- a second thing
   travelling for the same reason, against doctrine rule 5, and a ghost of the
   old label dragged across the new one. One marker moves. Only one.
   ------------------------------------------------------------------------ */
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[aria-current="page"] {
  view-transition-name: none;
}
html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-navmo-pill {
  view-transition-name: sas-vt-navdot;
}

/* Tied to --sas-vt-in rather than to a number of its own: the marker and the
   page under it are one event and must not be tunable apart. The linear()
   samples were generated so their natural settle is 419ms, which is that
   token's value -- so the curve plays at its true pace AND the group can never
   outlast the chrome group beside it. */
:root::view-transition-group(sas-vt-navdot) {
  animation-duration: var(--sas-vt-in, 420ms);
  animation-timing-function: var(--sas-navmo-spring);
}

/* Same argument as the SAS-VT block's: these run different curves from the root
   pair, so their opacities sum above 1 partway through, and the UA's
   `plus-lighter` blows that out to white rather than reading as slightly
   bright. Normal alpha cannot flash. */
:root::view-transition-image-pair(sas-vt-navdot) { isolation: auto; }
:root::view-transition-old(sas-vt-navdot),
:root::view-transition-new(sas-vt-navdot) { mix-blend-mode: normal; }

/* ONLY THE NEW IMAGE PAINTS, AND THE FIRST VERSION OF THIS SHIPPED A VISIBLE
   GLITCH BY PAINTING BOTH.

   The marker's fill is `color-mix(var(--sas-red) 18%, transparent)` -- a
   TRANSLUCENT tint, and the snapshot preserves its alpha. Holding old and new
   at `opacity: 1` therefore stacks two 18% reds: 1-(1-0.18)^2 = 33%. The pill
   visibly deepened for the whole 420ms of every tab change and popped back to
   its real colour when the transition ended. The reasoning that put it there
   ("both images are the same flat tint, so a crossfade between them is
   invisible") was right about the crossfade and wrong about the compositing --
   two identical translucent layers are not the same as one.

   They are the same tint, so nothing is lost by showing one. The old image is
   held at zero rather than cross-faded, because a crossfade of two identical
   shapes is work with no visible result. */
:root::view-transition-old(sas-vt-navdot) { animation: none; opacity: 0; }
:root::view-transition-new(sas-vt-navdot) {
  animation: none;
  opacity: 1;

  /* THE SECOND HALF OF THE SAME GLITCH. The UA sizes these snapshots
     `inline-size: 100%; block-size: auto`, so the image keeps the ASPECT RATIO
     it was captured at while the group animates its width. A marker morphing
     92px -> 84px wide was therefore also breathing 37px -> 34px TALL, on a bar
     whose every other element is exactly 34. Fill the group instead: the
     marker is a flat rounded rectangle, so stretching it is exactly right, and
     the 9px radius distorts by less than a pixel over that range. */
  block-size: 100%;
  object-fit: fill;
}

/* One side of the navigation has no marker: arriving at a record page from a
   tab, or landing on a tab from one. `:only-child` is how the pseudo tree says
   a group has no counterpart -- the old and new images are siblings inside
   ::view-transition-image-pair, so a one-sided group has exactly one child.
   It fades in place rather than sliding out of nowhere.

   Both keyframes state `from` explicitly. An animation's underlying value is
   whatever the rules above set, and the old image is now pinned at `opacity:
   0` -- so a fade-out written only as `to { opacity: 0 }` would animate 0 -> 0
   and the marker would vanish instantly instead of leaving. */
:root::view-transition-new(sas-vt-navdot):only-child {
  animation: sas-navmo-fade-in var(--sas-vt-in, 420ms) var(--sas-navmo-ease-out) both;
}
:root::view-transition-old(sas-vt-navdot):only-child {
  animation: sas-navmo-fade-out var(--sas-vt-out, 180ms) linear both;
}
@keyframes sas-navmo-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes sas-navmo-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* ---------------------------------------------------------------------------
   7. Reduced motion -- KEEP THIS BLOCK LAST

   What goes is the TRAVEL, not the speed: the marker is repositioned instantly
   by the integrator (which reads the query itself, and re-reads it when the
   user changes the setting mid-session) and the view-transition group is
   neutralised so the browser cannot re-introduce the slide it just skipped.

   What stays is the colour. The ink crossfade carries no self-motion, and it is
   the only remaining signal that the selection moved -- removing it would leave
   these users with a marker that teleports and nothing to say why.

   The press duration is neutralised to 1ms rather than to 0 or `none` so that
   transitionend still fires. Nothing in sas_nav_motion.js listens for it today
   -- `will-change` is released on the spring's own rest -- but a press handler
   that cleans up on transitionend is the obvious next edit here, and `none`
   would strand it with no error.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  :root {
    --sas-navmo-press: 1ms;
    --sas-navmo-ink: 120ms;
  }
  html[data-sas-navmotion="on"] body.sas-chrome #header .sas-nav .sas-nav-item[data-sas-navpress] {
    transform: none;
  }
  :root::view-transition-group(sas-vt-navdot),
  :root::view-transition-old(sas-vt-navdot),
  :root::view-transition-new(sas-vt-navdot) {
    animation: none !important;
  }
}
