/* ══ juice ═══════════════════════════════════════════════════════════════
 * The half-second after a press. Nothing here carries information: every rule
 * is about how something *felt*, and the screen still says the same words with
 * all of it switched off.
 *
 * Loaded last on purpose. It reaches across every feature - a button in the
 * market, a number on the character card, a celebration over the whole app -
 * and the cascade rule for this sheet is that a file may override anything
 * above it. This is the file that gets to say "and everything moves like this".
 *
 * ── the one performance rule ────────────────────────────────────────────
 *
 * **Only `transform` and `opacity` are ever animated.** Both are handled on the
 * compositor, so an animation here costs no layout and no paint however many
 * are running. Animating `width`, `top`, `margin` or a color would drag the
 * main thread into every frame - which on the phone that is half of Orbis's
 * audience is the difference between juice and jank.
 *
 * The one deliberate exception is `background-color` on a tally flash, which
 * paints a single small element for 700ms and never moves anything.
 */

/* ── touch: what every press feels like ──────────────────────────────────
 * `primitives.css` already gives a button a hard shadow to stand on and the
 * travel into it. What was missing is the *return*: pressing was instant and
 * releasing was instant, so a button read as a state rather than as an object
 * you pushed. Easing only the way back up is what makes it feel sprung - the
 * press should be immediate, because the player's finger did that part. */
button, .chip, .card-menu, .tab, .seg > * {
  transition: transform .16s cubic-bezier(.2, .9, .3, 1.2), box-shadow .16s ease,
              background-color .12s ease, border-color .12s ease;
}
button:active:not(:disabled) { transition-duration: .02s; }

/* A press that is *the* press on a screen - work the shift, craft the thing -
   earns a little more than a state change. Squashed rather than scaled: a
   button that gets smaller looks like it retreated, and one that gets wider as
   it gets shorter looks like it took the weight. */
.btn-go:active:not(:disabled) { transform: translateY(var(--lift)) scale(1.03, .94); }

/* Cards you can open lift toward the pointer. Only where hovering is a thing a
   device does - on a phone this would fire on tap and read as a glitch. */
@media (hover: hover) {
  .card.clickable:hover, .person-card:hover, .shelf-row:hover {
    transform: translateY(-2px);
    transition: transform .18s cubic-bezier(.2, .9, .3, 1.2);
  }
}

/* ── numbers that moved ──────────────────────────────────────────────────
 * A number arriving somewhere is worth a beat of color: green when it grew,
 * red when it shrank. The colors are the sheet's own - `--ok` is the body and
 * things that grow, `--err` is loss - so a purse flashing green means the same
 * thing a green bar does, which is the whole point of color-as-system. */
/* `inline-block` so the scale has something to scale: a bare inline box ignores
   `transform` outright. Every number this lands on is already tabular (see the
   `--font-num` rule in `base.css`), so counting through 9→10 does not shuffle
   the layout beside it. */
.tally { display: inline-block; }
/* Sized to be *seen*, not glimpsed. The first cut ran .7s with the color gone
   by 40% - under a third of a second of actual green, which players read as "it
   blinked" rather than "I earned something". The pop settles fast (movement past
   half a second reads as lag) but the color stays for most of a second,
   because color is the half of the flash that carries the meaning. */
.tally.up { animation: tally-up 1.1s ease-out; }
.tally.down { animation: tally-down 1.1s ease-out; }
/* No `color` in the last frame, deliberately. Naming one - even `inherit` -
   would end the animation on *that* color, and a purse is gold rather than
   whatever its parent is, so the last frame would be a visible wrong-colored
   blink. Left out, the browser interpolates back to the element's own color,
   whatever the sheet said that was. */
@keyframes tally-up {
  0% { color: var(--ok); transform: translateY(-3px) scale(1.18); }
  30% { color: var(--ok); transform: none; }
  65% { color: var(--ok); }
}
@keyframes tally-down {
  0% { color: var(--err); transform: translateY(2px) scale(.94); }
  30% { color: var(--err); transform: none; }
  65% { color: var(--err); }
}
/* The same news with the motion taken out, for `body.calm` below: the color
   holds and fades, nothing moves. */
