Redesign Artists page: rich hero section, full-bleed cards, multi-source genres

Artists page hero section:
- Large portrait artist photo (400x480px, rounded rectangle)
- Blurred saturated background from artist image
- 2.6em bold name with text shadow
- Real service logo badges (Spotify, MusicBrainz, Deezer, iTunes,
  Last.fm, Genius, Tidal, Qobuz) — matching library page
- Genre pills merged from metadata cache + Last.fm tags
- Last.fm bio with read more/show less toggle
- Last.fm listener count + playcount stats (large bold numbers)
- Backend enriches discography response with artist_info from
  metadata cache + library (all service IDs, Last.fm data, genres)

Album/Single/EP cards:
- Full-bleed cover art filling entire card with gradient overlay
- Album name + year overlaid at bottom over dark gradient
- Image zoom on hover, accent glow for dynamic-glow cards
- Responsive grid (220px desktop, 170px tablet, 140px mobile)

Similar artist cards:
- Full-bleed image cards matching library artist card style
- Gradient overlay with name at bottom, aspect-ratio 0.8
- Grid-controlled sizing via existing responsive breakpoints

Genre explorer (multi-source):
- Queries all allowed sources (iTunes+Deezer always, Spotify when
  authed) via _get_genre_allowed_sources() helper
- Deezer genre support: genre_id mapping from search results,
  one-time backfill from stored raw_json, album-to-artist propagation
- Genre deep dive deduplicates artists across sources
- Source dots on artists/tracks in deep dive modal
- Artist clicks route through source-specific client
- Album endpoint falls back across sources when IDs don't match
- Genre explorer cached 24hr in-memory, positioned at top of
  Discover page below hero slider

All changes mobile responsive with proper breakpoints.
This commit is contained in:
Broque Thomas 2026-03-25 11:49:51 -07:00
parent e08462a002
commit d248c36da1
5 changed files with 761 additions and 220 deletions

View file

