/* ════════════════════════════════════════════════════════════════════════════
   LOAD-IN, MOTION AND STATE  —  the styling half of js/reveal.js.
   ────────────────────────────────────────────────────────────────────────────
   Every data-bearing panel carries an explicit state and is styled from it:

     data-state="skeleton"   placed the instant the panel mounts, before any
                             fetch resolves. Never an empty box.
     data-state="revealing"  data has landed; the stagger is running.
     data-state="ready"      stagger complete; no animation classes remain.
     data-state="error"      the fetch failed — a message in the SAME box, at
                             the same height, with a way forward.

   Every duration and easing here comes from the motion tokens in app.css. There
   is no literal ms and no literal cubic-bezier() in this file, by design: one
   component on its own clock is what makes an interface feel assembled rather
   than designed, and a stray transition is what makes a palette swap smear.

   The rules the whole system obeys:
     • Only opacity, transform, background-color, border-color, color and
       box-shadow animate. Height changes go through grid-template-rows 0fr→1fr,
       which animates to CONTENT height with no magic number to keep in sync.
     • Motion distance is small and constant: reveals rise 6px, popovers 4px,
       hovers do not lift at all. Nothing bounces, overshoots, spins, or scales
       above 1.0. This is a trading terminal; motion exists to show causality and
       continuity, not personality.
     • Nothing runs longer than --dur-slow except the skeleton shimmer and the
       value flash.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Skeletons ─────────────────────────────────────────────────────────────── */

/* Skeleton geometry mirrors the real markup rather than approximating it: the
   table skeleton is a real table with real cells, so the column widths, row
   height and header are the page's own and the swap to real data cannot shift
   anything. */
/* The skeleton's own wrapper. It has to stack its placeholders exactly the way
   the view stacks the real blocks, because it sits BETWEEN the view and the
   blocks: `.view { gap }` reaches its children, and once a skeleton is painted
   its children are this element's, not the view's. Left as display:block it
   swallowed the gap entirely, so every placeholder butted flush and the page
   jumped ~90px when the data arrived — the exact layout shift the skeleton is
   there to prevent. */
.g10-sk {
  display: flex;
  flex-direction: column;
  gap: var(--gap-stack);
}

.g10-sk-bar,
.g10-sk-line {
  display: block;
  height: 0.72em;
  margin: 0.19em 0;
  border-radius: var(--r-xs);
  background: var(--surface-sunken);
  /* One shared clock. --sk-delay is a NEGATIVE offset set by reveal.js from a
     single epoch, so a skeleton painted mid-cycle starts mid-cycle and every
     skeleton on screen sweeps in phase instead of each running its own loop. */
  background-image: linear-gradient(
    90deg,
    var(--surface-raised) 0%,
    var(--surface-row-active) 50%,
    var(--surface-raised) 100%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: g10-sk-sweep var(--dur-shimmer) var(--ease-shimmer) infinite;
  animation-delay: var(--sk-delay, var(--dur-instant));
}

/* A numeric column's placeholder is short and right-aligned like the number it
   stands in for; a full-width bar there reads as prose and makes the skeleton
   look nothing like the table it is standing in for. */
.g10-sk-num { margin-left: auto; }

.g10-sk-title { height: 0.95em; margin-bottom: 0.6em; }
.g10-sk-lab   { height: 0.62em; }
.g10-sk-val   { height: 1.05em; }

.g10-sk table { width: 100%; }
.g10-sk td, .g10-sk th { vertical-align: middle; }

.g10-sk-tiles { display: flex; gap: 1px; }
.g10-sk-tile  { flex: 1 1 0; padding: 11px 12px; background: var(--surface-kpi); }
.g10-sk-panel { padding: 11px 12px; }

@keyframes g10-sk-sweep {
  from { background-position: 120% 0; }
  to   { background-position: -120% 0; }
}

/* ── The staged reveal ─────────────────────────────────────────────────────── */

/* Per element: opacity 0→1 and a 6px rise, staggered by --stagger-step and
   capped at --stagger-max so a long table does not take a visible age to finish.
   `both` holds the from-state before the delay elapses, which is what stops a
   late child flashing at full opacity before its turn. */
.g10-rv {
  animation: g10-rv-in var(--dur-base) var(--ease-out) both;
  animation-delay: calc(var(--rv-i, 0) * var(--stagger-step));
}

@keyframes g10-rv-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* ── Error state ───────────────────────────────────────────────────────────── */

/* Same box, same height, no layout shift — and a way forward. An error panel
   that is just a blank space is indistinguishable from a panel with no data,
   which is the confusion this replaces. */
.g10-err {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 14px 16px;
  border: 1px solid var(--bear-edge);
  border-radius: var(--r-sm);
  background: var(--bear-tint);
  color: var(--text-2);
  font: 400 12.5px/1.5 var(--font-sans);
}
.g10-err-msg    { color: var(--bear-text); font-weight: 500; }
.g10-err-detail { color: var(--text-muted); font-family: var(--font-sans); font-size: 11px; }
.g10-err-retry {
  margin-left: auto;
  background: transparent; color: var(--accent);
  border: 1px solid var(--accent-edge); border-radius: var(--r-sm);
  padding: 4px 12px; font: 500 11.5px var(--font-sans); cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out),
              color var(--dur-micro) var(--ease-out);
}
.g10-err-retry:hover { background: var(--diff-hi); border-color: var(--accent); }
.g10-err-retry:active { transform: scale(.985); }
.g10-err-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Value change on an already-loaded page ────────────────────────────────── */