@keyframes tally-up-still {
  0%, 60% { color: var(--ok); }
}
@keyframes tally-down-still {
  0%, 60% { color: var(--err); }
}

/* A bar that fills rather than jumps. Vitals, xp, a building's condition - all
   of them are a width, and a width is the one thing this sheet animates that
   is not a transform. It is affordable because a bar is a small box with
   nothing inside it: the layout it invalidates is its own. */
.bar > i, .bar-fill, .meter > i {
  transition: width .45s cubic-bezier(.2, .8, .25, 1);
}

/* ── the celebration layer ───────────────────────────────────────────────
 * Above everything and catching nothing. `pointer-events: none` on the layer
 * rather than on each particle, so a celebration can never eat the click that
 * caused it - which is exactly the moment a player is most likely to be
 * pressing again. */
.juice { position: fixed; inset: 0; z-index: 200; pointer-events: none;
  overflow: hidden; contain: layout paint; }

/* One thrown thing. Every value that differs per particle arrives as a custom
   property from Rust, so one keyframe animates all of them and the browser
   never sees a new stylesheet rule. */
.juice-bit {
  position: absolute; font-size: 20px; line-height: 1;
  will-change: transform, opacity;
  animation: juice-fly var(--ms, 1s) cubic-bezier(.15, .6, .4, 1) forwards;
  color: var(--gold);
  text-shadow: 0 0 10px rgba(255, 196, 66, .65);
}
/* An impact spark is the same figure at 70%: it plays on every delivery, so it
   must stay clearly smaller than a celebration - the throw is shortened in Rust
   (`spark`), the size and the fall here. */
.juice-bit.small { font-size: 14px; --drop: 133px; }
/* Thrown, then falling: the arc comes from ending `--drop` below wherever the
   throw was aimed, so every particle is on its own parabola without a physics
   step anywhere. */
@keyframes juice-fly {
  0% { transform: translate(0, 0) rotate(0deg) scale(.4); opacity: 0; }
  12% { opacity: 1; transform: translate(calc(var(--dx) * .18), calc(var(--dy) * .18))
        rotate(calc(var(--spin) * .18)) scale(1.15); }
  60% { opacity: 1; }
  100% { transform: translate(var(--dx), calc(var(--dy) + var(--drop, 190px)))
         rotate(var(--spin)) scale(.75); opacity: 0; }
}

/* ── things going into your pocket ───────────────────────────────────────
 * The one effect in this file with a *destination*. Everything else says
 * "something happened"; this says "and it went here", which is the difference
 * between being told you earned something and watching it arrive.
 *
 * Two phases in one set of keyframes, and the phases are the idle-game grammar
 * for "collected": OUT - every coin explodes from the press to its own scatter
 * point (`--sx/--sy`, a full circle of directions), braking hard as it gets
 * there - a beat of hang - then IN, accelerating from wherever it scattered to
 * into the target (`--dx/--dy`). The first cut flew one thin arc per coin and
 * it read as a dribble drifting off; the burst is what makes the amount
 * *visible*, and the gathered rush into one point is what makes the
 * destination unmistakable.
 *
 * The per-segment `animation-timing-function` declarations are the whole
 * trick: an easing named inside a keyframe governs the segment that starts
 * there, so one animation brakes at the scatter point and then dives at the
 * purse - no JS, no per-frame work, one compositor animation per coin. */
.fly { position: absolute; font-size: 21px; line-height: 1;
  will-change: transform, opacity;
  color: var(--gold); text-shadow: 0 0 12px rgba(255, 196, 66, .7);
  animation: fly-collect var(--fly, 760ms) linear var(--delay, 0ms) both; }
