/* Tiệm Mì Cay — kitchen and customer effects: the #fx-layer nodes of src/fx.js and in-place details on the app's
   own markup. Load after style.css: <link rel="stylesheet" href="./src/fx.css">.

   Performance: every animation here moves transform or opacity, so it runs on the compositor with no layout and
   no per-frame repaint (checked with Chrome's trace: compositeFailed 0 for each one). One-shots keep no forwards
   fill, so a later fx.js transform on the same node is not blocked. Loops on HTML boxes use the
   individual translate/rotate/scale properties, so fx.js one-shots (pop, squash, shake, quake, boing), which use
   transform, add to them instead of replacing them. Loops on SVG elements (the pot, its flame, the bowl's steam,
   the chili bottle) must use transform: Chrome does not composite translate/rotate/scale on SVG targets.
   Nothing here adds in-flow height: decorations are absolutely positioned pseudo-elements, pointer-events:none.

   Motion: every animation sits inside @media (prefers-reduced-motion: no-preference) and under
   :root:not(.reduced-motion) (the app's motion setting), so with motion off nothing loops. While <dialog> is open
   (or the tab is hidden) fx.js sets html.fx-paused and every CSS animation in the app and the layer pauses.

   Selector cost: no rule here ends in a universal selector or uses :has(). Those are matched against the whole page
   on every style pass, and a body:has(dialog[open]) rule made Chrome restyle all of #app about nine times a second
   while the game updated its DOM (trace: 571 elements per recalc, +170 ms/s on a 4x-slowed phone).

   What the app toggles (and when):
   - .pot-button.boiling, .stage-raw/.stage-ideal/.stage-soft/.stage-danger (already set by paintPot): flame
     flicker, bubbles from the ideal zone on, boil jiggle (0.45 s, 0.3 s in the ideal zone, faster near burning), a slow
     simmer wobble while idle; a pot about to burn glows in an opacity pulse instead of the box-shadow pulse.
   - .assembled-bowl: a plop each time replaceContents() inserts it, which must be only when the bowl changes
     (compare with the last HTML written: SVG never serialises back to the same string, so comparing innerHTML
     re-inserts the bowl on every tick and the plop would never stop). .has-broth: steam wisps. .fx-hot on
     .assembled-bowl while spice >= 5: flames licking at the rim.
   - .fx-ready on [data-action="serve"] while the bowl can be served (best: it matches the selected order): halo.
   - .chili-bottle sways while .chili-button is enabled. fx.squash(bottle) plays the reference squeeze (1.14/0.82,
     0.35 s) by swapping the sway for .fx-squeeze/.fx-squeeze-b (alternating, so quick taps restart it).
   - .customer.fx-arrive on a newly created card (remove after ~0.5 s or leave it): walk-in. --fx-delay on each
     .customer (a negative time, e.g. -1234ms): staggers the 2.4 s idle bob. [data-fx-sweat] on .customer while
     patience is .30-.50: a sweat drop. .customer.urgent: the hurry swell.
   - .fx-hot on #combo-count while combo >= 3: a small flame. .fx-up/.fx-down: set by fx.countTo on its node (or
     on { flag } — pass .wallet for the 1.12 bump). .fx-gain on .top-rating for ~1 s: a star sparkle.
   - #puddle needs nothing: it wears a wet-floor sign. */

