Unify artist detail: route source artists to standalone page, retire inline Artists page

Completes the artist-detail unification. Source artists now land on
the same /artist-detail page as library artists (with the source-aware
backend endpoint from earlier this session handling the data fetch).
The inline Artists page is gone — artists.js deleted, #artists-page
HTML block removed, /artists URL aliases to /search.

  Source-artist callsites re-migrated from selectArtistForDetail to
  navigateToArtistDetail (search results, global widget, download
  modal, Discover hero / Your Artists cards / artmap context / genre
  deep-dive, watchlist artist detail).

  Visual upgrade to standalone hero: added .artist-detail-hero-bg +
  .artist-detail-hero-overlay (blurred image bg, dark gradient — same
  treatment as the inline page). library.js sets the bg image when
  loading an artist.

  Library-only UI hidden via CSS for source artists (existing rules
  from the previous commit cover Enhanced toggle, Status filter,
  completion bars, enrichment coverage, Top Tracks sidebar, Radio /
  Enhance buttons).

  Final 2 helpers (lazyLoadArtistImages used by wishlist-tools,
  showCompletionError used by completion checker) moved from
  artists.js into shared-helpers.js. The inline-page candidate set
  was dropped from _resolveSimilarArtistsTargets.

  init.js: 'artists' alias added at top of navigateToPage (same
  pattern as the existing 'downloads' alias). 'case artists:' handler
  removed from loadPageData. _getPageFromPath now maps artist-detail
  to library as its parent (matches the existing nav highlight at
  init.js:2161).

  tests/test_script_split_integrity.py: artists.js removed from
  SPLIT_MODULES; KNOWN_CROSS_FILE_DUPES updated to point escapeHtml
  at shared-helpers.js instead of artists.js. 354/354 tests pass.

  Net delta: -1700 lines.

Stays at 2.39. Once you've verified end-to-end (library artist ->
hero looks like inline visual; source artist from Search -> same
page, similar artists works, no 404s; /artists URL -> /search), a
follow-up commit bumps to 2.40 with the full WHATS_NEW entry that's
already prepped.
pull/361/head
Broque Thomas 1 month ago
parent 5219780c01
commit 93f1941829