@keyframes fly-collect {
  /* Out: launched hard, braking at the scatter point. */
  0% { transform: translate(0, 0) scale(.4); opacity: 0;
       animation-timing-function: cubic-bezier(.1, .65, .3, 1); }
  /* The hang: a short drift outward at nearly no speed - the pause is what
     separates "thrown, then gathered" from one continuous smear. */
  34% { transform: translate(var(--sx), var(--sy)) scale(1.2); opacity: 1;
        animation-timing-function: cubic-bezier(.4, 0, .7, .2); }
  /* In: pulled to the target, gaining speed the whole way - and at FULL SIZE
     the whole way. The first cut let the scale interpolate down across the
     entire dive, so the coin was already half-shrunk before it got anywhere
     near the purse and the impact read as a fizzle. Held at 1 until 90%, the
     shrink belongs entirely to the moment of contact. */
  48% { transform: translate(calc(var(--sx) * 1.07), calc(var(--sy) * 1.07)) scale(1.05);
        animation-timing-function: cubic-bezier(.5, -.1, .8, .5); }
  90% { transform: translate(calc(var(--dx) * .96), calc(var(--dy) * .96)) scale(1);
        opacity: 1; animation-timing-function: ease-in; }
  /* Contact: the last 10% covers the final few pixels while the coin crushes
     down and winks out ON the target - then the sparks and the target's own
     pulse take over. */
  100% { transform: translate(var(--dx), var(--dy)) scale(.35); opacity: 0; }
}

/* The catch. Applied to the purse or the bag itself, and restarted by `bump`
   for EVERY coin that arrives - eight bumps in rhythm with the landing ticks,
   not one pulse when the volley is over. That per-hit restart is what shapes
   these numbers: the whole thing is shorter than the gap between two arrivals
   is generous, and the peak sits early (~50ms in) so a restart at the 55ms
   stagger still reaches full size each time instead of forever interrupting
   its own wind-up. The last coin's pulse is the only one that plays out. */
.caught { animation: caught .3s cubic-bezier(.2, .9, .3, 1.4); }
/* The purse is a `.tally` as well, and `.tally.up` (two classes) beats plain
   `.caught` (one) for the `animation` property - which once made the purse the
   one target in the game that never visibly reacted to being hit. This single
   rule owns a caught tally outright: equal specificity to `.tally.up`/`.down`
   and written after them in this same sheet, so it wins the tie while `caught`
   is on - whether or not the swing class is. That "whether or not" is
   load-bearing: `swing` retires its class after 700ms, mid-volley, and a
   changed animation list is a RESTARTED animation list - but since this rule
   applied all along, the class coming or going changes nothing and no phantom
   flash replays. The color half is the green `-still` variant unconditionally,
   because a caught tally IS a gain: `bump` only ever runs on deliveries. */
.tally.caught { animation: tally-up-still 1.1s ease-out,
  caught .3s cubic-bezier(.2, .9, .3, 1.4); }
@keyframes caught {
  0% { transform: scale(1); }
  17% { transform: scale(1.3); filter: brightness(1.7); }
  100% { transform: scale(1); }
}
/* Food and drink land on one vital's row (or, without lines to aim by, the
   whole block), and a box that wide taking the purse's scale(1.3) would lurch
   the sidebar. Same hit, panel-sized. */
.bars.caught, .topbar-vitals.caught, .vital-row.caught, .mini-bar.caught {
  animation: caught-soft .3s cubic-bezier(.2, .9, .3, 1.4); }
@keyframes caught-soft {
  0% { transform: scale(1); }
  17% { transform: scale(1.05); filter: brightness(1.4); }
  100% { transform: scale(1); }
}
/* The nav button behind the bag glyph glows too, so a phone - where the icon is
   small and the label is tiny - still shows something arriving. */
.nav-btn .nav-icon.caught { display: inline-block; }
/* The screen itself reacting. A vignette rather than a wash: tinting the whole
   frame would put a color between the player and what they just made, and the
   thing they want to look at is the item, not the effect. Lighting the edges
   says "something happened here" while leaving the middle alone. */
.juice-glow { position: absolute; inset: 0; opacity: 0;
  animation: juice-glow .9s ease-out forwards; }
.juice-glow.notable {
  box-shadow: inset 0 0 90px -10px rgba(88, 214, 141, .5); }
/* Violet, because `--magic` is discovery and the ballot - the things you win.
   A rare craft and a found recipe are exactly that, so they get the color the
   rest of Orbis already uses for it. */
