/* ---------------------------------------------------------------------------
   effects.css — ambience, transitions and celebration.

   Rules that hold for everything in this file:
     1. transform and opacity only. Nothing here may animate a layout property.
     2. every effect has a reduced-motion answer — usually "show the end state
        immediately", never "flash once and stop".
     3. nothing here may capture a pointer event.

   The reduced-motion guard in base.css already crushes durations to 1ms, which
   is the right answer for a transition but the wrong one for a looping ambient
   gradient: it would spin. So the loops are switched off by name below.
--------------------------------------------------------------------------- */

/* =========================================================================
   KEYFRAMES  (shared vocabulary — components.css references these too)
   ========================================================================= */
@keyframes fx-fade-in    { from { opacity: 0; } to { opacity: 1; } }
@keyframes fx-pop-in     { from { opacity: 0; transform: scale(0.94) translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes fx-sheet-up   { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes fx-toast-in   { from { opacity: 0; transform: translateY(14px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes fx-toast-out  { to { opacity: 0; transform: translateY(8px) scale(0.98); } }
@keyframes fx-slide-in-r { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
@keyframes fx-slide-in-l { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: none; } }
@keyframes fx-rise       { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes fx-drift-a    { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); } 50% { transform: translate3d(6%, -5%, 0) scale(1.12); } }
@keyframes fx-drift-b    { 0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); } 50% { transform: translate3d(-7%, 6%, 0) scale(0.95); } }
@keyframes fx-ripple     { to { transform: scale(2.6); opacity: 0; } }
@keyframes fx-confetti   { to { transform: translate3d(var(--cx, 0), var(--cy, 120px), 0) rotate(var(--cr, 360deg)); opacity: 0; } }
@keyframes fx-burst      { 0% { transform: scale(0.3); opacity: 0.55; } 100% { transform: scale(2.4); opacity: 0; } }
@keyframes fx-tick       { 0% { transform: scale(1); } 40% { transform: scale(1.14); } 100% { transform: scale(1); } }

/* =========================================================================
   AMBIENT BACKGROUND
   Two soft blobs behind the whole shell. They are the only thing in the app
   that runs a continuous animation, so they are also the first thing the
   reduced-motion guard turns off.
   ========================================================================= */
.fx-ambient {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  contain: strict;
}
/* The drift scales the blob, so every frame re-rasterises the blurred surface
   rather than compositing it — the cost is the blurred area, not the motion.
   40vmax at 48px of blur reads the same behind a page of cards and asks the
   phone for roughly half the pixels 60vmax at 70px did. */
.fx-ambient__blob {
  position: absolute;
  width: 40vmax;
  height: 40vmax;
  border-radius: 50%;
  filter: blur(48px);
  opacity: 0.5;
}
/* will-change rides with the animation, not with the element: the hint is only
   worth a layer while something is actually moving. */
.fx-ambient__blob--a {
  top: -18vmax; inset-inline-start: -12vmax;
  background: radial-gradient(circle at 50% 50%, var(--c-accent) 0%, transparent 66%);
  animation: fx-drift-a 26s var(--e-in-out) infinite;
  will-change: transform;
}
.fx-ambient__blob--b {
  bottom: -22vmax; inset-inline-end: -14vmax;
  background: radial-gradient(circle at 50% 50%, var(--c-long) 0%, transparent 66%);
  opacity: 0.28;
  animation: fx-drift-b 34s var(--e-in-out) infinite;
  will-change: transform;
}
:root[data-mode="dark"] .fx-ambient__blob--a { opacity: 0.28; }
:root[data-mode="dark"] .fx-ambient__blob--b { opacity: 0.18; }
/* The brutal face is flat by definition — no glow, a hatched ground instead. */
:root[data-theme="brutal"] .fx-ambient__blob { display: none; }
:root[data-theme="brutal"] .fx-ambient {
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 14px,
    color-mix(in srgb, var(--c-ink) 4%, transparent) 14px 15px
  );
}
/* When a session is running the ambience tints to the kind of the session. */
#app[data-running="true"] .fx-ambient__blob--a { opacity: 0.62; }
#app[data-kind="short"] .fx-ambient__blob--a,
#app[data-kind="long"] .fx-ambient__blob--a {
  background: radial-gradient(circle at 50% 50%, var(--c-break) 0%, transparent 66%);
}

/* =========================================================================
   PAGE TRANSITIONS
   The router adds one of these classes to the incoming screen and removes it
   on animationend. Direction comes from the tab order, so moving right along
   the tab bar slides in from the right.
   ========================================================================= */
.fx-enter-forward { animation: fx-slide-in-r var(--d-3) var(--e-out); }
.fx-enter-back    { animation: fx-slide-in-l var(--d-3) var(--e-out); }
.fx-enter-fade    { animation: fx-fade-in var(--d-2) var(--e-out); }
.fx-enter-up      { animation: fx-rise var(--d-3) var(--e-out); }

/* Staggered list reveal. The router sets --i per child; six steps is enough —
   past that the delay is longer than the visitor's patience. */
.fx-stagger > * {
  animation: fx-rise var(--d-3) var(--e-out) backwards;
  animation-delay: calc(min(var(--i, 0), 6) * 40ms);
}

/* =========================================================================
   RIPPLE — attached by dom.js to any [data-ripple] element on pointerdown.
   The span is positioned by inline custom properties and removes itself.
   ========================================================================= */
.fx-ripple-host { position: relative; overflow: hidden; }
.fx-ripple {
  position: absolute;
  top: var(--ry, 50%);
  left: var(--rx, 50%);
  width: var(--rs, 40px);
  height: var(--rs, 40px);
  margin-top: calc(var(--rs, 40px) / -2);
  margin-left: calc(var(--rs, 40px) / -2);
  border-radius: 50%;
  background: currentColor;
  opacity: 0.16;
  pointer-events: none;
  transform: scale(0.3);
  animation: fx-ripple var(--d-4) var(--e-out) forwards;
}

/* =========================================================================
   TIMER FEEDBACK
   ========================================================================= */
.fx-tick { animation: fx-tick var(--d-3) var(--e-spring); }

/* =========================================================================
   CELEBRATION — confetti and the burst ring.
   celebrate.js appends N .fx-confetti children with --cx/--cy/--cr/--cd set;
   the whole layer removes itself when the longest animation ends.
   ========================================================================= */
.fx-celebrate {
  position: fixed;
  inset: 0;
  z-index: var(--z-fx);
  pointer-events: none;
  overflow: hidden;
  contain: strict;
}
.fx-confetti {
  position: absolute;
  top: var(--ct, 40%);
  left: var(--cl, 50%);
  width: var(--cw, 8px);
  height: var(--ch, 14px);
  background: var(--cc, var(--c-accent));
  border-radius: var(--r-xs);
  opacity: 1;
  will-change: transform, opacity;
  animation: fx-confetti var(--cd, 1400ms) var(--e-out) forwards;
  animation-delay: var(--cdelay, 0ms);
}
.fx-confetti--round { border-radius: 50%; width: var(--cw, 10px); height: var(--cw, 10px); }

.fx-burst {
  position: absolute;
  top: 50%; left: 50%;
  width: 160px; height: 160px;
  margin: -80px 0 0 -80px;
  border-radius: 50%;
  border: 3px solid var(--c-accent);
  opacity: 0;
  animation: fx-burst var(--d-5) var(--e-out) forwards;
}
.fx-burst--delayed { animation-delay: 140ms; border-color: var(--c-accent-lift); }

/* =========================================================================
   REDUCED MOTION
   base.css already flattens durations. Here the looping ambience is switched
   off outright — a 1ms infinite animation is a strobe, not a courtesy — and
   the celebration is replaced by a still burst that simply fades.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  /* will-change goes too: a visitor who asked for stillness should not be left
     paying for two promoted layers that will never move again. */
  :root:not([data-motion="full"]) .fx-ambient__blob { animation: none; will-change: auto; }
  :root:not([data-motion="full"]) .fx-confetti { animation: none; opacity: 0; }
  :root:not([data-motion="full"]) .fx-burst { animation: none; opacity: 0.35; transform: scale(1.6); }
  :root:not([data-motion="full"]) .fx-ripple { animation: none; opacity: 0; }
}
:root[data-motion="reduced"] .fx-ambient__blob { animation: none; will-change: auto; }
:root[data-motion="reduced"] .fx-confetti { animation: none; opacity: 0; }
:root[data-motion="reduced"] .fx-burst { animation: none; opacity: 0.35; transform: scale(1.6); }
:root[data-motion="reduced"] .fx-ripple { animation: none; opacity: 0; }
