[READ-ONLY] Mirror of https://github.com/flo-bit/streamplace-overlay.
flo-bit.dev/streamplace-overlay/
25 kB
1032 lines
1/* ===== CSS Custom Properties (Design Tokens) ===== */
2 :root {
3 --font-system:
4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
5 --font-mono: ui-monospace, "SF Mono", Menlo, monospace;
6 --font-comic:
7 "Comic Sans MS", "Comic Sans", "Chalkboard SE", "Marker Felt",
8 system-ui, sans-serif;
9 --color-text-dim: rgba(255, 255, 255, 0.45);
10 --color-text-faint: rgba(255, 255, 255, 0.35);
11 --color-bg-overlay: rgba(20, 20, 28, 0.85);
12 --color-bg-pill: rgba(0, 0, 0, 0.32);
13 --color-shadow-text: 0 0 3px rgba(0, 0, 0, 0.8);
14 --color-shadow-pet: 0 6px 8px rgba(0, 0, 0, 0.35);
15 --pet-size: 110px;
16 --pet-bottom: 14px;
17 --anim-speed-default: 1;
18 --rate-limit-glow: drop-shadow(0 0 10px rgba(255, 40, 40, 0.95))
19 drop-shadow(0 0 22px rgba(255, 0, 0, 0.65));
20 }
21
22 html,
23 body {
24 margin: 0;
25 padding: 0;
26 width: 100%;
27 height: 100%;
28 background: transparent;
29 overflow: hidden;
30 font-family: var(--font-system);
31 }
32
33 #stage {
34 position: fixed;
35 left: 0;
36 right: 0;
37 bottom: 0;
38 height: 200px;
39 pointer-events: none;
40 }
41
42 #status {
43 position: fixed;
44 top: 6px;
45 right: 8px;
46 font-size: 10px;
47 color: var(--color-text-dim);
48 font-family: var(--font-mono);
49 pointer-events: none;
50 text-shadow: var(--color-shadow-text);
51 }
52 #status.hidden {
53 display: none;
54 }
55
56 #version {
57 position: fixed;
58 bottom: 4px;
59 right: 8px;
60 font-size: 10px;
61 color: var(--color-text-faint);
62 font-family: var(--font-mono);
63 pointer-events: none;
64 text-shadow: var(--color-shadow-text);
65 }
66
67 #reset-btn {
68 position: fixed;
69 bottom: 4px;
70 left: 8px;
71 width: 24px;
72 height: 24px;
73 background: transparent;
74 border: none;
75 color: var(--color-text-faint);
76 font-size: 14px;
77 cursor: pointer;
78 opacity: 0;
79 transition: opacity 0.3s;
80 z-index: 999;
81 pointer-events: auto;
82 }
83 #reset-btn:hover {
84 opacity: 0.8;
85 color: rgba(255, 255, 255, 0.8);
86 }
87 #stage:hover ~ #reset-btn,
88 #reset-btn:hover,
89 #reset-btn:focus-visible {
90 opacity: 0.6;
91 }
92
93 #reload-toast {
94 position: fixed;
95 top: 24px;
96 left: 50%;
97 transform: translateX(-50%) translateY(-12px);
98 padding: 8px 18px;
99 background: var(--color-bg-overlay);
100 color: #fff;
101 font-size: 13px;
102 font-weight: 600;
103 font-family: var(--font-mono);
104 letter-spacing: 0.06em;
105 border-radius: 999px;
106 border: 1px solid rgba(255, 255, 255, 0.18);
107 box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
108 pointer-events: none;
109 opacity: 0;
110 z-index: 500;
111 animation: reload-toast 3.6s ease-out forwards;
112 }
113 @keyframes reload-toast {
114 0% {
115 opacity: 0;
116 transform: translateX(-50%) translateY(-12px);
117 }
118 8% {
119 opacity: 1;
120 transform: translateX(-50%) translateY(0);
121 }
122 80% {
123 opacity: 1;
124 transform: translateX(-50%) translateY(0);
125 }
126 100% {
127 opacity: 0;
128 transform: translateX(-50%) translateY(-8px);
129 }
130 }
131
132 /* ===== Pet ===== */
133 .pet {
134 position: absolute;
135 bottom: var(--pet-bottom);
136 left: 0;
137 width: var(--pet-size);
138 height: var(--pet-size);
139 transform-origin: 50% 100%;
140 will-change: transform;
141 filter: var(--color-shadow-pet);
142 --anim-speed: var(--anim-speed-default);
143 }
144
145 .pet-flip {
146 width: 100%;
147 height: 100%;
148 transform-origin: 50% 100%;
149 position: relative;
150 transition: filter 0.25s ease;
151 }
152 .pet-flip svg {
153 width: 100%;
154 height: 100%;
155 overflow: visible;
156 }
157
158 /* Rate-limit indicator — red glow around the pet */
159 .pet.rate-limited .pet-flip {
160 filter: var(--rate-limit-glow);
161 }
162
163 .rpg-sprite {
164 position: absolute;
165 bottom: 0;
166 left: 50%;
167 background-repeat: no-repeat;
168 image-rendering: pixelated;
169 image-rendering: crisp-edges;
170 }
171
172 /* ===== Label (handle pill + chat bubble) ===== */
173 .pet-label {
174 position: absolute;
175 left: 50%;
176 transform: translateX(-50%);
177 font-size: 11px;
178 font-weight: 700;
179 color: #fff;
180 text-shadow:
181 -1px -1px 0 #000,
182 1px -1px 0 #000,
183 -1px 1px 0 #000,
184 1px 1px 0 #000,
185 0 0 6px rgba(0, 0, 0, 0.85);
186 white-space: nowrap;
187 overflow: hidden;
188 text-overflow: ellipsis;
189 max-width: 240px;
190 pointer-events: none;
191 letter-spacing: 0.02em;
192 background: var(--color-bg-pill);
193 padding: 1px 6px;
194 border-radius: 999px;
195 backdrop-filter: blur(2px);
196 transition:
197 background-color 0.18s,
198 color 0.18s;
199 }
200 .pet-label.chatting {
201 background: rgba(255, 255, 255, 0.96);
202 color: #1a1a1a;
203 text-shadow: none;
204 font-weight: 600;
205 font-family: var(--font-comic);
206 border-radius: 6px;
207 padding: 3px 8px;
208 border: 2px solid #1a1a1a;
209 box-shadow: 2px 2px 0 #1a1a1a;
210 white-space: normal;
211 text-align: center;
212 line-height: 1.25;
213 min-width: 80px;
214 max-width: min(80ch, 540px);
215 display: -webkit-box;
216 -webkit-line-clamp: 3;
217 -webkit-box-orient: vertical;
218 overflow: hidden;
219 word-break: break-word;
220 }
221
222 /* ===== Animations (all use calc() with --anim-speed) ===== */
223 .pet:not(.sprite) .body {
224 transform-origin: 50% 100%;
225 animation: bob calc(0.42s / var(--anim-speed)) ease-in-out infinite;
226 }
227 .pet:not(.sprite).idle .body {
228 animation: breathe calc(2.6s / var(--anim-speed)) ease-in-out infinite;
229 }
230 .pet:not(.sprite).sleep .body {
231 animation: sleep-breathe calc(3.4s / var(--anim-speed)) ease-in-out
232 infinite;
233 }
234
235 @keyframes bob {
236 0%,
237 100% {
238 transform: translateY(0);
239 }
240 50% {
241 transform: translateY(-3px);
242 }
243 }
244 @keyframes breathe {
245 0%,
246 100% {
247 transform: scaleY(1) scaleX(1);
248 }
249 50% {
250 transform: scaleY(0.97) scaleX(1.02);
251 }
252 }
253 @keyframes sleep-breathe {
254 0%,
255 100% {
256 transform: scaleY(0.96) scaleX(1.04) translateY(2px);
257 }
258 50% {
259 transform: scaleY(0.92) scaleX(1.06) translateY(4px);
260 }
261 }
262
263 .pet.bunny .body {
264 animation: hop calc(0.55s / var(--anim-speed))
265 cubic-bezier(0.4, 0, 0.2, 1) infinite;
266 }
267 .pet.bunny.idle .body {
268 animation: breathe calc(2.6s / var(--anim-speed)) ease-in-out infinite;
269 }
270 .pet.bunny.sleep .body {
271 animation: sleep-breathe calc(3.4s / var(--anim-speed)) ease-in-out
272 infinite;
273 }
274 @keyframes hop {
275 0%,
276 100% {
277 transform: translateY(0) scaleY(1);
278 }
279 25% {
280 transform: translateY(-22px) scaleY(1.04);
281 }
282 50% {
283 transform: translateY(0) scaleY(0.94);
284 }
285 75% {
286 transform: translateY(0) scaleY(1.02);
287 }
288 }
289
290 .pet.duck .body {
291 animation: waddle calc(0.55s / var(--anim-speed)) ease-in-out infinite;
292 }
293 .pet.duck.idle .body {
294 animation: breathe calc(2.6s / var(--anim-speed)) ease-in-out infinite;
295 }
296 .pet.duck.sleep .body {
297 animation: sleep-breathe calc(3.4s / var(--anim-speed)) ease-in-out
298 infinite;
299 }
300 @keyframes waddle {
301 0%,
302 100% {
303 transform: rotate(-5deg) translateY(0);
304 }
305 50% {
306 transform: rotate(5deg) translateY(-1px);
307 }
308 }
309
310 .pet.goose .body {
311 animation: goose-strut calc(0.7s / var(--anim-speed)) ease-in-out
312 infinite;
313 }
314 .pet.goose.idle .body {
315 animation: breathe calc(2.6s / var(--anim-speed)) ease-in-out infinite;
316 }
317 .pet.goose.sleep .body {
318 animation: sleep-breathe calc(3.4s / var(--anim-speed)) ease-in-out
319 infinite;
320 }
321 @keyframes goose-strut {
322 0%,
323 100% {
324 transform: rotate(-3deg) translateY(0);
325 }
326 50% {
327 transform: rotate(3deg) translateY(-2px);
328 }
329 }
330
331 .tail-wag {
332 transform-origin: 0% 50%;
333 animation: wag calc(0.5s / var(--anim-speed)) ease-in-out infinite;
334 }
335 .tail-swish {
336 transform-origin: 0% 100%;
337 animation: swish calc(1.6s / var(--anim-speed)) ease-in-out infinite;
338 }
339 @keyframes wag {
340 0%,
341 100% {
342 transform: rotate(-15deg);
343 }
344 50% {
345 transform: rotate(15deg);
346 }
347 }
348 @keyframes swish {
349 0%,
350 100% {
351 transform: rotate(-8deg);
352 }
353 50% {
354 transform: rotate(20deg);
355 }
356 }
357
358 .leg-front {
359 transform-origin: 50% 0%;
360 animation: leg-a calc(0.42s / var(--anim-speed)) ease-in-out infinite;
361 }
362 .leg-back {
363 transform-origin: 50% 0%;
364 animation: leg-b calc(0.42s / var(--anim-speed)) ease-in-out infinite;
365 }
366 @keyframes leg-a {
367 0%,
368 100% {
369 transform: rotate(20deg);
370 }
371 50% {
372 transform: rotate(-20deg);
373 }
374 }
375 @keyframes leg-b {
376 0%,
377 100% {
378 transform: rotate(-20deg);
379 }
380 50% {
381 transform: rotate(20deg);
382 }
383 }
384 .pet.idle .leg-front,
385 .pet.idle .leg-back,
386 .pet.sleep .leg-front,
387 .pet.sleep .leg-back {
388 animation: none;
389 transform: rotate(0);
390 }
391
392 .eye-lid {
393 transform-origin: 50% 50%;
394 transform: scaleY(0);
395 animation: blink calc(4s / var(--anim-speed)) ease-in-out infinite;
396 }
397 @keyframes blink {
398 0%,
399 92%,
400 100% {
401 transform: scaleY(0);
402 }
403 94%,
404 96% {
405 transform: scaleY(1);
406 }
407 }
408 .pet.sleep .eye-lid {
409 transform: scaleY(1);
410 animation: none;
411 }
412
413 /* ===== Zzz, Heart, Hand ===== */
414 .zzz {
415 position: absolute;
416 top: -8px;
417 right: 8px;
418 width: 36px;
419 height: 36px;
420 opacity: 0;
421 pointer-events: none;
422 }
423 .pet.sleep .zzz {
424 animation: zzz-float 2.2s ease-in-out infinite;
425 }
426 @keyframes zzz-float {
427 0% {
428 opacity: 0;
429 transform: translate(0, 0) scale(0.6);
430 }
431 25% {
432 opacity: 1;
433 }
434 100% {
435 opacity: 0;
436 transform: translate(14px, -28px) scale(1.1);
437 }
438 }
439
440 .heart {
441 position: absolute;
442 top: 8px;
443 left: 50%;
444 width: 26px;
445 height: 26px;
446 transform: translateX(-50%) scale(0);
447 pointer-events: none;
448 }
449 .pet.happy .heart {
450 animation: heart-pop 1.2s ease-out forwards;
451 }
452 @keyframes heart-pop {
453 0% {
454 transform: translate(-50%, 0) scale(0);
455 opacity: 0;
456 }
457 20% {
458 transform: translate(-50%, -10px) scale(1.3);
459 opacity: 1;
460 }
461 100% {
462 transform: translate(-50%, -44px) scale(0.85);
463 opacity: 0;
464 }
465 }
466
467 .pet-rub {
468 position: absolute;
469 top: -22px;
470 left: 50%;
471 width: 36px;
472 height: 36px;
473 margin-left: -18px;
474 pointer-events: none;
475 opacity: 0;
476 will-change: transform;
477 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45));
478 }
479 .pet.petted .pet-rub {
480 animation: pet-rub 1.8s ease-in-out forwards;
481 }
482 @keyframes pet-rub {
483 0% {
484 opacity: 0;
485 transform: translateY(8px) rotate(-12deg);
486 }
487 10% {
488 opacity: 1;
489 transform: translateY(2px) rotate(-12deg);
490 }
491 25% {
492 transform: translateY(-4px) rotate(8deg);
493 }
494 40% {
495 transform: translateY(2px) rotate(-12deg);
496 }
497 55% {
498 transform: translateY(-4px) rotate(8deg);
499 }
500 70% {
501 transform: translateY(2px) rotate(-12deg);
502 }
503 85% {
504 transform: translateY(-4px) rotate(8deg);
505 opacity: 1;
506 }
507 100% {
508 transform: translateY(-12px) rotate(0deg);
509 opacity: 0;
510 }
511 }
512
513 .pet.spawning {
514 animation: spawn-in 0.6s ease-out;
515 }
516 @keyframes spawn-in {
517 0% {
518 opacity: 0;
519 }
520 100% {
521 opacity: 1;
522 }
523 }
524
525 /* ===== Spawn star burst ===== */
526 .fx-spawn-stars {
527 position: fixed;
528 pointer-events: none;
529 z-index: 300;
530 overflow: visible;
531 }
532 .star-particle {
533 position: absolute;
534 animation: star-burst 0.9s ease-out forwards;
535 pointer-events: none;
536 line-height: 1;
537 user-select: none;
538 }
539 @keyframes star-burst {
540 0% {
541 opacity: 1;
542 transform: translate(0, 0) scale(1);
543 }
544 100% {
545 opacity: 0;
546 transform: translate(var(--star-dx, 0), var(--star-dy, -80px))
547 scale(0.2);
548 }
549 }
550
551 /* ===== Bang command effects (scene-wide overlays) ===== */
552 @keyframes fx-fade-out {
553 0% {
554 opacity: 1;
555 }
556 100% {
557 opacity: 0;
558 }
559 }
560
561 .fx-party {
562 position: fixed;
563 inset: 0;
564 pointer-events: none;
565 z-index: 200;
566 mix-blend-mode: screen;
567 animation: fx-fade-out 0.6s ease-out 7s forwards;
568 }
569 .party-light {
570 position: absolute;
571 width: 60vw;
572 height: 60vw;
573 border-radius: 50%;
574 filter: blur(48px);
575 opacity: 0.85;
576 will-change: transform;
577 }
578 .party-1 {
579 background: radial-gradient(circle, #ff3a8e 0%, transparent 60%);
580 animation: party-orbit 3.6s linear infinite;
581 }
582 .party-2 {
583 background: radial-gradient(circle, #3affc8 0%, transparent 60%);
584 animation: party-orbit 3.6s linear infinite -0.9s;
585 }
586 .party-3 {
587 background: radial-gradient(circle, #ffd93d 0%, transparent 60%);
588 animation: party-orbit 3.6s linear infinite -1.8s;
589 }
590 .party-4 {
591 background: radial-gradient(circle, #6a8eff 0%, transparent 60%);
592 animation: party-orbit 3.6s linear infinite -2.7s;
593 }
594 @keyframes party-orbit {
595 0% {
596 transform: translate(-30vw, -20vh);
597 }
598 25% {
599 transform: translate(70vw, -30vh);
600 }
601 50% {
602 transform: translate(80vw, 60vh);
603 }
604 75% {
605 transform: translate(-30vw, 50vh);
606 }
607 100% {
608 transform: translate(-30vw, -20vh);
609 }
610 }
611
612 .fx-snow,
613 .fx-confetti,
614 .fx-hearts,
615 .fx-rain {
616 position: fixed;
617 inset: 0;
618 pointer-events: none;
619 overflow: hidden;
620 z-index: 150;
621 animation: fx-fade-out 1s ease-out forwards;
622 }
623 .fx-snow {
624 animation-delay: 13s;
625 }
626 .fx-confetti {
627 animation-delay: 7s;
628 }
629 .fx-hearts {
630 animation-delay: 6s;
631 }
632 .fx-rain {
633 animation-delay: 11s;
634 }
635
636 .raindrop {
637 position: absolute;
638 top: -30px;
639 width: 2px;
640 background: linear-gradient(transparent, #6cb8ff 60%, #b3dbff);
641 border-radius: 1px;
642 animation-name: rain-fall;
643 animation-timing-function: linear;
644 animation-iteration-count: 1;
645 animation-fill-mode: forwards;
646 }
647 @keyframes rain-fall {
648 0% {
649 transform: translateY(0);
650 }
651 100% {
652 transform: translateY(110vh);
653 }
654 }
655
656 .snowflake {
657 position: absolute;
658 top: -30px;
659 color: white;
660 text-shadow:
661 0 0 6px rgba(150, 200, 255, 0.7),
662 0 0 12px rgba(255, 255, 255, 0.4);
663 animation-name: snow-fall;
664 animation-timing-function: linear;
665 animation-iteration-count: 1;
666 animation-fill-mode: forwards;
667 user-select: none;
668 }
669 @keyframes snow-fall {
670 0% {
671 transform: translateY(0) translateX(0);
672 }
673 100% {
674 transform: translateY(110vh) translateX(var(--sway, 0));
675 }
676 }
677
678 .confetti-piece {
679 position: absolute;
680 top: -30px;
681 border-radius: 2px;
682 animation-name: confetti-fall;
683 animation-timing-function: linear;
684 animation-iteration-count: 1;
685 animation-fill-mode: forwards;
686 }
687 @keyframes confetti-fall {
688 0% {
689 transform: translate(0, 0) rotate(0deg);
690 }
691 100% {
692 transform: translate(var(--sway, 0), 110vh)
693 rotate(var(--spin, 720deg));
694 }
695 }
696
697 .heart-piece {
698 position: absolute;
699 bottom: -40px;
700 font-size: 24px;
701 animation-name: heart-rise;
702 animation-timing-function: ease-out;
703 animation-iteration-count: 1;
704 animation-fill-mode: forwards;
705 filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
706 user-select: none;
707 }
708 @keyframes heart-rise {
709 0% {
710 transform: translate(0, 0) scale(0.4);
711 opacity: 0;
712 }
713 15% {
714 opacity: 1;
715 }
716 100% {
717 transform: translate(var(--sway, 0), -110vh)
718 scale(var(--end-scale, 1.1));
719 opacity: 0;
720 }
721 }
722
723 .fx-barrelroll {
724 transform-origin: 50% 50%;
725 animation: barrelroll 1.4s cubic-bezier(0.5, 0, 0.5, 1) forwards;
726 }
727 @keyframes barrelroll {
728 0% {
729 transform: rotate(0deg);
730 }
731 100% {
732 transform: rotate(360deg);
733 }
734 }
735
736 /* !fireworks — scene-wide bursts */
737 .fx-fireworks {
738 position: fixed;
739 inset: 0;
740 pointer-events: none;
741 overflow: hidden;
742 z-index: 160;
743 animation: fx-fade-out 1s ease-out 7.5s forwards;
744 }
745 .firework-particle {
746 position: absolute;
747 width: 6px;
748 height: 6px;
749 border-radius: 50%;
750 box-shadow: 0 0 12px 2px rgba(255, 255, 255, 0.55);
751 opacity: 0;
752 animation-name: firework-burst;
753 animation-timing-function: cubic-bezier(0.25, 0.8, 0.35, 1);
754 animation-fill-mode: forwards;
755 }
756 @keyframes firework-burst {
757 0% {
758 transform: translate(0, 0) scale(0.5);
759 opacity: 0;
760 }
761 8% {
762 opacity: 1;
763 }
764 100% {
765 transform: translate(var(--dx), var(--dy)) scale(0.4);
766 opacity: 0;
767 }
768 }
769
770 .fx-quake {
771 animation: quake 0.55s linear;
772 }
773 @keyframes quake {
774 0%,
775 100% {
776 transform: translate(0, 0);
777 }
778 8% {
779 transform: translate(-12px, 5px);
780 }
781 18% {
782 transform: translate(10px, -4px);
783 }
784 28% {
785 transform: translate(-8px, 6px);
786 }
787 38% {
788 transform: translate(7px, -3px);
789 }
790 48% {
791 transform: translate(-6px, 5px);
792 }
793 58% {
794 transform: translate(5px, -4px);
795 }
796 68% {
797 transform: translate(-4px, 3px);
798 }
799 78% {
800 transform: translate(3px, -2px);
801 }
802 88% {
803 transform: translate(-1px, 1px);
804 }
805 }
806
807 /* Jump */
808 .pet.jumping .pet-flip {
809 animation: jump 0.75s cubic-bezier(0.3, 0, 0.3, 1);
810 }
811 @keyframes jump {
812 0% {
813 translate: 0 0;
814 }
815 45% {
816 translate: 0 -58px;
817 }
818 100% {
819 translate: 0 0;
820 }
821 }
822
823 /* Dance */
824 .pet.dancing .pet-flip {
825 animation: dance 3s cubic-bezier(0.45, 0, 0.45, 1);
826 }
827 @keyframes dance {
828 0% {
829 translate: 0 0;
830 rotate: 0deg;
831 scale: 1;
832 }
833 5% {
834 translate: -22px -2px;
835 rotate: -32deg;
836 scale: 1;
837 }
838 10% {
839 translate: 18px -16px;
840 rotate: 28deg;
841 scale: 1.06;
842 }
843 15% {
844 translate: -22px -2px;
845 rotate: -32deg;
846 scale: 1;
847 }
848 20% {
849 translate: 18px -16px;
850 rotate: 28deg;
851 scale: 1.06;
852 }
853 25% {
854 translate: 0 0;
855 rotate: 0deg;
856 scale: 1;
857 }
858 35% {
859 translate: 0 -55px;
860 rotate: 360deg;
861 scale: 1.12;
862 }
863 45% {
864 translate: 0 -25px;
865 rotate: 720deg;
866 scale: 1;
867 }
868 52% {
869 translate: -22px 0;
870 rotate: -32deg;
871 scale: 1;
872 }
873 58% {
874 translate: 20px -14px;
875 rotate: 30deg;
876 scale: 1.06;
877 }
878 64% {
879 translate: -22px 0;
880 rotate: -32deg;
881 scale: 1;
882 }
883 70% {
884 translate: 18px -14px;
885 rotate: 28deg;
886 scale: 1.06;
887 }
888 76% {
889 translate: 0 -2px;
890 rotate: 0deg;
891 scale: 1;
892 }
893 86% {
894 translate: 0 -40px;
895 rotate: 380deg;
896 scale: 1.18;
897 }
898 96% {
899 translate: 0 -2px;
900 rotate: 0deg;
901 scale: 0.94;
902 }
903 100% {
904 translate: 0 0;
905 rotate: 0deg;
906 scale: 1;
907 }
908 }
909
910 /* Barrel roll — Star Fox style: arc up while rotating 360 */
911 .pet.rolling .pet-flip {
912 animation: barrel-roll 0.75s cubic-bezier(0.4, 0, 0.4, 1);
913 }
914 @keyframes barrel-roll {
915 0% {
916 translate: 0 0;
917 rotate: 0deg;
918 }
919 35% {
920 translate: 0 -32px;
921 rotate: 130deg;
922 }
923 65% {
924 translate: 0 -32px;
925 rotate: 230deg;
926 }
927 100% {
928 translate: 0 0;
929 rotate: 360deg;
930 }
931 }
932
933 /* Teleport — fade out, JS moves the pet, fade in */
934 .pet.teleporting {
935 animation: teleport 0.7s ease-in-out;
936 }
937 @keyframes teleport {
938 0% {
939 opacity: 1;
940 }
941 35% {
942 opacity: 0;
943 }
944 65% {
945 opacity: 0;
946 }
947 100% {
948 opacity: 1;
949 }
950 }
951
952 /* Fart cloud */
953 .fart {
954 position: absolute;
955 bottom: 4px;
956 width: 90px;
957 height: 70px;
958 opacity: 0;
959 pointer-events: none;
960 }
961 .fart svg {
962 width: 100%;
963 height: 100%;
964 overflow: visible;
965 }
966 .fart-puff {
967 transform-box: fill-box;
968 transform-origin: center;
969 animation-name: fart-puff;
970 animation-timing-function: ease-out;
971 animation-fill-mode: forwards;
972 opacity: 0;
973 }
974 .pet.farting .fart {
975 opacity: 1;
976 }
977 @keyframes fart-puff {
978 0% {
979 transform: translate(0, 0) scale(var(--fart-start, 0.2));
980 opacity: 0;
981 }
982 18% {
983 opacity: var(--fart-peak, 0.85);
984 }
985 100% {
986 transform: translate(var(--fart-dx, 0), var(--fart-dy, -25px))
987 scale(var(--fart-end, 1.4));
988 opacity: 0;
989 }
990 }
991
992 /* Per-pet smoke — wispy gray puffs from upper face */
993 .smoke {
994 position: absolute;
995 top: -14px;
996 left: 50%;
997 margin-left: -35px;
998 width: 70px;
999 height: 90px;
1000 opacity: 0;
1001 pointer-events: none;
1002 }
1003 .smoke svg {
1004 width: 100%;
1005 height: 100%;
1006 overflow: visible;
1007 }
1008 .smoke-puff {
1009 transform-box: fill-box;
1010 transform-origin: center;
1011 animation-name: smoke-puff;
1012 animation-timing-function: ease-out;
1013 animation-fill-mode: forwards;
1014 opacity: 0;
1015 }
1016 .pet.smoking .smoke {
1017 opacity: 1;
1018 }
1019 @keyframes smoke-puff {
1020 0% {
1021 transform: translate(0, 0) scale(var(--smoke-start, 0.2));
1022 opacity: 0;
1023 }
1024 20% {
1025 opacity: var(--smoke-peak, 0.7);
1026 }
1027 100% {
1028 transform: translate(var(--smoke-dx, 0), var(--smoke-dy, -55px))
1029 scale(var(--smoke-end, 1.8));
1030 opacity: 0;
1031 }
1032 }