/* ==========================================================================
   motion.css — FLIP filter motion for the publications list
   --------------------------------------------------------------------------
   The transform/opacity transitions behind the publication filter's FLIP
   (First, Last, Invert, Play) re-layout, driven by js/publications.js. That
   module owns all state; this file only describes how each state moves:

     `.pub.is-flipping`  a SURVIVOR gliding from its old position to its new
                         one. The JS stages an inline
                         `transform: translate(dx, dy)` with the transition
                         off (INVERT), then clears it (PLAY) — the transition
                         below is what animates the card home.
     `.pub.is-entering`  a card the new filter ADDS. Staged inline at
                         opacity 0 + scale(--flip-scale), then released.
     `.pub.is-leaving`   a card the new filter REMOVES. The JS pins it out of
                         flow at its measured position (so the survivors
                         reflow immediately) and fades/scales it away, then
                         applies `.hidden`.
     `.pub.flip-owned`   sticky: set on the first animated pass to hand
                         transform/opacity over from the reveal cascade in
                         css/animations.css (see §2 — read that one).

   Only opacity and transform animate — no layout property is touched, so the
   whole pass stays on the compositor (Req 5.3, 8.1).

   Tokens consumed from css/variables.css: --ease-out.
   Tokens defined here: --flip-glide, --flip-fade, --flip-scale. These MIRROR
   the FLIP_MS / FADE_MS / EXIT_SCALE constants in js/publications.js, which
   times its cleanup off them — change one side and change the other, keeping
   both within the ≤400ms filter budget (Req 5.3).

   Requirements: 5.3 (filter animation ≤400ms), 3.4 / 8.5 (reduced motion),
   8.1 (transform/opacity only).
   ========================================================================== */

:root {
  /* Survivor glide — the longest step, still inside the 400ms budget. */
  --flip-glide: 360ms;
  /* Enter/exit fade + scale. Shorter than the glide so removals read as
     "gone" while the survivors are still settling. */
  --flip-fade: 220ms;
  /* The pose a card leaves to and enters from: a barely-there shrink. */
  --flip-scale: 0.96;
}

/* ==========================================================================
   1. Containing block for pinned (leaving) cards
   --------------------------------------------------------------------------
   js/publications.js pins a leaving card with `position: absolute` and offsets
   computed from `#pub-list`'s own rect plus its scroll offsets, so the list
   must be the card's containing block. `position: relative` provides that
   without affecting the flex layout of the in-flow cards.

   `#pub-list` is a scroll container (`max-height` + `overflow-y: auto` in
   css/components.css), and an absolutely positioned child resolves against the
   PADDING box. The list has no border, so its padding box and border box share
   a top-left corner and the JS's rect-based math lands exactly. The existing
   `overflow-x: hidden` also means a mid-glide transform can never produce a
   horizontal scrollbar. Cards do glide/fade inside the scrollport and are
   clipped at its edges — that is intended: nothing here scrolls the container.
   ========================================================================== */

#pub-list {
  position: relative;
}

/* ==========================================================================
   2. Ownership handoff from the reveal cascade (the subtle one)
   --------------------------------------------------------------------------
   The publication cards are also `.stagger` children of a `.reveal` section, so
   css/animations.css animates them with
   `animation: reveal-rise 0.55s var(--ease-out) both` (Req 3.2). `both`
   includes a FORWARDS fill, so that animation keeps asserting its final
   `opacity: 1; transform: none` for as long as it applies — and the animation
   origin outranks normal author declarations, INCLUDING inline styles. Left
   alone it would silently swallow every FLIP translate the JS writes.

   So js/publications.js marks the cards `.flip-owned` on the first real filter
   pass, and this rule hands transform/opacity over:
     * `animation: none` retires the filled reveal animation, which has already
       played by the time anyone clicks a filter (a reveal still in flight is
       cut short — the right trade once filtering starts).
     * `opacity`/`transform` restate the resting pose the fill was providing.
       Without them the card would fall back to the pre-reveal hidden state
       (`opacity: 0; transform: translateY(18px)`) from animations.css and
       vanish. Inline styles outrank these, so the FLIP still drives the card.

   The `#pub-list` prefix is what makes this work without `!important`: the
   animations.css rules score 0,4,1 / 0,5,1, and one id (1,2,0) beats any number
   of classes, whatever order the stylesheets load in. Sections 3–7 keep the
   same prefix so the whole file has one predictable precedence story.

   Cards are only marked on an animated pass, so an untouched page — and a
   reduced-motion visitor — keeps the stagger cascade exactly as animations.css
   defines it.

   The cost of that id is that `transform: none` above would also outrank the
   hover lift in css/components.css (`.pub:hover`, 0,2,0), so the second rule
   restores it at 1,3,0, under the same hover/pointer guard. The `1.02` is
   duplicated from components.css, which owns the hover treatment (Req 5.1) —
   keep the two in sync. Section 6 re-suppresses it mid-FLIP: those selectors
   also score 1,3,0 and come later in this file, so they win the tie.
   ========================================================================== */

