revert: remove end-of-list animation (mobile perf regression) (#381)

The animation created 576 DOM elements (3 waves × 16 dots × 12 ghosts)
each with CSS animations and blur filters running continuously.
This caused severe performance issues on mobile devices.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 911af1eb 79f39009

Changed files
-130
frontend
src
lib
components
routes
-128
frontend/src/lib/components/EndOfList.svelte
··· 1 - <script lang="ts"> 2 - import { fade } from "svelte/transition"; 3 - 4 - interface Props { 5 - message?: string; 6 - } 7 - 8 - let { message = "you've reached the end!" }: Props = $props(); 9 - 10 - const count = 16; 11 - const ghostCount = 12; // trailing ghosts per dot 12 - </script> 13 - 14 - <div class="end-of-list" in:fade={{ duration: 400 }}> 15 - <div class="canvas"> 16 - {#each Array(count) as _, i} 17 - {#each Array(ghostCount) as _, g} 18 - <i 19 - style="--i:{i}; --g:{g}" 20 - class:ghost={g > 0} 21 - ></i> 22 - {/each} 23 - {/each} 24 - <!-- second wave: 1/3 phase offset --> 25 - {#each Array(count) as _, i} 26 - {#each Array(ghostCount) as _, g} 27 - <i 28 - style="--i:{i}; --g:{g}" 29 - class:ghost={g > 0} 30 - class="phase2" 31 - ></i> 32 - {/each} 33 - {/each} 34 - <!-- third wave: 2/3 phase offset --> 35 - {#each Array(count) as _, i} 36 - {#each Array(ghostCount) as _, g} 37 - <i 38 - style="--i:{i}; --g:{g}" 39 - class:ghost={g > 0} 40 - class="phase3" 41 - ></i> 42 - {/each} 43 - {/each} 44 - </div> 45 - <p class="message">{message}</p> 46 - </div> 47 - 48 - <style> 49 - .end-of-list { 50 - display: flex; 51 - flex-direction: column; 52 - align-items: center; 53 - gap: 1rem; 54 - padding: 2rem 1rem; 55 - margin-top: 1rem; 56 - } 57 - 58 - .canvas { 59 - position: relative; 60 - width: 100px; 61 - height: 32px; 62 - } 63 - 64 - i { 65 - --wave-color: var(--accent); 66 - position: absolute; 67 - left: calc(var(--i) * 6px); 68 - top: 50%; 69 - width: 2px; 70 - height: 2px; 71 - background: var(--wave-color); 72 - border-radius: 50%; 73 - opacity: 0.6; 74 - box-shadow: 0 0 2px var(--wave-color); 75 - animation: drift 2s ease-in-out infinite; 76 - /* base delay for wave + ghost offset for trail */ 77 - animation-delay: calc((var(--i) * -0.15s) + (var(--g) * 0.07s)); 78 - } 79 - 80 - i.ghost { 81 - opacity: calc(0.35 - (var(--g) * 0.025)); 82 - width: 1px; 83 - height: 1px; 84 - border-radius: 50%; 85 - filter: blur(calc(var(--g) * 0.2px)); 86 - box-shadow: none; 87 - } 88 - 89 - @keyframes drift { 90 - 0%, 91 - 100% { 92 - transform: translateY(8px); 93 - } 94 - 50% { 95 - transform: translateY(-8px); 96 - } 97 - } 98 - 99 - /* 2s duration, so 0.667s = 1/3 phase, 1.333s = 2/3 phase */ 100 - /* each wave gets a color variation mixed from the accent */ 101 - i.phase2 { 102 - --wave-color: color-mix(in oklch, var(--accent) 70%, #fff); 103 - animation-delay: calc( 104 - (var(--i) * -0.15s) + (var(--g) * 0.07s) - 0.667s 105 - ); 106 - } 107 - 108 - i.phase3 { 109 - --wave-color: color-mix(in oklch, var(--accent) 60%, #000); 110 - animation-delay: calc( 111 - (var(--i) * -0.15s) + (var(--g) * 0.07s) - 1.333s 112 - ); 113 - } 114 - 115 - .message { 116 - margin: 0; 117 - color: var(--text-tertiary); 118 - font-size: 0.8rem; 119 - letter-spacing: 0.02em; 120 - } 121 - 122 - @media (prefers-reduced-motion: reduce) { 123 - i { 124 - animation: none; 125 - opacity: 0.5; 126 - } 127 - } 128 - </style>
-2
frontend/src/routes/+page.svelte
··· 4 4 import TrackItem from '$lib/components/TrackItem.svelte'; 5 5 import Header from '$lib/components/Header.svelte'; 6 6 import WaveLoading from '$lib/components/WaveLoading.svelte'; 7 - import EndOfList from '$lib/components/EndOfList.svelte'; 8 7 import { player } from '$lib/player.svelte'; 9 8 import { queue } from '$lib/queue.svelte'; 10 9 import { tracksCache } from '$lib/tracks.svelte'; ··· 107 106 /> 108 107 {/each} 109 108 </div> 110 - <EndOfList /> 111 109 {/if} 112 110 </section> 113 111 </main>