/* ══════════════════════════════════════════════════════════════════════
   nav_transitions.css — presentation layer for the dashboard's soft
   navigation (see static/js/nav_transitions.js).

   Three concerns live here and nowhere else, so no page re-implements them:
     1. the top progress bar,
     2. the content enter/exit motion (both the JS-driven swap and the
        native cross-document View Transition used on opt-out pages),
     3. the shared skeleton primitives.

   Everything animates opacity/transform only. Motion is gated on
   prefers-reduced-motion at the bottom of the file — one block, so there
   is a single place to check what "reduced" actually means here.
   ══════════════════════════════════════════════════════════════════════ */

/* Screen-reader-only utility — used by the router's live region, which has
   to be in the accessibility tree but never visible. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── 1. Top progress bar ──────────────────────────────────────────────
   Fixed above every other layer (the app header sits at z-index 100).
   Progress is driven by scaleX rather than width so it never triggers
   layout during a navigation that is already doing real work. */
.lx-navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2.5px;
  z-index: 2147483000;
  pointer-events: none;
  opacity: 0;
  /* Governs the fade-out only. */
  transition: opacity 200ms var(--ease-standard);
}
/* Appearing is instantaneous, and that is load-bearing rather than a taste
   call: the DOM commit lands within a frame or two of the click on a
   prefetched page, and a view transition freezes whatever the bar looked
   like at that moment for its whole duration. A fade-in meant the frozen
   frame was a bar at ~0 opacity — the bar simply disappeared until the
   transition ended. Removing the transition on the way in is what keeps it
   on screen throughout. */
.lx-navbar.is-visible { opacity: 1; transition: none; }

.lx-navbar-fill {
  position: relative;
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent3), var(--accent) 55%, var(--accent2));
  box-shadow: 0 0 10px var(--glow), 0 0 3px var(--accent-tint-30);
  transition: transform 220ms cubic-bezier(0, 0, .2, 1);
  will-change: transform;
}

/* Leading glow, so the bar reads as travelling rather than stretching. */
.lx-navbar-fill::after {
  content: '';
  position: absolute;
  top: -1px;
  right: 0;
  width: 90px;
  height: calc(100% + 2px);
  background: linear-gradient(90deg, transparent, var(--accent2));
  opacity: .75;
  filter: blur(2px);
}

/* ── 2. Content transition ────────────────────────────────────────────
   Applied by the router to the swapped region only — the sidebar, app
   header and topbar shell are never re-rendered, so they never animate. */
/* Tuned so a tab change reads as clearly as a project change. Between tabs
   the content box keeps the same geometry, so the group morph contributes
   nothing and this rise/fade is the entire effect — it has to carry it. */
@keyframes lxContentIn {
  from { opacity: 0; transform: translateY(7px) scale(.996); }
  to   { opacity: 1; transform: none; }
}
@keyframes lxContentOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-4px) scale(.998); }
}

.lx-nav-enter {
  animation: lxContentIn 150ms cubic-bezier(0, 0, .2, 1) both;
}

/* Fallback path only: used when the browser has no View Transition API,
   where the old content has to be faded out by hand before the swap. */
.lx-nav-leave {
  animation: lxContentOut 120ms cubic-bezier(.4, 0, 1, 1) both;
}

/* ── Native View Transitions ──────────────────────────────────────────
   Serves two paths at once. Same-document: the router wraps its DOM swap
   in document.startViewTransition(), which gives the topbar a real FLIP
   morph (the "layout animation" — buttons sliding, a search field growing
   in — without a JS animation library). Cross-document: pages that opt out
   of soft navigation still cross-fade instead of flashing white. */
@view-transition { navigation: auto; }

/* After every swap the router focuses [data-nav-content] (tabindex="-1") so
   keyboard and screen-reader users land on the new content rather than the
   unchanged sidebar. That focus is purely programmatic, but the UA still
   paints a focus ring around the whole region — a stray light border around
   the page's content on arrival. Suppress it here; this container is never a
   focus target a sighted user reaches by tabbing. */
[data-nav-content]:focus { outline: none; }

/* Named regions are matched across the two snapshots and interpolated.
   The shell parts are named purely so they hold still: identical old/new
   content means the morph resolves to no visible change, which is what
   keeps the sidebar from cross-fading on every navigation. */
.app-header  { view-transition-name: lx-header; }
.app-body > .sidebar { view-transition-name: lx-sidebar; }
[data-nav-topbar]    { view-transition-name: lx-topbar; }
[data-nav-content]   { view-transition-name: lx-content; }

@keyframes lxVtFadeIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes lxVtFadeOut { from { opacity: 1; } to { opacity: 0; } }

/* Content: the outgoing snapshot leaves quickly while the incoming one
   rises in over it. Overlapping the two is what keeps the previous page
   visible right up to the moment the next one lands.

   Kept short deliberately. Nothing is being waited on here — the HTML is
   usually already in the prefetch cache and the DOM is swapped before the
   first frame of this animation — so every millisecond of it is latency
   the user feels but does not get anything for. */