@ -39,7 +39,6 @@ SPLIT_MODULES = [
"downloads.js",
"wishlist-tools.js",
"sync-services.js",
"artists.js",
"api-monitor.js",
"library.js",
"beatport-ui.js",
@ -57,7 +56,7 @@ NON_SPLIT_JS = {"setup-wizard.js", "docs.js", "helper.js", "particles.js", "work
# In a plain <script> context the last-loaded declaration wins. These are NOT
# regressions from the split — they should be deduplicated in a follow-up.
KNOWN_CROSS_FILE_DUPES = {
"escapeHtml", # downloads.js, artists.js, discover.js
"escapeHtml", # downloads.js, shared-helpers.js, discover.js
"formatDuration", # sync-spotify.js, wishlist-tools.js, sync-services.js
"matchedDownloadTrack", # downloads.js, wishlist-tools.js
"matchedDownloadAlbum", # downloads.js, wishlist-tools.js

@ -2100,146 +2100,6 @@
</div>
</div>
<!-- Artists Page -->
<div class="page" id="artists-page">
<!-- Initial Search State -->
<div class="artists-search-state" id="artists-search-state">
<div class="artists-search-container">
<div class="artists-welcome-section">
<h2 class="artists-welcome-title"><img src="/static/discover.png" class="page-header-icon" alt=""><span>Discover Artists</span></h2>
<p class="artists-welcome-subtitle">Search for your favorite artists and explore their
complete discography</p>
</div>
<div class="artists-search-input-container">
<input type="text" id="artists-search-input" class="artists-search-input"
placeholder="Search for an artist...">
<div class="artists-search-icon">🔍</div>
</div>
<div class="artists-search-status" id="artists-search-status">
Start typing to search for artists
</div>
</div>
</div>
<!-- Search Results State -->
<div class="artists-results-state hidden" id="artists-results-state">
<div class="artists-results-header">
<button class="artists-back-button" id="artists-back-button">
<span class="back-icon"></span>
<span>Back to Search</span>
</button>
<div class="artists-search-header">
<input type="text" id="artists-header-search-input" class="artists-header-search-input"
placeholder="Search for an artist...">
</div>
</div>
<div class="artists-results-content">
<div class="artists-results-title">Search Results</div>
<div class="artists-cards-container" id="artists-cards-container">
<!-- Artist cards will be dynamically populated here -->
</div>
</div>
</div>
<!-- Artist Detail State -->
<div class="artist-detail-state hidden" id="artist-detail-state">
<!-- 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="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>
<button class="discog-download-btn discog-btn-compact" id="discog-download-btn-artists" onclick="openDiscographyModal()" style="display:none;">
<span class="discog-btn-icon"></span>
<span class="discog-btn-text">Download Discography</span>
<span class="discog-btn-shimmer"></span>
</button>
</div>
</div>
<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">
<span class="tab-icon">💿</span>
<span>Albums</span>
</button>
<button class="artist-tab" data-tab="singles" id="singles-tab">
<span class="tab-icon">🎵</span>
<span>Singles & EPs</span>
</button>
</div>
<div class="artist-detail-discography">
<div class="tab-content active" id="albums-content">
<div class="album-cards-container" id="album-cards-container">
<!-- Album cards will be populated here -->
</div>
</div>
<div class="tab-content" id="singles-content">
<div class="singles-cards-container" id="singles-cards-container">
<!-- Singles cards will be populated here -->
</div>
</div>
</div>
<!-- Similar Artists Section -->
<div class="similar-artists-section" id="similar-artists-section">
<div class="similar-artists-header">
<h3 class="similar-artists-title">Similar Artists</h3>
<p class="similar-artists-subtitle">Discover artists with a similar sound</p>
</div>
<!-- Loading State -->
<div class="similar-artists-loading hidden" id="similar-artists-loading">
<div class="loading-spinner-small"></div>
<span>Finding similar artists...</span>
</div>
<!-- Error State -->
<div class="similar-artists-error hidden" id="similar-artists-error">
<span class="error-icon">⚠️</span>
<span class="error-text">Unable to load similar artists</span>
</div>
<!-- Similar Artists Bubbles Container -->
<div class="similar-artists-bubbles-container" id="similar-artists-bubbles-container">
<!-- Artist bubble cards will be populated here -->
</div>
</div>
</div>
</div>
</div>
<!-- Automations Page -->
<div class="page" id="automations-page">
@ -2497,6 +2357,10 @@
<!-- Artist Hero Section -->
<div class="artist-hero-section" id="artist-hero-section">
<!-- Blurred background image + dark overlay (inline-Artists hero treatment) -->
<div class="artist-detail-hero-bg" id="artist-detail-hero-bg"></div>
<div class="artist-detail-hero-overlay"></div>
<div class="artist-hero-content">
<!-- Left: Image -->
<div class="artist-image-container">
@ -8017,7 +7881,6 @@
<script src="{{ url_for('static', filename='downloads.js') }}"></script>
<script src="{{ url_for('static', filename='wishlist-tools.js') }}"></script>
<script src="{{ url_for('static', filename='sync-services.js') }}"></script>
<script src="{{ url_for('static', filename='artists.js') }}"></script>
<script src="{{ url_for('static', filename='api-monitor.js') }}"></script>
<script src="{{ url_for('static', filename='library.js') }}"></script>
<script src="{{ url_for('static', filename='beatport-ui.js') }}"></script>

@ -2385,16 +2385,8 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : 'itunes';
}
if (discogId) {
// Watchlist discogId is a metadata-source id (Spotify/Deezer/iTunes),
// not a library PK — route through the Artists page inline view.
closeWatchlistArtistDetailView();
navigateToPage('artists');
setTimeout(() => {
selectArtistForDetail(
{ id: discogId, name: artistName, image_url: artist.image_url || '' },
{ source: source }
);
}, 200);
navigateToArtistDetail(discogId, artistName, source);
}
});

File diff suppressed because it is too large Load Diff

