/* ============================================================================
   Control Panel — operation animations
   Load AFTER panel-v2.css: every colour here comes from that file's tokens
   (--ok/--warn/--danger/--surface/--line), so a theme switch carries through
   without a second palette to keep in sync.

   Everything is gated on body classes written by app-animations.js:
     .cp-anim-on      master switch — nothing below runs without it
     .cp-fx-*         one per effect, so a user can keep toasts and drop shake
   and scaled by --cp-anim-scale, the single "speed" knob. Durations are
   written as calc(<base> * var(--cp-anim-scale)) rather than hardcoded, which
   is what lets one slider slow the whole thing down instead of eleven.
   ========================================================================= */

body {
  /* 1 = the tuned default. The Settings slider writes 0.5 (twice as fast)
     through 2 (half speed) onto body's inline style. Declared here rather than
     on :root so the inline override lands on the same element the effects
     below resolve against. */
  --cp-anim-scale: 1;
}

/* The OS-level "reduce motion" preference wins over the config by default —
   app-animations.js adds .cp-anim-reduced when it is honouring it. Movement is
   removed; the toasts themselves stay, because a person who has asked for less
   motion still wants to know whether their deploy worked. */
body.cp-anim-reduced .cp-toast,
body.cp-anim-reduced .cp-toast.is-leaving {
  animation: none !important;
  transform: none !important;
}

/* ---------------------------------------------------------------- toasts --
   The stack is a fixed-position column and new toasts are appended, so the
   newest is the last child. It should arrive at the edge the stack is pinned
   to and push the older ones inward: at the bottom that is plain column (last
   child lowest), at the top it is column-reverse (last child highest). */
#cp-toasts {
  position: fixed;
  z-index: 4000;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* The column is only as wide as it needs to be, and never covers content it
     is not currently drawing on: pointer-events return per-toast below. */
  pointer-events: none;
  max-width: min(420px, calc(100vw - 32px));
}
#cp-toasts.cp-pos-tr { top: 18px; right: 18px; align-items: flex-end; flex-direction: column-reverse; }
#cp-toasts.cp-pos-tl { top: 18px; left: 18px; align-items: flex-start; flex-direction: column-reverse; }
#cp-toasts.cp-pos-br { bottom: 18px; right: 18px; align-items: flex-end; }
#cp-toasts.cp-pos-bl { bottom: 18px; left: 18px; align-items: flex-start; }
#cp-toasts.cp-pos-tc { top: 18px; left: 50%; transform: translateX(-50%); align-items: center; flex-direction: column-reverse; }
#cp-toasts.cp-pos-bc { bottom: 18px; left: 50%; transform: translateX(-50%); align-items: center; }

