/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   ORPHEUS — MOTION
   Every @keyframes and every animation in the app, in one file, for one reason: the switch at the
   bottom. prefers-reduced-motion has to be able to reach ALL of it, and it can only do that if
   none of it is scattered through the component styles. If you add an animation somewhere else,
   that media query does not cover it and you have shipped something that will make somebody ill.

   Three rules held here:
     1. Only transform and opacity are animated. Anything else (height, top, box-shadow on a large
        surface, background-position) is laid out or painted every frame.
     2. Ambient motion — anything that moves with no-one touching it — is also stopped when the tab
        is hidden, by orpheus.js. A background tab must not hold a phone's GPU awake.
     3. Nothing waits on an animation. Every entrance starts from a visible-enough state or runs
        forwards to one, so a dropped frame or a blocked stylesheet never leaves content invisible.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── Entrances ─────────────────────────────────────────────────────────────────────────────── */

@keyframes o-rise {
    from { opacity: 0; transform: translate3d(0, 14px, 0); }
    to   { opacity: 1; transform: none; }
}

@keyframes o-sweep {
    from { transform: scaleX(0); opacity: 0; }
    to   { transform: scaleX(1); opacity: 1; }
}

/* The page itself, on first load and on every enhanced navigation. orpheus.js restarts this by
   removing and re-adding .o-enter, which is why the animation is on a class and not on .o-content
   directly — an animation cannot be replayed without taking it off the element first. */
.o-content.o-enter {
    animation: o-rise var(--o-t-slow) var(--o-ease-out) both;
}

/* Staggered reveals. The index comes from the markup as --o-i, so the delay is the item's own
   position and not a hard-coded nth-child list that breaks when the list length changes.
   Capped at 10 steps: past about 400ms of stagger the last card feels broken rather than staged. */
.o-stagger > * {
    animation: o-rise var(--o-t-base) var(--o-ease-out) both;
    animation-delay: calc(min(var(--o-i, 0), 10) * 45ms);
}

/* ── The navigation rail ───────────────────────────────────────────────────────────────────── */

.o-nav a,
.o-nav button {
    transition: background-color var(--o-t-fast) var(--o-ease),
                color var(--o-t-fast) var(--o-ease);
}

/* The gold bar grows from the middle of the newly-active item. Because every item owns a bar of
   height 0, changing page shrinks one and grows another over the same 240ms, which reads as the
   light moving down the rail rather than as a block being repainted. */
.o-nav a::before {
    transition: height var(--o-t-base) var(--o-ease-out),
                box-shadow var(--o-t-base) var(--o-ease);
}

.o-icon {
    transition: opacity var(--o-t-fast) var(--o-ease),
                color var(--o-t-fast) var(--o-ease),
                transform var(--o-t-base) var(--o-ease-out);
}

.o-nav a:hover .o-icon {
    transform: translateX(1px);
    opacity: 1;
}

.o-theme button {
    transition: background-color var(--o-t-fast) var(--o-ease),
                color var(--o-t-fast) var(--o-ease);
}

/* ── The mark ──────────────────────────────────────────────────────────────────────────────────
   Hovering the brand runs a highlight down the strings, as if a hand had brushed them. */

@keyframes o-strum {
    0%   { opacity: .72; }
    35%  { opacity: 1; }
    100% { opacity: .72; }
}

.o-mark {
    transition: transform var(--o-t-base) var(--o-ease-out),
                filter var(--o-t-base) var(--o-ease);
}

.o-mark-strings path {
    transform-origin: center;
    transition: opacity var(--o-t-fast) var(--o-ease);
}

.o-brand:hover .o-mark {
    filter: drop-shadow(0 0 8px rgba(var(--o-gold-rgb), .45));
}

.o-brand:hover .o-mark-strings path { animation: o-strum .5s var(--o-ease) both; }
.o-brand:hover .o-mark-strings path:nth-child(2) { animation-delay: .07s; }
.o-brand:hover .o-mark-strings path:nth-child(3) { animation-delay: .14s; }

/* ── The ambient backdrop ──────────────────────────────────────────────────────────────────────
   Two star layers at different speeds and directions: parallax is what makes a flat tile read as
   depth. Both translate only, so the compositor handles them with no layout or paint.

   .o-paused is set by orpheus.js when the tab is hidden. A background tab that keeps animating is
   a battery drain nobody can see to complain about. */

@keyframes o-drift-near {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-420px, -210px, 0); }   /* one tile across, half a tile down */
}

@keyframes o-drift-far {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(260px, -130px, 0); }
}

