From 40f3621c48a9b6c524b614e191b2ea4a01c07f86 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:33:14 -0700 Subject: [PATCH] =?UTF-8?q?Redesign=20redownload=20modal=20=E2=80=94=20col?= =?UTF-8?q?umns,=20glass=20blur,=20Deezer=20fix,=20cover=20art?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major redesign: - All metadata sources shown as side-by-side columns (not tabs) - Frosted glass modal background with blur(40px) saturate(1.4) - Album cover art in header from DB thumb_url (resolved for Plex) - 1100px width, all elements scaled up, white text on accent buttons Bug fixes: - Deezer: use global singleton client, title-only fallback search, strip version suffixes from query - Track.__init__: added missing popularity=0 parameter - Overlay: dedicated .redownload-overlay class avoids CSS conflicts --- web_server.py | 42 +++++++-- webui/static/script.js | 119 ++++++++++++++----------- webui/static/style.css | 193 ++++++++++++++++++++++++++++++----------- 3 files changed, 246 insertions(+), 108 deletions(-) diff --git a/web_server.py b/web_server.py index afa8c3ef..6ef35e4a 100644 --- a/web_server.py +++ b/web_server.py @@ -12987,6 +12987,7 @@ def redownload_search_metadata(track_id): cursor.execute(""" SELECT t.id, t.title, t.file_path, t.bitrate, t.duration, ar.name AS artist_name, al.title AS album_title, + al.thumb_url AS album_thumb_url, t.spotify_track_id, t.deezer_id FROM tracks t JOIN artists ar ON t.artist_id = ar.id @@ -13001,13 +13002,31 @@ def redownload_search_metadata(track_id): track_title = row['title'] artist_name = row['artist_name'] - query = f"{artist_name} {track_title}" + # Clean the title for better search results โ€” strip version/edition suffixes + import re as _re + clean_title = _re.sub(r'\s*[\(\[](single version|album version|remaster|deluxe|bonus|explicit|clean|radio edit)[\)\]]', '', track_title, flags=_re.IGNORECASE).strip() + query = f"{artist_name} {clean_title}" # Resolve file info file_path = row['file_path'] or '' ext = os.path.splitext(file_path)[1].lstrip('.').upper() if file_path else '' fmt = ext if ext in ('FLAC', 'MP3', 'OPUS', 'OGG', 'M4A', 'WAV') else '' + # Resolve album thumb URL (may be relative Plex path) + thumb_url = row['album_thumb_url'] or '' + if thumb_url and not thumb_url.startswith('http'): + _ab = '' + _at = '' + if plex_client and plex_client.server: + _ab = getattr(plex_client.server, '_baseurl', '') or '' + _at = getattr(plex_client.server, '_token', '') or '' + if not _ab: + _pc = config_manager.get_plex_config() + _ab = (_pc.get('base_url', '') or '').rstrip('/') + _at = _at or _pc.get('token', '') + if _ab and thumb_url.startswith('/'): + thumb_url = f"{_ab}{thumb_url}?X-Plex-Token={_at}" if _at else f"{_ab}{thumb_url}" + current_track = { 'id': row['id'], 'title': track_title, @@ -13019,6 +13038,7 @@ def redownload_search_metadata(track_id): 'bitrate': row['bitrate'] or 0, 'spotify_track_id': row['spotify_track_id'], 'deezer_id': row['deezer_id'], + 'thumb_url': thumb_url, } # Search all available metadata sources in parallel @@ -13041,17 +13061,22 @@ def redownload_search_metadata(track_id): try: from core.itunes_client import iTunesClient sources_to_search.append(('itunes', iTunesClient())) - except Exception: - pass + except Exception as e: + logger.debug(f"iTunes client not available for redownload search: {e}") try: - from core.deezer_client import DeezerClient - sources_to_search.append(('deezer', DeezerClient())) - except Exception: - pass + sources_to_search.append(('deezer', _get_deezer_client())) + except Exception as e: + logger.debug(f"Deezer client not available for redownload search: {e}") def _search_source(source_name, client): try: + logger.info(f"[Redownload] Searching {source_name} for: {query}") track_objs = client.search_tracks(query, limit=10) + # If no results with full query, try title only + if not track_objs and clean_title: + logger.info(f"[Redownload] {source_name} got 0 results, trying title only: {clean_title}") + track_objs = client.search_tracks(clean_title, limit=10) + logger.info(f"[Redownload] {source_name} returned {len(track_objs)} results") results = [] for t in track_objs: r = { @@ -13073,7 +13098,7 @@ def redownload_search_metadata(track_id): results.sort(key=lambda x: (-int(x['is_current_match']), -x['match_score'])) return source_name, results except Exception as e: - logger.debug(f"Metadata search failed for {source_name}: {e}") + logger.error(f"[Redownload] Metadata search failed for {source_name}: {e}", exc_info=True) return source_name, [] with ThreadPoolExecutor(max_workers=3) as pool: @@ -13118,6 +13143,7 @@ def redownload_search_sources(track_id): artists=[metadata.get('artist', '')], album=metadata.get('album', ''), duration_ms=metadata.get('duration_ms', 0), + popularity=0, ) # Generate search queries diff --git a/webui/static/script.js b/webui/static/script.js index 79d3845e..70e282e9 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -42837,8 +42837,7 @@ async function showTrackSourceInfo(track, anchorEl) { async function showTrackRedownloadModal(track, album) { const overlay = document.createElement('div'); overlay.id = 'redownload-overlay'; - overlay.className = 'modal-overlay'; - overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; + overlay.className = 'redownload-overlay'; overlay.onclick = e => { if (e.target === overlay) overlay.remove(); }; const artistName = artistDetailPageState.enhancedData?.artist?.name || ''; @@ -42848,19 +42847,31 @@ async function showTrackRedownloadModal(track, album) { overlay.innerHTML = `
-

Redownload Track

+
+

Redownload Track

+

Find the correct version and download from your preferred source

+
-
-
${_esc(track.title)}
-
${_esc(artistName)} ยท ${_esc(album?.title || '')}${fmt ? ` ยท ${fmt}` : ''}${track.bitrate ? ` ยท ${track.bitrate}kbps` : ''}
+
+
+
๐ŸŽต
+
+
+
${_esc(track.title)}
+
${_esc(artistName)} ยท ${_esc(album?.title || '')}
+
+
+ ${fmt ? `${fmt}` : ''} + ${track.bitrate ? `${track.bitrate}k` : ''} +
-
1. Metadata
-
โ†’
-
2. Source
-
โ†’
-
3. Download
+
1 Choose Metadata
+
+
2 Choose Source
+
+
3 Downloading
@@ -42882,6 +42893,13 @@ async function showTrackRedownloadModal(track, album) { const res = await fetch(`/api/library/track/${track.id}/redownload/search-metadata`, { method: 'POST' }); const data = await res.json(); if (!data.success) throw new Error(data.error); + + // Set album art in header if available + const artEl = document.getElementById('redownload-current-art'); + if (artEl && data.current_track?.thumb_url) { + artEl.innerHTML = ``; + } + _renderRedownloadStep1(overlay, track, data); } catch (e) { document.getElementById('redownload-body').innerHTML = `
Error: ${_esc(e.message)}
`; @@ -42899,56 +42917,59 @@ function _renderRedownloadStep1(overlay, track, data) { } const bestSource = data.best_match?.source || sources[0]; - let selectedMeta = null; + const sourceIcons = { spotify: '๐ŸŸข', itunes: '๐ŸŽ', deezer: '๐ŸŸฃ', hydrabase: '๐Ÿ”ท' }; + const sourceLabels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', hydrabase: 'Hydrabase' }; - // Build source tabs + results - const tabsHtml = sources.map(s => ``).join(''); - - const resultsHtml = sources.map(source => { + // Build columns โ€” one per source, side by side + const columnsHtml = sources.map(source => { const results = data.metadata_results[source] || []; - if (results.length === 0) return `
No results from ${source}
`; + const icon = sourceIcons[source] || '๐Ÿ“‹'; + const label = sourceLabels[source] || source; - const items = results.map((r, i) => { - const pct = Math.round((r.match_score || 0) * 100); - const cls = pct >= 90 ? 'high' : pct >= 70 ? 'medium' : 'low'; - const dur = r.duration_ms ? `${Math.floor(r.duration_ms / 60000)}:${String(Math.floor((r.duration_ms % 60000) / 1000)).padStart(2, '0')}` : ''; - const checked = (source === bestSource && i === 0) ? 'checked' : ''; - return ` - `; - }).join(''); + let itemsHtml; + if (results.length === 0) { + itemsHtml = `
No results
`; + } else { + itemsHtml = results.slice(0, 8).map((r, i) => { + const pct = Math.round((r.match_score || 0) * 100); + const cls = pct >= 90 ? 'high' : pct >= 70 ? 'medium' : 'low'; + const dur = r.duration_ms ? `${Math.floor(r.duration_ms / 60000)}:${String(Math.floor((r.duration_ms % 60000) / 1000)).padStart(2, '0')}` : ''; + const checked = (source === bestSource && i === 0) ? 'checked' : ''; + return ` + `; + }).join(''); + } - return `
${items}
`; + return ` +
+
+ ${icon} + ${label} + ${results.length} +
+
${itemsHtml}
+
`; }).join(''); body.innerHTML = ` -
${tabsHtml}
-
${resultsHtml}
+
${columnsHtml}
- +
`; - // Tab switching - body.querySelectorAll('.redownload-tab').forEach(tab => { - tab.addEventListener('click', () => { - body.querySelectorAll('.redownload-tab').forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - body.querySelectorAll('.redownload-source-panel').forEach(p => p.style.display = 'none'); - const panel = body.querySelector(`.redownload-source-panel[data-source="${tab.dataset.source}"]`); - if (panel) panel.style.display = ''; - }); - }); - // Next button document.getElementById('redownload-next-btn').addEventListener('click', async () => { const checked = body.querySelector('input[name="metadata-choice"]:checked'); diff --git a/webui/static/style.css b/webui/static/style.css index 287330a7..5776e88b 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -52049,111 +52049,197 @@ tr.tag-diff-same { TRACK REDOWNLOAD MODAL ================================================================================== */ +.redownload-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} + .redownload-modal { - background: rgba(20, 20, 28, 0.98); - backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 20px; - max-width: 600px; + background: rgba(16, 16, 24, 0.85); + backdrop-filter: blur(40px) saturate(1.4); + -webkit-backdrop-filter: blur(40px) saturate(1.4); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; + max-width: 1100px; width: 95vw; - max-height: 85vh; + max-height: 88vh; display: flex; flex-direction: column; - box-shadow: 0 32px 80px rgba(0, 0, 0, 0.6); + box-shadow: 0 40px 100px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255,255,255,0.06) inset, 0 0 80px rgba(0,0,0,0.3) inset; } .redownload-header { display: flex; justify-content: space-between; - align-items: center; - padding: 20px 24px 0; + align-items: flex-start; + padding: 28px 32px 0; } -.redownload-header h3 { font-size: 16px; font-weight: 700; color: #fff; margin: 0; } +.redownload-header h3 { font-size: 20px; font-weight: 800; color: #fff; margin: 0; letter-spacing: -0.02em; } +.redownload-header-sub { font-size: 13px; color: rgba(255,255,255,0.3); margin: 6px 0 0; } .redownload-close { - width: 30px; height: 30px; border: none; background: rgba(255,255,255,0.06); + width: 32px; height: 32px; border: none; background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.4); border-radius: 50%; font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; + flex-shrink: 0; } .redownload-close:hover { background: rgba(255,255,255,0.12); color: #fff; } +/* Current track card */ .redownload-current { - padding: 10px 24px 0; + display: flex; align-items: center; gap: 16px; + margin: 20px 32px 0; padding: 16px 20px; + background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); + border-radius: 16px; } -.redownload-current-title { font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.8); } -.redownload-current-meta { font-size: 11px; color: rgba(255,255,255,0.3); margin-top: 2px; } +.redownload-current-art { + width: 56px; height: 56px; border-radius: 12px; + overflow: hidden; flex-shrink: 0; + background: rgba(var(--accent-rgb), 0.1); + display: flex; align-items: center; justify-content: center; +} +.redownload-current-art img { + width: 100%; height: 100%; object-fit: cover; +} +.redownload-current-art .redownload-art-empty { + font-size: 22px; width: 100%; height: 100%; + display: flex; align-items: center; justify-content: center; + background: rgba(var(--accent-rgb), 0.1); border-radius: 0; +} +.redownload-current-info { flex: 1; min-width: 0; } +.redownload-current-title { font-size: 16px; font-weight: 700; color: rgba(255,255,255,0.95); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.redownload-current-meta { font-size: 12px; color: rgba(255,255,255,0.35); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.redownload-current-badges { display: flex; gap: 4px; flex-shrink: 0; } +.redownload-badge { + font-size: 10px; font-weight: 700; padding: 4px 10px; border-radius: 6px; + letter-spacing: 0.03em; +} +.redownload-badge.fmt { background: rgba(var(--accent-rgb), 0.12); color: var(--accent); } +.redownload-badge.bitrate { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.4); } /* Step indicator */ .redownload-steps { - display: flex; align-items: center; gap: 8px; padding: 14px 24px; + display: flex; align-items: center; gap: 0; padding: 22px 32px 10px; } .redownload-step { - font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.2); - padding: 4px 10px; border-radius: 6px; transition: all 0.2s; + font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.2); + padding: 8px 16px; border-radius: 10px; transition: all 0.25s; + display: flex; align-items: center; gap: 10px; white-space: nowrap; +} +.redownload-step.active { + color: #fff; background: rgba(var(--accent-rgb), 0.12); +} +.redownload-step-num { + width: 24px; height: 24px; border-radius: 50%; + background: rgba(255,255,255,0.06); display: flex; + align-items: center; justify-content: center; + font-size: 11px; font-weight: 800; +} +.redownload-step.active .redownload-step-num { + background: var(--accent); color: #fff; +} +.redownload-step-line { + flex: 1; height: 2px; background: rgba(255,255,255,0.06); + margin: 0 6px; min-width: 20px; border-radius: 1px; } -.redownload-step.active { color: var(--accent); background: rgba(var(--accent-rgb), 0.1); } -.redownload-step-arrow { color: rgba(255,255,255,0.1); font-size: 11px; } -.redownload-body { padding: 0 24px 20px; overflow-y: auto; flex: 1; } +.redownload-body { padding: 8px 32px 28px; overflow-y: auto; flex: 1; } .redownload-loading, .redownload-error, .redownload-empty { text-align: center; padding: 30px; color: rgba(255,255,255,0.25); font-size: 12px; } .redownload-error { color: #ef5350; } -/* Step 1 โ€” Metadata tabs + results */ -.redownload-tabs { display: flex; gap: 4px; margin-bottom: 12px; } -.redownload-tab { - padding: 6px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.06); - background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.5); font-size: 11px; - font-weight: 600; cursor: pointer; transition: all 0.2s; +/* Step 1 โ€” Metadata source columns */ +.redownload-columns { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; } -.redownload-tab.active { background: rgba(var(--accent-rgb),0.12); color: var(--accent); border-color: rgba(var(--accent-rgb),0.2); } -.redownload-tab:hover:not(.active) { background: rgba(255,255,255,0.06); } -.redownload-results-wrap { max-height: 300px; overflow-y: auto; } -.redownload-results-wrap::-webkit-scrollbar { width: 4px; } -.redownload-results-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; } +.redownload-source-col { + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 14px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.redownload-col-header { + display: flex; align-items: center; gap: 8px; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.02); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + font-size: 13px; font-weight: 700; color: rgba(255, 255, 255, 0.6); +} +.redownload-col-icon { font-size: 16px; } +.redownload-col-label { flex: 1; } +.redownload-col-count { + font-size: 10px; padding: 2px 8px; border-radius: 10px; + background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.35); + font-variant-numeric: tabular-nums; +} + +.redownload-col-results { + max-height: 380px; overflow-y: auto; padding: 6px; +} +.redownload-col-results::-webkit-scrollbar { width: 3px; } +.redownload-col-results::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 2px; } + +.redownload-col-empty { + padding: 30px 16px; text-align: center; + font-size: 12px; color: rgba(255, 255, 255, 0.2); +} .redownload-result { - display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; - cursor: pointer; transition: background 0.15s; margin-bottom: 2px; + display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 12px; + cursor: pointer; transition: all 0.15s; margin-bottom: 3px; + border: 1px solid transparent; } -.redownload-result:hover { background: rgba(255,255,255,0.04); } -.redownload-result input[type="radio"] { accent-color: var(--accent); flex-shrink: 0; } +.redownload-result:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.06); } +.redownload-result:has(input:checked) { background: rgba(var(--accent-rgb), 0.06); border-color: rgba(var(--accent-rgb), 0.15); } +.redownload-result input[type="radio"] { accent-color: var(--accent); flex-shrink: 0; width: 16px; height: 16px; } -.redownload-result-art { width: 40px; height: 40px; border-radius: 6px; overflow: hidden; flex-shrink: 0; } +.redownload-result-art { width: 48px; height: 48px; border-radius: 10px; overflow: hidden; flex-shrink: 0; } .redownload-result-art img { width: 100%; height: 100%; object-fit: cover; } .redownload-art-empty { width: 100%; height: 100%; background: rgba(255,255,255,0.04); border-radius: 6px; } .redownload-result-info { flex: 1; min-width: 0; } -.redownload-result-title { font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.85); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.redownload-result-meta { font-size: 10px; color: rgba(255,255,255,0.35); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.redownload-result-title { font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.redownload-result-meta { font-size: 11px; color: rgba(255,255,255,0.35); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; } .redownload-current-badge { font-size: 8px; background: rgba(var(--accent-rgb),0.15); color: var(--accent); padding: 1px 5px; border-radius: 3px; font-weight: 700; text-transform: uppercase; } -.redownload-result-score { font-size: 10px; font-weight: 700; flex-shrink: 0; padding: 2px 6px; border-radius: 4px; } +.redownload-result-score { font-size: 11px; font-weight: 700; flex-shrink: 0; padding: 4px 10px; border-radius: 6px; } .redownload-result-score.high { background: rgba(76,175,80,0.12); color: #4caf50; } .redownload-result-score.medium { background: rgba(255,183,77,0.12); color: #ffb74d; } .redownload-result-score.low { background: rgba(239,83,80,0.12); color: #ef5350; } +.redownload-result-right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; } .redownload-result-dur { font-size: 10px; color: rgba(255,255,255,0.2); flex-shrink: 0; min-width: 30px; text-align: right; } /* Step 2 โ€” Download candidates */ -.redownload-candidates-wrap { max-height: 320px; overflow-y: auto; } +.redownload-candidates-wrap { max-height: 450px; overflow-y: auto; } .redownload-candidates-wrap::-webkit-scrollbar { width: 4px; } .redownload-candidates-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; } .redownload-candidate { - display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; - cursor: pointer; transition: background 0.15s; margin-bottom: 2px; border: 1px solid transparent; + display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 12px; + cursor: pointer; transition: all 0.15s; margin-bottom: 3px; border: 1px solid transparent; } -.redownload-candidate:hover { background: rgba(255,255,255,0.04); } -.redownload-candidate input[type="radio"] { accent-color: var(--accent); flex-shrink: 0; } +.redownload-candidate:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.06); } +.redownload-candidate:has(input:checked) { background: rgba(var(--accent-rgb), 0.06); border-color: rgba(var(--accent-rgb), 0.15); } +.redownload-candidate input[type="radio"] { accent-color: var(--accent); flex-shrink: 0; width: 16px; height: 16px; } .redownload-candidate.blacklisted { opacity: 0.35; cursor: not-allowed; } .redownload-candidate-info { flex: 1; min-width: 0; } -.redownload-candidate-name { font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.85); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.redownload-candidate-meta { font-size: 10px; color: rgba(255,255,255,0.35); } +.redownload-candidate-name { font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.redownload-candidate-meta { font-size: 11px; color: rgba(255,255,255,0.35); margin-top: 2px; } .redownload-candidate-details { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } .redownload-fmt { font-size: 9px; font-weight: 700; padding: 2px 5px; border-radius: 3px; background: rgba(var(--accent-rgb),0.12); color: var(--accent); } @@ -52182,14 +52268,19 @@ tr.tag-diff-same { .redownload-progress-status { font-size: 11px; color: rgba(255,255,255,0.3); margin-top: 12px; } /* Action buttons */ -.redownload-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 14px; } +.redownload-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.05); margin-top: 12px; } .redownload-btn { - padding: 8px 18px; border-radius: 10px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; border: none; + padding: 12px 28px; border-radius: 12px; font-size: 14px; font-weight: 700; cursor: pointer; transition: all 0.2s; border: none; } -.redownload-btn.primary { background: var(--accent); color: #000; } -.redownload-btn.primary:hover { filter: brightness(1.1); } +.redownload-btn.primary { + background: linear-gradient(135deg, rgba(var(--accent-rgb), 1), rgba(var(--accent-rgb), 0.8)); + color: #fff; + box-shadow: 0 4px 16px rgba(var(--accent-rgb), 0.3); + letter-spacing: 0.01em; +} +.redownload-btn.primary:hover { filter: brightness(1.15); transform: translateY(-1px); box-shadow: 0 6px 24px rgba(var(--accent-rgb), 0.4); } .redownload-btn.secondary { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.6); } -.redownload-btn.secondary:hover { background: rgba(255,255,255,0.1); } +.redownload-btn.secondary:hover { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.8); } /* Redownload button in track table */ .enhanced-redownload-btn {