/* ==========================================================================
   CIBL Obsidian — motion layer. Loaded right after obsidian.css by both
   shells (portal + console), before the app stylesheets.

   Doctrine, in the order it is enforced here:
   · one choreographed entrance per page load, plus ONE living signature
     element; everything else stands still — ambient never competes with
     the data;
   · transform / opacity only (stroke-dash on SVG), so nothing here can
     cause layout work at 60fps;
   · nothing in a form field loops;
   · every animation sits under prefers-reduced-motion: no-preference, and
     the reduce block at the end settles every scene on its FINAL frame
     (nothing is ever left invisible);
   · motion never touches a money or regulatory figure. The count-up in
     portal/base.html and console-polish.js is the only thing that rewrites
     one, and it settles itself. A .ring-progress fills its stroke; its
     label is server text.
   Timings are ported from the reference build: 60–90 ms cascade steps,
   0.5 s panel slides, 0.24 s exits, 0.42 s shakes, spring pops.
   ========================================================================== */

/* ---- keyframes (names are the contract obsidian-motion.js listens for) --- */
@keyframes obs-rise { from { opacity: 0; transform: translate3d(0, 16px, 0); } to { opacity: 1; transform: none; } }
@keyframes obs-card-in { from { opacity: 0; transform: translate3d(0, 24px, 0) scale(0.985); } to { opacity: 1; transform: none; } }
@keyframes obs-pop { from { opacity: 0; transform: translateY(8px) scale(0.92); } to { opacity: 1; transform: none; } }
@keyframes obs-dot-pop { 0% { transform: scale(0); } 70% { transform: scale(1.3); } 100% { transform: scale(1); } }
@keyframes obs-tick { 0% { opacity: 0; transform: scale(0.4); } 60% { opacity: 1; transform: scale(1.25); } 100% { opacity: 1; transform: scale(1); } }
@keyframes obs-shake {
  0%, 100% { transform: none; }
  20% { transform: translateX(-7px); } 40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); } 80% { transform: translateX(3px); }
}
@keyframes obs-err-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes obs-flash { from { opacity: 1; } to { opacity: 0; } }
@keyframes obs-particle {
  0% { transform: translate(0, 0) scale(0.6) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--tx, 0), var(--ty, 0)) scale(1) rotate(200deg); opacity: 0; }
}
@keyframes obs-bell {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(14deg); } 30% { transform: rotate(-12deg); }
  45% { transform: rotate(9deg); } 60% { transform: rotate(-6deg); } 75% { transform: rotate(3deg); }
}
@keyframes obs-glow { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@keyframes obs-dot-glow { 0%, 100% { opacity: 0.7; transform: scale(1); } 50% { opacity: 1; transform: scale(1.18); } }
@keyframes obs-ring-in { from { stroke-dashoffset: 100; } }
@keyframes obs-spark {
  0% { transform: translateX(-100%); opacity: 0; } 10% { opacity: 1; }
  75% { transform: translateX(0); opacity: 1; } 100% { transform: translateX(0); opacity: 0; }
}
@keyframes obs-spark-v {
  0% { transform: translateY(-100%); opacity: 0; } 10% { opacity: 1; }
  75% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(0); opacity: 0; }
}
@keyframes obs-flow { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes obs-flow-v { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@keyframes obs-ring-pulse { 0% { transform: scale(1); opacity: 0.45; } 100% { transform: scale(1.7); opacity: 0; } }
@keyframes obs-panel-in-r { from { opacity: 0; transform: translateX(46px); } to { opacity: 1; transform: none; } }
@keyframes obs-panel-in-l { from { opacity: 0; transform: translateX(-46px); } to { opacity: 1; transform: none; } }
@keyframes obs-panel-out-l { to { opacity: 0; transform: translateX(-36px); } }
@keyframes obs-panel-out-r { to { opacity: 0; transform: translateX(36px); } }
@keyframes obs-sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@keyframes obs-sheen { from { transform: translateX(-160%) skewX(-18deg); } to { transform: translateX(300%) skewX(-18deg); } }
@keyframes obs-caret { 50% { opacity: 0; } }
@keyframes obs-scan { from { transform: translateY(-100%); } to { transform: translateY(300%); } }
@keyframes obs-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.015); } }
@keyframes obs-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
/* scene actor loops: one cycle = --fx-dur; every loop ends on its final frame */
@keyframes obs-fx-draw {
  0%, 10% { stroke-dashoffset: 1; opacity: 0; } 14% { opacity: 1; }
  60%, 90% { stroke-dashoffset: 0; opacity: 1; } 100% { stroke-dashoffset: 0; opacity: 0; }
}
@keyframes obs-fx-pop {
  0%, 40% { opacity: 0; transform: scale(0); } 50% { opacity: 1; transform: scale(1.3); }
  58%, 88% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1); }
}
@keyframes obs-fx-drop {
  0% { opacity: 0; transform: translateY(-18px); } 12% { opacity: 1; }
  45% { opacity: 1; transform: translateY(0); } 60% { opacity: 0; transform: translateY(6px) scale(0.8); }
  100% { opacity: 0; transform: translateY(6px) scale(0.8); }
}
@keyframes obs-fx-spin { 0%, 45% { transform: rotate(0deg); } 72%, 100% { transform: rotate(-120deg); } }
@keyframes obs-fx-glint { 0%, 60% { opacity: 0; } 72% { opacity: 0.45; } 86%, 100% { opacity: 0; } }
@keyframes obs-fx-rise {
  0%, 6% { transform: translateY(26px); } 32% { transform: translateY(-3px); }
  42%, 88% { transform: translateY(0); } 100% { transform: translateY(26px); }
}