/* The tint is present on frame 1 and fades out — no fade IN. The number itself
   never moves, never crossfades and never re-staggers: the change is the signal,
   and re-animating the text would bury it under the animation.
   Direction is information: improving flashes bull, worsening bear, and a
   definitional change flashes accent because it is neither good nor bad. */
.g10-flash-up      { animation: g10-flash-up var(--dur-flash) var(--ease-out) 1; }
.g10-flash-down    { animation: g10-flash-down var(--dur-flash) var(--ease-out) 1; }
.g10-flash-neutral { animation: g10-flash-neutral var(--dur-flash) var(--ease-out) 1; }

@keyframes g10-flash-up      { from { background-color: var(--bull-tint); } to { background-color: transparent; } }
@keyframes g10-flash-down    { from { background-color: var(--bear-tint); } to { background-color: transparent; } }
@keyframes g10-flash-neutral { from { background-color: var(--diff-hi); }   to { background-color: transparent; } }

/* A row arriving in a list rises like anything else; a row leaving fades over
   --dur-fast and then collapses its height over --dur-base, so the list below
   closes the gap instead of jumping. Height is animated through
   grid-template-rows rather than height/max-height because it resolves to the
   row's own content height — a max-height would need a magic number that
   silently clips the day a row grows. */
.g10-row-leave {
  display: grid; grid-template-rows: 1fr; overflow: hidden;
  animation: g10-row-leave var(--dur-base) var(--ease-in) both;
}
@keyframes g10-row-leave {
  0%   { grid-template-rows: 1fr; opacity: 1; }
  44%  { grid-template-rows: 1fr; opacity: 0; }   /* fade first (--dur-fast) */
  100% { grid-template-rows: 0fr; opacity: 0; }   /* then close the gap */
}

/* ── Expand / collapse ─────────────────────────────────────────────────────── */

/* grid-template-rows 0fr→1fr, not max-height: it animates to the panel's own
   content height, so nothing needs a magic number and nothing clips when the
   content grows. The content fades in over --dur-base starting at 40% of the
   height animation, so text does not smear while the box is still opening.
   Collapsing is faster than opening — closing something you have read should
   not make you wait. */