@ -9238,10 +9238,108 @@ def get_artist_discography(artist_id):
for single in singles_list:
print(f"🎵 Single/EP: {single['name']} ({single['album_type']}) - {single['release_date']}")
# Gather artist enrichment info from cache + library
artist_info = {}
try:
cache = get_metadata_cache()
# Try metadata cache for genres, image, followers
cached = cache.get_entity(active_source or 'spotify', 'artist', artist_id)
if not cached and active_source != 'spotify':
cached = cache.get_entity('spotify', 'artist', artist_id)
if not cached:
# Try by name across all sources
for src in ['spotify', 'itunes', 'deezer']:
if artist_name:
db_tmp = get_database()
conn_tmp = db_tmp._get_connection()
try:
cur = conn_tmp.cursor()
cur.execute("""
SELECT genres, image_url, followers, popularity, external_urls
FROM metadata_cache_entities
WHERE entity_type = 'artist' AND name COLLATE NOCASE = ? AND source = ?
LIMIT 1
""", (artist_name, src))
row = cur.fetchone()
if row:
cached = dict(row)
break
finally:
conn_tmp.close()
if cached:
try:
artist_info['genres'] = json.loads(cached.get('genres', '[]')) if isinstance(cached.get('genres'), str) else (cached.get('genres') or [])
except Exception:
artist_info['genres'] = []
artist_info['image_url'] = cached.get('image_url')
artist_info['followers'] = cached.get('followers')
artist_info['popularity'] = cached.get('popularity')
try:
artist_info['external_urls'] = json.loads(cached.get('external_urls', '{}')) if isinstance(cached.get('external_urls'), str) else (cached.get('external_urls') or {})
except Exception:
artist_info['external_urls'] = {}
# Try library for full enrichment (Last.fm bio, stats, service IDs)
if artist_name:
db_lib = get_database()
conn_lib = db_lib._get_connection()
try:
cur_lib = conn_lib.cursor()
cur_lib.execute("""
SELECT id, summary, genres, thumb_url,
spotify_artist_id, musicbrainz_id, deezer_id, itunes_artist_id,
audiodb_id, tidal_id, qobuz_id, genius_id, soul_id,
lastfm_bio, lastfm_listeners, lastfm_playcount, lastfm_tags,
lastfm_url, genius_url, style, mood, label
FROM artists WHERE name COLLATE NOCASE = ? LIMIT 1
""", (artist_name,))
lib_row = cur_lib.fetchone()
if lib_row:
lib = dict(lib_row)
artist_info['library_id'] = lib['id']
# Image fallback
if not artist_info.get('image_url') and lib['thumb_url']:
artist_info['image_url'] = fix_artist_image_url(lib['thumb_url'])
# Genres fallback
if not artist_info.get('genres') and lib['genres']:
try:
artist_info['genres'] = json.loads(lib['genres'])
except Exception:
pass
# Last.fm enrichment
if lib.get('lastfm_bio'):
artist_info['lastfm_bio'] = lib['lastfm_bio']
if lib.get('lastfm_listeners'):
artist_info['lastfm_listeners'] = lib['lastfm_listeners']
if lib.get('lastfm_playcount'):
artist_info['lastfm_playcount'] = lib['lastfm_playcount']
if lib.get('lastfm_tags'):
try:
artist_info['lastfm_tags'] = json.loads(lib['lastfm_tags']) if isinstance(lib['lastfm_tags'], str) else lib['lastfm_tags']
except Exception:
pass
if lib.get('lastfm_url'):
artist_info['lastfm_url'] = lib['lastfm_url']
if lib.get('genius_url'):
artist_info['genius_url'] = lib['genius_url']
# Service IDs for badges
for key in ['spotify_artist_id', 'musicbrainz_id', 'deezer_id', 'itunes_artist_id',
'audiodb_id', 'tidal_id', 'qobuz_id', 'genius_id', 'soul_id']:
if lib.get(key):
artist_info[key] = lib[key]
# Bio fallback from summary
if not artist_info.get('lastfm_bio') and lib.get('summary'):
artist_info['bio'] = lib['summary']
finally:
conn_lib.close()
except Exception as e:
logger.debug(f"Artist info enrichment failed (non-fatal): {e}")
return jsonify({
"albums": album_list,
"singles": singles_list,
"source": active_source or "spotify"
"source": active_source or "spotify",
"artist_info": artist_info,
})
except Exception as e:

View file

@ -2283,33 +2283,41 @@
<!-- Artist Detail State -->
<div class="artist-detail-state hidden" id="artist-detail-state">
<div class="artist-detail-header">
<div class="artist-detail-header-left">
<button class="artist-detail-back-button" id="artist-detail-back-button">
<span class="back-icon"></span>
<span>Back to Results</span>
<!-- Hero Section -->
<div class="artists-hero-section" id="artists-hero-section">
<div class="artists-hero-bg" id="artists-hero-bg"></div>
<div class="artists-hero-overlay"></div>
<div class="artists-hero-content">
<button class="artists-hero-back" id="artist-detail-back-button">
<span></span> Back
</button>
<div class="artist-detail-info">
<div class="artist-detail-image" id="search-artist-detail-image"></div>
<div class="artist-detail-text">
<h2 class="artist-detail-name" id="search-artist-detail-name">Artist Name</h2>
<p class="artist-detail-genres" id="search-artist-detail-genres">Genres</p>
<div class="artists-hero-main">
<div class="artists-hero-image" id="artists-hero-image"></div>
<div class="artists-hero-info">
<h1 class="artists-hero-name" id="artists-hero-name">Artist Name</h1>
<div class="artists-hero-badges" id="artists-hero-badges"></div>
<div class="artists-hero-genres" id="artists-hero-genres"></div>
<div class="artists-hero-bio" id="artists-hero-bio"></div>
<div class="artists-hero-stats" id="artists-hero-stats"></div>
</div>
</div>
</div>
<div class="artist-detail-watchlist-group">
<button class="artist-detail-watchlist-btn" id="artist-detail-watchlist-btn">
<span class="watchlist-icon">👁️</span>
<span class="watchlist-text">Add to Watchlist</span>
</button>
<button class="artist-detail-watchlist-settings-btn hidden" id="artist-detail-watchlist-settings-btn" title="Watchlist Settings">
&#9881;
</button>
<div class="artists-hero-actions">
<button class="artist-detail-watchlist-btn" id="artist-detail-watchlist-btn">
<span class="watchlist-icon">👁️</span>
<span class="watchlist-text">Add to Watchlist</span>
</button>
<button class="artist-detail-watchlist-settings-btn hidden" id="artist-detail-watchlist-settings-btn" title="Watchlist Settings">
&#9881;
</button>
</div>
</div>
</div>
<!-- Keep old hidden elements for backward compatibility with JS refs -->
<div id="search-artist-detail-image" style="display:none"></div>
<div id="search-artist-detail-name" style="display:none"></div>
<div id="search-artist-detail-genres" style="display:none"></div>
<div class="artist-detail-content">
<div class="artist-detail-tabs">
<button class="artist-tab active" data-tab="albums" id="albums-tab">

