discover progress

This commit is contained in:
Broque Thomas 2025-11-17 08:30:38 -08:00
parent 08a4decda8
commit 5a13b5fe94
5 changed files with 564 additions and 140 deletions

View file

@ -73,7 +73,7 @@ class PersonalizedPlaylistsService:
def get_decade_playlist(self, decade: int, limit: int = 100) -> List[Dict]: def get_decade_playlist(self, decade: int, limit: int = 100) -> List[Dict]:
""" """
Get tracks from a specific decade. Get tracks from a specific decade from discovery pool with diversity filtering.
Args: Args:
decade: Decade year (e.g., 2020 for 2020s, 2010 for 2010s) decade: Decade year (e.g., 2020 for 2020s, 2010 for 2010s)
@ -86,56 +86,59 @@ class PersonalizedPlaylistsService:
with self.database._get_connection() as conn: with self.database._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Check if release_year column exists # Query discovery_pool - get 10x more for diversity filtering
cursor.execute("PRAGMA table_info(tracks)") cursor.execute("""
columns = [row['name'] for row in cursor.fetchall()] SELECT
spotify_track_id,
if 'release_year' in columns: track_name,
cursor.execute(""" artist_name,
SELECT album_name,
t.id, album_cover_url,
t.spotify_track_id, duration_ms,
t.title as track_name, popularity,
t.duration_ms, release_date
ar.name as artist_name, FROM discovery_pool
al.title as album_name, WHERE release_date IS NOT NULL
al.cover_url as album_cover_url, AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) BETWEEN ? AND ?
t.popularity, ORDER BY RANDOM()
t.release_year LIMIT ?
FROM tracks t """, (start_year, end_year, limit * 10))
LEFT JOIN artists ar ON t.artist_id = ar.id
LEFT JOIN albums al ON t.album_id = al.id
WHERE t.spotify_track_id IS NOT NULL
AND t.release_year BETWEEN ? AND ?
ORDER BY t.popularity DESC
LIMIT ?
""", (start_year, end_year, limit))
else:
# Try to extract year from album release_date
logger.warning("release_year column not found - using album release_date")
cursor.execute("""
SELECT
t.id,
t.spotify_track_id,
t.title as track_name,
t.duration_ms,
ar.name as artist_name,
al.title as album_name,
al.cover_url as album_cover_url,
t.popularity,
al.release_date
FROM tracks t
LEFT JOIN artists ar ON t.artist_id = ar.id
LEFT JOIN albums al ON t.album_id = al.id
WHERE t.spotify_track_id IS NOT NULL
AND al.release_date IS NOT NULL
AND CAST(SUBSTR(al.release_date, 1, 4) AS INTEGER) BETWEEN ? AND ?
ORDER BY t.popularity DESC
LIMIT ?
""", (start_year, end_year, limit))
rows = cursor.fetchall() rows = cursor.fetchall()
return [dict(row) for row in rows] all_tracks = [dict(row) for row in rows]
if not all_tracks:
logger.warning(f"No tracks found for {decade}s")
return []
# Apply diversity constraint: max 3 tracks per album, max 4 per artist
# Shuffle first for randomness
import random
random.shuffle(all_tracks)
tracks_by_album = {}
tracks_by_artist = {}
diverse_tracks = []
for track in all_tracks:
album = track['album_name']
artist = track['artist_name']
# Count current tracks for this album/artist
album_count = tracks_by_album.get(album, 0)
artist_count = tracks_by_artist.get(artist, 0)
# Apply more lenient limits: max 3 per album, max 4 per artist
if album_count < 3 and artist_count < 4:
diverse_tracks.append(track)
tracks_by_album[album] = album_count + 1
tracks_by_artist[artist] = artist_count + 1
if len(diverse_tracks) >= limit:
break
logger.info(f"Found {len(diverse_tracks)} tracks from {decade}s in discovery pool (with diversity filtering)")
return diverse_tracks[:limit]
except Exception as e: except Exception as e:
logger.error(f"Error getting decade playlist for {decade}s: {e}") logger.error(f"Error getting decade playlist for {decade}s: {e}")