.g10-exp {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-base) var(--ease-inout);
}
.g10-exp > * { overflow: hidden; }
.g10-exp > * > * {
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-in);
}
.g10-exp[data-open="1"] {
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--dur-slow) var(--ease-inout);
}
.g10-exp[data-open="1"] > * > * {
  opacity: 1;
  transition: opacity var(--dur-base) var(--ease-out) calc(var(--dur-slow) * 0.4);
}

/* ── Tab / lens crossfade ──────────────────────────────────────────────────── */

/* The outgoing panel starts leaving, and 60ms later the incoming one starts
   arriving — a short overlap, not a hard cut and not a full sequential swap.
   The container is height-locked to the taller of the two during the swap by the
   call site so nothing below jumps, and released on completion. */
.g10-swap-out { animation: g10-swap-out var(--dur-fast) var(--ease-in) both; }
.g10-swap-in  { animation: g10-swap-in var(--dur-base) var(--ease-out) both;
                animation-delay: var(--delay-swap-overlap); }

@keyframes g10-swap-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes g10-swap-in  { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* The active indicator slides between tabs rather than disappearing and
   reappearing — one movement reads as one event, two fades read as two. */
.g10-indicator {
  transition: transform var(--dur-base) var(--ease-inout),
              width var(--dur-base) var(--ease-inout);
}

/* ── Drawers, modals, tooltips ─────────────────────────────────────────────── */

.g10-scrim {
  background: var(--scrim);
  animation: g10-fade-in var(--dur-base) var(--ease-out) both;
}
.g10-scrim[data-closing="1"] { animation: g10-fade-out var(--dur-base) var(--ease-in) both; }

.g10-modal {
  border-radius: var(--r-lg);
  box-shadow: var(--modal-shadow);
  animation: g10-modal-in var(--dur-slow) var(--ease-out) both;
}
.g10-modal[data-closing="1"] { animation: g10-modal-out var(--dur-base) var(--ease-in) both; }

@keyframes g10-modal-in  { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes g10-modal-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(6px); } }

/* A drawer slides its own full width — the one place a larger travel is right,
   because the distance IS the affordance (it came from that edge). */
.g10-drawer {
  border-radius: var(--r-lg) 0 0 var(--r-lg);
  box-shadow: var(--drawer-shadow);
  animation: g10-drawer-in var(--dur-slow) var(--ease-out) both;
}
.g10-drawer[data-closing="1"] { animation: g10-drawer-out var(--dur-base) var(--ease-in) both; }

@keyframes g10-drawer-in  { from { transform: translateX(100%); } to { transform: none; } }
@keyframes g10-drawer-out { from { transform: none; } to { transform: translateX(100%); } }

@keyframes g10-fade-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes g10-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* Open and close delays stop a strobe when the pointer crosses between two
   adjacent triggers. */
.g10-pop {
  opacity: 0; transform: translateY(4px);
  transition: opacity var(--dur-micro) var(--ease-out) var(--delay-pop-close),
              transform var(--dur-micro) var(--ease-out) var(--delay-pop-close);
}
.g10-pop[data-open="1"] {
  opacity: 1; transform: none;
  transition: opacity var(--dur-micro) var(--ease-out) var(--delay-pop-open),
              transform var(--dur-micro) var(--ease-out) var(--delay-pop-open);
}

/* ── Sorting a table ───────────────────────────────────────────────────────── */

/* Rows are NOT animated to their new positions. A FLIP here is more distraction
   than signal: thirty rows moving at once tells the reader nothing about which
   column they were sorted by. Fade the body out, reorder, fade back in — and
   rotate the header's arrow so the CAUSE is what moves. */
.g10-sorting { animation: g10-swap-out var(--dur-fast) var(--ease-in) both; }
.g10-sorted  { animation: g10-swap-in var(--dur-base) var(--ease-out) both;
               animation-delay: var(--dur-instant); }   /* no overlap when sorting */
.g10-sort-arrow { transition: transform var(--dur-fast) var(--ease-inout); }
.g10-sort-arrow[data-dir="desc"] { transform: rotate(180deg); }