.juice-glow.rare {
  box-shadow: inset 0 0 150px -10px rgba(194, 172, 255, .75),
              inset 0 0 60px -20px rgba(255, 196, 66, .5); }
@keyframes juice-glow {
  0% { opacity: 0; } 18% { opacity: 1; } 100% { opacity: 0; }
}

/* A toast that is a moment rather than a receipt. It arrives with a bounce
   instead of a slide, and a rare one keeps a slow shimmer for as long as it is
   on screen - the same treatment `.stars.legendary` already gives the item
   itself, so the toast and the row it is about agree. */
.toast.notable { animation: toast-pop .4s cubic-bezier(.2, .9, .3, 1.3); }
.toast.rare { animation: toast-pop .4s cubic-bezier(.2, .9, .3, 1.3);
  border-color: var(--magic);
  box-shadow: 0 12px 30px rgba(0, 0, 0, .55), 0 0 22px -4px rgba(194, 172, 255, .8); }
.toast.rare .toast-title { color: var(--magic); }
@keyframes toast-pop {
  0% { transform: scale(.8) translateX(24px); opacity: 0; }
  60% { transform: scale(1.04) translateX(0); opacity: 1; }
  100% { transform: scale(1) translateX(0); }
}

/* ── the sound switch ────────────────────────────────────────────────────
   A speaker at the foot of the sidebar, beside the language picker: both are
   set once and then never thought about again, which is what makes that the
   right shelf for them. */
/* Language, sound and motion share the foot of the sidebar. The picker takes
   the room it needs and each glyph takes what a glyph needs - one `auto` per
   toggle, because a grid with fewer columns than children quietly wraps the
   extra one onto a second row, which is exactly the truncated-looking bar this
   comment is here to prevent coming back. */
.sidebar-prefs { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 6px; }
.sound-toggle { background: transparent; border: none; box-shadow: none;
  padding: 4px 6px; min-height: 0; font-size: 15px; line-height: 1;
  color: var(--muted); cursor: pointer; opacity: .75; }
.sound-toggle:active:not(:disabled) { transform: scale(.9); }
@media (hover: hover) { .sound-toggle:hover { opacity: 1; color: var(--text); } }
.sound-toggle.on { color: var(--accent-2); opacity: 1; }
/* On the way-in screen it sits in a row of fine print rather than in a panel. */
.entry-foot .sound-toggle { font-size: 14px; }

/* ── when the player asked for less ──────────────────────────────────────
 * Keyed off `body.calm`, not the media query, and that is the whole point:
 * `juice::calm()` writes that class from the OS preference *and* the in-game ✨
 * toggle, so a player overriding a reduced-motion default their machine chose
 * for them actually gets their effects back. A media query here would win over
 * the toggle and make the override a lie.
 *
 * Rust already refuses to emit particles or the screen glow when calm, so what
 * is left here is the CSS half: everything that would move on its own stops
 * moving. The color flashes stay - a tally still goes green - because this
 * setting is about *motion*, and somebody who asked for less of it did not ask
 * to be told less about their own purse. */
body.calm .juice-bit, body.calm .juice-glow, body.calm .fly { display: none; }
body.calm .caught { animation: none; }
/* The drain bar stays. It is a clock rather than a flourish - taking it away
   would leave a toast that still disappears without saying it was going to. */
body.calm .toast.notable, body.calm .toast.rare { animation: none; }
body.calm .btn-go:active:not(:disabled) { transform: translateY(var(--lift)); }
@media (hover: hover) {
  body.calm .card.clickable:hover, body.calm .person-card:hover,
  body.calm .shelf-row:hover { transform: none; }
}
/* Not `.01ms`: that killed the color along with the motion, breaking the
   promise two comments up. The still variants move nothing and still say it. */
body.calm .tally.up { animation: tally-up-still 1s ease-out; }
body.calm .tally.down { animation: tally-down-still 1s ease-out; }
body.calm .bar > i, body.calm .bar-fill, body.calm .meter > i { transition: none; }