View file

@ -14538,12 +14538,18 @@ def get_discover_hero():
try: try:
database = get_database() database = get_database()
# Get top similar artists (by occurrence count) # Get top similar artists (by occurrence count) - get 20 for variety
similar_artists = database.get_top_similar_artists(limit=10) similar_artists = database.get_top_similar_artists(limit=20)
if not similar_artists: if not similar_artists:
return jsonify({"success": True, "artists": []}) return jsonify({"success": True, "artists": []})
# Shuffle for variety and take top 10
import random
shuffled = list(similar_artists)
random.shuffle(shuffled)
similar_artists = shuffled[:10]
# Convert to JSON format with Spotify data enrichment # Convert to JSON format with Spotify data enrichment
hero_artists = [] hero_artists = []
for artist in similar_artists: for artist in similar_artists:
@ -15110,6 +15116,91 @@ def generate_custom_playlist():
traceback.print_exc() traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500 return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/decades/available', methods=['GET'])
def get_available_decades():
"""Get list of decades that have content in discovery pool"""
try:
database = get_database()
with database._get_connection() as conn:
cursor = conn.cursor()
# Get distinct decades from discovery pool
cursor.execute("""
SELECT DISTINCT
(CAST(SUBSTR(release_date, 1, 4) AS INTEGER) / 10) * 10 as decade,
COUNT(*) as track_count
FROM discovery_pool
WHERE release_date IS NOT NULL
AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) >= 1950
AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) <= 2029
GROUP BY decade
HAVING track_count >= 10
ORDER BY decade ASC
""")
rows = cursor.fetchall()
decades = []
for row in rows:
decades.append({
'year': row[0],
'track_count': row[1]
})
return jsonify({
"success": True,
"decades": decades
})
except Exception as e:
print(f"Error getting available decades: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/decade/<int:decade>', methods=['GET'])
def get_discover_decade_playlist(decade):
"""Get tracks from a specific decade for discovery page"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_decade_playlist(decade, limit=50)
if not tracks:
return jsonify({
"success": True,
"tracks": [],
"decade": decade,
"message": f"No tracks found for the {decade}s"
}), 200
# Convert to Spotify format for modal compatibility
spotify_tracks = []
for track in tracks:
spotify_tracks.append({
'id': track.get('spotify_track_id', track.get('id')),
'name': track.get('track_name', track.get('name')),
'artists': [track.get('artist_name', 'Unknown')],
'album': {
'name': track.get('album_name', 'Unknown'),
'images': [{'url': track.get('album_cover_url')}] if track.get('album_cover_url') else []
},
'duration_ms': track.get('duration_ms', 0)
})
return jsonify({
"success": True,
"tracks": spotify_tracks,
"decade": decade
})
except Exception as e:
print(f"Error getting decade playlist: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/metadata/start', methods=['POST']) @app.route('/api/metadata/start', methods=['POST'])
def start_metadata_update(): def start_metadata_update():
"""Start the metadata update process - EXACT copy of dashboard.py logic""" """Start the metadata update process - EXACT copy of dashboard.py logic"""

View file

@ -1673,6 +1673,9 @@
<div class="discover-hero-label">FEATURED ARTIST</div> <div class="discover-hero-label">FEATURED ARTIST</div>
<h1 class="discover-hero-title" id="discover-hero-title">Loading...</h1> <h1 class="discover-hero-title" id="discover-hero-title">Loading...</h1>
<p class="discover-hero-subtitle" id="discover-hero-subtitle">Discover new music tailored to your taste</p> <p class="discover-hero-subtitle" id="discover-hero-subtitle">Discover new music tailored to your taste</p>
<div class="discover-hero-meta" id="discover-hero-meta">
<!-- Popularity and genres will be populated here -->
</div>
<div class="discover-hero-actions"> <div class="discover-hero-actions">
<button class="discover-hero-button secondary" id="discover-hero-discography" onclick="viewDiscoverHeroDiscography()"> <button class="discover-hero-button secondary" id="discover-hero-discography" onclick="viewDiscoverHeroDiscography()">
<span class="button-icon">📀</span> <span class="button-icon">📀</span>
@ -2054,20 +2057,31 @@
</div> </div>
</div> </div>
<!-- More Playlists Section --> <!-- Decade Browser Section -->
<div class="discover-section"> <div class="discover-section">
<div class="discover-section-header"> <div class="discover-section-header">
<h2 class="discover-section-title">More For You</h2> <h2 class="discover-section-title">⏰ Time Machine</h2>
<p class="discover-section-subtitle">Curated playlists based on your taste</p> <p class="discover-section-subtitle">Travel through the decades</p>
</div> </div>
<div class="discover-playlists-grid" id="more-playlists-grid"> <div class="discover-carousel" id="decade-browser-carousel">
<!-- Content will be populated dynamically --> <!-- Content will be populated dynamically -->
<div class="discover-loading"> <div class="discover-loading">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<p>Loading playlists...</p> <p>Loading decades...</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Quick Picks Section -->
<div class="discover-section">
<div class="discover-section-header">
<h2 class="discover-section-title">⚡ Quick Picks</h2>
<p class="discover-section-subtitle">Jump into curated experiences</p>
</div>
<div class="quick-picks-grid" id="quick-picks-grid">
<!-- Content will be populated dynamically -->
</div>
</div>
</div> </div>
</div> </div>

View file

@ -4204,6 +4204,7 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
virtualPlaylistId.startsWith('discover_') ? 'SoulSync' : virtualPlaylistId.startsWith('discover_') ? 'SoulSync' :
virtualPlaylistId.startsWith('seasonal_') ? 'SoulSync' : virtualPlaylistId.startsWith('seasonal_') ? 'SoulSync' :
virtualPlaylistId.startsWith('build_playlist_') ? 'SoulSync' : virtualPlaylistId.startsWith('build_playlist_') ? 'SoulSync' :
virtualPlaylistId.startsWith('decade_') ? 'SoulSync' :
virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' : virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' :
'YouTube'; 'YouTube';
@ -24741,7 +24742,8 @@ async function loadDiscoverPage() {
loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites
loadDiscoveryShuffle(), // NEW: Discovery Shuffle loadDiscoveryShuffle(), // NEW: Discovery Shuffle
loadFamiliarFavorites(), // NEW: Familiar Favorites loadFamiliarFavorites(), // NEW: Familiar Favorites
loadMoreForYou() loadDecadeBrowser(), // Decade browser
loadQuickPicks() // Quick picks action cards
]); ]);
// Check for active syncs after page load // Check for active syncs after page load
@ -24878,6 +24880,7 @@ async function loadDiscoverHero() {
function displayDiscoverHeroArtist(artist) { function displayDiscoverHeroArtist(artist) {
const titleEl = document.getElementById('discover-hero-title'); const titleEl = document.getElementById('discover-hero-title');
const subtitleEl = document.getElementById('discover-hero-subtitle'); const subtitleEl = document.getElementById('discover-hero-subtitle');
const metaEl = document.getElementById('discover-hero-meta');
const imageEl = document.getElementById('discover-hero-image'); const imageEl = document.getElementById('discover-hero-image');
const bgEl = document.getElementById('discover-hero-bg'); const bgEl = document.getElementById('discover-hero-bg');
@ -24893,14 +24896,37 @@ function displayDiscoverHeroArtist(artist) {
} else { } else {
subtitle = 'Similar to an artist in your watchlist'; subtitle = 'Similar to an artist in your watchlist';
} }
subtitleEl.textContent = subtitle;
}
// Add genre context if available // Build metadata section with popularity and genres
if (artist.genres && artist.genres.length > 0) { if (metaEl) {
const topGenres = artist.genres.slice(0, 2).join(', '); let metaHTML = '<div class="discover-hero-meta-content">';
subtitle += `${topGenres}`;
// Add popularity indicator
if (artist.popularity !== undefined && artist.popularity > 0) {
const popularityClass = artist.popularity >= 80 ? 'high' :
artist.popularity >= 50 ? 'medium' : 'low';
metaHTML += `
<div class="hero-meta-item hero-popularity ${popularityClass}">
<span class="meta-icon"></span>
<span class="meta-value">${artist.popularity}/100</span>
<span class="meta-label">Popularity</span>
</div>
`;
} }
subtitleEl.textContent = subtitle; // Add genre tags
if (artist.genres && artist.genres.length > 0) {
metaHTML += '<div class="hero-meta-item hero-genres">';
artist.genres.slice(0, 3).forEach(genre => {
metaHTML += `<span class="genre-tag">${genre}</span>`;
});
metaHTML += '</div>';
}
metaHTML += '</div>';
metaEl.innerHTML = metaHTML;
} }
if (imageEl && artist.image_url) { if (imageEl && artist.image_url) {
@ -25225,98 +25251,152 @@ async function loadDiscoverWeekly() {
} }
} }
async function loadMoreForYou() { // ===============================
// DECADE BROWSER
// ===============================
let selectedDecade = null;
let decadeTracks = [];
async function loadDecadeBrowser() {
try { try {
const grid = document.getElementById('more-playlists-grid'); const carousel = document.getElementById('decade-browser-carousel');
if (!grid) return; if (!carousel) return;
grid.innerHTML = '<div class="discover-loading"><div class="loading-spinner"></div><p>Loading playlists...</p></div>'; // Fetch available decades from backend
const response = await fetch('/api/discover/decades/available');
// Fetch discovery pool tracks to create curated playlists
const response = await fetch('/api/discover/weekly');
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch tracks for playlists'); throw new Error('Failed to fetch available decades');
}
const data = await response.json();
if (!data.success || !data.decades || data.decades.length === 0) {
carousel.innerHTML = '<div class="discover-empty"><p>No decade content available yet. Run a watchlist scan to populate your discovery pool!</p></div>';
return;
}
// Build decade cards from available decades
let html = '';
data.decades.forEach(decade => {
const icon = getDecadeIcon(decade.year);
const label = `${decade.year}s`;
html += `
<div class="decade-card" onclick="openDecadePlaylist(${decade.year})">
<div class="decade-card-content">
<div class="decade-icon">${icon}</div>
<div class="decade-year">${label}</div>
<div class="decade-title">${decade.track_count} tracks</div>
</div>
</div>
`;
});
carousel.innerHTML = html;
} catch (error) {
console.error('Error loading decade browser:', error);
const carousel = document.getElementById('decade-browser-carousel');
if (carousel) {
carousel.innerHTML = '<div class="discover-empty"><p>Failed to load decades</p></div>';
}
}
}
function getDecadeIcon(year) {
const icons = {
1950: '🎺',
1960: '🎸',
1970: '🕺',
1980: '📻',
1990: '💿',
2000: '📱',
2010: '🎧',
2020: '🌐'
};
return icons[year] || '🎵';
}
async function openDecadePlaylist(decade) {
try {
showLoadingOverlay(`Loading ${decade}s playlist...`);
const response = await fetch(`/api/discover/decade/${decade}`);
if (!response.ok) {
throw new Error('Failed to fetch decade playlist');
} }
const data = await response.json(); const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) { if (!data.success || !data.tracks || data.tracks.length === 0) {
grid.innerHTML = '<div class="discover-empty"><p>No playlists available yet</p></div>'; const message = data.message || `No tracks found for the ${decade}s`;
showToast(message, 'info');
hideLoadingOverlay();
return; return;
} }
const tracks = data.tracks; selectedDecade = decade;
decadeTracks = data.tracks;
// Create curated playlists // Open download modal
const playlists = []; const playlistName = `${decade}s Classics`;
const virtualPlaylistId = `decade_${decade}`;
// 1. Popular Picks (by popularity score) await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, data.tracks);
const popularTracks = [...tracks].sort((a, b) => b.popularity - a.popularity).slice(0, 20); hideLoadingOverlay();
if (popularTracks.length > 0) {
playlists.push({
name: 'Popular Picks',
description: 'Trending tracks from similar artists',
track_count: popularTracks.length,
cover: popularTracks[0].album_cover_url
});
}
// 2. Deep Cuts (lower popularity, hidden gems) } catch (error) {
const deepCuts = [...tracks].filter(t => t.popularity < 50).slice(0, 20); console.error(`Error opening ${decade}s playlist:`, error);
if (deepCuts.length > 0) { showToast(`Failed to load ${decade}s playlist`, 'error');
playlists.push({ hideLoadingOverlay();
name: 'Deep Cuts', }
description: 'Hidden gems you might have missed', }
track_count: deepCuts.length,
cover: deepCuts[0].album_cover_url
});
}
// 3. Fresh Finds (newest additions to pool) // ===============================
const freshFinds = [...tracks].slice(0, 20); // QUICK PICKS
if (freshFinds.length > 0) { // ===============================
playlists.push({
name: 'Fresh Finds',
description: 'Recently added to your discovery pool',
track_count: freshFinds.length,
cover: freshFinds[0].album_cover_url
});
}
// 4. Artist Mix (group by artist diversity) function loadQuickPicks() {
const artistMap = {}; try {
tracks.forEach(track => { const grid = document.getElementById('quick-picks-grid');
if (!artistMap[track.artist_name]) { if (!grid) return;
artistMap[track.artist_name] = [];
const quickPicks = [
{
icon: '🎲',
title: 'Surprise Me',
description: 'Shuffle your discovery pool',
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
action: 'scrollToSection("personalized-discovery-shuffle")'
},
{
icon: '🔥',
title: 'Trending Now',
description: 'High popularity discoveries',
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
action: 'scrollToSection("personalized-popular-picks")'
},
{
icon: '💎',
title: 'Deep Cuts',
description: 'Underground hidden gems',
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
action: 'scrollToSection("personalized-hidden-gems")'
},
{
icon: '⏰',
title: 'Time Machine',
description: 'Browse by decade',
gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
action: 'scrollToSection("decade-browser-carousel")'
} }
if (artistMap[track.artist_name].length < 3) { ];
artistMap[track.artist_name].push(track);
}
});
const mixTracks = Object.values(artistMap).flat().slice(0, 25);
if (mixTracks.length > 0) {
playlists.push({
name: 'Artist Mix',
description: 'Diverse selection from multiple artists',
track_count: mixTracks.length,
cover: mixTracks[0].album_cover_url
});
}
// Build playlist grid HTML
let html = ''; let html = '';
playlists.forEach(playlist => { quickPicks.forEach(pick => {
const coverUrl = playlist.cover || '/static/placeholder-album.png';
html += ` html += `
<div class="discover-playlist-card"> <div class="quick-pick-card" onclick="${pick.action}" style="background: ${pick.gradient};">
<div class="discover-playlist-cover"> <div class="quick-pick-icon">${pick.icon}</div>
<img src="${coverUrl}" alt="${playlist.name}"> <h3 class="quick-pick-title">${pick.title}</h3>
<div class="playlist-play-overlay"></div> <p class="quick-pick-description">${pick.description}</p>
</div>
<div class="discover-playlist-info">
<h4 class="discover-playlist-name">${playlist.name}</h4>
<p class="discover-playlist-description">${playlist.description}</p>
<p class="discover-playlist-count">${playlist.track_count} tracks</p>
</div>
</div> </div>
`; `;
}); });
@ -25324,12 +25404,31 @@ async function loadMoreForYou() {
grid.innerHTML = html; grid.innerHTML = html;
} catch (error) { } catch (error) {
console.error('Error loading more for you:', error); console.error('Error loading quick picks:', error);
const grid = document.getElementById('more-playlists-grid'); }
if (grid) { }
grid.innerHTML = '<div class="discover-empty"><p>Failed to load playlists</p></div>';
function scrollToSection(sectionId) {
const section = document.getElementById(sectionId);
if (!section) {
console.warn(`Section not found: ${sectionId}`);
return;
}
// Show section if it's hidden
const parentSection = section.closest('.discover-section');
if (parentSection) {
const currentDisplay = window.getComputedStyle(parentSection).display;
if (currentDisplay === 'none') {
parentSection.style.display = 'block';
console.log(`Showing hidden section: ${sectionId}`);
} }
} }
// Wait a tick for layout, then scroll
setTimeout(() => {
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
} }
// =============================== // ===============================

View file

@ -16239,11 +16239,82 @@ body {
.discover-hero-subtitle { .discover-hero-subtitle {
font-size: 19px; font-size: 19px;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
margin-bottom: 36px; margin-bottom: 20px;
line-height: 1.7; line-height: 1.7;
font-weight: 400; font-weight: 400;
} }
/* Hero Meta Section (Popularity & Genres) */
.discover-hero-meta {
margin-bottom: 28px;
}
.discover-hero-meta-content {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.hero-meta-item {
display: flex;
align-items: center;
gap: 8px;
}
.hero-popularity {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
font-size: 14px;
font-weight: 600;
}
.hero-popularity.high {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.hero-popularity.medium {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.hero-popularity.low {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.hero-popularity .meta-icon {
font-size: 16px;
}
.hero-popularity .meta-value {
font-weight: 700;
}
.hero-popularity .meta-label {
opacity: 0.8;
font-size: 12px;
}
.hero-genres {
gap: 10px;
}
.genre-tag {
padding: 6px 14px;
background: rgba(255, 255, 255, 0.15);
border-radius: 16px;
font-size: 13px;
font-weight: 500;
color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
transition: all 0.2s ease;
}
.genre-tag:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
}
.discover-hero-actions { .discover-hero-actions {
display: flex; display: flex;
gap: 16px; gap: 16px;
@ -17223,3 +17294,149 @@ body {
.build-playlist-metadata strong { .build-playlist-metadata strong {
color: #1db954; color: #1db954;
} }
/* =======================
QUICK PICKS SECTION
======================= */
.quick-picks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 16px;
}
.quick-pick-card {
position: relative;
padding: 32px 24px;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.quick-pick-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: inherit;
opacity: 0.9;
transition: opacity 0.3s ease;
}
.quick-pick-card:hover::before {
opacity: 1;
}
.quick-pick-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
.quick-pick-icon {
position: relative;
font-size: 48px;
margin-bottom: 16px;
z-index: 1;
}
.quick-pick-title {
position: relative;
font-size: 20px;
font-weight: 700;
color: #fff;
margin: 0 0 8px 0;
z-index: 1;
}
.quick-pick-description {
position: relative;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin: 0;
z-index: 1;
}
/* =======================
DECADE BROWSER SECTION
======================= */
.decade-card {
position: relative;
width: 100%;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.decade-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
}
.decade-card-content {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
background: linear-gradient(135deg,
rgba(102, 126, 234, 0.15) 0%,
rgba(118, 75, 162, 0.15) 100%);
}
.decade-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.decade-card:hover::before {
opacity: 0.2;
}
.decade-icon {
position: relative;
font-size: 80px;
line-height: 1;
z-index: 1;
transition: transform 0.3s ease;
}
.decade-card:hover .decade-icon {
transform: scale(1.1);
}
.decade-year {
position: relative;
font-size: 36px;
font-weight: 900;
color: #fff;
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
letter-spacing: -1px;
z-index: 1;
}
.decade-title {
position: relative;
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
text-transform: uppercase;
letter-spacing: 2px;
z-index: 1;
}