.o-backdrop-stars {
    animation: o-drift-near var(--o-t-ambient) linear infinite;
}

.o-backdrop-stars.is-far {
    animation: o-drift-far calc(var(--o-t-ambient) * 2.4) linear infinite;
}

.o-backdrop.o-paused * {
    animation-play-state: paused;
}

/* ── Lyra ──────────────────────────────────────────────────────────────────────────────────────
   Vega is the brightest star in the northern sky after Arcturus, and the focal point of the
   panel; it breathes. The other stars twinkle on long, prime-ish offsets so the field never
   pulses in unison, which is the thing that makes a star field look like a screensaver. */

@keyframes o-vega {
    0%, 100% { opacity: 1;   transform: scale(1); }
    50%      { opacity: .82; transform: scale(1.14); }
}

@keyframes o-twinkle {
    0%, 100% { opacity: .85; }
    50%      { opacity: .35; }
}

@keyframes o-trace {
    from { stroke-dashoffset: 1; }
    to   { stroke-dashoffset: 0; }
}

.o-lyra-vega {
    transform-origin: center;
    transform-box: fill-box;
    animation: o-vega 5.5s var(--o-ease) infinite;
}

.o-lyra-star { animation: o-twinkle 7s var(--o-ease) infinite; }
.o-lyra-star:nth-of-type(2n) { animation-duration: 9s;  animation-delay: -2.2s; }
.o-lyra-star:nth-of-type(3n) { animation-duration: 11s; animation-delay: -4.7s; }

/* The lines between the stars draw themselves once, when the panel first appears. pathLength="1"
   in the markup makes the dash values fractions, so one rule covers lines of any real length. */
.o-lyra-line {
    stroke-dasharray: 1;
    animation: o-trace 1.8s var(--o-ease-out) both;
    animation-delay: .35s;
}

/* ── Live indicators ───────────────────────────────────────────────────────────────────────── */

@keyframes o-bounce {
    0%, 100% { height: 30%; }
    50%      { height: 100%; }
}

/* height, not transform: these bars are 2.5px wide and 16px tall, and the layout cost of four of
   them is nothing. A scaleY would blur the rounded caps. */
.o-eq span { animation: o-bounce .9s var(--o-ease) infinite; }
.o-eq span:nth-child(2) { animation-duration: .68s; animation-delay: -.2s; }
.o-eq span:nth-child(3) { animation-duration: 1.08s; animation-delay: -.5s; }
.o-eq span:nth-child(4) { animation-duration: .82s; animation-delay: -.34s; }

/* ── The Spotify usage ring ────────────────────────────────────────────────────────────────────
   The drain transition already lived in SpotifyUsageRing.razor.css; this adds the state that
   needed saying loudest — at the limit, nothing is left and no arc remains to colour, so the
   empty track pulses instead. */

@keyframes o-ring-alarm {
    0%, 100% { opacity: .45; }
    50%      { opacity: .9; }
}

.usage-red .usage-track {
    animation: o-ring-alarm 2.4s var(--o-ease) infinite;
}

/* ── Surfaces ──────────────────────────────────────────────────────────────────────────────── */

.card {
    transition: border-color var(--o-t-base) var(--o-ease),
                transform var(--o-t-base) var(--o-ease-out),
                box-shadow var(--o-t-base) var(--o-ease);
}

.o-rule {
    animation: o-sweep var(--o-t-slow) var(--o-ease-out) both;
    transform-origin: left center;
}

/* ═══ THE SWITCH ═══════════════════════════════════════════════════════════════════════════════
   Everything above, off. Not slowed — off.

   `animation: none` rather than a near-zero duration, because an infinite animation with a 1ms
   duration still runs the compositor every frame and still moves; it just moves too fast to see,
   which is worse than moving slowly for anyone who gets motion sickness from it.

   Transitions are collapsed to 1ms rather than removed so that state still ARRIVES — a :hover
   that never resolves its colour is a bug, not an accessibility feature.

   Entrance animations are exempted back to a plain opacity change, because their `both` fill mode
   means an element with `animation: none` and no fill would be left at its animated start state
   (opacity 0) forever. Turning the animation off must never hide content. */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation: none !important;
        transition-duration: 1ms !important;
        transition-delay: 0ms !important;
        scroll-behavior: auto !important;
    }

    .o-content.o-enter,
    .o-stagger > *,
    .o-lyra-line,
    .o-rule {
        opacity: 1 !important;
        transform: none !important;
        stroke-dasharray: none !important;
    }
}