/* ---- The layer (src/fx.js builds and pools every node inside it; each one carries an fx- class) */
#fx-layer{position:fixed;inset:0;z-index:90;overflow:hidden;pointer-events:none;contain:strict}
.fx-p,.fx-sprite,.fx-ghost,.fx-gline,.fx-float,.fx-caption{position:absolute;left:0;top:0}
.fx-p,.fx-sprite,.fx-hop,.fx-body,.fx-img,.fx-text,.fx-ghost,.fx-gm,.fx-gring,.fx-gface,.fx-gname,.fx-gline,.fx-float,.fx-caption{box-sizing:border-box;pointer-events:none;user-select:none;-webkit-user-select:none}
.fx-p[hidden],.fx-sprite[hidden],.fx-img[hidden],.fx-text[hidden],.fx-ghost[hidden],.fx-gring[hidden],.fx-gface[hidden],.fx-gline[hidden],.fx-float[hidden],.fx-caption[hidden]{display:none!important}
.fx-p{width:12px;height:12px;margin:-6px 0 0 -6px;opacity:0;background:center/100% 100% no-repeat}
.fx-p.fx-k-spark{width:10px;height:10px;margin:-5px 0 0 -5px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 .5l2.4 7.1 7.1 2.4-7.1 2.4L10 19.5l-2.4-7.1L.5 10l7.1-2.4z' fill='%23f7c242'/%3E%3Cpath d='M10 5l1.2 3.8L15 10l-3.8 1.2L10 15l-1.2-3.8L5 10l3.8-1.2z' fill='%23fff8d8'/%3E%3C/svg%3E")}
.fx-p.fx-k-star{width:15px;height:15px;margin:-7.5px 0 0 -7.5px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.6l3.1 6.5 7.1.9-5.2 4.9 1.3 7.1L12 17.6l-6.3 3.4L7 13.9 1.8 9l7.1-.9z' fill='%23f7c242' stroke='%234a2a22' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M8.2 9.8q1.4-.5 2.6-.4' stroke='%23fff' stroke-width='1.3' stroke-linecap='round' fill='none' opacity='.55'/%3E%3C/svg%3E")}
.fx-p.fx-k-heart{width:18px;height:16.5px;margin:-8px 0 0 -9px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 22'%3E%3Cpath d='M12 20.4C5 15.6 1.6 12 1.6 7.6 1.6 4.3 4 1.8 7.1 1.8c2 0 3.8 1 4.9 2.7 1.1-1.7 2.9-2.7 4.9-2.7 3.1 0 5.5 2.5 5.5 5.8 0 4.4-3.4 8-10.4 12.8z' fill='%23f2556b' stroke='%234a2a22' stroke-width='1.7' stroke-linejoin='round'/%3E%3Cpath d='M5.4 6.4q.7-1.9 2.8-2.1' stroke='%23fff' stroke-width='1.6' stroke-linecap='round' fill='none' opacity='.55'/%3E%3C/svg%3E")}
.fx-p.fx-k-puff{width:26px;height:19.5px;margin:-10px 0 0 -13px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cg fill='none' stroke='%23e2cfc0' stroke-width='2.6'%3E%3Ccircle cx='10' cy='14' r='6.5'/%3E%3Ccircle cx='17' cy='9.5' r='7.5'/%3E%3Ccircle cx='24' cy='14.5' r='5.5'/%3E%3Cellipse cx='17' cy='16.5' rx='9.5' ry='4.6'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Ccircle cx='10' cy='14' r='6.5'/%3E%3Ccircle cx='17' cy='9.5' r='7.5'/%3E%3Ccircle cx='24' cy='14.5' r='5.5'/%3E%3Cellipse cx='17' cy='16.5' rx='9.5' ry='4.6'/%3E%3C/g%3E%3Cpath d='M12 10.6q2-3.4 5.4-3.6' stroke='%23fff' stroke-width='1.6' stroke-linecap='round' fill='none' opacity='.55'/%3E%3C/svg%3E")}
.fx-p.fx-k-smoke{width:28px;height:21px;margin:-10.5px 0 0 -14px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cg fill='none' stroke='%235f5650' stroke-width='2.6'%3E%3Ccircle cx='10' cy='14' r='6.5'/%3E%3Ccircle cx='17' cy='9.5' r='7.5'/%3E%3Ccircle cx='24' cy='14.5' r='5.5'/%3E%3Cellipse cx='17' cy='16.5' rx='9.5' ry='4.6'/%3E%3C/g%3E%3Cg fill='%239a918b'%3E%3Ccircle cx='10' cy='14' r='6.5'/%3E%3Ccircle cx='17' cy='9.5' r='7.5'/%3E%3Ccircle cx='24' cy='14.5' r='5.5'/%3E%3Cellipse cx='17' cy='16.5' rx='9.5' ry='4.6'/%3E%3C/g%3E%3Cpath d='M12 10.6q2-3.4 5.4-3.6' stroke='%23fff' stroke-width='1.6' stroke-linecap='round' fill='none' opacity='.55'/%3E%3C/svg%3E")}
.fx-p.fx-k-bubble{width:9px;height:9px;margin:-4.5px 0 0 -4.5px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.3' fill='%23e9f7fd' fill-opacity='.6' stroke='%234f9fd0' stroke-width='1.4'/%3E%3Cpath d='M5 6.8a3.2 3.2 0 0 1 2.3-2.4' stroke='%23fff' stroke-width='1.4' stroke-linecap='round' fill='none'/%3E%3C/svg%3E")}
.fx-p.fx-k-drop,.fx-p.fx-k-chili{width:7px;height:7px;margin:-3.5px 0 0 -3.5px;border-radius:50%;background:var(--c,#6cc3ef);box-shadow:inset -1px -1.2px 0 #0000002e,inset 1px 1px 0 #ffffff8c,0 0 0 .9px #4a2a2299}
.fx-p.fx-k-chili{background:var(--c,#e8402f)}
.fx-p.fx-k-ring{width:40px;height:14px;margin:-7px 0 0 -20px;border:2px solid var(--c,#fff3dc);border-radius:50%;background:none;box-shadow:0 0 0 1px #4a2a2233}
.fx-sprite,.fx-hop{width:0;height:0}
.fx-hop{position:absolute;left:0;top:0}
.fx-body{position:absolute;left:-24px;top:-24px;width:48px;height:48px;opacity:0}
.fx-img{display:block;width:100%;height:100%;max-width:none;object-fit:contain}
.fx-text{display:grid;place-items:center;width:100%;height:100%;font-size:32px;line-height:1}
.fx-gm{position:absolute;inset:0;opacity:0}
.fx-gring{position:absolute;display:block;padding:3px;border-radius:50%;background:var(--ring,#8fc86f);box-shadow:0 3px 0 #49243833}
.fx-gface{display:block;width:100%;height:100%;max-width:none;border-radius:50%;background:#ffead6;object-fit:cover}
.fx-gname{position:absolute;left:0;right:0;overflow:hidden;font:700 12.5px/1.25 var(--font,'Mali','Segoe UI',sans-serif);color:#fff1df;text-align:center;white-space:nowrap;text-overflow:ellipsis;text-shadow:0 1px 2px #000d,0 0 6px #0009}
.fx-ghost.fx-pill .fx-gm{border:2px solid #e88841;border-radius:10px;background:#fff0d0}
.fx-ghost.fx-pill .fx-gname{color:#6b4430;text-shadow:none}
.fx-gline{padding:3px 8px;border-radius:10px;background:#fff;color:#5b3326;font:800 11.5px/1.3 var(--font,'Mali','Segoe UI',sans-serif);white-space:nowrap;box-shadow:0 2px 0 #0002;opacity:0}
.fx-gline::after{content:'';position:absolute;pointer-events:none;left:50%;bottom:-5px;margin-left:-5px;border:5px solid transparent;border-top-color:#fff;border-bottom:0}
.fx-float{font:800 17px/1.1 var(--font,'Mali','Segoe UI',sans-serif);color:#2f8f4e;white-space:nowrap;text-shadow:0 2px 0 #fff,0 0 4px #fff;opacity:0}
.fx-float.fx-tone-bad{color:#d8321f}
.fx-float.fx-tone-xp{color:#7a3fc4}
.fx-float.fx-tone-gold{color:#b4760c}
.fx-float.fx-tone-neutral{color:#5b3326}
.fx-caption{padding:4px 14px;font:400 clamp(34px,11vw,62px)/1.1 var(--display,'Paytone One','Trebuchet MS',sans-serif);letter-spacing:.5px;white-space:nowrap;color:#ffd34d;text-shadow:3px 0 0 #4a2a22,-3px 0 0 #4a2a22,0 3px 0 #4a2a22,0 -3px 0 #4a2a22,2px 2px 0 #4a2a22,-2px 2px 0 #4a2a22,2px -2px 0 #4a2a22,-2px -2px 0 #4a2a22,0 7px 0 #4a2a22;opacity:0}
.fx-caption.fx-tone-bad,.fx-caption.fx-tone-hot{color:#ff6b3d}
.fx-caption.fx-tone-neutral{color:#fff3dc}

/* ---- In-place decorations (static, so they read with motion off too) */
#combo-count{display:inline-block;position:relative}
.top-rating,.serve{position:relative}
.fx-up,.wallet.fx-up{color:#9be870}
.fx-down,.wallet.fx-down{color:#ff8f80}
.customer{--fx-ring:46px}
@media (min-width:600px){.customer{--fx-ring:56px}}
@media (orientation:portrait) and (max-height:640px){.customer{--fx-ring:42px}}
@media (orientation:landscape) and (max-width:899px),(orientation:landscape) and (max-height:559px){.customer{--fx-ring:42px}}
@media (min-width:900px) and (min-height:560px){.customer{--fx-ring:60px}}
@media (min-width:900px) and (min-height:560px) and (max-height:720px){.customer{--fx-ring:52px}}
/* The ring clips its paint (contain: layout paint), so the sweat drop hangs on the card, by the ring's top right. */
.customer[data-fx-sweat]::before{content:'';position:absolute;z-index:1;left:calc(50% + var(--fx-ring) * .27);top:calc(var(--fx-ring) * .04);width:11px;height:14px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 18'%3E%3Cpath d='M7 1.4C9.7 6 12.4 8.8 12.4 12a5.4 5.4 0 0 1-10.8 0C1.6 8.8 4.3 6 7 1.4z' fill='%236cc3ef' stroke='%234a2a22' stroke-width='1.3' stroke-linejoin='round'/%3E%3Cpath d='M4.5 11.8q0-1.9 1.5-3' stroke='%23fff' stroke-width='1.3' stroke-linecap='round' fill='none' opacity='.75'/%3E%3C/svg%3E") center/contain no-repeat;pointer-events:none}
.assembled-bowl.fx-hot::before{content:'';position:absolute;left:18%;right:18%;top:-9%;height:34%;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 30'%3E%3Cdefs%3E%3Cg id='f'%3E%3Cpath d='M10 27C4.6 27 2 23.2 2.6 18.8 3.2 13.8 7.6 11.6 7 4.6c3.6 2 6.4 5.8 6.2 10.6 1-.8 1.8-2.4 1.8-4 2.4 2.2 3.6 5 3.4 8C18.2 23.6 15.4 27 10 27z' fill='%23ef5a3c' stroke='%234a2a22' stroke-width='1.6' stroke-linejoin='round'/%3E%3Cpath d='M10 25c-2.6 0-4-1.8-3.6-4.2.4-2.2 2.4-3.4 2.6-6 2.6 1.8 4.6 4.4 4.4 7-.2 2-1.4 3.2-3.4 3.2z' fill='%23f7c242'/%3E%3C/g%3E%3C/defs%3E%3Cuse href='%23f' transform='translate(1 9) scale(.72)'/%3E%3Cuse href='%23f' transform='translate(21.5 1)'/%3E%3Cuse href='%23f' transform='translate(47 11) scale(.66)'/%3E%3C/svg%3E") center bottom/contain no-repeat;transform-origin:50% 100%;pointer-events:none}
.serve.fx-ready::after{content:'';position:absolute;inset:0;border-radius:inherit;box-shadow:0 0 0 3px #ffe08a,0 0 14px 3px #ffd34d8c;pointer-events:none}
#combo-count.fx-hot::after{content:'';position:absolute;left:50%;bottom:82%;width:10px;height:14px;margin-left:-5px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 28'%3E%3Cpath d='M10 27C4.6 27 2 23.2 2.6 18.8 3.2 13.8 7.6 11.6 7 4.6c3.6 2 6.4 5.8 6.2 10.6 1-.8 1.8-2.4 1.8-4 2.4 2.2 3.6 5 3.4 8C18.2 23.6 15.4 27 10 27z' fill='%23ef5a3c' stroke='%234a2a22' stroke-width='1.6' stroke-linejoin='round'/%3E%3Cpath d='M10 25c-2.6 0-4-1.8-3.6-4.2.4-2.2 2.4-3.4 2.6-6 2.6 1.8 4.6 4.4 4.4 7-.2 2-1.4 3.2-3.4 3.2z' fill='%23f7c242'/%3E%3C/svg%3E") center/contain no-repeat;transform-origin:50% 100%;pointer-events:none}
#puddle::before{content:'';position:absolute;left:-7px;top:-15px;width:16px;height:19px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 24'%3E%3Cpath d='M3 22.6L7 2h6l4 20.6' fill='none' stroke='%234a2a22' stroke-width='3.4' stroke-linejoin='round'/%3E%3Cpath d='M6.2 2h7.6l4.6 20.4H1.6z' fill='%23f7c242' stroke='%234a2a22' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M10 6.6v6.2' stroke='%234a2a22' stroke-width='2.3' stroke-linecap='round'/%3E%3Ccircle cx='10' cy='16.6' r='1.35' fill='%234a2a22'/%3E%3Cpath d='M7.4 4.6l-.9 4' stroke='%23fff' stroke-width='1.2' stroke-linecap='round' opacity='.55'/%3E%3C/svg%3E") center/contain no-repeat;pointer-events:none}

/* ---- Motion (only when the player allows it) */
@media (prefers-reduced-motion:no-preference){
  /* Pots: simmer while idle; jiggle and flame flicker while boiling, bubbles once the water rolls (ideal zone on). */
  :root:not(.reduced-motion) .pot-button .pot-art{transform-origin:50% 92%;animation:fx-simmer 2.8s ease-in-out infinite}
  :root:not(.reduced-motion) .pot-button.boiling .pot-art{animation:fx-boil .45s ease-in-out infinite}
  :root:not(.reduced-motion) .pot-button.boiling.stage-ideal .pot-art{animation-duration:.3s}
  :root:not(.reduced-motion) .pot-button.boiling.stage-danger .pot-art{animation:fx-boil-hard .22s ease-in-out infinite}
  :root:not(.reduced-motion) .pot-button.boiling .pot-flame{transform-box:fill-box;transform-origin:50% 100%;animation:fx-flicker .18s ease-in-out infinite alternate}
  :root:not(.reduced-motion) .pot-button.boiling:not(.stage-raw) .pot-icon::before{content:'';position:absolute;left:27%;top:10%;width:46%;height:32%;background:radial-gradient(circle at 20% 78%,#fff 0 1.2px,#6aa9c9 1.4px 2px,transparent 2.3px),radial-gradient(circle at 56% 42%,#fff 0 1.6px,#6aa9c9 1.8px 2.5px,transparent 2.8px),radial-gradient(circle at 86% 70%,#fff 0 1px,#6aa9c9 1.2px 1.8px,transparent 2.1px);opacity:0;pointer-events:none;animation:fx-bubbles .9s ease-out infinite}
  /* A pot about to burn: an opacity glow in place of style.css's repainting box-shadow pulse. */
  :root:not(.reduced-motion) .pot-button.stage-danger:not(.coach-target){animation:none}
  :root:not(.reduced-motion) .pot-button.stage-danger::before{content:'';position:absolute;inset:-1px;border-radius:inherit;box-shadow:0 0 0 3px #ef4b3f80;opacity:0;pointer-events:none;animation:fx-glow .45s ease-in-out infinite alternate}
  /* Bowl: a plop each time it is inserted (each content change), steam over hot broth, flames from spice 5. */
  :root:not(.reduced-motion) .assembled-bowl{transform-origin:50% 92%;animation:fx-plop .36s ease-out backwards}
  :root:not(.reduced-motion) .assembled-bowl.has-broth .bowl-steam{transform-box:fill-box;transform-origin:50% 100%;animation:fx-wisp 2.4s ease-out infinite}
  :root:not(.reduced-motion) .assembled-bowl.fx-hot::before{animation:fx-lick .28s ease-in-out infinite alternate}
  /* Serve: a halo while the bowl is ready. */
  :root:not(.reduced-motion) .serve.fx-ready::after{opacity:0;animation:fx-halo 1s ease-out infinite}
  /* Chili bottle (an <svg>, so transform only): idle sway, a full cycle from 0deg so it restarts seamlessly. The
     squeeze replaces the sway instead of stacking on it: two transform animations on one SVG element would both
     fall back to the main thread. */
  :root:not(.reduced-motion) .chili-bottle{transform-origin:50% 90%}
  :root:not(.reduced-motion) .chili-button:not(:disabled) .chili-bottle{animation:fx-sway 3s ease-in-out infinite}
  :root:not(.reduced-motion) .chili-button .chili-bottle.fx-squeeze,:root:not(.reduced-motion) .fx-squeeze{animation:fx-squeeze .35s ease-out}
  :root:not(.reduced-motion) .chili-button .chili-bottle.fx-squeeze-b,:root:not(.reduced-motion) .fx-squeeze-b{animation:fx-squeeze-b .35s ease-out}
  /* Customers: idle bob (staggered by --fx-delay), walk-in, hurry swell, sweat. The bob keeps its timing when the
     walk-in or the swell joins it, so nothing jumps. */
  :root:not(.reduced-motion) .customer .patience-ring{animation:fx-bob 1.2s ease-in-out var(--fx-delay,0s) infinite alternate}
  :root:not(.reduced-motion) .customer.fx-arrive .patience-ring{animation:fx-bob 1.2s ease-in-out var(--fx-delay,0s) infinite alternate,fx-walk-in .45s ease-out backwards}
  :root:not(.reduced-motion) .customer.urgent .patience-ring{animation:fx-bob 1.2s ease-in-out var(--fx-delay,0s) infinite alternate,fx-hurry .7s ease-in-out infinite alternate}
  :root:not(.reduced-motion) .customer[data-fx-sweat]::before{animation:fx-sweat 1.3s ease-in infinite}
  /* Counters. */
  :root:not(.reduced-motion) #combo-count.fx-hot::after{animation:fx-flame .26s ease-in-out infinite alternate}
  :root:not(.reduced-motion) .wallet.fx-up,:root:not(.reduced-motion) .wallet.fx-down{animation:fx-bump .32s ease-out}
  :root:not(.reduced-motion) .top-rating.fx-gain::after{content:'';position:absolute;right:-9px;top:-7px;width:14px;height:14px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 .5l2.4 7.1 7.1 2.4-7.1 2.4L10 19.5l-2.4-7.1L.5 10l7.1-2.4z' fill='%23f7c242'/%3E%3Cpath d='M10 5l1.2 3.8L15 10l-3.8 1.2L10 15l-1.2-3.8L5 10l3.8-1.2z' fill='%23fff8d8'/%3E%3C/svg%3E") center/contain no-repeat;pointer-events:none;animation:fx-gain .9s ease-out both}
  /* style.css pulses that animated box-shadow (a repaint every frame), redone as compositor animations. */
  :root:not(.reduced-motion) .tea-button{animation:fx-tea .9s ease-in-out infinite alternate}
  :root:not(.reduced-motion) .top-clock.warn{animation:fx-tick .8s ease-in-out infinite alternate}
}
@keyframes fx-simmer{0%,100%{transform:rotate(-.6deg)}50%{transform:translateY(-.4px) rotate(.6deg)}}
@keyframes fx-boil{0%,100%{transform:translate(0,0) rotate(-1.2deg)}25%{transform:translate(.4px,-1px) rotate(0deg)}50%{transform:translate(0,0) rotate(1.2deg)}75%{transform:translate(-.4px,-.7px) rotate(0deg)}}
@keyframes fx-boil-hard{0%,100%{transform:translate(0,0) rotate(-1.8deg)}25%{transform:translate(.8px,-1.3px) rotate(0deg)}50%{transform:translate(0,0) rotate(1.8deg)}75%{transform:translate(-.8px,-1px) rotate(0deg)}}
@keyframes fx-flicker{from{transform:scale(1.04,.85)}to{transform:scale(.96,1.1)}}
@keyframes fx-bubbles{0%{translate:0 3px;scale:.5;opacity:0}20%{opacity:1}75%{opacity:1}100%{translate:0 -5px;scale:1.1;opacity:0}}
@keyframes fx-glow{to{opacity:1}}
@keyframes fx-plop{0%{transform:scale(1.07,.86)}55%{transform:scale(.98,1.04)}100%{transform:none}}
@keyframes fx-wisp{0%{opacity:0;transform:translateY(6px)}30%{opacity:1}100%{opacity:0;transform:translateY(-16px)}}
@keyframes fx-lick{from{scale:.94 .88}to{scale:1.05 1.12}}
@keyframes fx-halo{0%{opacity:.95;scale:1}100%{opacity:0;scale:1.05 1.18}}
@keyframes fx-sway{0%,100%{transform:rotate(0deg)}25%{transform:rotate(3deg)}75%{transform:rotate(-3deg)}}
@keyframes fx-squeeze{0%{transform:none}35%{transform:scale(1.14,.82)}70%{transform:scale(.96,1.05)}100%{transform:none}}
@keyframes fx-squeeze-b{0%{transform:none}35%{transform:scale(1.14,.82)}70%{transform:scale(.96,1.05)}100%{transform:none}}
@keyframes fx-walk-in{0%{transform:translateY(30px);opacity:0}30%{opacity:1}72%{transform:translateY(-4px)}100%{transform:none;opacity:1}}
@keyframes fx-bob{from{translate:0 0}to{translate:0 -3px}}
@keyframes fx-hurry{from{scale:1}to{scale:1.08}}
@keyframes fx-sweat{0%{opacity:0;translate:0 -3px}18%{opacity:1}65%{opacity:1}100%{opacity:0;translate:0 10px}}
@keyframes fx-flame{from{scale:.9 .85;rotate:-5deg}to{scale:1.05 1.1;rotate:5deg}}
@keyframes fx-bump{0%{scale:1}40%{scale:1.12}100%{scale:1}}
@keyframes fx-gain{0%{opacity:0;scale:.3;rotate:0deg}30%{opacity:1;scale:1.2}100%{opacity:0;scale:.7;rotate:120deg}}
@keyframes fx-tea{from{scale:1}to{scale:1.05}}
@keyframes fx-tick{from{scale:1}to{scale:1.08}}

/* ---- An open dialog or a hidden tab pauses every CSS animation behind it: fx.js sets html.fx-paused (and pauses
   its own animations). This also stops the loops from making the dialog's blurred backdrop recompute every frame.
   While the class is absent the browser rejects this rule from its ancestor filter, at almost no cost. */
.fx-paused :is(#app,.play,#fx-layer) *,
.fx-paused :is(#app,.play,#fx-layer) *::before,
.fx-paused :is(#app,.play,#fx-layer) *::after{animation-play-state:paused}