::view-transition-old(lx-content) {
  animation: lxContentOut 100ms cubic-bezier(.4, 0, 1, 1) both;
}
::view-transition-new(lx-content) {
  animation: lxContentIn 160ms cubic-bezier(0, 0, .2, 1) both;
}
/* Both snapshots are painted at once during the overlap; without this the
   incoming page darkens as it fades over the outgoing one. */
::view-transition-image-pair(lx-content) { isolation: auto; }
::view-transition-old(lx-content),
::view-transition-new(lx-content) { mix-blend-mode: normal; }

/* A cross-shell navigation (projects list → a project's pages) changes the
   content width — the top shell is narrower than a project's 1440px canvas.
   By default the group morph stretches the outgoing snapshot to the new width
   as it fades, so the old page's cards visibly grow mid-transition. Pinning
   the outgoing snapshot to its captured size makes that a straight cross-fade
   instead. Same-shell swaps are unaffected: old and new share a width, so
   there is nothing to stretch. */
::view-transition-old(lx-content) {
  object-fit: none;
  object-position: top center;
}

/* Topbar: let the group morph position/size, and cross-fade the contents
   so appearing/disappearing controls resolve smoothly. */
::view-transition-group(lx-topbar) {
  animation-duration: 160ms;
  animation-timing-function: cubic-bezier(0, 0, .2, 1);
}
::view-transition-old(lx-topbar) { animation: lxVtFadeOut 90ms linear both; }
::view-transition-new(lx-topbar) { animation: lxVtFadeIn 140ms linear both; }

/* Header and sidebar keep the default cross-fade. When their content is
   unchanged the two snapshots are identical and nothing is visible; when
   the sidebar's active item moves, that cross-fade *is* the animation. */
::view-transition-group(lx-sidebar),
::view-transition-group(lx-header) {
  animation-duration: 160ms;
}

/* The root snapshot covers the ambient background, which is mounted
   outside the swapped shell and never changes. Cross-fading it would make
   the whole viewport pulse on every navigation, so it stays put. */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* ── 3. Skeletons ─────────────────────────────────────────────────────
   Primitives shared by every data-fetching page. Compose them with the
   data-skel attribute (auto-filled by the router) or by hand for layouts
   that need to match something specific. */
.lx-skel {
  position: relative;
  overflow: hidden;
  background: var(--glass);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
}

/* Sweep is a translated overlay rather than an animated background-position
   so it stays on the compositor. */
.lx-skel::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--accent-tint-08) 50%, transparent);
  transform: translateX(-100%);
  animation: lxSkelSweep 1.5s cubic-bezier(.4, 0, .2, 1) infinite;
  will-change: transform;
}
@keyframes lxSkelSweep {
  to { transform: translateX(100%); }
}

/* Bare bars — no border, for text-line placeholders inside a card. */
.lx-skel-line {
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--accent-tint-08);
  height: 11px;
}
.lx-skel-line + .lx-skel-line { margin-top: 9px; }
.lx-skel-line.is-title { height: 15px; width: 42%; }
.lx-skel-line.is-short { width: 58%; }
.lx-skel-line.is-tiny  { height: 9px; width: 30%; }

/* Layout wrappers */
.lx-skel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-4);
}
.lx-skel-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Card — mirrors the tool/MCP/guardrail card shape those pages render. */
.lx-skel-card {
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  min-height: 132px;
}
.lx-skel-card .lx-skel-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.lx-skel-card .lx-skel-avatar {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-md);
  background: var(--accent-tint-08);
  flex-shrink: 0;
}

/* Row — for log/table style lists. */
.lx-skel-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
}
.lx-skel-row .lx-skel-line { flex: 1; margin: 0; }
.lx-skel-row .lx-skel-line.is-fixed { flex: 0 0 82px; }

/* Chart — a block the size of a chart canvas, so swapping in the real
   chart does not reflow the page. */
.lx-skel-chart {
  height: 220px;
  border-radius: var(--radius-lg);
}

/* Skeletons are decorative; the router announces real state changes. */
[data-skel-filled] { pointer-events: none; }

/* ── 4. Reduced motion ────────────────────────────────────────────────
   Movement is removed everywhere; short opacity transitions are kept so
   changes are still legible as changes. The progress bar keeps its
   scaleX (it is the progress readout, not decoration) but loses the
   sweeping shimmer and the content translate. */
@media (prefers-reduced-motion: reduce) {
  .lx-nav-enter { animation: lxVtFadeIn 120ms linear both; }
  .lx-nav-leave { animation: lxVtFadeOut 90ms linear both; }

  .lx-skel::after { animation: none; }

  ::view-transition-old(lx-content) { animation: lxVtFadeOut 90ms linear both; }
  ::view-transition-new(lx-content) { animation: lxVtFadeIn 120ms linear both; }
  ::view-transition-group(lx-topbar) { animation-duration: 1ms; }
  ::view-transition-old(lx-topbar),
  ::view-transition-new(lx-topbar) { animation-duration: 100ms; }
}
