# Plan — Refactor y unificación del frontend (web-player) > Plan de trabajo para agentes/IA con contexto limitado (~128k). Sustituye a > `design-system-refactor-plan.md` (cuyo contenido ya está mayormente aplicado). > Basado en una lectura completa de **todos** los `.svelte`, `.css` y `.ts` de cliente > de `web-player/src` (2026-09-18, rama `design-prot`). ## Cómo usar este documento 1. **Lee siempre la sección A (Contexto común)** — es corta y todas las partes la asumen. 2. Elige **una parte** (P0…P14). Cada parte indica: objetivo, prerrequisitos, archivos a leer, tareas, API exacta de lo que se crea, tabla de mapeo y criterios de cierre. 3. Una parte = un commit/PR. No mezcles partes. 4. Si una parte necesita algo de otra (p. ej. un token o un helper) y **no existe todavía**, créalo tú siguiendo la especificación de la sección B — están descritos con ruta y API exactas para que cualquier parte pueda crearlos de forma idéntica. 5. Al terminar una parte, marca su casilla en la sección D (Registro). Orden recomendado (las flechas son dependencias **blandas**, ver punto 4): ``` P0 → P1 → P2 ↘ P3 (Artwork/Avatar) ─┐ ↘ P4 (Botones) ───────┤ ↘ P5 (Helpers TS) ─────┼→ P6 (Menús) → P7 (Filas) → P8 (Tablas) → P9 (Tarjetas) └→ P10 (Cabeceras) → P11 (Formularios) → P12 (Chrome) → P13 (Páginas) → P14 (Barrido final) ``` --- ## A. Contexto común (leer siempre) ### A.1 Stack y reglas del proyecto (de `web-player/CLAUDE.md`) - SvelteKit 2 + **Svelte 5 runes** (`$props()` con `interface Props`, `$state`, `$derived`, `$effect`; nada de `export let` ni `$:`; `{#each}` siempre con clave). - **Tailwind 4**: no hay `tailwind.config.js`; la config vive en `src/routes/layout.css` (`@theme`, `@utility`). Tokens de color crudos en `src/lib/theme/theme.css` (`--mf-*`), tokens dinámicos por hue en `src/lib/theme/tokens.ts` (`buildThemeTokens`). - Sin comentarios en código salvo imprescindibles. - Cierre de cada parte: `npm run check` y `npm run lint` **limpios** (desde `web-player/`). - Iconos: `@lucide/svelte/icons/`. ### A.2 Gotchas conocidos (no reintroducirlos) 1. **Tokens animados por hue** (`--mf-bg`, `--mf-elevated`, `--mf-panel-bg`, `--mf-bar-bg`, `--mf-hairline`, `--mf-hero-bg`, `--mf-modal-glow`, `--mf-spotlight-bg`, `--mf-logo-grad`, `--mf-cover-grad`, `--mf-accent*`, `--mf-ambient`) se aplican como **estilo inline** en `` desde `+layout.svelte`. Un override en `:root[data-theme='light']` **no gana**: la variante clara va en `buildThemeTokens()`. 2. **Conflictos de clases Tailwind**: nunca `class="bg-a {cond ? 'bg-b' : ''}"`. Siempre mutuamente excluyente: `class={cond ? 'bg-b' : 'bg-a'}`. Overrides de la clase base de un componente compartido → prop/variante, no `!important`. 3. Texto sin clase de tamaño hereda 16px del navegador. Todo texto visible debe tener una receta tipográfica (ver B.1.6). 4. `--mf-ink` es "texto sobre CTA" y se invierte en claro; `--mf-tile-shade`, `--mf-cover-grad`, `--mf-liked-grad` son "arte" (no invertir). 5. Referencia visual: **Inicio** (`src/routes/+page.svelte`). Un cambio de refactor no debe cambiar el aspecto salvo donde la parte lo diga explícitamente (columna "Δ"). ### A.3 Verificación de cada parte ```sh cd web-player npm run check npm run lint ``` Visual (Browser pane, `.claude/launch.json` → `musify-web`; login de dev en la memoria del proyecto): abre cada página tocada **y** Inicio; prueba estado activo (una canción sonando), hover, móvil (375px) y modo claro (menú de cuenta → "Modo blanco"). ### A.4 Línea base medida (para saber si avanzas) | Métrica | Hoy | | ------------------------------------------------- | --------------------------------------- | | Componentes `.svelte` (lib + rutas `components/`) | 61 | | Líneas `.svelte`/`.css`/`.ts` cliente | ~8.700 | | Clases arbitrarias `x-[…]` | 44 | | Espaciados fraccionarios (`h-3.75`, `gap-2.75`…) | ~190 apariciones, 60 valores distintos | | Radios distintos usados | 13 (6 tokens + 7 de Tailwind por defecto) | | `strokeWidth` distintos | 13 valores | | Atributos `style="…"` | 24 | | Pluralizaciones a mano `=== 1 ?` | 15 | | Errores de `eslint` | 2 (`liked/+page.svelte`) | Objetivo global: ~61 → ~45 componentes, −1.500/−2.000 líneas, 0 arbitrarios fuera de una lista blanca, 1 escala por dimensión. --- ## B. Especificaciones compartidas (fuente única) Cualquier parte que necesite algo de aquí y no exista, lo crea **exactamente así**. ### B.1 Tokens (en `src/routes/layout.css`, dentro de `@theme`) Tailwind 4 genera utilidades a partir de estos namespaces: `--radius-*` → `rounded-*`, `--spacing-*` → `size-* w-* h-* p-* m-* gap-*`, `--container-*` → `max-w-*`, `--tracking-*` → `tracking-*`, `--leading-*` → `leading-*`. #### B.1.1 Radios (`--radius-*`) | Token | Valor | Para qué | | ----------------- | ---------- | ----------------------------------------- | | `rounded-tag` | `0.25rem` | badges, celdas mini (ExplicitBadge, índice) | | `rounded-thumb` | `0.5rem` | carátulas ≤ 36px, logo | | `rounded-control` | `0.625rem` | inputs, filas, items de menú, botones | | `rounded-art` | `0.8125rem`| carátulas 42–88px, tarjetas de grid | | `rounded-art-lg` | `1.125rem` | carátulas ≥ 136px, dropzones | | `rounded-panel` | `1.125rem` | contenedores, menús, modales, tiles | | `rounded-panel-lg`| `1.375rem` | hero, spotlight, "mejor resultado" | | `rounded-full` | (Tailwind) | pastillas, avatares, botones redondos | Eliminar `--radius-art-round` (0 usos). Mapeo de lo existente: | Hoy | px | Usos | Pasa a | Δ | | ---------------------------- | -- | ---- | ----------------- | ---- | | `rounded-sm`, `rounded` | 4 | 2 | `rounded-tag` | 0 | | `rounded-md` | 6 | 1 | `rounded-thumb` | +2px | | `rounded-lg` | 8 | 4 | `rounded-thumb` | 0 | | `rounded-xl` | 12 | 6 | `rounded-control` | −2px | | `rounded-2xl` | 16 | 8 | `rounded-art-lg` (arte) / `rounded-panel` (contenedor) | +2px | | `rounded-3xl` | 24 | 1 | `rounded-panel-lg`| −2px | #### B.1.2 Tamaños de carátula/arte (`--spacing-cover-*`) | Token | Valor | Usos actuales que absorbe (Δ) | | ---------------- | ------------------------------ | --------------------------------------------------------------- | | `cover-xs` | `2.25rem` (36) | thumbs de menús `h-9`; sidebar `h-8.5` (+2) | | `cover-sm` | `2.625rem` (42) | filas de tabla `h-10.5`; cola y "Populares" `h-10` (+2); player móvil `h-11` (−2) | | `cover-md` | `3rem` (48) | player escritorio `md:h-12` | | `cover-lg` | `3.5rem` (56) | "Continuar" `h-14`; filas de búsqueda `h-13` (+4); tarjeta playlist de Inicio `h-15.5` (−6) | | `cover-xl` | `5.5rem` (88) | "Mejor resultado" `h-22` | | `cover-2xl` | `8.5rem` (136) | spotlight de Inicio `h-34` | | `cover-hero` | `9rem` (144) | cabeceras de colección, dropzones, avatar de perfil | | `cover-hero-sm` | `10.25rem` (164) | cabeceras de colección a partir de `sm:` | Uso: `size-cover-sm` (Tailwind 4 `size-*` = `width`+`height`). **Nunca** `h-X w-X` en pareja: siempre `size-*`. #### B.1.3 Iconos (`--spacing-icon-*`) y trazo | Token | Valor | Absorbe | | ---------- | ---------- | ---------------------------------------------- | | `icon-xs` | `0.875rem` | `h-3.5` (14) | | `icon-sm` | `1rem` | `h-3.75` (15), `h-4` (16), `h-4.25` (17) | | `icon-md` | `1.125rem` | `h-4.5` (18) | | `icon-lg` | `1.25rem` | `h-5` (20), `h-6` (24, login móvil) | | `icon-xl` | `2.25rem` | `h-9`/`h-10` (estados vacíos, dropzone, Me gusta) | Trazo: en `layout.css`, fuera de `@theme`: ```css @layer base { svg.lucide { stroke-width: 1.8; } } @utility stroke-thin { stroke-width: 1.3; } @utility stroke-regular { stroke-width: 1.8; } @utility stroke-bold { stroke-width: 2.5; } ``` La propiedad CSS `stroke-width` gana al atributo de presentación que pone Lucide, así que el prop `strokeWidth` deja de tener efecto: **se elimina de todos los usos**. Mapeo: `1–1.4` → `stroke-thin`, `1.5–2` → (nada, default), `2.5–3` → `stroke-bold`, `strokeWidth={0}` + `fill` → clase `stroke-0`. Verifica en el navegador (DevTools → Computed `stroke-width`) que el default aplica antes de migrar en masa. #### B.1.4 Anchos (`--container-*`) y layout | Token | Valor | Absorbe | | --------------------- | -------- | --------------------------------------------------------- | | `max-w-prose-sm` | `26rem` | `max-w-105` (error), `max-w-108` (hero p), `max-w-110`, `max-w-115` (sin resultados) | | `max-w-hero` | `34rem` | `max-w-135` (hero, seekbar) | | `max-w-search` | `30rem` | `max-w-120` (buscador TopBar) | | `max-w-cta` | `20rem` | `max-w-80` (botones login) | Ya existen y se mantienen en `theme.css`: `--mf-sidebar-w`, `--mf-queue-w`, `--mf-nav-h`, `--mf-player-h`, `--mf-safe-b`. Añadir: ```css --mf-player-side-w: 16rem; /* w-64 de PlayerBar/PlayerExtras */ --mf-menu-w: 15rem; /* w-60 de menús contextuales (= MENU_WIDTH 240) */ --mf-submenu-w: 16rem; /* w-64 (= SUBMENU_WIDTH 256) */ --mf-range-h: 3.5px; /* barra de progreso y volumen */ --mf-range-thumb: 11px; --mf-volume-w: 84px; ``` Las constantes TS `MENU_WIDTH`/`SUBMENU_WIDTH` (duplicadas hoy en `TrackContextMenu` y `AlbumContextMenu`) pasan a `src/lib/config.ts` con un comentario-no: el nombre basta. #### B.1.5 Capas (`z-index`) En `theme.css` `:root`: `--z-raised: 10; --z-sticky: 20; --z-backdrop: 30; --z-menu: 40; --z-submenu: 50; --z-modal: 60; --z-grain: 80;`. Uso: `z-(--z-menu)`. Mapeo 1:1 con los valores actuales (`z-10`,`z-20`,`z-30`,`z-40`,`z-50`,`z-[60]`, `body::after` 80). Nota: `MobileHeader` y `PlayerDock` usan `z-40` pero son "sticky", no menú → `--z-sticky` (verificar que el menú de cuenta sigue encima del header: menú 40 > sticky 20). #### B.1.6 Tipografía (recetas `@utility` en `layout.css`) Escala tipográfica = la de Tailwind (`text-xs`…`text-4xl`), **sin tamaños arbitrarios**. Añadir en `@theme`: `--tracking-display: -0.02em; --tracking-eyebrow: 0.1em; --leading-body: 1.65;`. Recetas (el color **no** va en la receta, se añade aparte): ```css @utility text-display-1 { @apply font-display text-3xl font-medium tracking-tight sm:text-4xl; } @utility text-display-2 { @apply font-display text-2xl font-medium tracking-tight; } @utility text-display-3 { @apply font-display text-xl font-medium tracking-display; } @utility text-display-4 { @apply font-display text-lg font-medium tracking-tight; } @utility text-eyebrow { @apply text-xs font-medium tracking-widest uppercase; } @utility text-body { @apply text-sm leading-body; } @utility text-count { @apply text-xs tabular-nums; } ``` > Si `@apply` con variante `sm:` dentro de `@utility` no compila, usa > `@media (width >= 40rem) { font-size: var(--text-4xl); line-height: var(--text-4xl--line-height); }` > como hace `page-x`. Mapeo de lo existente: | Patrón hoy | Receta | | -------------------------------------------------------------------------------------------- | ------------------------------ | | `font-display text-3xl … sm:text-4xl` (PageHeader, CollectionHeader, hero Inicio, h1 búsqueda `text-2xl sm:text-3xl font-semibold tracking-[-0.03em]`) | `text-display-1` (Δ búsqueda: +1 paso) | | `font-display text-2xl` (stats Inicio, título spotlight, "¡Subida!") | `text-display-2` | | `font-display text-xl … tracking-[-0.02em]` (Modal, EmptyState, +error, "Sin resultados", diálogos de borrado) | `text-display-3` (todos `font-medium`) | | `font-display text-lg` (SectionHeading, "Mejor resultado", Queue "En cola" `text-base`) | `text-display-4` | | `text-xs font-medium tracking-widest uppercase` (+ variantes `tracking-[0.14em]`, `[0.16em]`, `tracking-wider`, `text-sm font-semibold`) | `text-eyebrow` | | `text-sm leading-[1.65] text-fg-2` | `text-body text-fg-2` | | `text-xs text-muted tabular-nums` | `text-count text-muted` | | `tracking-[-0.01em]` (MixTile) | quitar | #### B.1.7 Recetas de superficie y movimiento (`@utility` en `layout.css`) ```css @utility theme-transition { transition: background 0.5s ease-out; } /* 8× transition-[background] duration-500 */ @utility glass-bar { /* PlayerBar ×2, TabsBar */ background-image: var(--mf-bar-bg); border-top: 1px solid var(--mf-hairline); backdrop-filter: blur(24px); transition: background 0.5s ease-out; } @utility grid-cards { /* MediaGrid por defecto: 150/165 */ display: grid; gap: --spacing(3); grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr)); @media (width >= 40rem) { gap: --spacing(4); grid-template-columns: repeat(auto-fill, minmax(10.3125rem, 1fr)); } } @utility grid-cards-lg { /* /playlists y /u/[id]: 180/210 */ display: grid; gap: --spacing(3); grid-template-columns: repeat(auto-fill, minmax(11.25rem, 1fr)); @media (width >= 40rem) { gap: --spacing(4); grid-template-columns: repeat(auto-fill, minmax(13.125rem, 1fr)); } } @utility grid-tiles { /* mixes de Inicio: 164 */ display: grid; gap: --spacing(5); grid-template-columns: repeat(auto-fill, minmax(10.25rem, 1fr)); } @utility grid-wide { /* playlists de Inicio (258) y géneros (268) → 16.5rem, Δ ±6px */ display: grid; gap: --spacing(3.5); grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr)); } ``` `.glass-panel` ya existe; `Modal` debe usarla en lugar de su `style=` inline duplicado. Stagger (sustituye los 5 `style="animation-delay:…"` con 40 **o** 45ms): ```css .animate-enter { animation-delay: calc(min(var(--i, 0), 10) * 45ms); } ``` Uso: `style="--i:{i}"`. Único `style=` permitido junto con los de B.4. #### B.1.8 Colores: limpieza - Borrar utilidades sin uso: `--color-accent-dim`, `--color-accent-glow`, `--color-accent-hair`, `--color-accent-line`, `--color-accent-muted` (0 usos como clase; `--mf-accent-dim` sí se usa en `::selection`, se mantiene la variable `--mf-`). - `--color-on-accent` es alias de `--mf-on-art` (igual que `--color-on-art`) → borrar y usar `on-art`. `--color-hover` = `--mf-surface-2` (igual que `surface-2`) → mantener **solo** `hover` como semántico de hover y dejar `surface-2` para fondos en reposo. - `--mf-sidebar-bg` no lo usa ningún componente (Sidebar usa `bg-bg`) → borrar de `theme.css` y de `buildThemeTokens`. - Botón: `--mf-btn-bg` = `--mf-surface-2`; `--mf-btn-secondary-bg` solo difiere en modo claro. Exponer en `@theme` `--color-btn`, `--color-btn-hover`, `--color-btn-glass`, `--color-btn-glass-hover` y eliminar los `bg-[var(--mf-btn-…)]` de `Button.svelte`. - Scrollbar: `rgba(255,255,255,.18/.32)` fijo en `layout.css` (roto en claro) → tokens `--mf-scrollbar`, `--mf-scrollbar-hover` con variante en `:root[data-theme='light']`. - `MixTile`: `to-black/42`, `bg-ink/76`, `border-on-art/12` → `to-scrim/40`, receta de `PlayButton` (B.3). ### B.2 Helpers TypeScript (fuente única) | Archivo (nuevo) | API | Sustituye | | ----------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | | `src/lib/actions/pressable.ts` | `pressable(node, fn)` → añade `role="button"`, `tabindex=0`, click + Enter/Espacio | 7 copias de `onkeydown` Enter/Space (Inicio ×5, Queue, TrackTitleCell) | | `src/lib/actions/clickOutside.ts` | `clickOutside(node, fn)` | `onDocumentClick` de `AccountMenu` | | `src/lib/toggle.svelte.ts` | `createToggle(initial=false)` → `{ get open, show(), close(), toggle() }` | `playlists.svelte.ts` y `player/queuePanel.svelte.ts` (idénticos) | | `src/lib/format.ts` (ampliar) | `plural(n, one, many)` → `"1 canción"`; `fmtCount(n,'canción','canciones')`; **minúscula siempre** | 15 pluralizaciones a mano, con mayúsculas inconsistentes ("Canción"/"canción") | | `src/lib/navigation.svelte.ts` (ampliar) | `searchHref(q)`; `appNavLinks(user, counts?)` → `{href,label,icon,count?,primary:boolean}[]` | `buildHref` en TopBar y explore; listas de links de Sidebar y TabsBar | | `src/lib/player/player.svelte.ts` (ampliar) | `isQueueCurrent(items)`, `playAllOrToggle(items)`, `playShuffled(items)`; renombrar `addToQueue` → `playNextItem` | `playAll`/`isCurrentQueue` en álbum, playlist, liked, mixes, spotlight de Inicio (5 copias) | | `src/lib/player/player.svelte.ts` | `toQueueItem(track)` único (normaliza `duration`/`listensCount` a `number`) | mapeos a mano en `recentlyPlayed.ts` (×2), `liked.svelte.ts` (×2), `liked/+page`, `tracks.ts#targetForQueueItem` y los `Number(…)` dispersos | | `src/lib/menus.svelte.ts` | `createTrackMenu()` → `{ state, open(e, track), close(), playNext() }` | estado + handler de menú contextual en Inicio, Explorar, Mezcla | ### B.3 Componentes nuevos (API) | Componente (`$lib/components/ui/`) | Props | Sustituye / absorbe | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `Artwork.svelte` | `src?`, `trackIds?: (string\|number)[]`, `size: 'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'2xl'\|'hero'`, `shape?: 'auto'\|'round'`, `imageSize?: 'small'\|'medium'\|'large'` (auto por `size`), `fallback?: 'music'\|'gradient'\|'none'`, `class?`, `children?` (overlay) | `Cover`, `PlaylistArt`, `MixArt`; radio automático por tamaño (xs→thumb, sm–xl→art/control, 2xl/hero→art-lg). Prioridad: `src` → mosaico si ≥4 `trackIds` → primera carátula → fallback. Un único `onerror`. | | `Avatar.svelte` | `name`, `src?`, `size: 'sm'\|'md'\|'lg'\|'hero'`, `class?` | iniciales/imagen en TopBar, MobileHeader, Explorar (×2), `ArtistAvatar` | | `ListRow.svelte` | `href?` \| `onclick?`, `oncontextmenu?`, `active?`, `size?: 'sm'\|'md'\|'lg'`, `title`, `subtitle?`, `subtitleHref?`, `explicit?`, `meta?`, `leading?: Snippet`, `art: Snippet<[string]>` \| artwork props, `trailing?: Snippet` | `SearchResultRow` + 7 filas a mano (ver P7) | | `MediaIdentity.svelte` | `title`, `subtitle?`, `subtitleHref?`, `explicit?`, `active?`, `art?: Snippet` \| `artwork` props, `titleClass?` | `TrackTitleCell` (renombrado + generalizado) y el bloque de `player/TrackInfo` | | `TrackList.svelte` | `tracks: TrackLike[]`, `columns?: ('album'\|'added'\|'uploaded'\|'plays')[]`, `index?`, `onPlay(i)`, `rowAction?: {action, icon, label}` \| `rowActionSnippet?`, `oncontextmenu?(e, track)` | `TrackTable`+`TrackRow`+`TrackIndexCell`+`TrackMeta` y las 6 tablas (P8) | | `ContextMenu.svelte` | `x`, `y`, `openLeft`, `onClose`, `items: {icon,label,onclick}[]`, `playlistAction?: {action: string, fields: Record, label}`, `playlists` | `TrackContextMenu` + `AlbumContextMenu` (90% idénticos) | | `PlayButton.svelte` | `playing?`, `size: 'sm'\|'md'\|'lg'`, `variant: 'strong'\|'glass'`, `label`, `onclick?`, `as?: 'button'\|'span'` | play de TransportControls, "Mejor resultado", overlay de MixTile | | `ConfirmDialog.svelte` | `open`, `onClose`, `title`, `description`, `confirmLabel`, `action` (form POST) | 2 modales de borrado con estilos distintos | | `Slider.svelte` | `value`, `max`, `label`, `oninput`, `variant: 'seek'\|'volume'` | `SeekBar` (click-only) + `VolumeControl` (`` (serializado con un helper `tokensToCss(tokens)` en `tokens.ts`). 3. Borrar de `theme.css` todos los tokens que genera `buildThemeTokens` (dark y light). `theme.css` queda solo con neutrales fijos, superficies, texto, layout y z-index. 4. `+layout.svelte`: los dos `div` de fondo con `style=` → `@utility app-backdrop` / `app-vignette` en `layout.css`. `main` con `padding-bottom: calc(…)` inline → clase con `--mf-dock-h` que se activa con `data-has-track` en el propio `main`. Cierre: con modo claro guardado, recarga dura sin flash; cambio de pista sigue animando el hue; check+lint. --- ### P3 — `Artwork` y `Avatar` (arte unificado) **Objetivo:** un único componente de carátula y uno de avatar. Hallazgos: - `Cover`, `PlaylistArt` y `MixArt` implementan la misma cascada (imagen → mosaico 2×2 → primera carátula → placeholder). `MixArt` ≡ `PlaylistArt` con `trackIds = items.map(i => i.trackId)`. - `AlbumCard` pasa el **id del álbum** como `trackId` a `Cover` solo para poder usar `src` (hack). - Avatar "imagen o inicial" reimplementado en `TopBar`, `MobileHeader`, Explorar (mejor resultado + fila de usuario) y `ArtistAvatar` (con rayado). - Cada uso decide su propio radio (`rounded-lg`, `rounded-xl`, `rounded-2xl`, `rounded-control`, `rounded-art-lg`…) y su tamaño (`h-8.5`, `h-9`, `h-10`, `h-10.5`, `h-11 md:h-12`, `h-13`, `h-14`, `h-15.5`, `h-22`, `h-34`, `h-36 sm:h-41`). Tareas: 1. Crear `Artwork.svelte` y `Avatar.svelte` según B.3 usando tokens B.1.1/B.1.2. `size` decide dimensión **y** radio; `class` solo para layout (`shrink-0`, márgenes). 2. Migrar **todos** los usos (buscar `Cover`, `PlaylistArt`, `MixArt`, `ArtistAvatar`, `user.picture`, `profilePictureUrl`) aplicando la tabla B.1.2. 3. `ArtistAvatar` → en `u/[id]/+page.svelte` usar `` (el nombre ya lo muestra la cabecera; el label bajo el avatar era redundante — **Δ visible**, confirmar). 4. Borrar `Cover.svelte`, `PlaylistArt.svelte`, `MixArt.svelte`, `ArtistAvatar.svelte`. Cierre: `grep -rn "Cover\b\|PlaylistArt\|MixArt\|ArtistAvatar" src` solo en `Artwork`; visual en Inicio, Explorar (búsqueda), Sidebar, Cola, player, cabeceras. --- ### P4 — Botones: `Button`, `IconButton`, `PlayButton` **Objetivo:** que no quede ningún `