.cp-toast {
  pointer-events: auto;
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 11px 13px;
  min-width: 240px;
  max-width: 100%;
  border: 1px solid var(--line, #1e2833);
  border-left: 3px solid var(--text-mute, #6d7987);
  border-radius: var(--radius, 10px);
  background: var(--surface, #111820);
  color: var(--text, #e6edf3);
  font-family: var(--font, Helvetica, Arial, sans-serif);
  font-size: 13px;
  line-height: 1.45;
  /* Two shadows: a tight one that seats the card, and a wide soft one that
     lifts it clear of whatever panel it lands on. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 28px rgba(0, 0, 0, 0.35);
  overflow: hidden;
  cursor: pointer;
}
.cp-toast-ok   { border-left-color: var(--ok, #3fb950); }
.cp-toast-warn { border-left-color: var(--warn, #d29922); }
.cp-toast-err  { border-left-color: var(--danger, #f85149); }

.cp-toast-icon { flex: 0 0 18px; width: 18px; height: 18px; margin-top: 1px; }
.cp-toast-icon circle { opacity: 0.16; }
.cp-toast-ok   .cp-toast-icon { color: var(--ok, #3fb950); }
.cp-toast-warn .cp-toast-icon { color: var(--warn, #d29922); }
.cp-toast-err  .cp-toast-icon { color: var(--danger, #f85149); }

.cp-toast-body { flex: 1 1 auto; min-width: 0; }
.cp-toast-title {
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim, #9aa7b4);
  margin-bottom: 2px;
}
.cp-toast-text {
  /* Operation messages carry paths and command output, so they wrap and break
     rather than pushing the card off-screen. Three lines, then ellipsis: the
     full text is always in the console anyway. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.cp-toast-count {
  flex: 0 0 auto;
  align-self: center;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(148, 163, 184, 0.18);
  color: var(--text-dim, #9aa7b4);
  font-size: 11px;
  font-weight: 600;
  text-align: center;
}

/* The life bar: how long is left before this toast leaves. Its width is
   animated rather than transitioned so pausing on hover is one property. */
.cp-toast-life {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  transform-origin: left center;
  background: currentColor;
  opacity: 0.5;
}
.cp-toast-ok   .cp-toast-life { color: var(--ok, #3fb950); }
.cp-toast-warn .cp-toast-life { color: var(--warn, #d29922); }
.cp-toast-err  .cp-toast-life { color: var(--danger, #f85149); }
.cp-toast:hover .cp-toast-life { animation-play-state: paused; }

/* --- entrances. One class per "style" in Settings; the left/right variants
       exist because sliding in from the wrong edge reads as a glitch. --- */
.cp-in-slide.cp-toast { animation: cp-toast-in-right calc(320ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) both; }
#cp-toasts.cp-pos-tl .cp-in-slide.cp-toast,
#cp-toasts.cp-pos-bl .cp-in-slide.cp-toast { animation-name: cp-toast-in-left; }
#cp-toasts.cp-pos-tc .cp-in-slide.cp-toast { animation-name: cp-toast-in-down; }
#cp-toasts.cp-pos-bc .cp-in-slide.cp-toast { animation-name: cp-toast-in-up; }

.cp-in-fade.cp-toast { animation: cp-toast-in-fade calc(300ms * var(--cp-anim-scale)) ease-out both; }
.cp-in-pop.cp-toast  { animation: cp-toast-in-pop calc(400ms * var(--cp-anim-scale)) cubic-bezier(0.34, 1.56, 0.64, 1) both; }
.cp-in-flip.cp-toast { animation: cp-toast-in-flip calc(420ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) both; }

/* Exit is one animation for every entrance style: collapsing the height as it
   goes is what makes the toasts below it slide up instead of jumping. */
.cp-toast.is-leaving {
  animation: cp-toast-out calc(260ms * var(--cp-anim-scale)) cubic-bezier(0.4, 0, 1, 1) both;
  pointer-events: none;
}

@keyframes cp-toast-in-right { from { opacity: 0; transform: translateX(24px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes cp-toast-in-left  { from { opacity: 0; transform: translateX(-24px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes cp-toast-in-down  { from { opacity: 0; transform: translateY(-20px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes cp-toast-in-up    { from { opacity: 0; transform: translateY(20px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes cp-toast-in-fade  { from { opacity: 0; } to { opacity: 1; } }
@keyframes cp-toast-in-pop   { from { opacity: 0; transform: scale(0.82); } to { opacity: 1; transform: scale(1); } }
@keyframes cp-toast-in-flip  { from { opacity: 0; transform: perspective(600px) rotateX(-70deg); transform-origin: top center; } to { opacity: 1; transform: none; } }
@keyframes cp-toast-out {
  from { opacity: 1; transform: none; max-height: 200px; margin-bottom: 0; }
  to   { opacity: 0; transform: scale(0.94); max-height: 0; margin-bottom: -10px; padding-top: 0; padding-bottom: 0; }
}
@keyframes cp-toast-life { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* The icon glyph draws itself. Two dashes the length of the path, offset by
   the same amount, animated to 0 — the standard stroke-draw trick. */
.cp-toast-icon .cp-glyph {
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  animation: cp-draw calc(400ms * var(--cp-anim-scale)) calc(120ms * var(--cp-anim-scale)) ease-out forwards;
}
@keyframes cp-draw { to { stroke-dashoffset: 0; } }
body.cp-anim-reduced .cp-toast-icon .cp-glyph { animation: none; stroke-dashoffset: 0; }

/* ---------------------------------------------------------------- ripple --
   Injected as a child of the clicked button, sized and positioned from the
   click point by app-animations.js. The host needs position/overflow, which
   is why the .cp-ripple-host class is added rather than styling every button
   class in the app. */
.cp-ripple-host { position: relative; overflow: hidden; }
.cp-ripple {
  position: absolute;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.35;
  pointer-events: none;
  transform: scale(0);
  animation: cp-ripple calc(520ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes cp-ripple { to { transform: scale(1); opacity: 0; } }

/* ------------------------------------------------------ result feedback --
   Applied to whatever element started the operation (the button, or a card
   the caller names). The flash is a box-shadow rather than a background so it
   cannot fight the element's own themed background. */
.cp-flash-ok   { animation: cp-flash-ok   calc(900ms * var(--cp-anim-scale)) ease-out; }
.cp-flash-warn { animation: cp-flash-warn calc(900ms * var(--cp-anim-scale)) ease-out; }
.cp-flash-err  { animation: cp-flash-err  calc(900ms * var(--cp-anim-scale)) ease-out; }
@keyframes cp-flash-ok {
  0%   { box-shadow: 0 0 0 0 rgba(63, 185, 80, 0.55); }
  35%  { box-shadow: 0 0 0 6px rgba(63, 185, 80, 0.28); }
  100% { box-shadow: 0 0 0 12px rgba(63, 185, 80, 0); }
}
@keyframes cp-flash-warn {
  0%   { box-shadow: 0 0 0 0 rgba(210, 153, 34, 0.55); }
  35%  { box-shadow: 0 0 0 6px rgba(210, 153, 34, 0.28); }
  100% { box-shadow: 0 0 0 12px rgba(210, 153, 34, 0); }
}
@keyframes cp-flash-err {
  0%   { box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.55); }
  35%  { box-shadow: 0 0 0 6px rgba(248, 81, 73, 0.28); }
  100% { box-shadow: 0 0 0 12px rgba(248, 81, 73, 0); }
}

.cp-shake { animation: cp-shake calc(420ms * var(--cp-anim-scale)) cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes cp-shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(3px); }
  30%, 50%, 70% { transform: translateX(-5px); }
  40%, 60% { transform: translateX(5px); }
}
body.cp-anim-reduced .cp-shake { animation: none; }

/* -------------------------------------------------------- progress bar --
   A 2px line pinned to the top of the viewport while jobs are queued. It
   creeps toward 90% on an ease-out so a slow operation still looks alive,
   then is driven to 100% and faded by the "done" call. */
#cp-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 100%;
  z-index: 4100;
  pointer-events: none;
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--accent, #5eb0ef), var(--ok, #3fb950));
  transition: transform calc(200ms * var(--cp-anim-scale)) ease-out,
              opacity calc(300ms * var(--cp-anim-scale)) ease-out;
}
#cp-progress.is-active { opacity: 1; animation: cp-progress-creep calc(9s * var(--cp-anim-scale)) cubic-bezier(0.1, 0.6, 0.2, 1) forwards; }
#cp-progress.is-done   { opacity: 0; transform: scaleX(1); animation: none; }
@keyframes cp-progress-creep { from { transform: scaleX(0.02); } to { transform: scaleX(0.9); } }

/* ------------------------------------------------------------ celebrate --
   Off by default. A short burst of particles from the toast that reported a
   successful operation — reserved for success, since a shower of confetti
   over a failed deploy is the wrong note. Particles are absolutely positioned
   in a host that covers the toast; --cp-dx/--cp-dy are set per particle. */
.cp-burst { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.cp-burst i {
  position: absolute;
  top: 50%;
  left: 14px;
  width: 6px;
  height: 6px;
  border-radius: 1px;
  opacity: 0;
  animation: cp-burst calc(900ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes cp-burst {
  0%   { opacity: 1; transform: translate(0, 0) rotate(0deg) scale(1); }
  100% { opacity: 0; transform: translate(var(--cp-dx), var(--cp-dy)) rotate(320deg) scale(0.3); }
}
body.cp-anim-reduced .cp-burst { display: none; }

/* ================================================================ fun pack =
   Everything below is off unless Settings -> Animations -> "Fun pack" is on
   (body.cp-fx-fun, plus one class per effect). It is deliberately a separate
   block from the effects above: those are feedback a person needs in order to
   know whether an operation worked, these are only enjoyable, so they are
   opt-in and every one of them is dropped under reduce-motion rather than
   softened. Same rules as the rest of the file — theme tokens for colour,
   --cp-anim-scale for duration.
   ========================================================================= */

/* --- playful toast entrances. Chosen from the same "Entrance" dropdown as
       slide/fade/pop/flip, so they are not gated on the fun pack: picking one
       is already the decision. --- */
.cp-in-jelly.cp-toast { animation: cp-toast-in-jelly calc(620ms * var(--cp-anim-scale)) cubic-bezier(0.22, 1, 0.36, 1) both; }
.cp-in-roll.cp-toast  { animation: cp-toast-in-roll  calc(560ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) both; }
.cp-in-drop.cp-toast  { animation: cp-toast-in-drop  calc(700ms * var(--cp-anim-scale)) cubic-bezier(0.3, 1.4, 0.5, 1) both; }

/* Rolling in from the wrong edge spins the card away from where it came from,
   so the left-hand positions roll the other way — the same reason the slide
   entrance has a left variant. */
#cp-toasts.cp-pos-tl .cp-in-roll.cp-toast,
#cp-toasts.cp-pos-bl .cp-in-roll.cp-toast { animation-name: cp-toast-in-roll-left; }

/* The jelly overshoots its own width, and scaling about the centre sends half
   of that overshoot past whichever screen edge the stack is pinned to. Growing
   from the anchored edge instead keeps the whole wobble on screen. */
.cp-in-jelly.cp-toast { transform-origin: right center; }
#cp-toasts.cp-pos-tl .cp-in-jelly.cp-toast,
#cp-toasts.cp-pos-bl .cp-in-jelly.cp-toast { transform-origin: left center; }
#cp-toasts.cp-pos-tc .cp-in-jelly.cp-toast,
#cp-toasts.cp-pos-bc .cp-in-jelly.cp-toast { transform-origin: center; }

@keyframes cp-toast-in-jelly {
  0%   { opacity: 0; transform: scale3d(0.3, 1.6, 1); }
  40%  { opacity: 1; transform: scale3d(1.16, 0.8, 1); }
  60%  { transform: scale3d(0.9, 1.12, 1); }
  80%  { transform: scale3d(1.04, 0.95, 1); }
  100% { opacity: 1; transform: none; }
}
@keyframes cp-toast-in-roll {
  from { opacity: 0; transform: translateX(90px) rotate(120deg); }
  to   { opacity: 1; transform: none; }
}
@keyframes cp-toast-in-roll-left {
  from { opacity: 0; transform: translateX(-90px) rotate(-120deg); }
  to   { opacity: 1; transform: none; }
}
@keyframes cp-toast-in-drop {
  0%   { opacity: 0; transform: translateY(-140px) rotate(-6deg); }
  55%  { opacity: 1; transform: translateY(0) rotate(1deg); }
  72%  { transform: translateY(-11px) rotate(-1deg); }
  86%  { transform: translateY(0); }
  93%  { transform: translateY(-4px); }
  100% { transform: none; }
}
/* A person who asked for less motion picked the entrance before they asked, or
   on another machine; either way the playful ones become the plain fade. */
body.cp-anim-reduced .cp-in-jelly.cp-toast,
body.cp-anim-reduced .cp-in-roll.cp-toast,
body.cp-anim-reduced .cp-in-drop.cp-toast { animation: cp-toast-in-fade calc(300ms * var(--cp-anim-scale)) ease-out both; }

/* --- the emoji reaction on a toast. Sits before the status glyph rather than
       replacing it: the glyph and the border colour are what actually carry
       ok/warn/err, and they keep working for anyone who reads the emoji as
       decoration. --- */
.cp-toast-emoji {
  flex: 0 0 auto;
  align-self: center;
  font-size: 17px;
  line-height: 1;
  animation: cp-emoji-pop calc(520ms * var(--cp-anim-scale)) cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes cp-emoji-pop {
  0%   { opacity: 0; transform: scale(0) rotate(-45deg); }
  60%  { opacity: 1; transform: scale(1.35) rotate(12deg); }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
body.cp-anim-reduced .cp-toast-emoji { animation: none; }

/* --- squish. Buttons squash when pressed and overshoot on the way back, which
       is the whole joke: it is one transition, not a keyframe, so it tracks a
       held press instead of playing over it. --- */
body.cp-fx-squish button,
body.cp-fx-squish .act,
body.cp-fx-squish .jbtn,
body.cp-fx-squish .env-btn,
body.cp-fx-squish .util-btn {
  transition: transform calc(260ms * var(--cp-anim-scale)) cubic-bezier(0.34, 1.56, 0.64, 1);
}
body.cp-fx-squish button:active:not(:disabled),
body.cp-fx-squish .act:active,
body.cp-fx-squish .jbtn:active,
body.cp-fx-squish .env-btn:active,
body.cp-fx-squish .util-btn:active {
  transform: scale(0.9, 0.86);
  transition-duration: calc(70ms * var(--cp-anim-scale));
}

/* --- rumble. The failure equivalent of the confetti: the whole app frame
       staggers for a moment. Applied to .container, not <body>, so the fixed
       toast stack and progress line stay where they are — a transformed body
       becomes the containing block for its fixed children and would drag them
       along with it. --- */
.cp-rumble { animation: cp-rumble calc(620ms * var(--cp-anim-scale)) cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
@keyframes cp-rumble {
  0%, 100% { transform: none; }
  8%   { transform: translate(-7px, 3px) rotate(-0.7deg); }
  20%  { transform: translate(6px, -4px) rotate(0.6deg); }
  33%  { transform: translate(-5px, 4px) rotate(-0.5deg); }
  47%  { transform: translate(5px, -2px) rotate(0.4deg); }
  61%  { transform: translate(-3px, 2px) rotate(-0.25deg); }
  78%  { transform: translate(2px, -1px) rotate(0.15deg); }
}
body.cp-anim-reduced .cp-rumble { animation: none; }

/* --- emoji rain. A full-viewport layer above the app and below the toasts, so
       a success shower never buries the card that reported the success. Each
       drop gets its own left/delay/duration/spin from JS. --- */
#cp-rain {
  position: fixed;
  inset: 0;
  z-index: 3900;
  pointer-events: none;
  overflow: hidden;
}
#cp-rain i {
  position: absolute;
  top: -8vh;
  font-size: 22px;
  font-style: normal;
  line-height: 1;
  will-change: transform;
  animation: cp-rain-fall var(--cp-fall, 2200ms) linear var(--cp-delay, 0ms) both;
}
@keyframes cp-rain-fall {
  0%   { opacity: 0; transform: translateY(0) rotate(0deg); }
  10%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(118vh) rotate(var(--cp-spin, 360deg)); }
}
body.cp-anim-reduced #cp-rain { display: none; }

/* --- barrel roll. The Konami-code easter egg. One turn of the app frame; the
       overflow guard is on <body> because a rotated container's corners sweep
       outside the viewport mid-turn and would otherwise show a scrollbar. --- */
body.cp-barrel-roll { overflow: hidden; }
body.cp-barrel-roll .container { animation: cp-barrel-roll calc(1400ms * var(--cp-anim-scale)) cubic-bezier(0.6, 0.05, 0.3, 0.95) both; }
@keyframes cp-barrel-roll { from { transform: rotate(0turn); } to { transform: rotate(1turn); } }

/* The fun pack in full retreat: master switch off, or reduce-motion honoured. */
body:not(.cp-anim-on) .cp-rumble,
body:not(.cp-anim-on) .cp-toast-emoji { animation: none; }
body:not(.cp-anim-on) #cp-rain { display: none; }
body.cp-anim-reduced.cp-barrel-roll { overflow: visible; }
body.cp-anim-reduced.cp-barrel-roll .container { animation: none; }

/* ------------------------------------------------- section transitions --
   The shell swaps sections by toggling .is-active (see app-shell.js). This
   only adds the entrance; nothing about the swap itself changes. */
body.cp-fx-transitions .cp-section.is-active {
  animation: cp-section-in calc(260ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes cp-section-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
body.cp-anim-reduced.cp-fx-transitions .cp-section.is-active { animation-name: cp-toast-in-fade; }

/* ------------------------------------------------- mobile bottom sheet --
   On a phone the sheet IS the nav: nine of the fourteen sections are only
   reachable through More, and the topbar actions only through "...". It was
   the one surface in the app that changed state with no motion at all --
   .is-open flips display, so the panel and a 60%-black backdrop replaced the
   screen between two frames, which reads as a glitch rather than as something
   arriving.
   It comes from the edge it is anchored to, which is also the direction it
   leaves in, so the movement says where it went. .is-closing is added by
   app-shell.js, which keeps .is-open on until the exit finishes -- without
   that the element is display:none before the animation has a frame to run
   in. Ordered after the .is-open rules deliberately: same specificity, so the
   exit has to come later to win. */
@media (max-width: 760px) {
  body.cp-anim-on .cp-m-sheetwrap.is-open .cp-m-sheet-backdrop {
    animation: cp-sheet-fade calc(220ms * var(--cp-anim-scale)) ease-out both;
  }
  body.cp-anim-on .cp-m-sheetwrap.is-open .cp-m-sheet {
    animation: cp-sheet-up calc(260ms * var(--cp-anim-scale)) cubic-bezier(0.16, 1, 0.3, 1) both;
  }
  body.cp-anim-on .cp-m-sheetwrap.is-closing .cp-m-sheet-backdrop {
    animation: cp-sheet-fade calc(200ms * var(--cp-anim-scale)) ease-in both reverse;
  }
  body.cp-anim-on .cp-m-sheetwrap.is-closing .cp-m-sheet {
    animation: cp-sheet-down calc(220ms * var(--cp-anim-scale)) cubic-bezier(0.4, 0, 1, 1) both;
  }
}
@keyframes cp-sheet-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes cp-sheet-up   { from { transform: translateY(100%); } to { transform: none; } }
@keyframes cp-sheet-down { from { transform: none; } to { transform: translateY(100%); } }

/* Reduce-motion keeps the sheet and drops the travel: a person who asked for
   less movement still needs the nav to appear. app-shell.js reads the same
   class and closes without waiting, so nothing lingers on an animation that
   is not running. */
body.cp-anim-reduced .cp-m-sheetwrap.is-open .cp-m-sheet,
body.cp-anim-reduced .cp-m-sheetwrap.is-closing .cp-m-sheet,
body.cp-anim-reduced .cp-m-sheetwrap.is-closing .cp-m-sheet-backdrop { animation: none; }

/* ------------------------------------------------------------- preview --
   The Settings panel's demo strip. Deliberately plain: it exists to show the
   toast and flash settings against a neutral background, not to be styled. */
.cp-anim-preview {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-top: 8px;
}

/* Nothing above should run when the master switch is off. The stack is kept
   in the DOM (removing it would strand a queued toast mid-flight) but is not
   rendered, and every effect class becomes inert. */
body:not(.cp-anim-on) #cp-toasts,
body:not(.cp-anim-on) #cp-progress { display: none; }
body:not(.cp-anim-on) .cp-flash-ok,
body:not(.cp-anim-on) .cp-flash-warn,
body:not(.cp-anim-on) .cp-flash-err,
body:not(.cp-anim-on) .cp-shake,
body:not(.cp-anim-on) .cp-ripple { animation: none; }

/* On a phone the corner positions waste the width they do not use, so every
   position collapses to a full-width strip at the bottom — above the tab bar
   the shell draws there. */
@media (max-width: 640px) {
  #cp-toasts {
    left: 12px !important;
    right: 12px !important;
    top: auto !important;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
    max-width: none;
    transform: none !important;
    align-items: stretch !important;
    flex-direction: column !important;   /* pinned to the bottom, so newest lowest */
  }
  .cp-toast { min-width: 0; }
}