@media (prefers-reduced-motion: no-preference) {

  /* ---- [data-cascade] page-load choreography ------------------------------
     <div data-cascade>          children rise 75 ms apart (card glides in)
       <section class="glass-card">
         <ul data-cascade-rows>  rows follow, 45 ms apart, from 260 ms
         <div data-cascade-controls>  controls pop last, from 520 ms
     --i comes from :nth-child up to 12 (obsidian-motion.js fills beyond);
     --d / --step override the start and spacing per container. A cascade
     placed directly under .main-inner carries the page entrance itself. */
  [data-cascade] > *, [data-cascade-rows] > *, [data-cascade-controls] > * {
    /* backwards, not both: once landed the element's OWN opacity rules again,
       so a :disabled button or a dimmed scene inside a cascade stays dimmed */
    animation: obs-rise 560ms var(--ease-out) backwards;
  }
  [data-cascade] > * { animation-delay: calc(var(--d, 60ms) + var(--i, 0) * var(--step, 75ms)); }
  [data-cascade] > .card, [data-cascade] > .glass-card, [data-cascade] > .terminal-card, [data-cascade] > .stat-strip {
    animation-name: obs-card-in; animation-duration: 680ms;
  }
  [data-cascade-rows] > * {
    animation-duration: 460ms;
    animation-delay: calc(var(--d, 260ms) + var(--i, 0) * var(--step, 45ms));
  }
  [data-cascade-controls] > * {
    animation-name: obs-pop; animation-duration: 520ms; animation-timing-function: var(--ease-spring);
    animation-delay: calc(var(--d, 520ms) + var(--i, 0) * var(--step, 70ms));
  }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(2) { --i: 1; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(3) { --i: 2; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(4) { --i: 3; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(5) { --i: 4; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(6) { --i: 5; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(7) { --i: 6; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(8) { --i: 7; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(9) { --i: 8; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(10) { --i: 9; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(11) { --i: 10; }
  :is([data-cascade], [data-cascade-rows], [data-cascade-controls], .reveal-stagger) > :nth-child(n+12) { --i: 11; }
  .main-inner > [data-cascade] { animation: none; }

  /* deal rail: journey dots pop in order along the rail, once, up to the current state */
  .deal-rail .step.is-done .step-dot, .deal-rail .step.is-active .step-dot {
    animation: obs-dot-pop 480ms var(--ease-spring) both;
    animation-delay: calc(140ms + var(--i, 0) * 90ms);
  }
  .deal-rail .step:nth-child(2) { --i: 1; } .deal-rail .step:nth-child(3) { --i: 2; }
  .deal-rail .step:nth-child(4) { --i: 3; } .deal-rail .step:nth-child(5) { --i: 4; }
  .deal-rail .step:nth-child(6) { --i: 5; } .deal-rail .step:nth-child(7) { --i: 6; }

  /* ---- .reveal / .reveal-stagger — entrance on scroll ----------------------
     obsidian-motion.js adds .in when observed; the hidden state only exists
     under html.obs-js, so with JS off (or reduced motion) everything is
     visible at rest. Do not put .reveal on a [data-cascade] child. */
  html.obs-js .reveal {
    opacity: 0; transform: translate3d(0, 18px, 0);
    transition: opacity 640ms var(--ease-out), transform 640ms var(--ease-out);
    transition-delay: calc(var(--i, 0) * 80ms);
  }
  html.obs-js .reveal.in { opacity: 1; transform: none; }

  /* ---- .glass-card: hover lift, edge warms --------------------------------- */
  .glass-card { transition: transform 320ms var(--ease-out), box-shadow 320ms var(--ease-out); }
  .glass-card::before { transition: opacity 320ms ease; }
  .glass-card:hover {
    transform: translateY(-3px);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 24px 48px rgba(var(--shadow-rgb), calc(0.4 * var(--shadow-k))), 0 0 0 1px rgba(255, 163, 51, 0.06);
  }
  .glass-card:hover::before { opacity: 1; }
  [data-tilt] { transition: transform 260ms var(--ease-out), box-shadow 320ms var(--ease-out); will-change: transform; }

  /* ---- .eyebrow-pill: the dot glows slowly (an ambient loop, not a beat) -- */
  /* only a pill that reports a LIVE state pulses; a plain eyebrow keeps its
     still glow, so a page of six book pills does not run six loops */
  .eyebrow-pill.is-ok::before, .eyebrow-pill.is-bad::before { animation: obs-dot-glow 3.2s ease-in-out infinite; }
  .eyebrow-pill.is-off::before { animation: none; }

  /* ---- .progress-rail: a dot pops as its section arrives ------------------ */
  .progress-rail a::after { transition: transform 320ms var(--ease-spring); }
  .progress-rail a::before { transition: opacity 180ms ease; }

  /* ---- .terminal-card: lines typed in, caret blinks, slow scan band ------- */
  .terminal-card[data-typed] .t-line {
    opacity: 0; transform: translateY(4px);
    transition: opacity 260ms ease, transform 300ms var(--ease-out);
  }
  .terminal-card[data-typed] .t-line.show { opacity: 1; transform: none; }
  html:not(.obs-js) .terminal-card[data-typed] .t-line { opacity: 1; transform: none; }
  .t-caret.on { opacity: 1; animation: obs-caret 850ms steps(1) infinite; }
  .terminal-card::before { opacity: 1; animation: obs-scan 9s linear infinite; }

  /* ---- .glow-ring: the one tile that needs a human breathes ---------------- */
  .glow-ring::after { animation: obs-glow 3.4s ease-in-out infinite; }

  /* ---- .sparkline: the line draws itself, the last point pops ------------- */
  .sparkline .spark-line { animation: obs-draw 1100ms var(--ease-out) 200ms both; }
  .sparkline .spark-dot { animation: obs-tick 420ms var(--ease-spring) 1150ms both; transform-box: fill-box; transform-origin: center; }
  .reveal:not(.in) .sparkline .spark-line, .reveal:not(.in) .sparkline .spark-dot { animation-play-state: paused; }

  /* ---- .step-rail: travelling fill, spark, pulsing active dot -------------- */
  .step-fill { transition: transform 700ms var(--ease-out); }
  .step-fill::after { opacity: 1; animation: obs-flow 5.5s linear infinite; }
  .step-spark { animation: obs-spark 3.2s ease-in-out infinite; }
  .step-rail-v .step-fill::after { animation-name: obs-flow-v; }
  .step-rail-v .step-spark { animation-name: obs-spark-v; }
  .step-dot { transition: background-color 400ms, border-color 400ms, color 400ms, box-shadow 400ms; }
  .step.is-active .step-dot::after {
    content: ""; position: absolute; inset: -4px; border-radius: 50%;
    border: 1.5px solid var(--ember-hi); animation: obs-ring-pulse 2.4s ease-out infinite;
  }
  .step.is-done .step-tick { animation: obs-tick 420ms var(--ease-spring) both; transform-box: fill-box; transform-origin: center; }

  /* ---- .ring-progress: fills once from empty; the label never moves ------- */
  .ring-progress .ring-fg { animation: obs-ring-in 1300ms var(--ease-out) 150ms both; transition: stroke-dashoffset 900ms var(--ease-out); }

  /* ---- .proc-steps: done ticks pop in order, the doing dot breathes -------- */
  .proc-steps > li { transition: opacity 500ms var(--ease-out), color 500ms; }
  .proc-steps .ps-dot { transition: background-color 400ms, border-color 400ms, box-shadow 400ms; }
  .proc-steps > li.done .ps-dot svg { animation: obs-tick 380ms var(--ease-spring) both; animation-delay: calc(200ms + var(--i, 0) * 90ms); }
  .proc-steps > li:nth-child(2) { --i: 1; } .proc-steps > li:nth-child(3) { --i: 2; }
  .proc-steps > li:nth-child(4) { --i: 3; } .proc-steps > li:nth-child(5) { --i: 4; }
  .proc-steps > li:nth-child(n+6) { --i: 5; }
  .proc-steps > li.doing .ps-dot::after { animation: obs-dot-glow 1.6s ease-in-out infinite; }

  /* ---- .panel-slide[data-dir] --------------------------------------------- */
  .panel-slide[data-dir="next"] { animation: obs-panel-in-r 500ms var(--ease-out) both; }
  .panel-slide[data-dir="back"] { animation: obs-panel-in-l 500ms var(--ease-out) both; }
  .panel-slide.is-leaving[data-dir="next"] { animation: obs-panel-out-l 240ms ease forwards; }
  .panel-slide.is-leaving[data-dir="back"] { animation: obs-panel-out-r 240ms ease forwards; }

  /* ---- .shake / errIn / .dot-pop / .flash / .burst ------------------------- */
  .shake { animation: obs-shake 420ms ease; }
  .form-error, .err-in { animation: obs-err-in 400ms var(--ease-out) both; }
  .shake-on-error:has(.form-error) { animation: obs-shake 450ms ease 420ms; }
  .dot-pop, input:checked + .dot-pop { animation: obs-dot-pop 480ms var(--ease-spring) both; }
  .flash::after, .flash-ember::after { animation: obs-flash 1000ms ease both; }
  .burst i { animation: obs-particle 760ms var(--ease-out) forwards; }

  /* ---- .nav-indicator slides; .bell-unread wiggles once -------------------- */
  .nav-indicator { transition: top 350ms var(--ease-out), height 350ms var(--ease-out), opacity 250ms ease; }
  .nav a { transition: color 150ms, background-color 150ms; }
  .bell-btn .bell-glyph { transform-origin: 50% 15%; }
  .bell-unread .bell-glyph { animation: obs-bell 900ms ease; }

  /* ---- .press / .field-lift ------------------------------------------------ */
  .press { transition: transform 90ms ease; }
  .press:active { transform: scale(0.97); }
  .field-lift { transition: transform 300ms var(--ease-out); }
  .field-lift:focus-within { transform: translateY(-2px); }
  .field-lift label { transition: color 250ms; }
  .field-lift:focus-within label { color: var(--ember-hi); }

  /* ---- buttons: sheen on hover; submit pill sweep -------------------------- */
  .btn-primary::before, .btn-ember::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 40%; pointer-events: none;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent);
    transform: translateX(-160%) skewX(-18deg);
  }
  .btn-primary:hover::before, .btn-ember:hover::before { animation: obs-sheen 900ms var(--ease-soft); }
  .is-submitting::before {
    content: ""; position: absolute; inset: 0; width: auto; pointer-events: none;
    background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.35) 50%, transparent 70%);
    animation: obs-sweep 1.1s ease-in-out infinite;
  }

  /* ---- dropzone breathes while a file hovers over it ----------------------- */
  .dropzone.is-over, .dropzone.dragover { animation: obs-breathe 1.4s ease-in-out infinite; }

  /* ---- .scene: the current one plays; done settle; the rest dim ------------
     Each actor loop takes --fx-dur (default 3.6 s) and --fx-delay (phase
     shift, may be negative). obsidian-motion.js flips .fx-hold on a
     [data-fx-resync] container for one frame to restart the loops when
     the tab is shown again — only the playing scene's actors and the
     dim scenes' breathing: a done scene's tick and a done step's pop are
     one-shots filled `both`, and holding those would make every settled
     completion blink out and pop again on each return. */
  .scene { transition: transform 450ms var(--ease-out), opacity 350ms, border-color 350ms, box-shadow 350ms; }
  .scene.is-current .fx-draw { animation: obs-fx-draw var(--fx-dur, 3.6s) ease-in-out var(--fx-delay, 0s) infinite; }
  .scene.is-current .fx-pop { animation: obs-fx-pop var(--fx-dur, 3.6s) var(--ease-spring) var(--fx-delay, 0s) infinite; }
  .scene.is-current .fx-drop { animation: obs-fx-drop var(--fx-dur, 3.6s) ease-in-out var(--fx-delay, 0s) infinite; }
  .scene.is-current .fx-spin { animation: obs-fx-spin var(--fx-dur, 3.6s) ease-in-out var(--fx-delay, 0s) infinite; }
  .scene.is-current .fx-glint { animation: obs-fx-glint var(--fx-dur, 3.6s) ease-in-out var(--fx-delay, 0s) infinite; }
  .scene.is-current .fx-rise { animation: obs-fx-rise var(--fx-dur, 3.6s) var(--ease-out) var(--fx-delay, 0s) infinite; }
  .scene-row .scene:not(.is-done):not(.is-current) { animation: obs-breathe 7s ease-in-out infinite; }
  .fx-hold .scene.is-current, .fx-hold .scene.is-current *,
  .fx-hold .scene:not(.is-done):not(.is-current) { animation: none !important; }
}

/* ==========================================================================
   Reduced motion: every scene on its final frame, nothing invisible.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  [data-cascade] > *, [data-cascade-rows] > *, [data-cascade-controls] > *,
  .reveal, .ticker-track, .eyebrow-pill::before, .glass-card, .glass-card::before, [data-tilt],
  .glow-ring::after, .step-fill, .step-fill::after, .step-spark, .step-dot, .step-tick,
  .ring-progress .ring-fg, .proc-steps > li, .proc-steps .ps-dot, .proc-steps svg,
  .panel-slide, .shake, .shake-on-error, .form-error, .err-in, .dot-pop, .flash::after, .flash-ember::after,
  .burst i, .nav-indicator, .bell-unread .bell-glyph, .press, .field-lift, .field-lift label,
  .btn-primary::before, .btn-ember::before, .is-submitting::before, .dropzone.is-over, .dropzone.dragover,
  .scene, .scene *, .terminal-card::before, .terminal-card[data-typed] .t-line, .t-caret,
  .sparkline .spark-line, .sparkline .spark-dot, .progress-rail a::after, .progress-rail a::before {
    animation: none !important; transition: none !important;
  }
  .reveal, .terminal-card[data-typed] .t-line { opacity: 1 !important; transform: none !important; }
  .t-caret { opacity: 0; }
  /* transient actors stay hidden; the settled frame (the defaults) shows */
  .step-spark, .scene .fx-drop, .scene .fx-glint { opacity: 0 !important; }
  /* a flash still marks the completion — as a still mint edge that
     obsidian-motion.js removes after a moment */
  .flash::after, .flash-ember::after { opacity: 1; }
  .press:active, .field-lift:focus-within, .glass-card:hover { transform: none; }
  .burst { display: none; }
}