/* ── Hover, focus, press ───────────────────────────────────────────────────── */

/* A table row tints and does nothing else: no lift, no shadow, no border change,
   no scale. Thirty rows that lift on hover is a surface that will not sit still. */
.g10-rowhover { transition: background-color var(--dur-micro) var(--ease-out); }
.g10-rowhover:hover { background-color: var(--surface-row-active); }

/* Every hover state also carries a non-colour cue (cursor here, plus the
   underline/ring on links and controls) so it survives a palette whose hover
   tint is subtle. */
.g10-rowhover:hover { cursor: pointer; }

.g10-press { transition: transform var(--dur-fast) var(--ease-out); }
.g10-press:active { transform: scale(.985); }

.g10-icon { opacity: .7; transition: opacity var(--dur-micro) var(--ease-out); }
.g10-icon:hover { opacity: 1; }

:where(a, button, [tabindex], summary, input, select, textarea):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── Charts ────────────────────────────────────────────────────────────────── */

/* Axes and gridlines draw instantly; the series is revealed by a left-to-right
   mask sweep rather than per-point animation, so the shape is never momentarily
   a DIFFERENT shape — a chart that animates its points shows values that were
   never in the data. */
.g10-series-reveal {
  animation: g10-series-sweep var(--dur-slow) var(--ease-out) both;
}
@keyframes g10-series-sweep {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* ── The top loading bar ───────────────────────────────────────────────────── */
/* Deliberately NOT restyled here. app.css owns #g10-loadbar, and reveal.css loads
   after it, so a second copy of those rules would silently win and then drift.
   Its restyle (2px, --accent, --r-pill, a --dur-base width transition) lives with
   the rest of the app's chrome; loading.js keeps every bit of its logic. */

/* ── Reduced motion ────────────────────────────────────────────────────────── */

/* No shimmer, no stagger, no rise, no slide, no scale. Opacity crossfades are
   KEPT but shortened — for many motion-sensitive readers an instant swap is more
   jarring than a quick fade, so the answer is faster, not absent.
   The value flash is kept in full: it is information, not decoration. What it
   loses is the pulse, not the signal.
   Everything below sets an explicit end state as well as removing the animation,
   because a reveal that never runs must not leave content stuck at opacity 0. */
@media (prefers-reduced-motion: reduce) {
  .g10-sk-bar, .g10-sk-line {
    animation: none;
    background-image: none;
    background: var(--surface-sunken);
  }

  .g10-rv {
    animation: g10-rv-fade var(--dur-fast) linear both;
    animation-delay: var(--dur-instant);      /* no stagger */
  }
  @keyframes g10-rv-fade { from { opacity: 0; } to { opacity: 1; } }

  .g10-swap-in, .g10-sorted {
    animation: g10-fade-in var(--dur-fast) linear both;
    animation-delay: var(--dur-instant);
  }
  .g10-swap-out, .g10-sorting {
    animation: g10-fade-out var(--dur-fast) linear both;
  }

  .g10-modal, .g10-drawer, .g10-scrim {
    animation: g10-fade-in var(--dur-fast) linear both;
  }
  .g10-modal[data-closing="1"], .g10-drawer[data-closing="1"], .g10-scrim[data-closing="1"] {
    animation: g10-fade-out var(--dur-fast) linear both;
  }

  .g10-exp, .g10-exp[data-open="1"] { transition: none; }
  .g10-exp > * > *, .g10-exp[data-open="1"] > * > * { transition: none; }

  .g10-pop, .g10-pop[data-open="1"] { transition: opacity var(--dur-fast) linear; transform: none; }
  .g10-indicator { transition: none; }
  .g10-press:active { transform: none; }
  .g10-series-reveal { animation: none; clip-path: none; }
  .g10-row-leave { animation: g10-fade-out var(--dur-fast) linear both; }

}