#pub-list .pub.flip-owned {
  animation: none;
  opacity: 1;
  transform: none;
}

@media (hover: hover) and (pointer: fine) {
  #pub-list .pub.flip-owned:hover {
    transform: scale(1.02);
  }
}

/* ==========================================================================
   3. Survivor glide — INVERT -> PLAY (Req 5.3)
   --------------------------------------------------------------------------
   Transform only. The JS suppresses the transition while it applies the
   inverting translate, so the glide starts precisely when the class is added
   and the inline transform is cleared.

   This also overrides the shared `.pub` transition in css/components.css
   (300ms for opacity/transform/box-shadow/border-color).
   ========================================================================== */

#pub-list .pub.is-flipping {
  transition: transform var(--flip-glide) var(--ease-out);
}

/* ==========================================================================
   4. Entering cards
   --------------------------------------------------------------------------
   Fade and scale up from the exit pose into place. Opacity + transform.
   ========================================================================== */

#pub-list .pub.is-entering {
  transition:
    opacity var(--flip-fade) var(--ease-out),
    transform var(--flip-fade) var(--ease-out);
}

/* ==========================================================================
   5. Leaving cards
   --------------------------------------------------------------------------
   Pinned out of flow by the JS (which writes position/top/left/width/height
   inline) and faded to the exit pose. `margin: 0` keeps the pinned geometry
   honest if a `.pub` margin is ever introduced, and `pointer-events: none`
   stops a card on its way out from swallowing clicks or showing a hover state
   over the survivors sliding underneath it.
   ========================================================================== */

#pub-list .pub.is-leaving {
  margin: 0;
  pointer-events: none;
  transition:
    opacity var(--flip-fade) var(--ease-out),
    transform var(--flip-fade) var(--ease-out);
}

/* ==========================================================================
   6. Hover-lift conflict (the important one)
   --------------------------------------------------------------------------
   css/components.css lifts a hovered card with `transform: scale(1.02)` (Req
   5.1). `transform` is a single property, so the hover scale and the FLIP
   translate cannot coexist — whichever wins replaces the other.

   During INVERT that is harmless: the JS writes the translate INLINE, which
   outranks any stylesheet rule. The problem is PLAY, when the inline transform
   is cleared: for a card sitting under the pointer, the transition's target
   would become `scale(1.02)` instead of the identity transform, so the card
   would glide to a scaled-up pose — and would jump again the moment the
   pointer left mid-flight.

   Fix: suppress the hover lift for exactly as long as a FLIP state class is on
   the card. Each selector below scores 1,3,0 against the hover rule's 0,2,0, so
   it wins wherever it applies (specificity is unaffected by the
   `(hover: hover) and (pointer: fine)` media query that guards the lift).
   `.is-leaving` is listed for completeness — that card's inline
   `scale(--flip-scale)` already outranks both rules.

   Once js/publications.js removes the state class the card is a plain `.pub`
   again, so the hover lift resumes with its normal 300ms transition. The
   alternative — composing hover and FLIP into one transform — would mean the
   JS reading and rewriting the hover scale on every pass, i.e. behaviour code
   owning a visual detail that belongs in CSS.
   ========================================================================== */

#pub-list .pub.is-flipping:hover,
#pub-list .pub.is-entering:hover,
#pub-list .pub.is-leaving:hover {
  transform: none;
}

/* ==========================================================================
   7. Reduced motion (Req 3.4, 8.5)
   --------------------------------------------------------------------------
   js/publications.js already applies the filter instantly under reduced
   motion: it adds no state class, writes no transform and starts no timer, so
   the rules above are never reached. This block is the belt to that braces —
   if a state class ever survives a mode change mid-pass, nothing animates and
   the card is left in its committed position.

   No `::view-transition` rules here: the filter ships FLIP on every browser
   (see the note in js/publications.js), so there is no view-transition
   pseudo-element tree to style.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  #pub-list .pub.flip-owned,
  #pub-list .pub.is-flipping,
  #pub-list .pub.is-entering,
  #pub-list .pub.is-leaving {
    transition: none !important;
    animation: none !important;
  }
}