@ -739,14 +739,7 @@ async function checkRecommendedWatchlistStatuses(artists) {
async function viewRecommendedArtistDiscography(artistId, artistName) {
closeRecommendedArtistsModal();
const artist = { id: artistId, name: artistName };
// Recommended artists come from the metadata source — route through the
// Artists page's inline view so the source-provided id resolves correctly.
navigateToPage('artists');
await new Promise(resolve => setTimeout(resolve, 100));
await selectArtistForDetail(artist);
navigateToArtistDetail(artistId, artistName);
}
async function checkAllHeroWatchlistStatus() {
@ -828,21 +821,8 @@ async function viewDiscoverHeroDiscography() {
return;
}
const artist = {
id: artistId,
name: artistName,
image_url: discoverHeroArtists[discoverHeroIndex]?.image_url || '',
genres: discoverHeroArtists[discoverHeroIndex]?.genres || [],
popularity: discoverHeroArtists[discoverHeroIndex]?.popularity || 0
};
console.log(`🎵 Navigating to artist detail for: ${artistName}`);
// Hero artists are source-provided recommendations — route through the
// Artists page's inline view so the source id resolves correctly.
navigateToPage('artists');
await new Promise(resolve => setTimeout(resolve, 100));
await selectArtistForDetail(artist);
navigateToArtistDetail(artistId, artistName);
}
function showDiscoverHeroEmpty() {
@ -4635,7 +4615,7 @@ function _renderYourArtistCard(artist) {
// Navigate to Artists page (name click) — source artist id, needs inline view
const navAction = hasId
? `event.stopPropagation(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artist.active_source_id)}', name:'${escapeForInlineJs(artist.artist_name)}', image_url:'${escapeForInlineJs(img)}'}), 200)`
? `event.stopPropagation(); navigateToArtistDetail('${escapeForInlineJs(artist.active_source_id)}', '${escapeForInlineJs(artist.artist_name)}')`
: '';
// Open info modal (card body click) — pass pool ID so we can look up all data
@ -4814,7 +4794,7 @@ async function openYourArtistInfoModal(poolId) {
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<span>Explore</span>
</button>
<button class="ya-header-btn ya-viewall-btn" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artistId)}', name:'${escapeForInlineJs(artistName)}', image_url:'${escapeForInlineJs(imageUrl)}'}, {source:'${escapeForInlineJs(pool.active_source || '')}'}), 200)">
<button class="ya-header-btn ya-viewall-btn" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove(); navigateToArtistDetail('${escapeForInlineJs(artistId)}', '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(pool.active_source || '')}' || null)">
<span>View Discography</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</button>
@ -6714,7 +6694,7 @@ function _artMapSetupInteraction(canvas) {
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); ${hasId ? `openYourArtistInfoModal_direct(${JSON.stringify(node).replace(/"/g, '&quot;')})` : ''}">
<span>&#9432;</span> Artist Info
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(bestId)}',name:'${escapeForInlineJs(node.name)}',image_url:'${escapeForInlineJs(node.image_url || '')}'},{source:'${bestSource}'}), 200)">
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToArtistDetail('${escapeForInlineJs(bestId)}', '${escapeForInlineJs(node.name)}', '${bestSource}' || null)">
<span>&#128191;</span> View Discography
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
@ -7402,7 +7382,7 @@ async function openGenreDeepDive(genre) {
// Always open on Artists page with discography — pass source for correct routing
const imgUrl = _esc(a.image_url || '');
const artSource = _esc(a.source || '');
const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToPage('artists');setTimeout(()=>selectArtistForDetail({id:'${_esc(a.entity_id)}',name:'${_esc(a.name)}',image_url:'${imgUrl}'},{source:'${artSource}'}),300)"`;
const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToArtistDetail('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`;
const srcClass = (a.source || '').toLowerCase();
return `<div class="genre-dive-artist" ${clickAction}>
<div class="genre-dive-artist-img" style="${a.image_url ? `background-image:url('${_esc(a.image_url)}')` : ''}">

@ -634,15 +634,7 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play
if (!artistName) return;
// Close the download modal
if (playlistId) closeDownloadMissingModal(playlistId);
// The id from a download modal is typically a metadata-source id; route via
// the Artists page inline view so the source-aware discography endpoint runs.
navigateToPage('artists');
setTimeout(() => {
selectArtistForDetail(
{ id: artistId || artistName, name: artistName, image_url: imageUrl || '' },
source ? { source: source } : undefined
);
}, 200);
navigateToArtistDetail(artistId || artistName, artistName, source || null);
}
async function closeDownloadMissingModal(playlistId) {
@ -5433,19 +5425,8 @@ function _gsSwitchSource(src) {
function _gsClickArtist(id, name, isLibrary) {
_gsDeactivate();
if (isLibrary) {
// Library artists: id is a local DB PK — use the standalone artist-detail page.
navigateToArtistDetail(id, name);
} else {
// Source artists: id is a Deezer/Spotify/iTunes id — route to the Artists
// page's inline view which fetches discography from the source.
navigateToPage('artists');
setTimeout(() => {
selectArtistForDetail({ id, name, image_url: '' }, {
source: _gsState.activeSource || '',
});
}, 150);
}
const source = isLibrary ? null : (_gsState.activeSource || null);
navigateToArtistDetail(id, name, source);
}
async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {

@ -2010,7 +2010,7 @@ function _getPageFromPath() {
const basePage = path.split('/')[0];
if (!_DEEPLINK_VALID_PAGES.has(basePage)) return 'dashboard';
// Context-dependent pages fall back to a sensible parent
if (basePage === 'artist-detail') return 'artists';
if (basePage === 'artist-detail') return 'library';
if (basePage === 'playlist-explorer') return 'library';
return basePage;
}
@ -2115,8 +2115,10 @@ function initializeWatchlist() {
}
function navigateToPage(pageId, options = {}) {
// Backwards-compat alias — the Search page used to live under id 'downloads'.
if (pageId === 'downloads') pageId = 'search';
// Backwards-compat aliases — both legacy ids fold into the unified Search page.
// 'downloads' was the Search page's old id; 'artists' was the retired inline
// Artists page, now replaced by clicking artists from the unified Search.
if (pageId === 'downloads' || pageId === 'artists') pageId = 'search';
if (pageId === currentPage) return;
@ -2218,15 +2220,7 @@ async function loadPageData(pageId) {
initializeSearchModeToggle();
initializeFilters();
break;
case 'artists':
// Only fully initialize if not already initialized
if (!artistsPageState.isInitialized) {
initializeArtistsPage();
} else {
// Just restore state if already initialized
restoreArtistsPageState();
}
break;
// 'artists' page retired — aliased to 'search' at the top of navigateToPage
case 'active-downloads':
loadActiveDownloadsPage();
break;

@ -1063,6 +1063,17 @@ function updateArtistHeaderStats(albumCount, trackCount) {
function updateArtistHeroSection(artist, discography) {
console.log("🖼️ Updating artist hero section");
// Blurred background image (inline-Artists hero treatment) — set whenever
// we have an image_url; falls back to clearing the bg if not.
const heroBg = document.getElementById("artist-detail-hero-bg");
if (heroBg) {
if (artist.image_url && artist.image_url.trim() !== "" && artist.image_url !== "null") {
heroBg.style.backgroundImage = `url('${artist.image_url}')`;
} else {
heroBg.style.backgroundImage = '';
}
}
// Update artist image with detailed debugging
const imageElement = document.getElementById("artist-detail-image");
const fallbackElement = document.getElementById("artist-detail-image-fallback");

@ -336,21 +336,11 @@ function initializeSearchModeToggle() {
name: artist.name,
meta: 'Artist',
badge: sourceBadge,
onClick: async () => {
onClick: () => {
const sourceOverride = _activeSearchSource;
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
hideDropdown();
// Source artists are NOT library entries — their id is a Deezer/
// Spotify/iTunes id, not a library PK. Route to the Artists page's
// inline selectArtistForDetail which fetches discography from the
// source directly, not the library's /api/artist-detail endpoint.
navigateToPage('artists');
await new Promise(resolve => setTimeout(resolve, 100));
await selectArtistForDetail(artist, {
source: sourceOverride,
plugin: artist.external_urls?.hydrabase_plugin,
});
navigateToArtistDetail(artist.id, artist.name, sourceOverride || null);
}
})
);

@ -2768,41 +2768,19 @@ function renderEnrichmentCards(enrichment) {
// container that live on the artist-detail page.
// ----------------------------------------------------------------------------
// Similar artists lives on two pages (the inline Artists page and the standalone
// artist-detail page), each with its own set of IDs so they don't collide in the
// DOM. This resolver picks the set whose `.page` ancestor is currently active.
// Similar artists section lives on the standalone artist-detail page with the
// 'ad-' prefixed ids. The resolver shape was originally designed for both the
// inline Artists page and the standalone page; the inline page has since been
// retired, so only the standalone candidate remains.
function _resolveSimilarArtistsTargets() {
const candidates = [
// standalone artist-detail page (scoped ids)
{ section: 'ad-similar-artists-section', loading: 'ad-similar-artists-loading', error: 'ad-similar-artists-error', bubbles: 'ad-similar-artists-bubbles-container' },
// legacy inline Artists page (base ids)
{ section: 'similar-artists-section', loading: 'similar-artists-loading', error: 'similar-artists-error', bubbles: 'similar-artists-bubbles-container' },
];
// Prefer the set whose parent .page is currently active.
for (const c of candidates) {
const el = document.getElementById(c.section);
if (el && el.closest('.page.active')) {
return {
section: el,
loadingEl: document.getElementById(c.loading),
errorEl: document.getElementById(c.error),
container: document.getElementById(c.bubbles),
};
}
}
// Fallback: return the first set that exists at all.
for (const c of candidates) {
const el = document.getElementById(c.section);
if (el) {
return {
section: el,
loadingEl: document.getElementById(c.loading),
errorEl: document.getElementById(c.error),
container: document.getElementById(c.bubbles),
};
}
}
return null;
const sectionEl = document.getElementById('ad-similar-artists-section');
if (!sectionEl) return null;
return {
section: sectionEl,
loadingEl: document.getElementById('ad-similar-artists-loading'),
errorEl: document.getElementById('ad-similar-artists-error'),
container: document.getElementById('ad-similar-artists-bubbles-container'),
};
}
async function loadSimilarArtists(artistName) {
@ -3110,21 +3088,78 @@ function createSimilarArtistBubble(artist) {
bubble.appendChild(genres);
}
// Add click handler — page-aware destination. From the standalone artist-
// detail page, navigate to the standalone route. From the inline Artists
// page, swap the inline view via selectArtistForDetail.
// Click → navigate to the standalone artist-detail page. Works for both
// library and source artists thanks to the source-aware backend endpoint.
bubble.addEventListener('click', () => {
console.log(`🎵 Clicked similar artist: ${artist.name} (ID: ${artist.id})`);
const onStandalone = !!document.querySelector('#artist-detail-page.active');
if (onStandalone && typeof navigateToArtistDetail === 'function') {
navigateToArtistDetail(artist.id, artist.name, artist.source || null);
} else if (typeof selectArtistForDetail === 'function') {
selectArtistForDetail(
artist,
artist.source ? { source: artist.source, plugin: artist.plugin } : {}
);
}
navigateToArtistDetail(artist.id, artist.name, artist.source || null);
});
return bubble;
}
// ----------------------------------------------------------------------------
// Lazy artist-card image loader (used by wishlist-tools.js + the legacy inline
// Artists page search results). Fetches /api/artist/<id>/image for each card
// flagged data-needs-image="true" in batches of 5.
// ----------------------------------------------------------------------------
async function lazyLoadArtistImages(container) {
if (!container) {
console.error('❌ lazyLoadArtistImages: container is null');
return;
}
const cardsNeedingImages = container.querySelectorAll('[data-needs-image="true"]');
if (cardsNeedingImages.length === 0) return;
const batchSize = 5;
const cards = Array.from(cardsNeedingImages);
for (let i = 0; i < cards.length; i += batchSize) {
const batch = cards.slice(i, i + batchSize);
await Promise.all(batch.map(async (card) => {
const artistId = card.dataset.artistId;
if (!artistId) return;
try {
const response = await fetch(`/api/artist/${artistId}/image`);
const data = await response.json();
if (data.success && data.image_url) {
if (card.classList.contains('suggestion-card')) {
card.style.backgroundImage = `url(${data.image_url})`;
card.style.backgroundSize = 'cover';
card.style.backgroundPosition = 'center';
} else if (card.classList.contains('artist-card')) {
const bgElement = card.querySelector('.artist-card-background');
if (bgElement) {
bgElement.style.cssText = `background-image: url('${data.image_url}'); background-size: cover; background-position: center;`;
}
}
card.dataset.needsImage = 'false';
}
} catch (error) {
console.error(`❌ Failed to load image for artist ${artistId}:`, error);
}
}));
}
}
// Legacy global alias — wishlist-tools.js falls back to window.lazyLoadArtistImages
window.lazyLoadArtistImages = lazyLoadArtistImages;
// ----------------------------------------------------------------------------
// Album-card completion overlay error state (called from checkDiscographyCompletion
// when the API request fails)
// ----------------------------------------------------------------------------
function showCompletionError() {
const allOverlays = document.querySelectorAll('.completion-overlay.checking');
allOverlays.forEach(overlay => {
overlay.classList.remove('checking');
overlay.classList.add('error');
overlay.innerHTML = '<span class="completion-status">Error</span>';
overlay.title = 'Failed to check completion status';
});
}

@ -59413,3 +59413,41 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt
body[data-artist-source="source"] #artist-detail-page .section-stats {
display: none !important;
}
/* =========================================================================
Standalone /artist-detail page hero blurred-background treatment
========================================================================= */
#artist-detail-page .artist-hero-section {
position: relative;
overflow: hidden;
}
#artist-detail-page .artist-detail-hero-bg {
position: absolute;
inset: -20px;
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
filter: blur(50px) brightness(0.35) saturate(1.4);
transform: scale(1.3);
z-index: 0;
pointer-events: none;
}
#artist-detail-page .artist-detail-hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg,
rgba(8, 8, 8, 0.5) 0%,
rgba(12, 12, 12, 0.7) 60%,
rgba(12, 12, 12, 0.9) 100%);
z-index: 1;
pointer-events: none;
}
#artist-detail-page .artist-hero-content {
position: relative;
z-index: 2;
}

Loading…
Cancel
Save