View file

@ -912,7 +912,68 @@
.album-cards-container,
.singles-cards-container {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.album-card-content {
padding: 10px;
}
.album-card-name {
font-size: 12px;
}
/* Artists page hero — mobile */
.artists-hero-section {
border-radius: 12px;
}
.artists-hero-main {
flex-direction: column;
align-items: center;
text-align: center;
gap: 16px;
}
.artists-hero-image {
width: 140px;
height: 175px;
border-radius: 10px;
}
.artists-hero-name {
font-size: 1.5em;
}
.artists-hero-genres {
justify-content: center;
}
.artists-hero-badges {
justify-content: center;
}
.artists-hero-content {
padding: 16px 14px 20px;
}
.artists-hero-actions {
position: static;
justify-content: center;
margin-top: 8px;
}
.artists-hero-bio {
text-align: left;
}
.artists-hero-stats {
justify-content: center;
}
.artists-hero-stat strong {
font-size: 15px;
}
/* --- Phase 5B: Library --- */

View file

@ -31909,10 +31909,19 @@ async function loadArtistDiscography(artistId, artistName = null, sourceOverride
...artistsPageState.selectedArtist,
...data.artist
};
// Refresh header to show MusicBrainz link
updateArtistDetailHeader(artistsPageState.selectedArtist);
}
// Merge artist_info enrichment from discography response
if (data.artist_info) {
artistsPageState.selectedArtist = {
...artistsPageState.selectedArtist,
artist_info: data.artist_info,
};
}
// Refresh header with all available data
updateArtistDetailHeader(artistsPageState.selectedArtist);
console.log(`✅ Loaded ${discography.albums.length} albums and ${discography.singles.length} singles`);
// Cache the results (use source-prefixed key if source override active)
@ -32887,63 +32896,132 @@ function retryLastSearch() {
* Update artist detail header with artist info
*/
function updateArtistDetailHeader(artist) {
const imageElement = document.getElementById('search-artist-detail-image');
const nameElement = document.getElementById('search-artist-detail-name');
const genresElement = document.getElementById('search-artist-detail-genres');
const _esc = (s) => (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const info = artist.artist_info || {};
const imageUrl = artist.image_url || info.image_url || '';
if (imageElement) {
if (artist.image_url) {
imageElement.style.backgroundImage = `url('${artist.image_url}')`;
// Background blur
const heroBg = document.getElementById('artists-hero-bg');
if (heroBg) {
heroBg.style.backgroundImage = imageUrl ? `url('${imageUrl}')` : 'none';
}
// Artist image
const heroImage = document.getElementById('artists-hero-image');
if (heroImage) {
if (imageUrl) {
heroImage.style.backgroundImage = `url('${imageUrl}')`;
heroImage.innerHTML = '';
} else {
// Lazy load image if missing (common for iTunes artists)
console.log(`🖼️ Lazy loading detail image for ${artist.name} (${artist.id})`);
heroImage.style.backgroundImage = 'none';
heroImage.innerHTML = '<span class="artists-hero-image-fallback">🎤</span>';
// Lazy load
fetch(`/api/artist/${artist.id}/image`)
.then(response => response.json())
.then(data => {
if (data.success && data.image_url) {
console.log(`✅ Loaded detail image for ${artist.name}`);
imageElement.style.backgroundImage = `url('${data.image_url}')`;
// Update the artist object in memory too
artist.image_url = data.image_url;
.then(r => r.json())
.then(d => {
if (d.success && d.image_url) {
heroImage.style.backgroundImage = `url('${d.image_url}')`;
heroImage.innerHTML = '';
if (heroBg) heroBg.style.backgroundImage = `url('${d.image_url}')`;
artist.image_url = d.image_url;
}
})
.catch(err => console.error('❌ Failed to load detail image:', err));
}).catch(() => {});
}
}
if (nameElement) {
nameElement.textContent = artist.name;
// Name
const heroName = document.getElementById('artists-hero-name');
if (heroName) heroName.textContent = artist.name || 'Unknown Artist';
// Add MusicBrainz link if available
if (artist.musicbrainz_id) {
// Remove existing MB link if any
const existingMb = nameElement.parentNode.querySelector('.mb-link-btn');
if (existingMb) existingMb.remove();
// Badges (service links — real logos matching library page)
const badgesEl = document.getElementById('artists-hero-badges');
if (badgesEl) {
const _hb = (logo, fallback, title, url) => {
const attr = url ? `data-url="${_esc(url)}" onclick="window.open(this.dataset.url,'_blank')"` : '';
const inner = logo
? `<img src="${logo}" alt="${fallback}" onerror="this.parentNode.textContent='${fallback}'">`
: `<span style="font-size:9px;font-weight:700;">${fallback}</span>`;
return `<div class="artists-hero-badge" title="${title}" ${attr}>${inner}</div>`;
};
const badges = [];
if (info.spotify_artist_id) badges.push(_hb(SPOTIFY_LOGO_URL, 'SP', 'Spotify', `https://open.spotify.com/artist/${info.spotify_artist_id}`));
if (info.musicbrainz_id || artist.musicbrainz_id) badges.push(_hb(MUSICBRAINZ_LOGO_URL, 'MB', 'MusicBrainz', `https://musicbrainz.org/artist/${info.musicbrainz_id || artist.musicbrainz_id}`));
if (info.deezer_id) badges.push(_hb(DEEZER_LOGO_URL, 'Dz', 'Deezer', `https://www.deezer.com/artist/${info.deezer_id}`));
if (info.itunes_artist_id) badges.push(_hb(ITUNES_LOGO_URL, 'IT', 'Apple Music', `https://music.apple.com/artist/${info.itunes_artist_id}`));
if (info.lastfm_url) badges.push(_hb(LASTFM_LOGO_URL, 'LFM', 'Last.fm', info.lastfm_url));
if (info.genius_url) badges.push(_hb(GENIUS_LOGO_URL, 'GEN', 'Genius', info.genius_url));
if (info.tidal_id) badges.push(_hb(TIDAL_LOGO_URL, 'TD', 'Tidal', `https://tidal.com/browse/artist/${info.tidal_id}`));
if (info.qobuz_id) badges.push(_hb(QOBUZ_LOGO_URL, 'Qz', 'Qobuz', `https://www.qobuz.com/artist/${info.qobuz_id}`));
badgesEl.innerHTML = badges.join('');
}
const mbLink = document.createElement('a');
mbLink.className = 'mb-link-btn';
mbLink.href = `https://musicbrainz.org/artist/${artist.musicbrainz_id}`;
mbLink.target = '_blank';
mbLink.title = 'View on MusicBrainz';
mbLink.innerHTML = `
<img src="https://custom-icon-badges.demolab.com/badge/-MusicBrainz-353535?style=flat&logo=musicbrainz&logoColor=white" style="width: auto; height: 16px; margin: 0;">
`;
// Simplified button style to just be the badge/icon for cleaner look header
mbLink.style.padding = '0';
mbLink.style.border = 'none';
mbLink.style.background = 'transparent';
mbLink.style.marginLeft = '12px';
mbLink.style.verticalAlign = 'middle';
nameElement.appendChild(mbLink);
// Genres (pill tags — merge with Last.fm tags, deduplicated)
const genresEl = document.getElementById('artists-hero-genres');
if (genresEl) {
let genres = info.genres || artist.genres || [];
// Merge Last.fm tags
const lfmTags = info.lastfm_tags || [];
if (Array.isArray(lfmTags) && lfmTags.length > 0) {
const existing = new Set(genres.map(g => g.toLowerCase()));
const newTags = lfmTags.filter(t => !existing.has(t.toLowerCase()));
genres = [...genres, ...newTags];
}
if (genres.length > 0) {
genresEl.innerHTML = genres.slice(0, 8).map(g =>
`<span class="artists-hero-genre-pill">${_esc(g)}</span>`
).join('');
} else {
genresEl.innerHTML = '';
}
}
if (genresElement) {
const genres = artist.genres?.slice(0, 4).join(' • ') || 'Various genres';
genresElement.textContent = genres;
// Bio (Last.fm bio or summary fallback — matching library page pattern)
const bioEl = document.getElementById('artists-hero-bio');
if (bioEl) {
const bio = info.lastfm_bio || info.bio || '';
if (bio) {
// Strip HTML tags and "Read more on Last.fm" links
let cleanBio = bio.replace(/<a\b[^>]*>.*?<\/a>/gi, '').replace(/<[^>]+>/g, '').trim();
if (cleanBio) {
bioEl.innerHTML = `<span class="artists-hero-bio-text">${_esc(cleanBio)}</span>
<span class="artists-hero-bio-toggle" onclick="this.parentElement.classList.toggle('expanded');this.textContent=this.parentElement.classList.contains('expanded')?'Show less':'Read more'">Read more</span>`;
bioEl.style.display = '';
} else {
bioEl.style.display = 'none';
}
} else {
bioEl.style.display = 'none';
}
}
// Stats (Last.fm listeners + playcount, with followers fallback)
const statsEl = document.getElementById('artists-hero-stats');
if (statsEl) {
const _fmtNum = (n) => {
if (!n || n <= 0) return '0';
if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
return n.toLocaleString();
};
let stats = '';
if (info.lastfm_listeners) {
stats += `<span class="artists-hero-stat"><strong>${_fmtNum(info.lastfm_listeners)}</strong> listeners</span>`;
}
if (info.lastfm_playcount) {
stats += `<span class="artists-hero-stat"><strong>${_fmtNum(info.lastfm_playcount)}</strong> plays</span>`;
}
if (!stats && info.followers) {
stats += `<span class="artists-hero-stat"><strong>${_fmtNum(info.followers)}</strong> followers</span>`;
}
statsEl.innerHTML = stats;
}
// Also update old hidden elements for any JS that references them
const oldImage = document.getElementById('search-artist-detail-image');
if (oldImage && imageUrl) oldImage.style.backgroundImage = `url('${imageUrl}')`;
const oldName = document.getElementById('search-artist-detail-name');
if (oldName) oldName.textContent = artist.name;
// Initialize watchlist button
initializeArtistDetailWatchlistButton(artist);
}

View file

@ -16890,6 +16890,298 @@ body {
transform: translateY(0);
}
/* ========================================= */
/* ARTISTS PAGE - HERO SECTION */
/* ========================================= */
.artists-hero-section {
position: relative;
border-radius: 20px;
overflow: hidden;
margin-bottom: 28px;
min-height: 300px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.artists-hero-bg {
position: absolute;
inset: -20px;
background-size: cover;
background-position: center top;
filter: blur(50px) brightness(0.35) saturate(1.4);
transform: scale(1.3);
z-index: 0;
}
.artists-hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg,
rgba(8, 8, 8, 0.5) 0%,
rgba(12, 12, 12, 0.75) 50%,
rgba(12, 12, 12, 0.92) 100%);
z-index: 1;
}
.artists-hero-content {
position: relative;
z-index: 2;
padding: 32px 36px 36px;
}
.artists-hero-back {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 6px 14px;
color: rgba(255, 255, 255, 0.5);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
margin-bottom: 24px;
display: inline-flex;
align-items: center;
gap: 6px;
letter-spacing: 0.02em;
}
.artists-hero-back:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.artists-hero-main {
display: flex;
gap: 36px;
align-items: center;
}
.artists-hero-image {
width: 400px;
height: 480px;
border-radius: 14px;
background-size: cover;
background-position: center top;
background-color: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
flex-shrink: 0;
box-shadow:
0 16px 48px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.04);
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.artists-hero-section:hover .artists-hero-image {
border-color: rgba(var(--accent-rgb), 0.2);
box-shadow:
0 16px 48px rgba(0, 0, 0, 0.6),
0 0 30px rgba(var(--accent-rgb), 0.1);
transform: scale(1.02);
}
.artists-hero-image-fallback {
font-size: 64px;
opacity: 0.2;
}
.artists-hero-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.artists-hero-name {
font-size: 2.6em;
font-weight: 800;
color: #fff;
margin: 0;
line-height: 1.1;
letter-spacing: -1px;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}
.artists-hero-badges {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.artists-hero-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
backdrop-filter: blur(8px);
}
.artists-hero-badge img {
width: 18px;
height: 18px;
object-fit: contain;
}
.artists-hero-badge:hover {
background: rgba(255, 255, 255, 0.14);
transform: translateY(-2px) scale(1.08);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.artists-hero-genres {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 2px;
}
.artists-hero-genre-pill {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20px;
padding: 5px 14px;
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.65);
letter-spacing: 0.01em;
backdrop-filter: blur(4px);
transition: all 0.15s ease;
}
.artists-hero-genre-pill:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
.artists-hero-bio {
max-width: 600px;
max-height: 3.9em;
overflow: hidden;
position: relative;
transition: max-height 0.3s ease;
}
.artists-hero-bio.expanded {
max-height: 500px;
}
.artists-hero-bio-text {
font-size: 13px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.55);
margin: 0;
display: inline;
}
.artists-hero-bio-toggle {
background: none;
border: none;
color: rgba(var(--accent-rgb), 0.9);
font-size: 12px;
font-weight: 600;
cursor: pointer;
padding: 0;
margin-left: 4px;
display: inline;
}
.artists-hero-bio-toggle:hover {
color: rgb(var(--accent-rgb));
}
.artists-hero-stats {
display: flex;
gap: 24px;
margin-top: 4px;
}
.artists-hero-stat {
font-size: 13px;
color: rgba(255, 255, 255, 0.4);
display: flex;
align-items: baseline;
gap: 5px;
}
.artists-hero-stat strong {
font-size: 18px;
color: #fff;
font-weight: 800;
letter-spacing: -0.5px;
}
.artists-hero-actions {
position: absolute;
top: 32px;
right: 36px;
display: flex;
gap: 8px;
align-items: center;
}
/* Mobile responsive */
@media (max-width: 768px) {
.artists-hero-section {
border-radius: 14px;
min-height: 0;
}
.artists-hero-main {
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
}
.artists-hero-image {
width: 160px;
height: 200px;
}
.artists-hero-name {
font-size: 1.6em;
}
.artists-hero-genres {
justify-content: center;
}
.artists-hero-badges {
justify-content: center;
}
.artists-hero-stats {
justify-content: center;
}
.artists-hero-content {
padding: 20px 16px 24px;
}
.artists-hero-actions {
position: static;
justify-content: center;
margin-top: 8px;
}
.artists-hero-stat strong {
font-size: 16px;
}
}
.artist-detail-header {
display: flex;
align-items: center;
@ -17737,122 +18029,147 @@ body {
.album-cards-container,
.singles-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 24px;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
}
/* Album Card Styles */
/* Album Card — full-bleed image with info overlay, matching similar artist cards */
.album-card {
position: relative;
background: linear-gradient(135deg,
rgba(26, 26, 26, 0.95) 0%,
rgba(18, 18, 18, 0.98) 100%);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(18, 18, 18, 1);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.06);
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(var(--accent-rgb), 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
aspect-ratio: 1;
}
.album-card:hover {
transform: translateY(-6px) scale(1.03);
box-shadow:
0 16px 40px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(var(--accent-rgb), 0.15),
0 0 20px rgba(var(--accent-rgb), 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
transform: translateY(-5px) scale(1.02);
border-color: rgba(var(--accent-rgb), 0.25);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5),
0 0 24px rgba(var(--accent-rgb), 0.12);
}
/* Dynamic glow effects for album cards */
.album-card.has-dynamic-glow {
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1),
filter 0.4s cubic-bezier(0.4, 0, 0.2, 1);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.album-card.has-dynamic-glow:hover {
filter: drop-shadow(0 0 6px var(--glow-color-1, rgb(var(--accent-rgb)))) drop-shadow(0 0 12px var(--glow-color-2, rgb(var(--accent-light-rgb))));
border-color: var(--glow-color-1, rgba(var(--accent-rgb), 0.3));
filter: none;
border-color: color-mix(in srgb, var(--glow-color-1) 30%, transparent);
box-shadow:
0 12px 40px rgba(0, 0, 0, 0.5),
0 0 20px color-mix(in srgb, var(--glow-color-1) 15%, transparent);
}
/* Gradient overlay for text readability */
.album-card::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(0deg,
rgba(0, 0, 0, 0.88) 0%,
rgba(0, 0, 0, 0.35) 40%,
transparent 65%);
pointer-events: none;
z-index: 1;
}
.album-card:hover::after {
background: linear-gradient(0deg,
rgba(0, 0, 0, 0.92) 0%,
rgba(0, 0, 0, 0.3) 45%,
transparent 70%);
}
.album-card-image {
position: absolute;
inset: 0;
width: 100%;
aspect-ratio: 1;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
background-color: rgba(255, 255, 255, 0.03);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 0;
}
/* Fallback gradient if no image */
background-image: linear-gradient(135deg,
rgba(var(--accent-rgb), 0.2) 0%,
rgba(24, 156, 71, 0.1) 100%);
.album-card:hover .album-card-image {
transform: scale(1.06);
}
.album-card-image::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg,
transparent 0%,
transparent 60%,
rgba(0, 0, 0, 0.3) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.album-card:hover .album-card-image::after {
opacity: 1;
display: none;
}
/* Info pinned at bottom over gradient */
.album-card-content {
padding: 20px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 14px;
z-index: 2;
}
.album-card-name {
font-size: 16px;
font-size: 14px;
font-weight: 700;
color: #ffffff;
margin-bottom: 6px;
line-height: 1.3;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
/* Text overflow handling */
color: #fff;
margin-bottom: 3px;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: -0.2px;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.album-card-year {
font-size: 13px;
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
margin-bottom: 8px;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
margin-bottom: 0;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.album-card-type {
display: inline-block;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
display: none; /* Type is in the completion overlay or implied by the tab */
}
background: rgba(var(--accent-rgb), 0.15);
color: rgba(var(--accent-rgb), 0.9);
border-radius: 8px;
border: 1px solid rgba(var(--accent-rgb), 0.2);
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
@media (max-width: 900px) {
.album-cards-container,
.singles-cards-container {
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 14px;
}
}
@media (max-width: 600px) {
.album-cards-container,
.singles-cards-container {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.album-card {
border-radius: 10px;
}
.album-card-content {
padding: 10px;
}
.album-card-name {
font-size: 12px;
}
}
/* Completion Status Overlay */
@ -24635,85 +24952,66 @@ body {
background: rgba(255, 255, 255, 0.3);
}
/* Artist Bubble Card */
/* Similar Artist Card — full-bleed image, library-card style */
.similar-artist-bubble {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px 16px;
border-radius: 16px;
display: block;
position: relative;
overflow: hidden;
border-radius: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* Subtle background */
background: linear-gradient(135deg,
rgba(26, 26, 26, 0.6) 0%,
rgba(18, 18, 18, 0.7) 100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(18, 18, 18, 1);
border: 1px solid rgba(255, 255, 255, 0.06);
/* Elegant shadow */
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
aspect-ratio: 0.8;
/* Progressive load animation */
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
transform: translateY(16px) scale(0.97);
animation: fadeInUp 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* Keyframe animation for bubbles */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(16px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.similar-artist-bubble:hover {
transform: translateY(-6px) scale(1.02);
background: linear-gradient(135deg,
rgba(32, 32, 32, 0.8) 0%,
rgba(24, 24, 24, 0.9) 100%);
border-color: rgba(var(--accent-rgb), 0.3);
box-shadow:
0 8px 20px rgba(0, 0, 0, 0.4),
0 0 20px rgba(var(--accent-rgb), 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
transform: translateY(-5px) scale(1.02);
border-color: rgba(var(--accent-rgb), 0.25);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5),
0 0 24px rgba(var(--accent-rgb), 0.12);
}
/* Circular Image Container */
/* Gradient overlay for text readability */
.similar-artist-bubble::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(0deg,
rgba(0, 0, 0, 0.85) 0%,
rgba(0, 0, 0, 0.3) 40%,
transparent 65%);
pointer-events: none;
z-index: 1;
}
/* Image fills the card */
.similar-artist-bubble-image {
position: relative;
width: 120px;
height: 120px;
margin: 0 auto;
border-radius: 50%;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: 0;
overflow: hidden;
/* Premium border */
border: 3px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: none;
box-shadow: none;
transition: transform 0.3s ease;
}
.similar-artist-bubble:hover .similar-artist-bubble-image {
border-color: rgba(var(--accent-rgb), 0.5);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.5),
0 0 25px rgba(var(--accent-rgb), 0.2),
inset 0 2px 4px rgba(0, 0, 0, 0.3);
transform: scale(1.05);
transform: scale(1.06);
}
.similar-artist-bubble-image img {
@ -24723,27 +25021,32 @@ body {
display: block;
}
/* Fallback for images that fail to load */
.similar-artist-bubble-image-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg,
rgba(var(--accent-rgb), 0.2) 0%,
rgba(var(--accent-light-rgb), 0.15) 100%);
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
font-size: 40px;
color: rgba(255, 255, 255, 0.2);
}
/* Artist Name */
/* Name pinned at bottom over gradient */
.similar-artist-bubble-name {
text-align: center;
font-size: 14px;
font-weight: 600;
color: #ffffff;
line-height: 1.3;
width: 100%;
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 12px;
z-index: 2;
text-align: left;
font-size: 13px;
font-weight: 700;
color: #fff;
line-height: 1.25;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
width: auto;
/* Truncate long names */
display: -webkit-box;
@ -24761,6 +25064,7 @@ body {
/* Genres (Optional - hidden by default for cleaner look) */
.similar-artist-bubble-genres {
display: none; /* Hidden in card layout — name + image is enough */
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
text-align: center;
@ -24808,17 +25112,9 @@ body {
gap: 16px;
}
.similar-artist-bubble {
padding: 16px 12px;
}
.similar-artist-bubble-image {
width: 100px;
height: 100px;
}
.similar-artist-bubble-name {
font-size: 13px;
font-size: 12px;
padding: 10px;
}
}