« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content/dependencies
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/dependencies')
-rw-r--r--src/content/dependencies/generateAbsoluteDatetimestamp.js108
-rw-r--r--src/content/dependencies/generateAdditionalFilesList.js3
-rw-r--r--src/content/dependencies/generateAdditionalFilesListChunk.js3
-rw-r--r--src/content/dependencies/generateAdditionalNamesBox.js15
-rw-r--r--src/content/dependencies/generateAdditionalNamesBoxItem.js3
-rw-r--r--src/content/dependencies/generateAlbumArtInfoBox.js3
-rw-r--r--src/content/dependencies/generateAlbumArtworkColumn.js57
-rw-r--r--src/content/dependencies/generateAlbumBanner.js3
-rw-r--r--src/content/dependencies/generateAlbumCommentaryPage.js27
-rw-r--r--src/content/dependencies/generateAlbumCommentarySidebar.js9
-rw-r--r--src/content/dependencies/generateAlbumGalleryAlbumGrid.js8
-rw-r--r--src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js3
-rw-r--r--src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js2
-rw-r--r--src/content/dependencies/generateAlbumGalleryPage.js21
-rw-r--r--src/content/dependencies/generateAlbumGalleryStatsLine.js86
-rw-r--r--src/content/dependencies/generateAlbumGalleryTrackGrid.js16
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js86
-rw-r--r--src/content/dependencies/generateAlbumNavAccent.js22
-rw-r--r--src/content/dependencies/generateAlbumReferencedArtworksPage.js15
-rw-r--r--src/content/dependencies/generateAlbumReferencingArtworksPage.js15
-rw-r--r--src/content/dependencies/generateAlbumReleaseInfo.js45
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNav.js9
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNavGroupPart.js9
-rw-r--r--src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js9
-rw-r--r--src/content/dependencies/generateAlbumSidebar.js93
-rw-r--r--src/content/dependencies/generateAlbumSidebarGroupBox.js10
-rw-r--r--src/content/dependencies/generateAlbumSidebarSeriesBox.js9
-rw-r--r--src/content/dependencies/generateAlbumSidebarTrackListBox.js12
-rw-r--r--src/content/dependencies/generateAlbumSidebarTrackSection.js26
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbed.js7
-rw-r--r--src/content/dependencies/generateAlbumSocialEmbedDescription.js2
-rw-r--r--src/content/dependencies/generateAlbumStyleRules.js107
-rw-r--r--src/content/dependencies/generateAlbumStyleTags.js62
-rw-r--r--src/content/dependencies/generateAlbumTrackList.js8
-rw-r--r--src/content/dependencies/generateAlbumTrackListItem.js7
-rw-r--r--src/content/dependencies/generateAlbumWallpaperStyleTag.js35
-rw-r--r--src/content/dependencies/generateArtTagAncestorDescendantMapList.js3
-rw-r--r--src/content/dependencies/generateArtTagGalleryPage.js16
-rw-r--r--src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js2
-rw-r--r--src/content/dependencies/generateArtTagGalleryPageShowingLine.js2
-rw-r--r--src/content/dependencies/generateArtTagInfoPage.js22
-rw-r--r--src/content/dependencies/generateArtTagNavLinks.js8
-rw-r--r--src/content/dependencies/generateArtTagSidebar.js9
-rw-r--r--src/content/dependencies/generateArtistArtworkColumn.js2
-rw-r--r--src/content/dependencies/generateArtistCredit.js174
-rw-r--r--src/content/dependencies/generateArtistCreditWikiEditsPart.js9
-rw-r--r--src/content/dependencies/generateArtistGalleryPage.js16
-rw-r--r--src/content/dependencies/generateArtistGroupContributionsInfo.js140
-rw-r--r--src/content/dependencies/generateArtistInfoPage.js62
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunk.js19
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js58
-rw-r--r--src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js5
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunk.js87
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkItem.js111
-rw-r--r--src/content/dependencies/generateArtistInfoPageChunkedList.js2
-rw-r--r--src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js103
-rw-r--r--src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js24
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunk.js14
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js4
-rw-r--r--src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js7
-rw-r--r--src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js2
-rw-r--r--src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js47
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunk.js47
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkItem.js41
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkedList.js5
-rw-r--r--src/content/dependencies/generateArtistNavLinks.js20
-rw-r--r--src/content/dependencies/generateArtistRollingWindowPage.js418
-rw-r--r--src/content/dependencies/generateBackToAlbumLink.js3
-rw-r--r--src/content/dependencies/generateBackToTrackLink.js3
-rw-r--r--src/content/dependencies/generateBanner.js2
-rw-r--r--src/content/dependencies/generateCollapsedContentEntrySection.js37
-rw-r--r--src/content/dependencies/generateColorStyleAttribute.js3
-rw-r--r--src/content/dependencies/generateColorStyleRules.js42
-rw-r--r--src/content/dependencies/generateColorStyleTag.js48
-rw-r--r--src/content/dependencies/generateColorStyleVariables.js20
-rw-r--r--src/content/dependencies/generateCommentaryContentHeading.js43
-rw-r--r--src/content/dependencies/generateCommentaryEntry.js19
-rw-r--r--src/content/dependencies/generateCommentaryEntryDate.js3
-rw-r--r--src/content/dependencies/generateCommentaryIndexPage.js81
-rw-r--r--src/content/dependencies/generateContentContentHeading.js73
-rw-r--r--src/content/dependencies/generateContentHeading.js2
-rw-r--r--src/content/dependencies/generateContributionList.js9
-rw-r--r--src/content/dependencies/generateContributionTooltip.js156
-rw-r--r--src/content/dependencies/generateContributionTooltipChronologySection.js55
-rw-r--r--src/content/dependencies/generateContributionTooltipExternalLinkSection.js8
-rw-r--r--src/content/dependencies/generateCoverArtwork.js74
-rw-r--r--src/content/dependencies/generateCoverArtworkArtTagDetails.js3
-rw-r--r--src/content/dependencies/generateCoverArtworkArtistDetails.js3
-rw-r--r--src/content/dependencies/generateCoverArtworkOriginDetails.js84
-rw-r--r--src/content/dependencies/generateCoverArtworkReferenceDetails.js3
-rw-r--r--src/content/dependencies/generateCoverCarousel.js2
-rw-r--r--src/content/dependencies/generateCoverGrid.js100
-rw-r--r--src/content/dependencies/generateDatetimestampTemplate.js3
-rw-r--r--src/content/dependencies/generateDotSwitcherTemplate.js2
-rw-r--r--src/content/dependencies/generateExpandableGallerySection.js92
-rw-r--r--src/content/dependencies/generateExternalHandle.js2
-rw-r--r--src/content/dependencies/generateExternalIcon.js2
-rw-r--r--src/content/dependencies/generateExternalPlatform.js2
-rw-r--r--src/content/dependencies/generateFlashActGalleryPage.js13
-rw-r--r--src/content/dependencies/generateFlashActNavAccent.js9
-rw-r--r--src/content/dependencies/generateFlashActSidebar.js6
-rw-r--r--src/content/dependencies/generateFlashActSidebarCurrentActBox.js8
-rw-r--r--src/content/dependencies/generateFlashActSidebarSideMapBox.js9
-rw-r--r--src/content/dependencies/generateFlashArtworkColumn.js2
-rw-r--r--src/content/dependencies/generateFlashIndexPage.js11
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js81
-rw-r--r--src/content/dependencies/generateFlashNavAccent.js9
-rw-r--r--src/content/dependencies/generateFooterLocalizationLinks.js9
-rw-r--r--src/content/dependencies/generateGridActionLinks.js2
-rw-r--r--src/content/dependencies/generateGridExpando.js37
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js21
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumGrid.js65
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js79
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js21
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js3
-rw-r--r--src/content/dependencies/generateGroupGalleryPageSeriesSection.js152
-rw-r--r--src/content/dependencies/generateGroupGalleryPageStyleSelector.js60
-rw-r--r--src/content/dependencies/generateGroupInfoPage.js14
-rw-r--r--src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js10
-rw-r--r--src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js26
-rw-r--r--src/content/dependencies/generateGroupInfoPageAlbumsListItem.js16
-rw-r--r--src/content/dependencies/generateGroupInfoPageAlbumsSection.js10
-rw-r--r--src/content/dependencies/generateGroupNavAccent.js8
-rw-r--r--src/content/dependencies/generateGroupNavLinks.js3
-rw-r--r--src/content/dependencies/generateGroupSecondaryNav.js5
-rw-r--r--src/content/dependencies/generateGroupSecondaryNavCategoryPart.js9
-rw-r--r--src/content/dependencies/generateGroupSidebar.js8
-rw-r--r--src/content/dependencies/generateGroupSidebarCategoryDetails.js8
-rw-r--r--src/content/dependencies/generateImageOverlay.js2
-rw-r--r--src/content/dependencies/generateInterpageDotSwitcher.js3
-rw-r--r--src/content/dependencies/generateIntrapageDotSwitcher.js34
-rw-r--r--src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js3
-rw-r--r--src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js9
-rw-r--r--src/content/dependencies/generateListAllAdditionalFilesChunk.js3
-rw-r--r--src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js3
-rw-r--r--src/content/dependencies/generateListRandomPageLinksAlbumLink.js2
-rw-r--r--src/content/dependencies/generateListingIndexList.js3
-rw-r--r--src/content/dependencies/generateListingPage.js103
-rw-r--r--src/content/dependencies/generateListingPageSameTargetListingsLine.js46
-rw-r--r--src/content/dependencies/generateListingSidebar.js9
-rw-r--r--src/content/dependencies/generateListingsIndexPage.js12
-rw-r--r--src/content/dependencies/generateLyricsEntry.js47
-rw-r--r--src/content/dependencies/generateLyricsSection.js38
-rw-r--r--src/content/dependencies/generateName.js33
-rw-r--r--src/content/dependencies/generateNearbyTrackList.js44
-rw-r--r--src/content/dependencies/generateNewsEntryNavAccent.js7
-rw-r--r--src/content/dependencies/generateNewsEntryPage.js10
-rw-r--r--src/content/dependencies/generateNewsEntryReadAnotherLinks.js22
-rw-r--r--src/content/dependencies/generateNewsIndexPage.js8
-rw-r--r--src/content/dependencies/generateNextLink.js2
-rw-r--r--src/content/dependencies/generatePageLayout.js119
-rw-r--r--src/content/dependencies/generatePageSidebar.js2
-rw-r--r--src/content/dependencies/generatePageSidebarBox.js2
-rw-r--r--src/content/dependencies/generatePageSidebarConjoinedBox.js3
-rw-r--r--src/content/dependencies/generatePreviousLink.js2
-rw-r--r--src/content/dependencies/generatePreviousNextLink.js2
-rw-r--r--src/content/dependencies/generateQuickDescription.js3
-rw-r--r--src/content/dependencies/generateReadCommentaryLine.js43
-rw-r--r--src/content/dependencies/generateReferencedArtworksPage.js14
-rw-r--r--src/content/dependencies/generateReferencedTracksList.js29
-rw-r--r--src/content/dependencies/generateReferencingArtworksPage.js14
-rw-r--r--src/content/dependencies/generateRelativeDatetimestamp.js35
-rw-r--r--src/content/dependencies/generateReleaseInfoContributionsLine.js10
-rw-r--r--src/content/dependencies/generateReleaseInfoListenLine.js156
-rw-r--r--src/content/dependencies/generateSearchSidebarBox.js20
-rw-r--r--src/content/dependencies/generateSecondaryNav.js2
-rw-r--r--src/content/dependencies/generateSecondaryNavParentSiblingsPart.js11
-rw-r--r--src/content/dependencies/generateSocialEmbed.js2
-rw-r--r--src/content/dependencies/generateStaticPage.js17
-rw-r--r--src/content/dependencies/generateStaticURLStyleTag.js20
-rw-r--r--src/content/dependencies/generateStickyHeadingContainer.js2
-rw-r--r--src/content/dependencies/generateStyleTag.js46
-rw-r--r--src/content/dependencies/generateTextWithTooltip.js2
-rw-r--r--src/content/dependencies/generateTooltip.js2
-rw-r--r--src/content/dependencies/generateTrackArtistCommentarySection.js93
-rw-r--r--src/content/dependencies/generateTrackArtworkColumn.js3
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js202
-rw-r--r--src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js3
-rw-r--r--src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js80
-rw-r--r--src/content/dependencies/generateTrackInfoPageOtherReleasesList.js42
-rw-r--r--src/content/dependencies/generateTrackInfoPagePreviousProductionLine.js38
-rw-r--r--src/content/dependencies/generateTrackList.js23
-rw-r--r--src/content/dependencies/generateTrackListDividedByGroups.js18
-rw-r--r--src/content/dependencies/generateTrackListItem.js76
-rw-r--r--src/content/dependencies/generateTrackListMissingDuration.js3
-rw-r--r--src/content/dependencies/generateTrackNavLinks.js14
-rw-r--r--src/content/dependencies/generateTrackReferencedArtworksPage.js15
-rw-r--r--src/content/dependencies/generateTrackReferencingArtworksPage.js15
-rw-r--r--src/content/dependencies/generateTrackReleaseBox.js8
-rw-r--r--src/content/dependencies/generateTrackReleaseInfo.js70
-rw-r--r--src/content/dependencies/generateTrackSocialEmbed.js7
-rw-r--r--src/content/dependencies/generateTrackSocialEmbedDescription.js2
-rw-r--r--src/content/dependencies/generateUnsafeMunchy.js2
-rw-r--r--src/content/dependencies/generateWallpaperStyleTag.js77
-rw-r--r--src/content/dependencies/generateWikiHomepageActionsRow.js2
-rw-r--r--src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js2
-rw-r--r--src/content/dependencies/generateWikiHomepageAlbumGridRow.js6
-rw-r--r--src/content/dependencies/generateWikiHomepageNewsBox.js8
-rw-r--r--src/content/dependencies/generateWikiHomepagePage.js11
-rw-r--r--src/content/dependencies/generateWikiHomepageSection.js9
-rw-r--r--src/content/dependencies/generateWikiWallpaperStyleTag.js35
-rw-r--r--src/content/dependencies/image.js109
-rw-r--r--src/content/dependencies/index.js16
-rw-r--r--src/content/dependencies/linkAdditionalFile.js2
-rw-r--r--src/content/dependencies/linkAlbum.js18
-rw-r--r--src/content/dependencies/linkAlbumCommentary.js2
-rw-r--r--src/content/dependencies/linkAlbumDynamically.js8
-rw-r--r--src/content/dependencies/linkAlbumGallery.js2
-rw-r--r--src/content/dependencies/linkAlbumReferencedArtworks.js2
-rw-r--r--src/content/dependencies/linkAlbumReferencingArtworks.js2
-rw-r--r--src/content/dependencies/linkAnythingMan.js21
-rw-r--r--src/content/dependencies/linkArtTagDynamically.js3
-rw-r--r--src/content/dependencies/linkArtTagGallery.js2
-rw-r--r--src/content/dependencies/linkArtTagInfo.js2
-rw-r--r--src/content/dependencies/linkArtist.js2
-rw-r--r--src/content/dependencies/linkArtistGallery.js2
-rw-r--r--src/content/dependencies/linkArtistRollingWindow.js6
-rw-r--r--src/content/dependencies/linkArtwork.js13
-rw-r--r--src/content/dependencies/linkCommentaryIndex.js2
-rw-r--r--src/content/dependencies/linkContribution.js16
-rw-r--r--src/content/dependencies/linkExternal.js41
-rw-r--r--src/content/dependencies/linkFlash.js2
-rw-r--r--src/content/dependencies/linkFlashAct.js2
-rw-r--r--src/content/dependencies/linkFlashIndex.js2
-rw-r--r--src/content/dependencies/linkFlashSide.js2
-rw-r--r--src/content/dependencies/linkGroup.js2
-rw-r--r--src/content/dependencies/linkGroupDynamically.js3
-rw-r--r--src/content/dependencies/linkGroupExtra.js7
-rw-r--r--src/content/dependencies/linkGroupGallery.js2
-rw-r--r--src/content/dependencies/linkListing.js3
-rw-r--r--src/content/dependencies/linkListingIndex.js2
-rw-r--r--src/content/dependencies/linkNewsEntry.js2
-rw-r--r--src/content/dependencies/linkNewsIndex.js2
-rw-r--r--src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js3
-rw-r--r--src/content/dependencies/linkPathFromMedia.js11
-rw-r--r--src/content/dependencies/linkPathFromRoot.js2
-rw-r--r--src/content/dependencies/linkPathFromSite.js2
-rw-r--r--src/content/dependencies/linkReferencedArtworks.js18
-rw-r--r--src/content/dependencies/linkReferencingArtworks.js18
-rw-r--r--src/content/dependencies/linkStaticPage.js2
-rw-r--r--src/content/dependencies/linkStationaryIndex.js3
-rw-r--r--src/content/dependencies/linkTemplate.js7
-rw-r--r--src/content/dependencies/linkThing.js32
-rw-r--r--src/content/dependencies/linkTrack.js2
-rw-r--r--src/content/dependencies/linkTrackAsRelease.js20
-rw-r--r--src/content/dependencies/linkTrackDynamically.js3
-rw-r--r--src/content/dependencies/linkTrackReferencedArtworks.js2
-rw-r--r--src/content/dependencies/linkTrackReferencingArtworks.js2
-rw-r--r--src/content/dependencies/linkWikiHomepage.js3
-rw-r--r--src/content/dependencies/listAlbumsByDate.js3
-rw-r--r--src/content/dependencies/listAlbumsByDateAdded.js3
-rw-r--r--src/content/dependencies/listAlbumsByDuration.js11
-rw-r--r--src/content/dependencies/listAlbumsByName.js3
-rw-r--r--src/content/dependencies/listAlbumsByTracks.js34
-rw-r--r--src/content/dependencies/listAllAdditionalFiles.js2
-rw-r--r--src/content/dependencies/listAllAdditionalFilesTemplate.js7
-rw-r--r--src/content/dependencies/listAllMidiProjectFiles.js2
-rw-r--r--src/content/dependencies/listAllSheetMusicFiles.js2
-rw-r--r--src/content/dependencies/listArtTagNetwork.js3
-rw-r--r--src/content/dependencies/listArtTagsByName.js3
-rw-r--r--src/content/dependencies/listArtTagsByUses.js3
-rw-r--r--src/content/dependencies/listArtistsByCommentaryEntries.js3
-rw-r--r--src/content/dependencies/listArtistsByContributions.js55
-rw-r--r--src/content/dependencies/listArtistsByDuration.js3
-rw-r--r--src/content/dependencies/listArtistsByGroup.js3
-rw-r--r--src/content/dependencies/listArtistsByLatestContribution.js9
-rw-r--r--src/content/dependencies/listArtistsByName.js3
-rw-r--r--src/content/dependencies/listGroupsByAlbums.js3
-rw-r--r--src/content/dependencies/listGroupsByCategory.js3
-rw-r--r--src/content/dependencies/listGroupsByDuration.js3
-rw-r--r--src/content/dependencies/listGroupsByLatestAlbum.js9
-rw-r--r--src/content/dependencies/listGroupsByName.js3
-rw-r--r--src/content/dependencies/listGroupsByTracks.js3
-rw-r--r--src/content/dependencies/listRandomPageLinks.js8
-rw-r--r--src/content/dependencies/listTracksByAlbum.js3
-rw-r--r--src/content/dependencies/listTracksByDate.js3
-rw-r--r--src/content/dependencies/listTracksByDuration.js3
-rw-r--r--src/content/dependencies/listTracksByDurationInAlbum.js3
-rw-r--r--src/content/dependencies/listTracksByName.js3
-rw-r--r--src/content/dependencies/listTracksByTimesReferenced.js3
-rw-r--r--src/content/dependencies/listTracksInFlashesByAlbum.js3
-rw-r--r--src/content/dependencies/listTracksInFlashesByFlash.js3
-rw-r--r--src/content/dependencies/listTracksNeedingLyrics.js7
-rw-r--r--src/content/dependencies/listTracksWithExtra.js3
-rw-r--r--src/content/dependencies/listTracksWithLyrics.js2
-rw-r--r--src/content/dependencies/listTracksWithMidiProjectFiles.js2
-rw-r--r--src/content/dependencies/listTracksWithSheetMusicFiles.js2
-rw-r--r--src/content/dependencies/transformContent.js286
288 files changed, 3972 insertions, 2962 deletions
diff --git a/src/content/dependencies/generateAbsoluteDatetimestamp.js b/src/content/dependencies/generateAbsoluteDatetimestamp.js
index 930b6f13..d006374a 100644
--- a/src/content/dependencies/generateAbsoluteDatetimestamp.js
+++ b/src/content/dependencies/generateAbsoluteDatetimestamp.js
@@ -1,15 +1,12 @@
 export default {
-  contentDependencies: [
-    'generateDatetimestampTemplate',
-    'generateTooltip',
-  ],
+  data: (date, contextDate) => ({
+    date,
 
-  extraDependencies: ['html', 'language'],
-
-  data: (date) =>
-    ({date}),
+    contextDate:
+      contextDate ?? null,
+  }),
 
-  relations: (relation) => ({
+  relations: (relation, _date, _contextDate) => ({
     template:
       relation('generateDatetimestampTemplate'),
 
@@ -19,35 +16,74 @@ export default {
 
   slots: {
     style: {
-      validate: v => v.is('full', 'year'),
+      validate: v => v.is(...[
+        'full',
+        'year',
+        'minimal-difference',
+        'year-difference',
+      ]),
       default: 'full',
     },
-
-    // Only has an effect for 'year' style.
-    tooltip: {
-      type: 'boolean',
-      default: false,
-    },
   },
 
-  generate: (data, relations, slots, {language}) =>
-    relations.template.slots({
-      mainContent:
-        (slots.style === 'full'
-          ? language.formatDate(data.date)
-       : slots.style === 'year'
-          ? data.date.getFullYear().toString()
-          : null),
-
-      tooltip:
-        slots.tooltip &&
-        slots.style === 'year' &&
-          relations.tooltip.slots({
-            content:
-              language.formatDate(data.date),
-          }),
-
-      datetime:
-        data.date.toISOString(),
-    }),
+  generate(data, relations, slots, {html, language}) {
+    if (!data.date) {
+      return html.blank();
+    }
+
+    relations.template.setSlots({
+      tooltip: relations.tooltip,
+      datetime: data.date.toISOString(),
+    });
+
+    let label = null;
+    let tooltip = null;
+
+    switch (slots.style) {
+      case 'full': {
+        label = language.formatDate(data.date);
+        break;
+      }
+
+      case 'year': {
+        label = language.formatYear(data.date);
+        tooltip = language.formatDate(data.date);
+        break;
+      }
+
+      case 'minimal-difference': {
+        if (data.date.toDateString() === data.contextDate?.toDateString()) {
+          return html.blank();
+        }
+
+        if (data.date.getFullYear() === data.contextDate?.getFullYear()) {
+          label = language.formatMonthDay(data.date);
+          tooltip = language.formatDate(data.date);
+        } else {
+          label = language.formatYear(data.date);
+          tooltip = language.formatDate(data.date);
+        }
+
+        break;
+      }
+
+      case 'year-difference': {
+        if (data.date.toDateString() === data.contextDate?.toDateString()) {
+          return html.blank();
+        }
+
+        if (data.date.getFullYear() === data.contextDate?.getFullYear()) {
+          label = language.formatDate(data.date);
+        } else {
+          label = language.formatYear(data.date);
+          tooltip = language.formatDate(data.date);
+        }
+      }
+    }
+
+    relations.template.setSlot('mainContent', label);
+    relations.tooltip.setSlot('content', tooltip);
+
+    return relations.template;
+  },
 };
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js
index 7e05b5b5..699c5f86 100644
--- a/src/content/dependencies/generateAdditionalFilesList.js
+++ b/src/content/dependencies/generateAdditionalFilesList.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateAdditionalFilesListChunk'],
-  extraDependencies: ['html'],
-
   relations: (relation, additionalFiles) => ({
     chunks:
       additionalFiles
diff --git a/src/content/dependencies/generateAdditionalFilesListChunk.js b/src/content/dependencies/generateAdditionalFilesListChunk.js
index 3cac851b..466a5d8d 100644
--- a/src/content/dependencies/generateAdditionalFilesListChunk.js
+++ b/src/content/dependencies/generateAdditionalFilesListChunk.js
@@ -1,9 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['linkAdditionalFile', 'transformContent'],
-  extraDependencies: ['getSizeOfMediaFile', 'html', 'language', 'urls'],
-
   relations: (relation, file) => ({
     description:
       relation('transformContent', file.description),
diff --git a/src/content/dependencies/generateAdditionalNamesBox.js b/src/content/dependencies/generateAdditionalNamesBox.js
index b7392dfd..6bd1ab42 100644
--- a/src/content/dependencies/generateAdditionalNamesBox.js
+++ b/src/content/dependencies/generateAdditionalNamesBox.js
@@ -1,18 +1,25 @@
 export default {
-  contentDependencies: ['generateAdditionalNamesBoxItem'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, additionalNames) => ({
     items:
       additionalNames
         .map(entry => relation('generateAdditionalNamesBoxItem', entry)),
   }),
 
-  generate: (relations, {html, language}) =>
+  slots: {
+    alwaysVisible: {
+      type: 'boolean',
+      default: false,
+    },
+  },
+
+  generate: (relations, slots, {html, language}) =>
     html.tag('div', {id: 'additional-names-box'},
       {class: 'drop'},
       {[html.onlyIfContent]: true},
 
+      slots.alwaysVisible &&
+        {class: 'always-visible'},
+
       [
         html.tag('p',
           {[html.onlyIfSiblings]: true},
diff --git a/src/content/dependencies/generateAdditionalNamesBoxItem.js b/src/content/dependencies/generateAdditionalNamesBoxItem.js
index e3e59a34..a39711c1 100644
--- a/src/content/dependencies/generateAdditionalNamesBoxItem.js
+++ b/src/content/dependencies/generateAdditionalNamesBoxItem.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['transformContent'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, entry) => ({
     nameContent:
       relation('transformContent', entry.name),
diff --git a/src/content/dependencies/generateAlbumArtInfoBox.js b/src/content/dependencies/generateAlbumArtInfoBox.js
index 8c44c930..5491192a 100644
--- a/src/content/dependencies/generateAlbumArtInfoBox.js
+++ b/src/content/dependencies/generateAlbumArtInfoBox.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateReleaseInfoContributionsLine'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, album) => ({
     wallpaperArtistContributionsLine:
       (album.wallpaperArtwork
diff --git a/src/content/dependencies/generateAlbumArtworkColumn.js b/src/content/dependencies/generateAlbumArtworkColumn.js
index e6762463..5346e56b 100644
--- a/src/content/dependencies/generateAlbumArtworkColumn.js
+++ b/src/content/dependencies/generateAlbumArtworkColumn.js
@@ -1,38 +1,51 @@
 export default {
-  contentDependencies: ['generateAlbumArtInfoBox', 'generateCoverArtwork'],
-  extraDependencies: ['html'],
-
-  relations: (relation, album) => ({
-    firstCover:
+  query: (album) => ({
+    nonAttachingArtworkIndex:
       (album.hasCoverArt
-        ? relation('generateCoverArtwork', album.coverArtworks[0])
+        ? album.coverArtworks.findIndex((artwork, index) =>
+            index > 1 &&
+            !artwork.attachAbove)
         : null),
+  }),
+
+  relations: (relation, query, album) => ({
+    firstCovers:
+      (album.hasCoverArt && query.nonAttachingArtworkIndex >= 1
+        ? album.coverArtworks
+            .slice(0, query.nonAttachingArtworkIndex)
+            .map(artwork => relation('generateCoverArtwork', artwork))
+
+     : album.hasCoverArt
+        ? album.coverArtworks
+            .map(artwork => relation('generateCoverArtwork', artwork))
 
-    restCovers:
-      (album.hasCoverArt
-        ? album.coverArtworks.slice(1).map(artwork =>
-            relation('generateCoverArtwork', artwork))
         : []),
 
     albumArtInfoBox:
       relation('generateAlbumArtInfoBox', album),
+
+    restCovers:
+      (album.hasCoverArt && query.nonAttachingArtworkIndex >= 1
+        ? album.coverArtworks
+            .slice(query.nonAttachingArtworkIndex)
+            .map(artwork => relation('generateCoverArtwork', artwork))
+
+        : []),
   }),
 
-  generate: (relations, {html}) =>
-    html.tags([
-      relations.firstCover?.slots({
+  generate(relations, {html}) {
+    for (const cover of [...relations.firstCovers, ...relations.restCovers]) {
+      cover.setSlots({
         showOriginDetails: true,
         showArtTagDetails: true,
         showReferenceDetails: true,
-      }),
+      });
+    }
 
+    return html.tags([
+      relations.firstCovers,
       relations.albumArtInfoBox,
-
-      relations.restCovers.map(cover =>
-        cover.slots({
-          showOriginDetails: true,
-          showArtTagDetails: true,
-          showReferenceDetails: true,
-        })),
-    ]),
+      relations.restCovers,
+    ]);
+  },
 };
diff --git a/src/content/dependencies/generateAlbumBanner.js b/src/content/dependencies/generateAlbumBanner.js
index 3cc141bc..dce258de 100644
--- a/src/content/dependencies/generateAlbumBanner.js
+++ b/src/content/dependencies/generateAlbumBanner.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateBanner'],
-  extraDependencies: ['html', 'language'],
-
   relations(relation, album) {
     if (!album.hasBannerArt) {
       return {};
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js
index 1e39b47d..4c203877 100644
--- a/src/content/dependencies/generateAlbumCommentaryPage.js
+++ b/src/content/dependencies/generateAlbumCommentaryPage.js
@@ -1,22 +1,6 @@
 import {empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAlbumCommentarySidebar',
-    'generateAlbumNavAccent',
-    'generateAlbumSecondaryNav',
-    'generateAlbumStyleRules',
-    'generateCommentaryEntry',
-    'generateContentHeading',
-    'generateCoverArtwork',
-    'generatePageLayout',
-    'linkAlbum',
-    'linkExternal',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album) {
     const query = {};
 
@@ -44,8 +28,8 @@ export default {
     relations.sidebar =
       relation('generateAlbumCommentarySidebar', album);
 
-    relations.albumStyleRules =
-      relation('generateAlbumStyleRules', album, null);
+    relations.albumStyleTags =
+      relation('generateAlbumStyleTags', album, null);
 
     relations.albumLink =
       relation('linkAlbum', album);
@@ -151,7 +135,7 @@ export default {
         headingMode: 'sticky',
 
         color: data.color,
-        styleRules: [relations.albumStyleRules],
+        styleTags: relations.albumStyleTags,
 
         mainClasses: ['long-content'],
         mainContent: [
@@ -266,7 +250,10 @@ export default {
                       }),
                   })),
 
-              cover?.slots({mode: 'commentary'}),
+              cover?.slots({
+                mode: 'commentary',
+                color: true,
+              }),
 
               trackDate &&
               trackDate !== data.date &&
diff --git a/src/content/dependencies/generateAlbumCommentarySidebar.js b/src/content/dependencies/generateAlbumCommentarySidebar.js
index 9ecec66d..4863f059 100644
--- a/src/content/dependencies/generateAlbumCommentarySidebar.js
+++ b/src/content/dependencies/generateAlbumCommentarySidebar.js
@@ -1,15 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAlbumSidebarTrackSection',
-    'generatePageSidebar',
-    'generatePageSidebarBox',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, album) => ({
     sidebar:
       relation('generatePageSidebar'),
diff --git a/src/content/dependencies/generateAlbumGalleryAlbumGrid.js b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js
index 7f152871..f9cd027e 100644
--- a/src/content/dependencies/generateAlbumGalleryAlbumGrid.js
+++ b/src/content/dependencies/generateAlbumGalleryAlbumGrid.js
@@ -1,14 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateCoverGrid',
-    'image',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query: (album) => ({
     artworks:
       (album.hasCoverArt
diff --git a/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js b/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js
index 7dcdf6de..0322e227 100644
--- a/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js
+++ b/src/content/dependencies/generateAlbumGalleryCoverArtistsLine.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkArtistGallery'],
-  extraDependencies: ['html', 'language'],
-
   relations(relation, coverArtists) {
     return {
       coverArtistLinks:
diff --git a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js
index ad99cb87..5932514e 100644
--- a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js
+++ b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'language'],
-
   generate: ({html, language}) =>
     html.tag('p', {class: 'quick-info'},
       language.$('albumGalleryPage.noTrackArtworksLine')),
diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js
index 2ba3b272..85b0fb74 100644
--- a/src/content/dependencies/generateAlbumGalleryPage.js
+++ b/src/content/dependencies/generateAlbumGalleryPage.js
@@ -2,21 +2,6 @@ import {stitchArrays, unique} from '#sugar';
 import {getKebabCase} from '#wiki-data';
 
 export default {
-  contentDependencies: [
-    'generateAlbumGalleryAlbumGrid',
-    'generateAlbumGalleryNoTrackArtworksLine',
-    'generateAlbumGalleryStatsLine',
-    'generateAlbumGalleryTrackGrid',
-    'generateAlbumNavAccent',
-    'generateAlbumSecondaryNav',
-    'generateAlbumStyleRules',
-    'generateIntrapageDotSwitcher',
-    'generatePageLayout',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album) {
     const query = {};
 
@@ -46,8 +31,8 @@ export default {
     layout:
       relation('generatePageLayout'),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', album, null),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', album, null),
 
     albumLink:
       relation('linkAlbum', album),
@@ -106,7 +91,7 @@ export default {
         headingMode: 'static',
 
         color: data.color,
-        styleRules: [relations.albumStyleRules],
+        styleTags: relations.albumStyleTags,
 
         mainClasses: ['top-index'],
         mainContent: [
diff --git a/src/content/dependencies/generateAlbumGalleryStatsLine.js b/src/content/dependencies/generateAlbumGalleryStatsLine.js
index 75bffb36..75341937 100644
--- a/src/content/dependencies/generateAlbumGalleryStatsLine.js
+++ b/src/content/dependencies/generateAlbumGalleryStatsLine.js
@@ -1,38 +1,56 @@
 import {getTotalDuration} from '#wiki-data';
 
 export default {
-  extraDependencies: ['html', 'language'],
-
-  data(album) {
-    return {
-      name: album.name,
-      date: album.date,
-      duration: getTotalDuration(album.tracks),
-      numTracks: album.tracks.length,
-    };
-  },
-
-  generate(data, {html, language}) {
-    const parts = ['albumGalleryPage.statsLine'];
-    const options = {};
-
-    options.tracks =
-      html.tag('b',
-        language.countTracks(data.numTracks, {unit: true}));
-
-    options.duration =
-      html.tag('b',
-        language.formatDuration(data.duration, {unit: true}));
-
-    if (data.date) {
-      parts.push('withDate');
-      options.date =
-        html.tag('b',
-          language.formatDate(data.date));
-    }
-
-    return (
-      html.tag('p', {class: 'quick-info'},
-        language.formatString(...parts, options)));
-  },
+  data: (album) => ({
+    date:
+      album.date,
+
+    hideDuration:
+      album.hideDuration,
+
+    duration:
+      (album.hideDuration
+        ? null
+        : getTotalDuration(album.tracks)),
+
+    tracks:
+      (album.hideDuration
+        ? null
+        : album.tracks.length),
+  }),
+
+  generate: (data, {html, language}) =>
+    html.tag('p', {class: 'quick-info'},
+      {[html.onlyIfContent]: true},
+
+      language.encapsulate('albumGalleryPage.statsLine', workingCapsule => {
+        const workingOptions = {};
+
+        if (data.hideDuration && !data.date) {
+          return html.blank();
+        }
+
+        if (!data.hideDuration) {
+          workingOptions.tracks =
+            html.tag('b',
+              language.countTracks(data.tracks, {unit: true}));
+
+          workingOptions.duration =
+            html.tag('b',
+              language.formatDuration(data.duration, {unit: true}));
+        }
+
+        if (data.date) {
+          workingCapsule += '.withDate';
+          workingOptions.date =
+            html.tag('b',
+              language.formatDate(data.date));
+        }
+
+        if (data.hideDuration) {
+          workingCapsule += '.noDuration';
+        }
+
+        return language.$(workingCapsule, workingOptions);
+      })),
 };
diff --git a/src/content/dependencies/generateAlbumGalleryTrackGrid.js b/src/content/dependencies/generateAlbumGalleryTrackGrid.js
index fb5ed7ea..a50448c6 100644
--- a/src/content/dependencies/generateAlbumGalleryTrackGrid.js
+++ b/src/content/dependencies/generateAlbumGalleryTrackGrid.js
@@ -1,16 +1,6 @@
 import {compareArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAlbumGalleryCoverArtistsLine',
-    'generateCoverGrid',
-    'image',
-    'linkAlbum',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album, label) {
     const query = {};
 
@@ -77,6 +67,9 @@ export default {
           ? artwork.artistContribs
               .map(contrib => contrib.artist.name)
           : null)),
+
+    allWarnings:
+      query.artworks.flatMap(artwork => artwork?.contentWarnings),
   }),
 
   slots: {
@@ -117,6 +110,9 @@ export default {
                 artists:
                   language.formatUnitList(artists),
               })),
+
+          revealAllWarnings:
+            data.allWarnings,
         }),
       ]),
 };
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index ed19bf75..a27074ff 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -1,33 +1,12 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAdditionalFilesList',
-    'generateAdditionalNamesBox',
-    'generateAlbumArtworkColumn',
-    'generateAlbumBanner',
-    'generateAlbumNavAccent',
-    'generateAlbumReleaseInfo',
-    'generateAlbumSecondaryNav',
-    'generateAlbumSidebar',
-    'generateAlbumSocialEmbed',
-    'generateAlbumStyleRules',
-    'generateAlbumTrackList',
-    'generateCommentaryEntry',
-    'generateContentHeading',
-    'generatePageLayout',
-    'linkAlbumCommentary',
-    'linkAlbumGallery',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, album) => ({
     layout:
       relation('generatePageLayout'),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', album, null),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', album, null),
 
     socialEmbed:
       relation('generateAlbumSocialEmbed', album),
@@ -64,23 +43,30 @@ export default {
         : null),
 
     commentaryLink:
-      ([album, ...album.tracks].some(({commentary}) => !empty(commentary))
+      (album.tracks.some(track => !empty(track.commentary))
         ? relation('linkAlbumCommentary', album)
         : null),
 
+    readCommentaryLine:
+      relation('generateReadCommentaryLine', album),
+
     trackList:
       relation('generateAlbumTrackList', album),
 
     additionalFilesList:
       relation('generateAdditionalFilesList', album.additionalFiles),
 
+    commentaryContentHeading:
+      relation('generateCommentaryContentHeading', album),
+
     artistCommentaryEntries:
       album.commentary
         .map(entry => relation('generateCommentaryEntry', entry)),
 
-    creditSourceEntries:
-      album.creditSources
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    creditingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        album.creditingSources,
+        album),
   }),
 
   data: (album) => ({
@@ -104,7 +90,7 @@ export default {
 
         color: data.color,
         headingMode: 'sticky',
-        styleRules: [relations.albumStyleRules],
+        styleTags: relations.albumStyleTags,
 
         additionalNames: relations.additionalNamesBox,
 
@@ -156,12 +142,16 @@ export default {
 
                 : html.blank()),
 
-              !html.isBlank(relations.creditSourceEntries) &&
-                language.encapsulate(capsule, 'readCreditSources', capsule =>
+              !relations.commentaryLink &&
+              !html.isBlank(relations.artistCommentaryEntries) &&
+                relations.readCommentaryLine,
+
+              !html.isBlank(relations.creditingSourcesSection) &&
+                language.encapsulate(capsule, 'readCreditingSources', capsule =>
                   language.$(capsule, {
                     link:
                       html.tag('a',
-                        {href: '#credit-sources'},
+                        {href: '#crediting-sources'},
                         language.$(capsule, 'link')),
                   })),
             ])),
@@ -170,14 +160,14 @@ export default {
 
           html.tag('p',
             {[html.onlyIfContent]: true},
-            {[html.joinChildren]: html.tag('br')},
 
-            language.encapsulate('releaseInfo', capsule => [
-              language.$(capsule, 'addedToWiki', {
-                [language.onlyIfOptions]: ['date'],
-                date: language.formatDate(data.dateAddedToWiki),
-              }),
-            ])),
+            language.$('releaseInfo.addedToWiki', {
+              [language.onlyIfOptions]: ['date'],
+              date: language.formatDate(data.dateAddedToWiki),
+            })),
+
+          !html.isBlank(relations.artistCommentaryEntries) &&
+            html.tag('hr', {class: 'main-separator'}),
 
           language.encapsulate('releaseInfo.additionalFiles', capsule =>
             html.tags([
@@ -191,24 +181,14 @@ export default {
             ])),
 
           html.tags([
-            relations.contentHeading.clone()
-              .slots({
-                attributes: {id: 'artist-commentary'},
-                title: language.$('misc.artistCommentary'),
-              }),
-
+            relations.commentaryContentHeading,
             relations.artistCommentaryEntries,
           ]),
 
-          html.tags([
-            relations.contentHeading.clone()
-              .slots({
-                attributes: {id: 'credit-sources'},
-                title: language.$('misc.creditSources'),
-              }),
-
-            relations.creditSourceEntries,
-          ]),
+          relations.creditingSourcesSection.slots({
+            id: 'crediting-sources',
+            string: 'misc.creditingSources',
+          }),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js
index 432c5f3d..237120f3 100644
--- a/src/content/dependencies/generateAlbumNavAccent.js
+++ b/src/content/dependencies/generateAlbumNavAccent.js
@@ -1,17 +1,6 @@
 import {atOffset, empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'generateNextLink',
-    'generatePreviousLink',
-    'linkTrack',
-    'linkAlbumCommentary',
-    'linkAlbumGallery',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album, track) {
     const query = {};
 
@@ -64,9 +53,8 @@ export default {
     hasMultipleTracks:
       album.tracks.length > 1,
 
-    commentaryPageIsStub:
-      [album, ...album.tracks]
-        .every(({commentary}) => empty(commentary)),
+    hasSubstantialCommentaryPage:
+      album.tracks.some(track => !empty(track.commentary)),
 
     galleryIsStub:
       album.tracks.every(t => !t.hasUniqueCoverArt),
@@ -97,14 +85,16 @@ export default {
         relations.nextLink.slot('link', relations.nextTrackLink);
 
     const galleryLink =
-      (!data.galleryIsStub || slots.currentExtra === 'gallery') &&
+      (!data.galleryIsStub ||
+       slots.currentExtra === 'gallery') &&
         relations.albumGalleryLink.slots({
           attributes: {class: slots.currentExtra === 'gallery' && 'current'},
           content: language.$(albumNavCapsule, 'gallery'),
         });
 
     const commentaryLink =
-      (!data.commentaryPageIsStub || slots.currentExtra === 'commentary') &&
+      (data.hasSubstantialCommentaryPage ||
+       slots.currentExtra === 'commentary') &&
         relations.albumCommentaryLink.slots({
           attributes: {class: slots.currentExtra === 'commentary' && 'current'},
           content: language.$(albumNavCapsule, 'commentary'),
diff --git a/src/content/dependencies/generateAlbumReferencedArtworksPage.js b/src/content/dependencies/generateAlbumReferencedArtworksPage.js
index 7586393c..e4022f0d 100644
--- a/src/content/dependencies/generateAlbumReferencedArtworksPage.js
+++ b/src/content/dependencies/generateAlbumReferencedArtworksPage.js
@@ -1,19 +1,10 @@
 export default {
-  contentDependencies: [
-    'generateAlbumStyleRules',
-    'generateBackToAlbumLink',
-    'generateReferencedArtworksPage',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, album) => ({
     page:
       relation('generateReferencedArtworksPage', album.coverArtworks[0]),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', album, null),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', album, null),
 
     albumLink:
       relation('linkAlbum', album),
@@ -35,7 +26,7 @@ export default {
             data.name,
         }),
 
-      styleRules: [relations.albumStyleRules],
+      styleTags: relations.albumStyleTags,
 
       navLinks: [
         {auto: 'home'},
diff --git a/src/content/dependencies/generateAlbumReferencingArtworksPage.js b/src/content/dependencies/generateAlbumReferencingArtworksPage.js
index d072d2f6..0dc1bf15 100644
--- a/src/content/dependencies/generateAlbumReferencingArtworksPage.js
+++ b/src/content/dependencies/generateAlbumReferencingArtworksPage.js
@@ -1,19 +1,10 @@
 export default {
-  contentDependencies: [
-    'generateAlbumStyleRules',
-    'generateBackToAlbumLink',
-    'generateReferencingArtworksPage',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, album) => ({
     page:
       relation('generateReferencingArtworksPage', album.coverArtworks[0]),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', album, null),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', album, null),
 
     albumLink:
       relation('linkAlbum', album),
@@ -35,7 +26,7 @@ export default {
             data.name,
         }),
 
-      styleRules: [relations.albumStyleRules],
+      styleTags: relations.albumStyleTags,
 
       navLinks: [
         {auto: 'home'},
diff --git a/src/content/dependencies/generateAlbumReleaseInfo.js b/src/content/dependencies/generateAlbumReleaseInfo.js
index 0abb412c..4cec4120 100644
--- a/src/content/dependencies/generateAlbumReleaseInfo.js
+++ b/src/content/dependencies/generateAlbumReleaseInfo.js
@@ -1,28 +1,14 @@
 import {accumulateSum, empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateReleaseInfoContributionsLine',
-    'linkExternal',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations(relation, album) {
     const relations = {};
 
     relations.artistContributionsLine =
       relation('generateReleaseInfoContributionsLine', album.artistContribs);
 
-    relations.wallpaperArtistContributionsLine =
-      relation('generateReleaseInfoContributionsLine', album.wallpaperArtistContribs);
-
-    relations.bannerArtistContributionsLine =
-      relation('generateReleaseInfoContributionsLine', album.bannerArtistContribs);
-
-    relations.externalLinks =
-      album.urls.map(url =>
-        relation('linkExternal', url));
+    relations.listenLine =
+      relation('generateReleaseInfoListenLine', album);
 
     return relations;
   },
@@ -43,7 +29,7 @@ export default {
         .map(track => track.duration)
         .filter(value => value > 0);
 
-    if (empty(durationTerms)) {
+    if (empty(durationTerms) || album.hideDuration) {
       data.duration = null;
       data.durationApproximate = null;
     } else {
@@ -87,21 +73,16 @@ export default {
         html.tag('p',
           {[html.onlyIfContent]: true},
 
-          language.$(capsule, 'listenOn', {
-            [language.onlyIfOptions]: ['links'],
-
-            links:
-              language.formatDisjunctionList(
-                relations.externalLinks
-                  .map(link =>
-                    link.slot('context', [
-                      'album',
-                      (data.numTracks === 0
-                        ? 'albumNoTracks'
-                     : data.numTracks === 1
-                        ? 'albumOneTrack'
-                        : 'albumMultipleTracks'),
-                    ]))),
+          relations.listenLine.slots({
+            context: [
+              'album',
+
+              (data.numTracks === 0
+                ? 'albumNoTracks'
+             : data.numTracks === 1
+                ? 'albumOneTrack'
+                : 'albumMultipleTracks'),
+            ],
           })),
       ])),
 };
diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js
index bfa48f03..2140bfdb 100644
--- a/src/content/dependencies/generateAlbumSecondaryNav.js
+++ b/src/content/dependencies/generateAlbumSecondaryNav.js
@@ -1,15 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAlbumSecondaryNavGroupPart',
-    'generateAlbumSecondaryNavSeriesPart',
-    'generateDotSwitcherTemplate',
-    'generateSecondaryNav',
-  ],
-
-  extraDependencies: ['html', 'wikiData'],
-
   sprawl: ({groupData}) => ({
     // TODO: Series aren't their own things, so we access them weirdly.
     seriesData:
diff --git a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js
index 22dfa51c..2f08804b 100644
--- a/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js
+++ b/src/content/dependencies/generateAlbumSecondaryNavGroupPart.js
@@ -2,15 +2,6 @@ import {sortChronologically} from '#sort';
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateSecondaryNavParentSiblingsPart',
-    'linkAlbumDynamically',
-    'linkGroup',
-  ],
-
-  extraDependencies: ['html'],
-
   query(group, album) {
     const query = {};
 
diff --git a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js
index 16f205e3..ee180f16 100644
--- a/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js
+++ b/src/content/dependencies/generateAlbumSecondaryNavSeriesPart.js
@@ -1,15 +1,6 @@
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateSecondaryNavParentSiblingsPart',
-    'linkAlbumDynamically',
-    'linkGroup',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(series, album) {
     const query = {};
 
diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js
index 7cf689cc..83a637b0 100644
--- a/src/content/dependencies/generateAlbumSidebar.js
+++ b/src/content/dependencies/generateAlbumSidebar.js
@@ -2,17 +2,6 @@ import {sortAlbumsTracksChronologically} from '#sort';
 import {stitchArrays, transposeArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAlbumSidebarGroupBox',
-    'generateAlbumSidebarSeriesBox',
-    'generateAlbumSidebarTrackListBox',
-    'generatePageSidebar',
-    'generatePageSidebarConjoinedBox',
-    'generateTrackReleaseBox',
-  ],
-
-  extraDependencies: ['html', 'wikiData'],
-
   sprawl: ({groupData}) => ({
     // TODO: Series aren't their own things, so we access them weirdly.
     seriesData:
@@ -46,7 +35,8 @@ export default {
 
       const allReleaseAlbums =
         sortAlbumsTracksChronologically(
-          Array.from(albumTrackMap.keys()));
+          Array.from(albumTrackMap.keys()),
+          {getDate: album => albumTrackMap.get(album).date});
 
       const currentReleaseIndex =
         allReleaseAlbums.indexOf(track.album);
@@ -108,39 +98,65 @@ export default {
         : null),
   }),
 
-  data: (_query, _sprawl, _album, track) => ({
+  data: (_query, _sprawl, album, track) => ({
     isAlbumPage: !track,
     isTrackPage: !!track,
+
+    albumStyle: album.style,
   }),
 
   generate(data, relations, {html}) {
+    const presentGroupsLikeAlbum =
+      data.isAlbumPage ||
+      data.albumStyle === 'single';
+
     for (const box of [
       ...relations.groupBoxes,
       ...relations.seriesBoxes.flat(),
       ...relations.disconnectedSeriesBoxes,
     ]) {
-      box.setSlot('mode',
-        data.isAlbumPage ? 'album' : 'track');
+      box.setSlot('mode', presentGroupsLikeAlbum ? 'album' : 'track');
     }
 
+    const groupBoxes =
+      (presentGroupsLikeAlbum
+        ? [
+            relations.disconnectedSeriesBoxes,
+
+            stitchArrays({
+              groupBox: relations.groupBoxes,
+              seriesBoxes: relations.seriesBoxes,
+            }).map(({groupBox, seriesBoxes}) => [
+                groupBox,
+                seriesBoxes.map(seriesBox => [
+                  html.tag('div',
+                    {class: 'sidebar-box-joiner'},
+                    {class: 'collapsible'}),
+                  seriesBox,
+                ]),
+              ]),
+          ]
+        : [
+            relations.conjoinedBox.slots({
+              attributes: {class: 'conjoined-group-sidebar-box'},
+              boxes:
+                ([relations.disconnectedSeriesBoxes,
+                  stitchArrays({
+                    groupBox: relations.groupBoxes,
+                    seriesBoxes: relations.seriesBoxes,
+                  }).flatMap(({groupBox, seriesBoxes}) => [
+                      groupBox,
+                      ...seriesBoxes,
+                    ]),
+                ]).flat()
+                  .map(box => box.content), /* TODO: Kludge. */
+            })
+          ]);
+
     return relations.sidebar.slots({
       boxes: [
-        data.isAlbumPage && [
-          relations.disconnectedSeriesBoxes,
-
-          stitchArrays({
-            groupBox: relations.groupBoxes,
-            seriesBoxes: relations.seriesBoxes,
-          }).map(({groupBox, seriesBoxes}) => [
-              groupBox,
-              seriesBoxes.map(seriesBox => [
-                html.tag('div',
-                  {class: 'sidebar-box-joiner'},
-                  {class: 'collapsible'}),
-                seriesBox,
-              ]),
-            ]),
-        ],
+        data.isAlbumPage &&
+          groupBoxes,
 
         data.isTrackPage &&
           relations.earlierTrackReleaseBoxes,
@@ -151,20 +167,7 @@ export default {
           relations.laterTrackReleaseBoxes,
 
         data.isTrackPage &&
-          relations.conjoinedBox.slots({
-            attributes: {class: 'conjoined-group-sidebar-box'},
-            boxes:
-              ([relations.disconnectedSeriesBoxes,
-                stitchArrays({
-                  groupBox: relations.groupBoxes,
-                  seriesBoxes: relations.seriesBoxes,
-                }).flatMap(({groupBox, seriesBoxes}) => [
-                    groupBox,
-                    ...seriesBoxes,
-                  ]),
-              ]).flat()
-                .map(box => box.content), /* TODO: Kludge. */
-          }),
+          groupBoxes,
       ],
     });
   },
diff --git a/src/content/dependencies/generateAlbumSidebarGroupBox.js b/src/content/dependencies/generateAlbumSidebarGroupBox.js
index f3be74f7..0a9c0db9 100644
--- a/src/content/dependencies/generateAlbumSidebarGroupBox.js
+++ b/src/content/dependencies/generateAlbumSidebarGroupBox.js
@@ -2,16 +2,6 @@ import {sortChronologically} from '#sort';
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generatePageSidebarBox',
-    'linkAlbum',
-    'linkExternal',
-    'linkGroup',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album, group) {
     const query = {};
 
diff --git a/src/content/dependencies/generateAlbumSidebarSeriesBox.js b/src/content/dependencies/generateAlbumSidebarSeriesBox.js
index 37616cb2..22f1fe72 100644
--- a/src/content/dependencies/generateAlbumSidebarSeriesBox.js
+++ b/src/content/dependencies/generateAlbumSidebarSeriesBox.js
@@ -1,15 +1,6 @@
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generatePageSidebarBox',
-    'linkAlbum',
-    'linkGroup',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album, series) {
     const query = {};
 
diff --git a/src/content/dependencies/generateAlbumSidebarTrackListBox.js b/src/content/dependencies/generateAlbumSidebarTrackListBox.js
index 3a244e3a..4e9437c9 100644
--- a/src/content/dependencies/generateAlbumSidebarTrackListBox.js
+++ b/src/content/dependencies/generateAlbumSidebarTrackListBox.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateAlbumSidebarTrackSection',
-    'generatePageSidebarBox',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html'],
-
   relations: (relation, album, track) => ({
     box:
       relation('generatePageSidebarBox'),
@@ -24,7 +16,9 @@ export default {
       attributes: {class: 'track-list-sidebar-box'},
 
       content: [
-        html.tag('h1', relations.albumLink),
+        html.tag('h1', {[html.onlyIfSiblings]: true},
+          relations.albumLink),
+
         relations.trackSections,
       ],
     })
diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js
index dae5fa03..68281bfe 100644
--- a/src/content/dependencies/generateAlbumSidebarTrackSection.js
+++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js
@@ -1,9 +1,6 @@
 import {empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['linkTrack'],
-  extraDependencies: ['getColors', 'html', 'language'],
-
   relations(relation, album, track, trackSection) {
     const relations = {};
 
@@ -22,10 +19,12 @@ export default {
       !empty(trackSection.tracks);
 
     data.isTrackPage = !!track;
+    data.albumStyle = album.style;
 
     data.name = trackSection.name;
     data.color = trackSection.color;
     data.isDefaultTrackSection = trackSection.isDefaultTrackSection;
+    data.hasSiblingSections = album.trackSections.length > 1;
 
     data.firstTrackNumber =
       (data.hasTrackNumbers
@@ -115,6 +114,21 @@ export default {
                   : trackLink),
             })));
 
+    const list =
+      (data.hasTrackNumbers
+        ? html.tag('ol',
+            {start: data.firstTrackNumber},
+            trackListItems)
+        : html.tag('ul', trackListItems));
+
+    if (data.albumStyle === 'single' && !data.hasSiblingSections) {
+      if (trackListItems.length <= 1) {
+        return html.blank();
+      } else {
+        return list;
+      }
+    }
+
     return html.tag('details',
       data.includesCurrentTrack &&
         {class: 'current'},
@@ -157,11 +171,7 @@ export default {
                 return language.$(workingCapsule, workingOptions);
               })))),
 
-        (data.hasTrackNumbers
-          ? html.tag('ol',
-              {start: data.firstTrackNumber},
-              trackListItems)
-          : html.tag('ul', trackListItems)),
+        list,
       ]);
   },
 };
diff --git a/src/content/dependencies/generateAlbumSocialEmbed.js b/src/content/dependencies/generateAlbumSocialEmbed.js
index e28a3fd0..1200ec8b 100644
--- a/src/content/dependencies/generateAlbumSocialEmbed.js
+++ b/src/content/dependencies/generateAlbumSocialEmbed.js
@@ -1,13 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateSocialEmbed',
-    'generateAlbumSocialEmbedDescription',
-  ],
-
-  extraDependencies: ['absoluteTo', 'language'],
-
   relations(relation, album) {
     return {
       socialEmbed:
diff --git a/src/content/dependencies/generateAlbumSocialEmbedDescription.js b/src/content/dependencies/generateAlbumSocialEmbedDescription.js
index 69c39c3a..db6da5b7 100644
--- a/src/content/dependencies/generateAlbumSocialEmbedDescription.js
+++ b/src/content/dependencies/generateAlbumSocialEmbedDescription.js
@@ -1,8 +1,6 @@
 import {accumulateSum} from '#sugar';
 
 export default {
-  extraDependencies: ['language'],
-
   data: (album) => ({
     duration:
       accumulateSum(album.tracks, track => track.duration),
diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js
deleted file mode 100644
index 6bfcc62e..00000000
--- a/src/content/dependencies/generateAlbumStyleRules.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import {empty, stitchArrays} from '#sugar';
-
-export default {
-  extraDependencies: ['to'],
-
-  data(album, track) {
-    const data = {};
-
-    data.hasWallpaper = !empty(album.wallpaperArtistContribs);
-    data.hasBanner = !empty(album.bannerArtistContribs);
-
-    if (data.hasWallpaper) {
-      if (!empty(album.wallpaperParts)) {
-        data.wallpaperMode = 'parts';
-
-        data.wallpaperPaths =
-          album.wallpaperParts.map(part =>
-            (part.asset
-              ? ['media.albumWallpaperPart', album.directory, part.asset]
-              : null));
-
-        data.wallpaperStyles =
-          album.wallpaperParts.map(part => part.style);
-      } else {
-        data.wallpaperMode = 'one';
-        data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension];
-        data.wallpaperStyle = album.wallpaperStyle;
-      }
-    }
-
-    if (data.hasBanner) {
-      data.hasBannerStyle = !!album.bannerStyle;
-      data.bannerStyle = album.bannerStyle;
-    }
-
-    data.albumDirectory = album.directory;
-
-    if (track) {
-      data.trackDirectory = track.directory;
-    }
-
-    return data;
-  },
-
-  generate(data, {to}) {
-    const indent = parts =>
-      (parts ?? [])
-        .filter(Boolean)
-        .join('\n')
-        .split('\n')
-        .map(line => ' '.repeat(4) + line)
-        .join('\n');
-
-    const rule = (selector, parts) =>
-      (!empty(parts.filter(Boolean))
-        ? [`${selector} {`, indent(parts), `}`]
-        : []);
-
-    const oneWallpaperRule =
-      data.wallpaperMode === 'one' &&
-        rule(`body::before`, [
-          `background-image: url("${to(...data.wallpaperPath)}");`,
-          data.wallpaperStyle,
-        ]);
-
-    const wallpaperPartRules =
-      data.wallpaperMode === 'parts' &&
-        stitchArrays({
-          path: data.wallpaperPaths,
-          style: data.wallpaperStyles,
-        }).map(({path, style}, index) =>
-            rule(`.wallpaper-part:nth-child(${index + 1})`, [
-              path && `background-image: url("${to(...path)}");`,
-              style,
-            ]));
-
-    const nukeBasicWallpaperRule =
-      data.wallpaperMode === 'parts' &&
-        rule(`body::before`, ['display: none']);
-
-    const wallpaperRules = [
-      oneWallpaperRule,
-      ...wallpaperPartRules || [],
-      nukeBasicWallpaperRule,
-    ];
-
-    const bannerRule =
-      data.hasBanner &&
-        rule(`#banner img`, [
-          data.bannerStyle,
-        ]);
-
-    const dataRule =
-      rule(`:root`, [
-        data.albumDirectory &&
-          `--album-directory: ${data.albumDirectory};`,
-        data.trackDirectory &&
-          `--track-directory: ${data.trackDirectory};`,
-      ]);
-
-    return (
-      [...wallpaperRules, bannerRule, dataRule]
-        .filter(Boolean)
-        .flat()
-        .join('\n'));
-  },
-};
diff --git a/src/content/dependencies/generateAlbumStyleTags.js b/src/content/dependencies/generateAlbumStyleTags.js
new file mode 100644
index 00000000..caf21dc4
--- /dev/null
+++ b/src/content/dependencies/generateAlbumStyleTags.js
@@ -0,0 +1,62 @@
+import {empty} from '#sugar';
+
+export default {
+  relations: (relation, album, _track) => ({
+    styleTag:
+      relation('generateStyleTag'),
+
+    wallpaperStyleTag:
+      relation('generateAlbumWallpaperStyleTag', album),
+  }),
+
+  data(album, track) {
+    const data = {};
+
+    data.hasBanner = !empty(album.bannerArtistContribs);
+
+    if (data.hasBanner) {
+      data.hasBannerStyle = !!album.bannerStyle;
+      data.bannerStyle = album.bannerStyle;
+    }
+
+    data.albumDirectory = album.directory;
+
+    if (track) {
+      data.trackDirectory = track.directory;
+    }
+
+    return data;
+  },
+
+  generate: (data, relations, {html}) =>
+    html.tags([
+      relations.wallpaperStyleTag,
+
+      relations.styleTag.clone().slots({
+        attributes: {class: 'album-banner-style'},
+
+        rules: [
+          data.hasBanner && {
+            select: '#banner img',
+            declare: [data.bannerStyle],
+          },
+        ],
+      }),
+
+      relations.styleTag.clone().slots({
+        attributes: {class: 'album-directory-style'},
+
+        rules: [
+          {
+            select: ':root',
+            declare: [
+              data.albumDirectory &&
+                `--album-directory: ${data.albumDirectory};`,
+              data.trackDirectory &&
+                `--track-directory: ${data.trackDirectory};`,
+            ],
+          },
+        ]
+      }),
+    ], {[html.joinChildren]: ''}),
+};
diff --git a/src/content/dependencies/generateAlbumTrackList.js b/src/content/dependencies/generateAlbumTrackList.js
index 0a949ded..93cb420b 100644
--- a/src/content/dependencies/generateAlbumTrackList.js
+++ b/src/content/dependencies/generateAlbumTrackList.js
@@ -35,14 +35,6 @@ function getDisplayMode(album) {
 }
 
 export default {
-  contentDependencies: [
-    'generateAlbumTrackListItem',
-    'generateContentHeading',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(album) {
     return {
       displayMode: getDisplayMode(album),
diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js
index 44297c15..68722a83 100644
--- a/src/content/dependencies/generateAlbumTrackListItem.js
+++ b/src/content/dependencies/generateAlbumTrackListItem.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateTrackListItem'],
-  extraDependencies: ['html'],
-
   query: (track, album) => ({
     trackHasDuration:
       !!track.duration,
@@ -20,7 +17,7 @@ export default {
     item:
       relation('generateTrackListItem',
         track,
-        track.album.artistContribs),
+        track.album.trackArtistContribs),
   }),
 
   data: (query, track, album) => ({
@@ -43,7 +40,7 @@ export default {
 
   generate: (data, relations, slots) =>
     relations.item.slots({
-      showArtists: true,
+      showArtists: 'auto',
 
       showDuration:
         (slots.collapseDurationScope === 'track'
diff --git a/src/content/dependencies/generateAlbumWallpaperStyleTag.js b/src/content/dependencies/generateAlbumWallpaperStyleTag.js
new file mode 100644
index 00000000..b3f74716
--- /dev/null
+++ b/src/content/dependencies/generateAlbumWallpaperStyleTag.js
@@ -0,0 +1,35 @@
+export default {
+  relations: (relation, album) => ({
+    wallpaperStyleTag:
+      (album.hasWallpaperArt
+        ? relation('generateWallpaperStyleTag')
+        : null),
+  }),
+
+  data: (album) => ({
+    singleWallpaperPath:
+      ['media.albumWallpaper', album.directory, album.wallpaperFileExtension],
+
+    singleWallpaperStyle:
+      album.wallpaperStyle,
+
+    wallpaperPartPaths:
+      album.wallpaperParts.map(part =>
+        (part.asset
+          ? ['media.albumWallpaperPart', album.directory, part.asset]
+          : null)),
+
+    wallpaperPartStyles:
+      album.wallpaperParts.map(part => part.style),
+  }),
+
+  generate: (data, relations, {html}) =>
+    (relations.wallpaperStyleTag
+      ? relations.wallpaperStyleTag.slots({
+          singleWallpaperPath: data.singleWallpaperPath,
+          singleWallpaperStyle: data.singleWallpaperStyle,
+          wallpaperPartPaths: data.wallpaperPartPaths,
+          wallpaperPartStyles: data.wallpaperPartStyles,
+        })
+      : html.blank()),
+};
diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
index 80d19b5a..37a32a94 100644
--- a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
+++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
@@ -6,9 +6,6 @@ import {
 } from '#sugar';
 
 export default {
-  contentDependencies: ['linkArtTagDynamically'],
-  extraDependencies: ['html', 'language'],
-
   // Recursion ain't too pretty!
 
   query(ancestorArtTag, targetArtTag) {
diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js
index cfd6d03e..f20babba 100644
--- a/src/content/dependencies/generateArtTagGalleryPage.js
+++ b/src/content/dependencies/generateArtTagGalleryPage.js
@@ -2,22 +2,6 @@ import {sortArtworksChronologically} from '#sort';
 import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAdditionalNamesBox',
-    'generateArtTagGalleryPageFeaturedLine',
-    'generateArtTagGalleryPageShowingLine',
-    'generateArtTagNavLinks',
-    'generateCoverGrid',
-    'generatePageLayout',
-    'generateQuickDescription',
-    'image',
-    'linkAnythingMan',
-    'linkArtTagGallery',
-    'linkExternal',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({wikiInfo}) {
     return {
       enableListings: wikiInfo.enableListings,
diff --git a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js
index b4620fa4..8593cc21 100644
--- a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js
+++ b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'language'],
-
   slots: {
     showing: {
       validate: v => v.is('all', 'direct', 'indirect'),
diff --git a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js
index 6df4d0e5..2a34ae57 100644
--- a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js
+++ b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'language'],
-
   slots: {
     showing: {
       validate: v => v.is('all', 'direct', 'indirect'),
diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js
index 9df51b77..683eeab6 100644
--- a/src/content/dependencies/generateArtTagInfoPage.js
+++ b/src/content/dependencies/generateArtTagInfoPage.js
@@ -1,20 +1,6 @@
 import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAdditionalNamesBox',
-    'generateArtTagNavLinks',
-    'generateArtTagSidebar',
-    'generateContentHeading',
-    'generatePageLayout',
-    'linkArtTagGallery',
-    'linkArtTagInfo',
-    'linkExternal',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     enableListings: wikiInfo.enableListings,
   }),
@@ -182,12 +168,12 @@ export default {
                       artTagLink: relations.relatedArtTagLinks,
                       annotation: data.relatedArtTagAnnotations,
                     }).map(({artTagLink, annotation}) =>
-                        (html.isBlank(annotation)
-                          ? artTagLink
-                          : language.$(capsule, 'tagWithAnnotation', {
+                        (annotation
+                          ? language.$(capsule, 'tagWithAnnotation', {
                               tag: artTagLink,
                               annotation,
-                            })))),
+                            })
+                          : artTagLink))),
               }))),
 
           html.tag('blockquote',
diff --git a/src/content/dependencies/generateArtTagNavLinks.js b/src/content/dependencies/generateArtTagNavLinks.js
index 9061a09f..1298ce99 100644
--- a/src/content/dependencies/generateArtTagNavLinks.js
+++ b/src/content/dependencies/generateArtTagNavLinks.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'linkArtTagInfo',
-    'linkArtTagGallery',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) =>
     ({enableListings: wikiInfo.enableListings}),
 
diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js
index 9e2f813c..60ea504f 100644
--- a/src/content/dependencies/generateArtTagSidebar.js
+++ b/src/content/dependencies/generateArtTagSidebar.js
@@ -1,15 +1,6 @@
 import {collectTreeLeaves, empty, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generatePageSidebar',
-    'generatePageSidebarBox',
-    'generateArtTagAncestorDescendantMapList',
-    'linkArtTagDynamically',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({artTagData}) =>
     ({artTagData}),
 
diff --git a/src/content/dependencies/generateArtistArtworkColumn.js b/src/content/dependencies/generateArtistArtworkColumn.js
index a4135489..19c66b8a 100644
--- a/src/content/dependencies/generateArtistArtworkColumn.js
+++ b/src/content/dependencies/generateArtistArtworkColumn.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generateCoverArtwork'],
-
   relations: (relation, artist) => ({
     coverArtwork:
       (artist.hasAvatar
diff --git a/src/content/dependencies/generateArtistCredit.js b/src/content/dependencies/generateArtistCredit.js
index bab32f7d..389de740 100644
--- a/src/content/dependencies/generateArtistCredit.js
+++ b/src/content/dependencies/generateArtistCredit.js
@@ -1,14 +1,7 @@
-import {compareArrays, empty} from '#sugar';
+import {compareArrays, empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateArtistCreditWikiEditsPart',
-    'linkContribution',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
-  query: (creditContributions, contextContributions) => {
+  query: (creditContributions, contextContributions, _formatText) => {
     const query = {};
 
     const featuringFilter = contribution =>
@@ -52,7 +45,10 @@ export default {
     return query;
   },
 
-  relations: (relation, query, _creditContributions, _contextContributions) => ({
+  relations: (relation, query,
+      _creditContributions,
+      _contextContributions,
+      formatText) => ({
     normalContributionLinks:
       query.normalContributions
         .map(contrib => relation('linkContribution', contrib)),
@@ -64,15 +60,26 @@ export default {
     wikiEditsPart:
       relation('generateArtistCreditWikiEditsPart',
         query.wikiEditContributions),
+
+    formatText:
+      relation('transformContent', formatText),
   }),
 
-  data: (query, _creditContributions, _contextContributions) => ({
+  data: (query, _creditContributions, _contextContributions, _formatText) => ({
     normalContributionArtistsDifferFromContext:
       query.normalContributionArtistsDifferFromContext,
 
     normalContributionAnnotationsDifferFromContext:
       query.normalContributionAnnotationsDifferFromContext,
 
+    normalContributionArtistDirectories:
+      query.normalContributions
+        .map(contrib => contrib.artist.directory),
+
+    featuringContributionArtistDirectories:
+      query.featuringContributions
+        .map(contrib => contrib.artist.directory),
+
     hasWikiEdits:
       !empty(query.wikiEditContributions),
   }),
@@ -105,6 +112,10 @@ export default {
   generate(data, relations, slots, {html, language}) {
     if (!slots.normalStringKey) return html.blank();
 
+    const effectivelyDiffers =
+      (slots.showAnnotation && data.normalContributionAnnotationsDifferFromContext) ||
+      (data.normalContributionArtistsDifferFromContext);
+
     for (const link of [
       ...relations.normalContributionLinks,
       ...relations.featuringContributionLinks,
@@ -132,63 +143,112 @@ export default {
       });
     }
 
-    if (empty(relations.normalContributionLinks)) {
-      return html.blank();
-    }
+    let formattedArtistList = null;
 
-    const artistsList =
-      (data.hasWikiEdits && slots.showWikiEdits
-        ? language.$('misc.artistLink.withEditsForWiki', {
-            artists:
-              language.formatConjunctionList(relations.normalContributionLinks),
+    if (!html.isBlank(relations.formatText)) {
+      formattedArtistList = relations.formatText;
 
-            edits:
-              relations.wikiEditsPart.slots({
-                showAnnotation: slots.showAnnotation,
-              }),
-          })
-        : language.formatConjunctionList(relations.normalContributionLinks));
+      const substituteContrib = ({link, directory}) => ({
+        match: {replacerKey: 'artist', replacerValue: directory},
+        substitute: link,
 
-    const featuringList =
-      language.formatConjunctionList(relations.featuringContributionLinks);
+        apply(link, node) {
+          if (node.data.label) {
+            link.setSlot('content', language.sanitize(node.data.label));
+          }
+        },
+      });
 
-    const everyoneList =
-      language.formatConjunctionList([
-        ...relations.normalContributionLinks,
-        ...relations.featuringContributionLinks,
-      ]);
+      relations.formatText.setSlots({
+        mode: 'inline',
 
-    const effectivelyDiffers =
-      (slots.showAnnotation && data.normalContributionAnnotationsDifferFromContext) ||
-      (data.normalContributionArtistsDifferFromContext);
+        substitute: [
+          stitchArrays({
+            link: relations.normalContributionLinks,
+            directory: data.normalContributionArtistDirectories,
+          }).map(substituteContrib),
 
-    if (empty(relations.featuringContributionLinks)) {
+          stitchArrays({
+            link: relations.featuringContributionLinks,
+            directory: data.featuringContributionArtistDirectories,
+          }).map(substituteContrib),
+        ].flat(),
+      });
+    }
+
+    let content;
+
+    if (formattedArtistList) {
       if (effectivelyDiffers) {
-        return language.$(slots.normalStringKey, {
-          ...slots.additionalStringOptions,
-          artists: artistsList,
+        content =
+          language.$(slots.normalStringKey, {
+            ...slots.additionalStringOptions,
+            artists: formattedArtistList,
+          });
+      }
+    } else {
+      if (empty(relations.normalContributionLinks)) {
+        return html.blank();
+      }
+
+      const artistsList =
+        (data.hasWikiEdits && slots.showWikiEdits
+          ? language.$('misc.artistLink.withEditsForWiki', {
+              artists:
+                language.formatConjunctionList(relations.normalContributionLinks),
+
+              edits:
+                relations.wikiEditsPart.slots({
+                  showAnnotation: slots.showAnnotation,
+                }),
+            })
+
+          : language.formatConjunctionList(relations.normalContributionLinks));
+
+      const featuringList =
+        language.formatConjunctionList(relations.featuringContributionLinks);
+
+      const everyoneList =
+        language.formatConjunctionList([
+          ...relations.normalContributionLinks,
+          ...relations.featuringContributionLinks,
+        ]);
+
+      if (empty(relations.featuringContributionLinks)) {
+        if (effectivelyDiffers) {
+          content =
+            language.$(slots.normalStringKey, {
+              ...slots.additionalStringOptions,
+              artists: artistsList,
+            });
+        } else {
+          return html.blank();
+        }
+      } else if (effectivelyDiffers && slots.normalFeaturingStringKey) {
+        content =
+          language.$(slots.normalFeaturingStringKey, {
+            ...slots.additionalStringOptions,
+            artists: artistsList,
+            featuring: featuringList,
         });
+      } else if (slots.featuringStringKey) {
+        content =
+          language.$(slots.featuringStringKey, {
+            ...slots.additionalStringOptions,
+            artists: featuringList,
+          });
       } else {
-        return html.blank();
+        content =
+          language.$(slots.normalStringKey, {
+            ...slots.additionalStringOptions,
+            artists: everyoneList,
+          });
       }
     }
 
-    if (effectivelyDiffers && slots.normalFeaturingStringKey) {
-      return language.$(slots.normalFeaturingStringKey, {
-        ...slots.additionalStringOptions,
-        artists: artistsList,
-        featuring: featuringList,
-      });
-    } else if (slots.featuringStringKey) {
-      return language.$(slots.featuringStringKey, {
-        ...slots.additionalStringOptions,
-        artists: featuringList,
-      });
-    } else {
-      return language.$(slots.normalStringKey, {
-        ...slots.additionalStringOptions,
-        artists: everyoneList,
-      });
-    }
+    // TODO: This is obviously evil.
+    return (
+      html.metatag('chunkwrap', {split: /,| (?=and)/},
+        html.resolve(content)));
   },
 };
diff --git a/src/content/dependencies/generateArtistCreditWikiEditsPart.js b/src/content/dependencies/generateArtistCreditWikiEditsPart.js
index 70296e39..4178928d 100644
--- a/src/content/dependencies/generateArtistCreditWikiEditsPart.js
+++ b/src/content/dependencies/generateArtistCreditWikiEditsPart.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateTextWithTooltip',
-    'generateTooltip',
-    'linkContribution',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, contributions) => ({
     textWithTooltip:
       relation('generateTextWithTooltip'),
@@ -48,6 +40,7 @@ export default {
                         showAnnotation: slots.showAnnotation,
                         trimAnnotation: true,
                         preventTooltip: true,
+                        preventWrapping: true,
                       }))),
                 }),
           }),
diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js
index 6a24275e..d8f1c4b1 100644
--- a/src/content/dependencies/generateArtistGalleryPage.js
+++ b/src/content/dependencies/generateArtistGalleryPage.js
@@ -1,16 +1,6 @@
 import {sortArtworksChronologically} from '#sort';
 
 export default {
-  contentDependencies: [
-    'generateArtistNavLinks',
-    'generateCoverGrid',
-    'generatePageLayout',
-    'image',
-    'linkAnythingMan',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query: (artist) => ({
     artworks:
       sortArtworksChronologically(
@@ -58,6 +48,10 @@ export default {
         .map(artwork => artwork.artistContribs
           .filter(contrib => contrib.artist !== artist)
           .map(contrib => contrib.artist.name)),
+
+    allWarnings:
+      query.artworks
+        .flatMap(artwork => artwork.contentWarnings),
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -93,6 +87,8 @@ export default {
 
                     artists: language.formatUnitList(names),
                   })),
+
+              revealAllWarnings: data.allWarnings,
             }),
         ],
 
diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js
index 3e0cd1d2..6940053f 100644
--- a/src/content/dependencies/generateArtistGroupContributionsInfo.js
+++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js
@@ -1,83 +1,87 @@
-import {empty, filterProperties, stitchArrays, unique} from '#sugar';
+import {accumulateSum, empty, stitchArrays, withEntries} from '#sugar';
 
 export default {
-  contentDependencies: ['linkGroup'],
-  extraDependencies: ['html', 'language', 'wikiData'],
+  sprawl: ({groupCategoryData}) => ({
+    groupOrder:
+      groupCategoryData.flatMap(category => category.groups),
+  }),
 
-  sprawl({groupCategoryData}) {
-    return {
-      groupOrder: groupCategoryData.flatMap(category => category.groups),
-    }
-  },
+  query(sprawl, contributions) {
+    const allGroupsUnordered =
+      new Set(contributions.flatMap(contrib => contrib.groups));
 
-  query(sprawl, tracksAndAlbums) {
-    const filteredAlbums = tracksAndAlbums.filter(thing => !thing.album);
-    const filteredTracks = tracksAndAlbums.filter(thing => thing.album);
+    const allGroupsOrdered =
+      sprawl.groupOrder.filter(group => allGroupsUnordered.has(group));
 
-    const allAlbums = unique([
-      ...filteredAlbums,
-      ...filteredTracks.map(track => track.album),
-    ]);
+    const groupToThingsCountedForContributions =
+      new Map(allGroupsOrdered.map(group => [group, new Set]));
 
-    const allGroupsUnordered = new Set(Array.from(allAlbums).flatMap(album => album.groups));
-    const allGroupsOrdered = sprawl.groupOrder.filter(group => allGroupsUnordered.has(group));
+    const groupToThingsCountedForDuration =
+      new Map(allGroupsOrdered.map(group => [group, new Set]));
 
-    const mapTemplate = allGroupsOrdered.map(group => [group, 0]);
-    const groupToCountMap = new Map(mapTemplate);
-    const groupToDurationMap = new Map(mapTemplate);
-    const groupToDurationCountMap = new Map(mapTemplate);
-
-    for (const album of filteredAlbums) {
-      for (const group of album.groups) {
-        groupToCountMap.set(group, groupToCountMap.get(group) + 1);
-      }
-    }
+    for (const contrib of contributions) {
+      for (const group of contrib.groups) {
+        if (contrib.countInContributionTotals) {
+          groupToThingsCountedForContributions.get(group).add(contrib.thing);
+        }
 
-    for (const track of filteredTracks) {
-      for (const group of track.album.groups) {
-        groupToCountMap.set(group, groupToCountMap.get(group) + 1);
-        if (track.duration && track.mainReleaseTrack === null) {
-          groupToDurationMap.set(group, groupToDurationMap.get(group) + track.duration);
-          groupToDurationCountMap.set(group, groupToDurationCountMap.get(group) + 1);
+        if (contrib.countInDurationTotals) {
+          groupToThingsCountedForDuration.get(group).add(contrib.thing);
         }
       }
     }
 
+    const groupToTotalContributions =
+      withEntries(
+        groupToThingsCountedForContributions,
+        entries => entries.map(
+          ([group, things]) =>
+          ([group, things.size])));
+
+    const groupToTotalDuration =
+      withEntries(
+        groupToThingsCountedForDuration,
+        entries => entries.map(
+          ([group, things]) =>
+          ([group, accumulateSum(things, thing => thing.duration)])))
+
     const groupsSortedByCount =
       allGroupsOrdered
-        .slice()
-        .sort((a, b) => groupToCountMap.get(b) - groupToCountMap.get(a));
+        .filter(group => groupToTotalContributions.get(group) > 0)
+        .sort((a, b) =>
+          (groupToTotalContributions.get(b)
+         - groupToTotalContributions.get(a)));
 
-    // The filter here ensures all displayed groups have at least some duration
-    // when sorting by duration.
     const groupsSortedByDuration =
       allGroupsOrdered
-        .filter(group => groupToDurationMap.get(group) > 0)
-        .sort((a, b) => groupToDurationMap.get(b) - groupToDurationMap.get(a));
+        .filter(group => groupToTotalDuration.get(group) > 0)
+        .sort((a, b) =>
+          (groupToTotalDuration.get(b)
+         - groupToTotalDuration.get(a)));
 
     const groupCountsSortedByCount =
       groupsSortedByCount
-        .map(group => groupToCountMap.get(group));
+        .map(group => groupToTotalContributions.get(group));
 
     const groupDurationsSortedByCount =
       groupsSortedByCount
-        .map(group => groupToDurationMap.get(group));
+        .map(group => groupToTotalDuration.get(group));
 
     const groupDurationsApproximateSortedByCount =
       groupsSortedByCount
-        .map(group => groupToDurationCountMap.get(group) > 1);
+        .map(group => groupToThingsCountedForDuration.get(group).size > 1);
 
     const groupCountsSortedByDuration =
       groupsSortedByDuration
-        .map(group => groupToCountMap.get(group));
+        .map(group => groupToTotalContributions.get(group));
 
     const groupDurationsSortedByDuration =
       groupsSortedByDuration
-        .map(group => groupToDurationMap.get(group));
+        .map(group => groupToTotalDuration.get(group));
 
     const groupDurationsApproximateSortedByDuration =
       groupsSortedByDuration
-        .map(group => groupToDurationCountMap.get(group) > 1);
+        .map(group => groupToThingsCountedForDuration.get(group).size > 1);
 
     return {
       groupsSortedByCount,
@@ -93,29 +97,35 @@ export default {
     };
   },
 
-  relations(relation, query) {
-    return {
-      groupLinksSortedByCount:
-        query.groupsSortedByCount
-          .map(group => relation('linkGroup', group)),
+  relations: (relation, query) => ({
+    groupLinksSortedByCount:
+      query.groupsSortedByCount
+        .map(group => relation('linkGroup', group)),
 
-      groupLinksSortedByDuration:
-        query.groupsSortedByDuration
-          .map(group => relation('linkGroup', group)),
-    };
-  },
+    groupLinksSortedByDuration:
+      query.groupsSortedByDuration
+        .map(group => relation('linkGroup', group)),
+  }),
 
-  data(query) {
-    return filterProperties(query, [
-      'groupCountsSortedByCount',
-      'groupDurationsSortedByCount',
-      'groupDurationsApproximateSortedByCount',
+  data: (query) => ({
+    groupCountsSortedByCount:
+      query.groupCountsSortedByCount,
 
-      'groupCountsSortedByDuration',
-      'groupDurationsSortedByDuration',
-      'groupDurationsApproximateSortedByDuration',
-    ]);
-  },
+    groupDurationsSortedByCount:
+      query.groupDurationsSortedByCount,
+
+    groupDurationsApproximateSortedByCount:
+      query.groupDurationsApproximateSortedByCount,
+
+    groupCountsSortedByDuration:
+      query.groupCountsSortedByDuration,
+
+    groupDurationsSortedByDuration:
+      query.groupDurationsSortedByDuration,
+
+    groupDurationsApproximateSortedByDuration:
+      query.groupDurationsApproximateSortedByDuration,
+  }),
 
   slots: {
     title: {
diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js
index 3a3cf8b7..cf8ce994 100644
--- a/src/content/dependencies/generateArtistInfoPage.js
+++ b/src/content/dependencies/generateArtistInfoPage.js
@@ -1,48 +1,18 @@
 import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateArtistArtworkColumn',
-    'generateArtistGroupContributionsInfo',
-    'generateArtistInfoPageArtworksChunkedList',
-    'generateArtistInfoPageCommentaryChunkedList',
-    'generateArtistInfoPageFlashesChunkedList',
-    'generateArtistInfoPageTracksChunkedList',
-    'generateArtistNavLinks',
-    'generateContentHeading',
-    'generatePageLayout',
-    'linkArtistGallery',
-    'linkExternal',
-    'linkGroup',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query: (artist) => ({
-    // Even if an artist has served as both "artist" (compositional) and
-    // "contributor" (instruments, production, etc) on the same track, that
-    // track only counts as one unique contribution in the list.
-    allTracks:
-      unique(
-        ([
-          artist.trackArtistContributions,
-          artist.trackContributorContributions,
-        ]).flat()
-          .map(({thing}) => thing)),
-
-    // Artworks are different, though. We intentionally duplicate album data
-    // objects when the artist has contributed some combination of cover art,
-    // wallpaper, and banner - these each count as a unique contribution.
-    allArtworkThings:
-      ([
-        artist.albumCoverArtistContributions,
-        artist.albumWallpaperArtistContributions,
-        artist.albumBannerArtistContributions,
-        artist.trackCoverArtistContributions,
-      ]).flat()
-        .filter(({annotation}) => !annotation?.startsWith('edits for wiki'))
-        .map(({thing}) => thing.thing),
+    trackContributions: [
+      ...artist.trackArtistContributions,
+      ...artist.trackContributorContributions,
+    ],
+
+    artworkContributions: [
+      ...artist.albumCoverArtistContributions,
+      ...artist.albumWallpaperArtistContributions,
+      ...artist.albumBannerArtistContributions,
+      ...artist.trackCoverArtistContributions,
+    ],
 
     // Banners and wallpapers don't show up in the artist gallery page, only
     // cover art.
@@ -93,7 +63,7 @@ export default {
       relation('generateArtistInfoPageTracksChunkedList', artist),
 
     tracksGroupInfo:
-      relation('generateArtistGroupContributionsInfo', query.allTracks),
+      relation('generateArtistGroupContributionsInfo', query.trackContributions),
 
     artworksChunkedList:
       relation('generateArtistInfoPageArtworksChunkedList', artist, false),
@@ -102,7 +72,7 @@ export default {
       relation('generateArtistInfoPageArtworksChunkedList', artist, true),
 
     artworksGroupInfo:
-      relation('generateArtistGroupContributionsInfo', query.allArtworkThings),
+      relation('generateArtistGroupContributionsInfo', query.artworkContributions),
 
     artistGalleryLink:
       (query.hasGallery
@@ -128,7 +98,11 @@ export default {
         .map(({annotation}) => annotation),
 
     totalTrackCount:
-      query.allTracks.length,
+      unique(
+        query.trackContributions
+          .filter(contrib => contrib.countInContributionTotals)
+          .map(contrib => contrib.thing))
+        .length,
 
     totalDuration:
       artist.totalDuration,
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js
index 66e4204a..eb15d54b 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunk.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunk.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunk',
-    'generateArtistInfoPageArtworksChunkItem',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html'],
-
   relations: (relation, album, contribs) => ({
     template:
       relation('generateArtistInfoPageChunk'),
@@ -33,18 +25,19 @@ export default {
     },
   },
 
-  generate: (data, relations, slots) =>
+  generate: (data, relations, slots, {html}) =>
     relations.template.slots({
       mode: 'album',
-      albumLink: relations.albumLink,
+      link: relations.albumLink,
 
       dates:
         (slots.filterEditsForWiki
           ? Array.from({length: data.dates}, () => null)
           : data.dates),
 
-      items:
-        relations.items.map(item =>
-          item.slot('filterEditsForWiki', slots.filterEditsForWiki)),
+      list:
+        html.tag('ul',
+          relations.items.map(item =>
+            item.slot('filterEditsForWiki', slots.filterEditsForWiki))),
     }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
index 2f2fe0c5..e3ba5342 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkItem.js
@@ -1,19 +1,13 @@
-export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunkItem',
-    'generateArtistInfoPageOtherArtistLinks',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
+import {empty} from '#sugar';
 
+export default {
   query: (contrib) => ({
     kind:
-      (contrib.isBannerArtistContribution
+      (contrib.thing.thingProperty === 'bannerArtwork'
         ? 'banner'
-     : contrib.isWallpaperArtistContribution
+     : contrib.thing.thingProperty === 'wallpaperArtwork'
         ? 'wallpaper'
-     : contrib.isForAlbum
+     : contrib.thing.thingProperty === 'coverArtworks'
         ? 'album-cover'
         : 'track-cover'),
   }),
@@ -29,6 +23,9 @@ export default {
 
     otherArtistLinks:
       relation('generateArtistInfoPageOtherArtistLinks', [contrib]),
+
+    originDetails:
+      relation('transformContent', contrib.thing.originDetails),
   }),
 
   data: (query, contrib) => ({
@@ -37,6 +34,9 @@ export default {
 
     annotation:
       contrib.annotation,
+
+    label:
+      contrib.thing.label,
   }),
 
   slots: {
@@ -51,9 +51,33 @@ export default {
       otherArtistLinks: relations.otherArtistLinks,
 
       annotation:
-        (slots.filterEditsForWiki
-          ? data.annotation?.replace(/^edits for wiki(: )?/, '')
-          : data.annotation),
+        language.encapsulate('artistPage.creditList.entry.artwork.accent', workingCapsule => {
+          const workingOptions = {};
+
+          const artworkLabel = data.label;
+
+          if (artworkLabel) {
+            workingCapsule += '.withLabel';
+            workingOptions.label =
+              language.typicallyLowerCase(artworkLabel);
+          }
+
+          const contribAnnotation =
+            (slots.filterEditsForWiki
+              ? data.annotation?.replace(/^edits for wiki(: )?/, '')
+              : data.annotation);
+
+          if (contribAnnotation) {
+            workingCapsule += '.withAnnotation';
+            workingOptions.annotation = contribAnnotation;
+          }
+
+          if (empty(Object.keys(workingOptions))) {
+            return html.blank();
+          }
+
+          return language.$(workingCapsule, workingOptions);
+        }),
 
       content:
         language.encapsulate('artistPage.creditList.entry', capsule =>
@@ -68,5 +92,11 @@ export default {
                  : data.kind === 'banner'
                     ? language.$(capsule, 'bannerArt')
                     : language.$(capsule, 'coverArt')))))),
+
+      originDetails:
+        relations.originDetails.slots({
+          mode: 'inline',
+          absorbPunctuationFollowingExternalLinks: false,
+        }),
     }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
index 75a4aa5a..40ffc5dd 100644
--- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js
@@ -3,11 +3,6 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically}
 import {chunkByConditions, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunkedList',
-    'generateArtistInfoPageArtworksChunk',
-  ],
-
   query(artist, filterEditsForWiki) {
     const query = {};
 
diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js
index fce68a7d..3fa46c61 100644
--- a/src/content/dependencies/generateArtistInfoPageChunk.js
+++ b/src/content/dependencies/generateArtistInfoPageChunk.js
@@ -1,8 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  extraDependencies: ['html', 'language'],
-
   slots: {
     mode: {
       validate: v => v.is('flash', 'album'),
@@ -10,17 +8,12 @@ export default {
 
     id: {type: 'string'},
 
-    albumLink: {
-      type: 'html',
-      mutable: false,
-    },
-
-    flashActLink: {
+    link: {
       type: 'html',
       mutable: false,
     },
 
-    items: {
+    list: {
       type: 'html',
       mutable: false,
     },
@@ -53,50 +46,43 @@ export default {
     }
 
     let accentedLink;
-
-    accent: {
-      switch (slots.mode) {
-        case 'album': {
-          accentedLink = slots.albumLink;
-
-          const options = {album: accentedLink};
-          const parts = ['artistPage.creditList.album'];
-
-          if (onlyDate) {
-            parts.push('withDate');
-            options.date = language.formatDate(onlyDate);
-          }
-
-          if (slots.duration) {
-            parts.push('withDuration');
-            options.duration =
-              language.formatDuration(slots.duration, {
-                approximate: slots.durationApproximate,
-              });
-          }
-
-          accentedLink = language.formatString(...parts, options);
-          break;
+    switch (slots.mode) {
+      case 'album': {
+        const options = {album: slots.link};
+        const parts = ['artistPage.creditList.album'];
+
+        if (onlyDate) {
+          parts.push('withDate');
+          options.date = language.formatDate(onlyDate);
         }
 
-        case 'flash': {
-          accentedLink = slots.flashActLink;
-
-          const options = {act: accentedLink};
-          const parts = ['artistPage.creditList.flashAct'];
+        if (slots.duration) {
+          parts.push('withDuration');
+          options.duration =
+            language.formatDuration(slots.duration, {
+              approximate: slots.durationApproximate,
+            });
+        }
 
-          if (onlyDate) {
-            parts.push('withDate');
-            options.date = language.formatDate(onlyDate);
-          } else if (earliestDate && latestDate) {
-            parts.push('withDateRange');
-            options.dateRange =
-              language.formatDateRange(earliestDate, latestDate);
-          }
+        accentedLink = language.formatString(...parts, options);
+        break;
+      }
 
-          accentedLink = language.formatString(...parts, options);
-          break;
+      case 'flash': {
+        const options = {act: slots.link};
+        const parts = ['artistPage.creditList.flashAct'];
+
+        if (onlyDate) {
+          parts.push('withDate');
+          options.date = language.formatDate(onlyDate);
+        } else if (earliestDate && latestDate) {
+          parts.push('withDateRange');
+          options.dateRange =
+            language.formatDateRange(earliestDate, latestDate);
         }
+
+        accentedLink = language.formatString(...parts, options);
+        break;
       }
     }
 
@@ -105,10 +91,7 @@ export default {
         slots.id && {id: slots.id},
         accentedLink),
 
-      html.tag('dd',
-        html.tag('ul',
-          {class: 'offset-tooltips'},
-          slots.items)),
+      html.tag('dd', slots.list),
     ]);
   },
 };
diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js
index 7987b642..8117ca9a 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js
@@ -1,9 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: ['generateTextWithTooltip'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation) => ({
     textWithTooltip:
       relation('generateTextWithTooltip'),
@@ -33,6 +30,11 @@ export default {
       type: 'html',
       mutable: false,
     },
+
+    originDetails: {
+      type: 'html',
+      mutable: false,
+    },
   },
 
   generate: (relations, slots, {html, language}) =>
@@ -40,52 +42,59 @@ export default {
       html.tag('li',
         slots.rerelease && {class: 'rerelease'},
 
-        language.encapsulate(entryCapsule, workingCapsule => {
-          const workingOptions = {entry: slots.content};
-
-          if (!html.isBlank(slots.rereleaseTooltip)) {
-            workingCapsule += '.rerelease';
-            workingOptions.rerelease =
-              relations.textWithTooltip.slots({
-                attributes: {class: 'rerelease'},
-                text: language.$(entryCapsule, 'rerelease.term'),
-                tooltip: slots.rereleaseTooltip,
-              });
-
-            return language.$(workingCapsule, workingOptions);
-          }
-
-          if (!html.isBlank(slots.firstReleaseTooltip)) {
-            workingCapsule += '.firstRelease';
-            workingOptions.firstRelease =
-              relations.textWithTooltip.slots({
-                attributes: {class: 'first-release'},
-                text: language.$(entryCapsule, 'firstRelease.term'),
-                tooltip: slots.firstReleaseTooltip,
-              });
-
-            return language.$(workingCapsule, workingOptions);
-          }
-
-          let anyAccent = false;
-
-          if (!empty(slots.otherArtistLinks)) {
-            anyAccent = true;
-            workingCapsule += '.withArtists';
-            workingOptions.artists =
-              language.formatConjunctionList(slots.otherArtistLinks);
-          }
-
-          if (!html.isBlank(slots.annotation)) {
-            anyAccent = true;
-            workingCapsule += '.withAnnotation';
-            workingOptions.annotation = slots.annotation;
-          }
-
-          if (anyAccent) {
-            return language.$(workingCapsule, workingOptions);
-          } else {
-            return slots.content;
-          }
-        }))),
+        html.tags([
+          language.encapsulate(entryCapsule, workingCapsule => {
+            const workingOptions = {entry: slots.content};
+
+            if (!html.isBlank(slots.rereleaseTooltip)) {
+              workingCapsule += '.rerelease';
+              workingOptions.rerelease =
+                relations.textWithTooltip.slots({
+                  attributes: {class: 'rerelease'},
+                  text: language.$(entryCapsule, 'rerelease.term'),
+                  tooltip: slots.rereleaseTooltip,
+                });
+
+              return language.$(workingCapsule, workingOptions);
+            }
+
+            if (!html.isBlank(slots.firstReleaseTooltip)) {
+              workingCapsule += '.firstRelease';
+              workingOptions.firstRelease =
+                relations.textWithTooltip.slots({
+                  attributes: {class: 'first-release'},
+                  text: language.$(entryCapsule, 'firstRelease.term'),
+                  tooltip: slots.firstReleaseTooltip,
+                });
+
+              return language.$(workingCapsule, workingOptions);
+            }
+
+            let anyAccent = false;
+
+            if (!empty(slots.otherArtistLinks)) {
+              anyAccent = true;
+              workingCapsule += '.withArtists';
+              workingOptions.artists =
+                language.formatConjunctionList(slots.otherArtistLinks);
+            }
+
+            if (!html.isBlank(slots.annotation)) {
+              anyAccent = true;
+              workingCapsule += '.withAnnotation';
+              workingOptions.annotation = slots.annotation;
+            }
+
+            if (anyAccent) {
+              return language.$(workingCapsule, workingOptions);
+            } else {
+              return slots.content;
+            }
+          }),
+
+          html.tag('span', {class: 'origin-details'},
+            {[html.onlyIfContent]: true},
+
+            slots.originDetails),
+        ]))),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js
index e7915ab7..54577885 100644
--- a/src/content/dependencies/generateArtistInfoPageChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     groupInfo: {
       type: 'html',
diff --git a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
index 88c5ed54..08446a2e 100644
--- a/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageCommentaryChunkedList.js
@@ -7,18 +7,6 @@ import {
 } from '#sort';
 
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunk',
-    'generateArtistInfoPageChunkItem',
-    'linkAlbum',
-    'linkFlash',
-    'linkFlashAct',
-    'linkTrack',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(artist, filterWikiEditorCommentary) {
     const processEntry = ({
       thing,
@@ -232,52 +220,57 @@ export default {
             (chunkType === 'album'
               ? chunk.slots({
                   mode: 'album',
-                  albumLink: chunkLink,
-                  items:
-                    stitchArrays({
-                      item: items,
-                      link: itemLinks,
-                      annotation: itemAnnotations,
-                      type: itemTypes,
-                    }).map(({item, link, annotation, type}) =>
-                      item.slots({
-                        annotation:
-                          annotation.slots({
-                            mode: 'inline',
-                            absorbPunctuationFollowingExternalLinks: false,
-                          }),
-
-                        content:
-                          (type === 'album'
-                            ? html.tag('i',
-                                language.$(capsule, 'album.commentary'))
-                            : language.$(capsule, 'track', {track: link})),
-                      })),
+                  link: chunkLink,
+
+                  list:
+                    html.tag('ul',
+                      stitchArrays({
+                        item: items,
+                        link: itemLinks,
+                        annotation: itemAnnotations,
+                        type: itemTypes,
+                      }).map(({item, link, annotation, type}) =>
+                        item.slots({
+                          annotation:
+                            annotation.slots({
+                              mode: 'inline',
+                              absorbPunctuationFollowingExternalLinks: false,
+                            }),
+
+                          content:
+                            (type === 'album'
+                              ? html.tag('i',
+                                  language.$(capsule, 'album.commentary'))
+                              : language.$(capsule, 'track', {track: link})),
+                        }))),
                 })
-           : chunkType === 'flash-act'
+
+             : chunkType === 'flash-act'
               ? chunk.slots({
                   mode: 'flash',
-                  flashActLink: chunkLink,
-                  items:
-                    stitchArrays({
-                      item: items,
-                      link: itemLinks,
-                      annotation: itemAnnotations,
-                    }).map(({item, link, annotation}) =>
-                      item.slots({
-                        annotation:
-                          (annotation
-                            ? annotation.slots({
-                                mode: 'inline',
-                                absorbPunctuationFollowingExternalLinks: false,
-                              })
-                            : null),
-
-                        content:
-                          language.$(capsule, 'flash', {
-                            flash: link,
-                          }),
-                      })),
+                  link: chunkLink,
+
+                  list:
+                    html.tag('ul',
+                      stitchArrays({
+                        item: items,
+                        link: itemLinks,
+                        annotation: itemAnnotations,
+                      }).map(({item, link, annotation}) =>
+                        item.slots({
+                          annotation:
+                            (annotation
+                              ? annotation.slots({
+                                  mode: 'inline',
+                                  absorbPunctuationFollowingExternalLinks: false,
+                                })
+                              : null),
+
+                          content:
+                            language.$(capsule, 'flash', {
+                              flash: link,
+                            }),
+                        }))),
                 })
               : null)))),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js
index f86dead7..1d498b9f 100644
--- a/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js
+++ b/src/content/dependencies/generateArtistInfoPageFirstReleaseTooltip.js
@@ -1,19 +1,19 @@
-import {sortChronologically} from '#sort';
+import {sortAlbumsTracksChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateTooltip',
-    'linkOtherReleaseOnArtistInfoPage',
-  ],
+query: (track, artist) => ({
+  rereleases:
+    sortAlbumsTracksChronologically(
+      track.otherReleases.filter(track => {
+        const contribs = [
+          ...track.artistContribs,
+          ...track.contributorContribs,
+        ];
 
-  extraDependencies: ['html', 'language'],
-
-  query: (track) => ({
-    rereleases:
-      sortChronologically(track.allReleases).slice(1),
-  }),
+        return contribs.some(contrib => contrib.artist === artist);
+      })),
+}),
 
   relations: (relation, query, track, artist) => ({
     tooltip:
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js
index 8aa7223a..ce89d80c 100644
--- a/src/content/dependencies/generateArtistInfoPageFlashesChunk.js
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunk.js
@@ -1,10 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunk',
-    'generateArtistInfoPageFlashesChunkItem',
-    'linkFlashAct',
-  ],
-
   relations: (relation, flashAct, contribs) => ({
     template:
       relation('generateArtistInfoPageChunk'),
@@ -24,11 +18,13 @@ export default {
         .map(contrib => contrib.date),
   }),
 
-  generate: (data, relations) =>
+  generate: (data, relations, {html}) =>
     relations.template.slots({
       mode: 'flash',
-      flashActLink: relations.flashActLink,
+      link: relations.flashActLink,
       dates: data.dates,
-      items: relations.items,
+
+      list:
+        html.tag('ul', relations.items),
     }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js
index e4908bf9..36d7945d 100644
--- a/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkItem.js
@@ -1,8 +1,4 @@
 export default {
-  contentDependencies: ['generateArtistInfoPageChunkItem', 'linkFlash'],
-
-  extraDependencies: ['language'],
-
   relations: (relation, contrib) => ({
     // Flashes and games can list multiple contributors as collaborative
     // credits, but we don't display these on the artist page, since they
diff --git a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
index b347faf5..762386a2 100644
--- a/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageFlashesChunkedList.js
@@ -3,13 +3,6 @@ import {sortContributionsChronologically, sortFlashesChronologically}
 import {chunkByConditions, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunkedList',
-    'generateArtistInfoPageFlashesChunk',
-  ],
-
-  extraDependencies: ['wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     enableFlashesAndGames:
       wikiInfo.enableFlashesAndGames,
diff --git a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
index dcee9c00..afb61c33 100644
--- a/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
+++ b/src/content/dependencies/generateArtistInfoPageOtherArtistLinks.js
@@ -1,8 +1,6 @@
 import {unique} from '#sugar';
 
 export default {
-  contentDependencies: ['linkArtist'],
-
   query(contribs) {
     const associatedContributionsByOtherArtists =
       contribs
diff --git a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js
index 1d849919..bf5fe616 100644
--- a/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js
+++ b/src/content/dependencies/generateArtistInfoPageRereleaseTooltip.js
@@ -1,18 +1,22 @@
-import {sortChronologically} from '#sort';
+import {sortAlbumsTracksChronologically} from '#sort';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateTooltip',
-    'linkOtherReleaseOnArtistInfoPage'
-  ],
+  query(track, artist) {
+    const query = {};
 
-  extraDependencies: ['html', 'language'],
+    query.firstRelease =
+      sortAlbumsTracksChronologically(track.allReleases)[0];
 
-  query: (track) => ({
-    firstRelease:
-      sortChronologically(track.allReleases)[0],
-  }),
+    const contribs = [
+      ...query.firstRelease.artistContribs,
+      ...query.firstRelease.contributorContribs,
+    ];
+
+    query.creditedOnFirstRelease =
+      contribs.some(contrib => contrib.artist === artist);
+
+    return query;
+  },
 
   relations: (relation, query, track, artist) => ({
     tooltip:
@@ -22,10 +26,15 @@ export default {
       relation('generateColorStyleAttribute', track.color),
 
     firstReleaseLink:
-      relation('linkOtherReleaseOnArtistInfoPage', query.firstRelease, artist),
+      (query.creditedOnFirstRelease
+        ? relation('linkOtherReleaseOnArtistInfoPage', query.firstRelease, artist)
+        : relation('linkTrackAsRelease', query.firstRelease)),
   }),
 
-  data: (query, track) => ({
+  data: (query, track, artist) => ({
+    artistName:
+      artist.name,
+
     rereleaseDate:
       track.dateFirstReleased ??
       track.album.date,
@@ -33,6 +42,9 @@ export default {
     firstReleaseDate:
       query.firstRelease.dateFirstReleased ??
       query.firstRelease.album.date,
+
+    creditedOnFirstRelease:
+      query.creditedOnFirstRelease,
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -56,6 +68,15 @@ export default {
             approximate: true,
             absolute: true,
           }),
+
+          !data.creditedOnFirstRelease && [
+            html.tag('hr', {class: 'cute'}),
+
+            html.tag('span', {class: 'not-credited-on-first-release'},
+              language.$(capsule, 'notCreditedOnFirstRelease', {
+                artist: data.artistName,
+              })),
+          ],
         ],
       })),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js
index f6d70901..7d00fdd6 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunk.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js
@@ -1,13 +1,8 @@
-import {unique} from '#sugar';
+import {sortAlbumsTracksChronologically} from '#sort';
+import {empty, unique} from '#sugar';
 import {getTotalDuration} from '#wiki-data';
 
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunk',
-    'generateArtistInfoPageTracksChunkItem',
-    'linkAlbum',
-  ],
-
   relations: (relation, artist, album, trackContribLists) => ({
     template:
       relation('generateArtistInfoPageChunk'),
@@ -24,7 +19,7 @@ export default {
           trackContribs)),
   }),
 
-  data(_artist, album, trackContribLists) {
+  data(artist, album, trackContribLists) {
     const data = {};
 
     const contribs =
@@ -49,19 +44,47 @@ export default {
     data.durationApproximate =
       durationTerms.length > 1;
 
+    const tracks =
+      trackContribLists.map(contribs => contribs[0].thing);
+
+    data.numLinkingOtherReleases =
+      tracks.filter(track => {
+        if (empty(track.otherReleases)) return false;
+
+        const releases =
+          sortAlbumsTracksChronologically(track.allReleases.slice());
+
+        // later releases always link to first release
+        if (track !== releases[0]) return true;
+
+        // first releases only link to later credited releases
+        return tracks.slice(1).some(track => {
+          const contribs = [
+            ...track.artistContribs,
+            ...track.contributorContribs,
+          ];
+
+          return contribs.some(contrib => contrib.artist === artist);
+        });
+      }).length;
+
     return data;
   },
 
-  generate: (data, relations) =>
+  generate: (data, relations, {html}) =>
     relations.template.slots({
       mode: 'album',
-
-      albumLink: relations.albumLink,
+      link: relations.albumLink,
 
       dates: data.dates,
       duration: data.duration,
       durationApproximate: data.durationApproximate,
 
-      items: relations.items,
+      list:
+        html.tag('ul',
+          data.numLinkingOtherReleases > 1 &&
+            {class: 'offset-tooltips'},
+
+          relations.items),
     }),
 };
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
index a42d6fee..e976c57f 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js
@@ -1,18 +1,8 @@
-import {sortChronologically} from '#sort';
+import {sortAlbumsTracksChronologically} from '#sort';
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunkItem',
-    'generateArtistInfoPageFirstReleaseTooltip',
-    'generateArtistInfoPageOtherArtistLinks',
-    'generateArtistInfoPageRereleaseTooltip',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
-  query (_artist, contribs) {
+  query(artist, contribs) {
     const query = {};
 
     // TODO: Very mysterious what to do if the set of contributions is,
@@ -22,11 +12,11 @@ export default {
 
     const creditedAsArtist =
       contribs
-        .some(contrib => contrib.isArtistContribution);
+        .some(contrib => contrib.thingProperty === 'artistContribs');
 
     const creditedAsContributor =
       contribs
-        .some(contrib => contrib.isContributorContribution);
+        .some(contrib => contrib.thingProperty === 'contributorContribs');
 
     const annotatedContribs =
       contribs
@@ -34,11 +24,11 @@ export default {
 
     const annotatedArtistContribs =
       annotatedContribs
-        .filter(contrib => contrib.isArtistContribution);
+        .filter(contrib => contrib.thingProperty === 'artistContribs');
 
     const annotatedContributorContribs =
       annotatedContribs
-        .filter(contrib => contrib.isContributorContribution);
+        .filter(contrib => contrib.thingProperty === 'contributorContribs');
 
     // Don't display annotations associated with crediting in the
     // Contributors field if the artist is also credited as an Artist
@@ -73,16 +63,23 @@ export default {
     // different - and it's the latter that determines whether the
     // track is a rerelease!
     const allReleasesChronologically =
-      sortChronologically(query.track.allReleases);
+      sortAlbumsTracksChronologically(query.track.allReleases);
 
     query.isFirstRelease =
       allReleasesChronologically[0] === query.track;
 
-    query.isRerelease =
+    query.isLaterRelease =
       allReleasesChronologically[0] !== query.track;
 
-    query.hasOtherReleases =
-      !empty(query.track.otherReleases);
+    query.hasOtherCreditedReleases =
+      query.track.otherReleases.some(track => {
+        const contribs = [
+          ...track.artistContribs,
+          ...track.contributorContribs,
+        ];
+
+        return contribs.some(contrib => contrib.artist === artist);
+      });
 
     return query;
   },
@@ -98,12 +95,12 @@ export default {
       relation('generateArtistInfoPageOtherArtistLinks', contribs),
 
     rereleaseTooltip:
-      (query.isRerelease
+      (query.isLaterRelease
         ? relation('generateArtistInfoPageRereleaseTooltip', query.track, artist)
         : null),
 
     firstReleaseTooltip:
-      (query.isFirstRelease && query.hasOtherReleases
+      (query.isFirstRelease && query.hasOtherCreditedReleases
         ? relation('generateArtistInfoPageFirstReleaseTooltip', query.track, artist)
         : null),
   }),
diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
index 84eb29ac..15588ed3 100644
--- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
+++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js
@@ -4,11 +4,6 @@ import {stitchArrays} from '#sugar';
 import {chunkArtistTrackContributions} from '#wiki-data';
 
 export default {
-  contentDependencies: [
-    'generateArtistInfoPageChunkedList',
-    'generateArtistInfoPageTracksChunk',
-  ],
-
   query(artist) {
     const query = {};
 
diff --git a/src/content/dependencies/generateArtistNavLinks.js b/src/content/dependencies/generateArtistNavLinks.js
index 1b4b6eca..69ae3e19 100644
--- a/src/content/dependencies/generateArtistNavLinks.js
+++ b/src/content/dependencies/generateArtistNavLinks.js
@@ -1,14 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'linkArtist',
-    'linkArtistGallery',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     enableListings:
       wikiInfo.enableListings,
@@ -34,6 +26,9 @@ export default {
       (query.hasGallery
         ? relation('linkArtistGallery', artist)
         : null),
+
+    artistRollingWindowLink:
+      relation('linkArtistRollingWindow', artist),
   }),
 
   data: (_query, sprawl) => ({
@@ -45,7 +40,7 @@ export default {
     showExtraLinks: {type: 'boolean', default: false},
 
     currentExtra: {
-      validate: v => v.is('gallery'),
+      validate: v => v.is('gallery', 'rolling-window'),
     },
   },
 
@@ -79,6 +74,7 @@ export default {
             }),
 
             slots.showExtraLinks &&
+            slots.currentExtra !== 'rolling-window' &&
               relations.artistGalleryLink?.slots({
                 attributes: [
                   slots.currentExtra === 'gallery' &&
@@ -87,6 +83,12 @@ export default {
 
                 content: language.$('misc.nav.gallery'),
               }),
+
+            slots.currentExtra === 'rolling-window' &&
+              relations.artistRollingWindowLink.slots({
+                attributes: {class: 'current'},
+                content: language.$('misc.nav.rollingWindow'),
+              }),
           ],
         }),
     },
diff --git a/src/content/dependencies/generateArtistRollingWindowPage.js b/src/content/dependencies/generateArtistRollingWindowPage.js
new file mode 100644
index 00000000..aafd1b55
--- /dev/null
+++ b/src/content/dependencies/generateArtistRollingWindowPage.js
@@ -0,0 +1,418 @@
+import {sortAlbumsTracksChronologically} from '#sort';
+import Thing from '#thing';
+
+import {
+  chunkByConditions,
+  filterMultipleArrays,
+  empty,
+  sortMultipleArrays,
+  stitchArrays,
+  unique,
+} from '#sugar';
+
+export default {
+  sprawl: ({groupCategoryData}) => ({
+    groupCategoryData,
+  }),
+
+  query(sprawl, artist) {
+    const query = {};
+
+    const musicContributions =
+      artist.musicContributions
+        .filter(contrib => contrib.date);
+
+    const artworkContributions =
+      artist.artworkContributions
+        .filter(contrib =>
+          contrib.date &&
+          contrib.thingProperty !== 'wallpaperArtistContribs' &&
+          contrib.thingProperty !== 'bannerArtistContribs');
+
+    const musicThings =
+      musicContributions
+        .map(contrib => contrib.thing);
+
+    const artworkThings =
+      artworkContributions
+        .map(contrib => contrib.thing.thing);
+
+    const musicContributionDates =
+      musicContributions
+        .map(contrib => contrib.date);
+
+    const artworkContributionDates =
+      artworkContributions
+        .map(contrib => contrib.date);
+
+    const musicContributionKinds =
+      musicContributions
+        .map(() => 'music');
+
+    const artworkContributionKinds =
+      artworkContributions
+        .map(() => 'artwork');
+
+    const allThings = [
+      ...artworkThings,
+      ...musicThings,
+    ];
+
+    const allContributionDates = [
+      ...artworkContributionDates,
+      ...musicContributionDates,
+    ];
+
+    const allContributionKinds = [
+      ...artworkContributionKinds,
+      ...musicContributionKinds,
+    ];
+
+    const sortedThings =
+      sortAlbumsTracksChronologically(allThings.slice(), {latestFirst: true});
+
+    sortMultipleArrays(
+      allThings,
+      allContributionDates,
+      allContributionKinds,
+      (thing1, thing2) =>
+        sortedThings.indexOf(thing1) -
+        sortedThings.indexOf(thing2));
+
+    const sourceIndices =
+      Array.from({length: allThings.length}, (_, i) => i);
+
+    const sourceChunks =
+      chunkByConditions(sourceIndices, [
+        (index1, index2) =>
+          allThings[index1] !==
+          allThings[index2],
+      ]);
+
+    const indicesTo = array => index => array[index];
+
+    query.things =
+      sourceChunks
+        .map(chunks => allThings[chunks[0]]);
+
+    query.thingGroups =
+      query.things.map(thing =>
+        (thing.constructor[Thing.referenceType] === 'album'
+          ? thing.groups
+       : thing.constructor[Thing.referenceType] === 'track'
+          ? thing.album.groups
+          : null));
+
+    query.thingContributionDates =
+      sourceChunks
+        .map(indices => indices
+          .map(indicesTo(allContributionDates)));
+
+    query.thingContributionKinds =
+      sourceChunks
+        .map(indices => indices
+          .map(indicesTo(allContributionKinds)));
+
+    // Matches the "kind" dropdown.
+    const kinds = ['artwork', 'music', 'flash'];
+
+    const allKinds =
+      unique(query.thingContributionKinds.flat(2));
+
+    query.kinds =
+      kinds
+        .filter(kind => allKinds.includes(kind));
+
+    query.firstKind =
+      query.kinds.at(0);
+
+    query.thingArtworks =
+      stitchArrays({
+        thing: query.things,
+        kinds: query.thingContributionKinds,
+      }).map(({thing, kinds}) =>
+          (kinds.includes('artwork')
+            ? (thing.coverArtworks ?? thing.trackArtworks ?? [])
+                .find(artwork => artwork.artistContribs
+                  .some(contrib => contrib.artist === artist))
+            : (thing.coverArtworks ?? thing.trackArtworks)?.[0] ??
+              thing.album?.coverArtworks[0] ??
+              null));
+
+    const allGroups =
+      unique(query.thingGroups.flat());
+
+    query.groupCategories =
+      sprawl.groupCategoryData.slice();
+
+    query.groupCategoryGroups =
+      sprawl.groupCategoryData
+        .map(category => category.groups
+          .filter(group => allGroups.includes(group)));
+
+    filterMultipleArrays(
+      query.groupCategories,
+      query.groupCategoryGroups,
+      (_category, groups) => !empty(groups));
+
+    const groupsMatchingFirstKind =
+      unique(
+        stitchArrays({
+          thing: query.things,
+          groups: query.thingGroups,
+          kinds: query.thingContributionKinds,
+        }).filter(({kinds}) => kinds.includes(query.firstKind))
+          .flatMap(({groups}) => groups));
+
+    query.firstGroup =
+      sprawl.groupCategoryData
+        .flatMap(category => category.groups)
+        .find(group => groupsMatchingFirstKind.includes(group));
+
+    query.firstGroupCategory =
+      query.firstGroup.category;
+
+    return query;
+  },
+
+  relations: (relation, query, sprawl, artist) => ({
+    layout:
+      relation('generatePageLayout'),
+
+    artistNavLinks:
+      relation('generateArtistNavLinks', artist),
+
+    sourceGrid:
+      relation('generateCoverGrid'),
+
+    sourceGridImages:
+      query.thingArtworks
+        .map(artwork => relation('image', artwork)),
+
+    sourceGridLinks:
+      query.things
+        .map(thing => relation('linkAnythingMan', thing)),
+  }),
+
+  data: (query, sprawl, artist) => ({
+    name:
+      artist.name,
+
+    categoryGroupDirectories:
+      query.groupCategoryGroups
+        .map(groups => groups
+          .map(group => group.directory)),
+
+    categoryGroupNames:
+      query.groupCategoryGroups
+        .map(groups => groups
+          .map(group => group.name)),
+
+    firstGroupCategoryIndex:
+      query.groupCategories
+        .indexOf(query.firstGroupCategory),
+
+    firstGroupIndex:
+      stitchArrays({
+        category: query.groupCategories,
+        groups: query.groupCategoryGroups,
+      }).find(({category}) => category === query.firstGroupCategory)
+        .groups
+          .indexOf(query.firstGroup),
+
+    kinds:
+      query.kinds,
+
+    sourceGridNames:
+      query.things
+        .map(thing => thing.name),
+
+    sourceGridGroupDirectories:
+      query.thingGroups
+        .map(groups => groups
+          .map(group => group.directory)),
+
+    sourceGridGroupNames:
+      query.thingGroups
+        .map(groups => groups
+          .map(group => group.name)),
+
+    sourceGridContributionKinds:
+      query.thingContributionKinds,
+
+    sourceGridContributionDates:
+      query.thingContributionDates,
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    relations.layout.slots({
+      title:
+        language.$('artistRollingWindowPage.title', {
+          artist: data.name,
+        }),
+
+      mainClasses: ['top-index'],
+      mainContent: [
+        html.tag('p', {id: 'timeframe-configuration'},
+          language.$('artistRollingWindowPage.windowConfigurationLine', {
+            timeBefore:
+              language.$('artistRollingWindowPage.timeframe.months', {
+                input:
+                  html.tag('input', {id: 'timeframe-months-before'},
+                    {type: 'number'},
+                    {value: 3, min: 0}),
+              }),
+
+            timeAfter:
+              language.$('artistRollingWindowPage.timeframe.months', {
+                input:
+                  html.tag('input', {id: 'timeframe-months-after'},
+                    {type: 'number'},
+                    {value: 3, min: 1}),
+              }),
+
+            peek:
+              language.$('artistRollingWindowPage.timeframe.months', {
+                input:
+                  html.tag('input', {id: 'timeframe-months-peek'},
+                    {type: 'number'},
+                    {value: 1, min: 0}),
+              }),
+          })),
+
+        html.tag('p', {id: 'contribution-configuration'},
+          language.$('artistRollingWindowPage.contributionConfigurationLine', {
+            kind:
+              html.tag('select', {id: 'contribution-kind'},
+                data.kinds.map(kind =>
+                  html.tag('option', {value: kind},
+                    language.$('artistRollingWindowPage.contributionKind', kind)))),
+
+            group:
+              html.tag('select', {id: 'contribution-group'}, [
+                html.tag('option', {value: '-'},
+                  language.$('artistRollingWindowPage.contributionGroup.all')),
+
+                stitchArrays({
+                  names: data.categoryGroupNames,
+                  directories: data.categoryGroupDirectories,
+                }).map(({names, directories}, categoryIndex) => [
+                    html.tag('hr'),
+
+                    stitchArrays({name: names, directory: directories})
+                      .map(({name, directory}, groupIndex) =>
+                        html.tag('option', {value: directory},
+                          categoryIndex === data.firstGroupCategoryIndex &&
+                          groupIndex === data.firstGroupIndex &&
+                            {selected: true},
+
+                          language.$('artistRollingWindowPage.contributionGroup.group', {
+                            group: name,
+                          }))),
+                  ]),
+              ]),
+          })),
+
+        html.tag('p', {id: 'timeframe-selection-info'}, [
+          html.tag('span', {id: 'timeframe-selection-some'},
+            {style: 'display: none'},
+
+            language.$('artistRollingWindowPage.timeframeSelectionLine', {
+              contributions:
+                html.tag('b', {id: 'timeframe-selection-contribution-count'}),
+
+              timeframes:
+                html.tag('b', {id: 'timeframe-selection-timeframe-count'}),
+
+              firstDate:
+                html.tag('b', {id: 'timeframe-selection-first-date'}),
+
+              lastDate:
+                html.tag('b', {id: 'timeframe-selection-last-date'}),
+            })),
+
+          html.tag('span', {id: 'timeframe-selection-none'},
+            {style: 'display: none'},
+            language.$('artistRollingWindowPage.timeframeSelectionLine.none')),
+        ]),
+
+        html.tag('p', {id: 'timeframe-selection-control'},
+          {style: 'display: none'},
+
+          language.$('artistRollingWindowPage.timeframeSelectionControl', {
+            timeframes:
+              html.tag('select', {id: 'timeframe-selection-menu'}),
+
+            previous:
+              html.tag('a', {id: 'timeframe-selection-previous'},
+                {href: '#'},
+                language.$('artistRollingWindowPage.timeframeSelectionControl.previous')),
+
+            next:
+              html.tag('a', {id: 'timeframe-selection-next'},
+                {href: '#'},
+                language.$('artistRollingWindowPage.timeframeSelectionControl.next')),
+          })),
+
+        html.tag('div', {id: 'timeframe-source-area'}, [
+          html.tag('p', {id: 'timeframe-empty'},
+            {style: 'display: none'},
+            language.$('artistRollingWindowPage.emptyTimeframeLine')),
+
+          relations.sourceGrid.slots({
+            attributes: {style: 'display: none'},
+
+            lazy: true,
+
+            links:
+              relations.sourceGridLinks.map(link =>
+                link.slot('attributes', {target: '_blank'})),
+
+            names:
+              data.sourceGridNames,
+
+            images:
+              relations.sourceGridImages,
+
+            info:
+              stitchArrays({
+                contributionKinds: data.sourceGridContributionKinds,
+                contributionDates: data.sourceGridContributionDates,
+                groupDirectories: data.sourceGridGroupDirectories,
+                groupNames: data.sourceGridGroupNames,
+              }).map(({
+                  contributionKinds,
+                  contributionDates,
+                  groupDirectories,
+                  groupNames,
+                }) => [
+                  stitchArrays({
+                    directory: groupDirectories,
+                    name: groupNames,
+                  }).map(({directory, name}) =>
+                    html.tag('data', {class: 'contribution-group'},
+                      {value: directory},
+                      name)),
+
+                  stitchArrays({
+                    kind: contributionKinds,
+                    date: contributionDates,
+                  }).map(({kind, date}) =>
+                      html.tag('time', {class: `${kind}-contribution-date`},
+                        {datetime: date.toUTCString()},
+                        language.formatDate(date))),
+                ]),
+          }),
+        ]),
+      ],
+
+      navLinkStyle: 'hierarchical',
+      navLinks:
+        relations.artistNavLinks
+          .slots({
+            showExtraLinks: true,
+            currentExtra: 'rolling-window',
+          })
+          .content,
+    }),
+}
diff --git a/src/content/dependencies/generateBackToAlbumLink.js b/src/content/dependencies/generateBackToAlbumLink.js
index 6648b463..08d33348 100644
--- a/src/content/dependencies/generateBackToAlbumLink.js
+++ b/src/content/dependencies/generateBackToAlbumLink.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkAlbum'],
-  extraDependencies: ['language'],
-
   relations: (relation, track) => ({
     trackLink:
       relation('linkAlbum', track),
diff --git a/src/content/dependencies/generateBackToTrackLink.js b/src/content/dependencies/generateBackToTrackLink.js
index 8677d811..90dfb6d5 100644
--- a/src/content/dependencies/generateBackToTrackLink.js
+++ b/src/content/dependencies/generateBackToTrackLink.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkTrack'],
-  extraDependencies: ['language'],
-
   relations: (relation, track) => ({
     trackLink:
       relation('linkTrack', track),
diff --git a/src/content/dependencies/generateBanner.js b/src/content/dependencies/generateBanner.js
index 15eb08eb..509b15c2 100644
--- a/src/content/dependencies/generateBanner.js
+++ b/src/content/dependencies/generateBanner.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'to'],
-
   slots: {
     path: {
       validate: v => v.validateArrayItems(v.isString),
diff --git a/src/content/dependencies/generateCollapsedContentEntrySection.js b/src/content/dependencies/generateCollapsedContentEntrySection.js
new file mode 100644
index 00000000..aec5fe28
--- /dev/null
+++ b/src/content/dependencies/generateCollapsedContentEntrySection.js
@@ -0,0 +1,37 @@
+export default {
+  relations: (relation, entries, thing) => ({
+    contentContentHeading:
+      relation('generateContentContentHeading', thing),
+
+    entries:
+      entries
+        .map(entry => relation('generateCommentaryEntry', entry)),
+  }),
+
+  slots: {
+    id: {type: 'string'},
+    string: {type: 'string'},
+  },
+
+  generate: (relations, slots, {html}) =>
+    html.tag('details',
+      {[html.onlyIfContent]: true},
+
+      slots.id && [
+        {class: 'memorable'},
+        {'data-memorable-id': slots.id},
+      ],
+
+      [
+        relations.contentContentHeading.slots({
+          attributes: [
+            slots.id && {id: slots.id},
+          ],
+
+          string: slots.string,
+          summary: true,
+        }),
+
+        relations.entries,
+      ]),
+};
diff --git a/src/content/dependencies/generateColorStyleAttribute.js b/src/content/dependencies/generateColorStyleAttribute.js
index 03d95ac5..277ec434 100644
--- a/src/content/dependencies/generateColorStyleAttribute.js
+++ b/src/content/dependencies/generateColorStyleAttribute.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateColorStyleVariables'],
-  extraDependencies: ['html'],
-
   relations: (relation) => ({
     colorVariables:
       relation('generateColorStyleVariables'),
diff --git a/src/content/dependencies/generateColorStyleRules.js b/src/content/dependencies/generateColorStyleRules.js
deleted file mode 100644
index c412b8f2..00000000
--- a/src/content/dependencies/generateColorStyleRules.js
+++ /dev/null
@@ -1,42 +0,0 @@
-export default {
-  contentDependencies: ['generateColorStyleVariables'],
-  extraDependencies: ['html'],
-
-  relations: (relation) => ({
-    variables:
-      relation('generateColorStyleVariables'),
-  }),
-
-  data: (color) => ({
-    color:
-      color ?? null,
-  }),
-
-  slots: {
-    color: {
-      validate: v => v.isColor,
-    },
-  },
-
-  generate(data, relations, slots) {
-    const color = data.color ?? slots.color;
-
-    if (!color) {
-      return '';
-    }
-
-    return [
-      `:root {`,
-      ...(
-        relations.variables
-          .slots({
-            color,
-            context: 'page-root',
-            mode: 'property-list',
-          })
-          .content
-          .map(line => line + ';')),
-      `}`,
-    ].join('\n');
-  },
-};
diff --git a/src/content/dependencies/generateColorStyleTag.js b/src/content/dependencies/generateColorStyleTag.js
new file mode 100644
index 00000000..b378fd1d
--- /dev/null
+++ b/src/content/dependencies/generateColorStyleTag.js
@@ -0,0 +1,48 @@
+export default {
+  relations: (relation) => ({
+    styleTag:
+      relation('generateStyleTag'),
+
+    variables:
+      relation('generateColorStyleVariables'),
+  }),
+
+  data: (color) => ({
+    color:
+      color ?? null,
+  }),
+
+  slots: {
+    color: {
+      validate: v => v.isColor,
+    },
+  },
+
+  generate(data, relations, slots, {html}) {
+    const color =
+      data.color ?? slots.color;
+
+    if (!color) {
+      return html.blank();
+    }
+
+    return relations.styleTag.slots({
+      attributes: [
+        {class: 'color-style'},
+        {'data-color': color},
+      ],
+
+      rules: [
+        {
+          select: ':root',
+          declare:
+            relations.variables.slots({
+              color,
+              context: 'page-root',
+              mode: 'declarations',
+            }).content,
+        },
+      ],
+    });
+  },
+};
diff --git a/src/content/dependencies/generateColorStyleVariables.js b/src/content/dependencies/generateColorStyleVariables.js
index 5270dbe4..0865ed3e 100644
--- a/src/content/dependencies/generateColorStyleVariables.js
+++ b/src/content/dependencies/generateColorStyleVariables.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'getColors'],
-
   slots: {
     color: {
       validate: v => v.isColor,
@@ -18,7 +16,7 @@ export default {
     },
 
     mode: {
-      validate: v => v.is('style', 'property-list'),
+      validate: v => v.is('style', 'declarations'),
       default: 'style',
     },
   },
@@ -50,15 +48,15 @@ export default {
       `--shadow-color: ${shadow}`,
     ];
 
-    let selectedProperties;
+    let selectedDeclarations;
 
     switch (slots.context) {
       case 'any-content':
-        selectedProperties = anyContent;
+        selectedDeclarations = anyContent;
         break;
 
       case 'image-box':
-        selectedProperties = [
+        selectedDeclarations = [
           `--primary-color: ${primary}`,
           `--dim-color: ${dim}`,
           `--deep-color: ${deep}`,
@@ -67,14 +65,14 @@ export default {
         break;
 
       case 'page-root':
-        selectedProperties = [
+        selectedDeclarations = [
           ...anyContent,
           `--page-primary-color: ${primary}`,
         ];
         break;
 
       case 'primary-only':
-        selectedProperties = [
+        selectedDeclarations = [
           `--primary-color: ${primary}`,
         ];
         break;
@@ -82,10 +80,10 @@ export default {
 
     switch (slots.mode) {
       case 'style':
-        return selectedProperties.join('; ');
+        return selectedDeclarations.join('; ');
 
-      case 'property-list':
-        return selectedProperties;
+      case 'declarations':
+        return selectedDeclarations.map(declaration => declaration + ';');
     }
   },
 };
diff --git a/src/content/dependencies/generateCommentaryContentHeading.js b/src/content/dependencies/generateCommentaryContentHeading.js
new file mode 100644
index 00000000..691762aa
--- /dev/null
+++ b/src/content/dependencies/generateCommentaryContentHeading.js
@@ -0,0 +1,43 @@
+import {empty} from '#sugar';
+
+export default {
+  query: (thing) => ({
+    entries:
+      (thing.isTrack
+        ? [...thing.commentary, ...thing.commentaryFromMainRelease]
+        : thing.commentary),
+  }),
+
+  relations: (relation, _query, thing) => ({
+    contentContentHeading:
+      relation('generateContentContentHeading', thing),
+  }),
+
+  data: (query, _thing) => ({
+    hasWikiEditorCommentary:
+      query.entries.some(entry => entry.isWikiEditorCommentary),
+
+    onlyWikiEditorCommentary:
+      !empty(query.entries) &&
+      query.entries.every(entry => entry.isWikiEditorCommentary),
+
+    hasAnyCommentary:
+      !empty(query.entries),
+  }),
+
+  generate: (data, relations, {language}) =>
+    relations.contentContentHeading.slots({
+      // It's #artist-commentary for legacy reasons... Sorry...
+      attributes: {id: 'artist-commentary'},
+
+      string:
+        language.encapsulate('misc.artistCommentary', capsule =>
+          (data.onlyWikiEditorCommentary
+            ? language.encapsulate(capsule, 'onlyWikiCommentary')
+         : data.hasWikiEditorCommentary
+            ? language.encapsulate(capsule, 'withWikiCommentary')
+         : data.hasAnyCommentary
+            ? capsule
+            : null)),
+    }),
+};
diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js
index 367de506..38eb6b43 100644
--- a/src/content/dependencies/generateCommentaryEntry.js
+++ b/src/content/dependencies/generateCommentaryEntry.js
@@ -1,15 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateCommentaryEntryDate',
-    'generateColorStyleAttribute',
-    'linkArtist',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, entry) => ({
     artistLinks:
       (!empty(entry.artists) && !entry.artistText
@@ -39,11 +30,16 @@ export default {
       relation('generateCommentaryEntryDate', entry),
   }),
 
+  data: (entry) => ({
+    isWikiEditorCommentary:
+      entry.isWikiEditorCommentary,
+  }),
+
   slots: {
     color: {validate: v => v.isColor},
   },
 
-  generate: (relations, slots, {html, language}) =>
+  generate: (data, relations, slots, {html, language}) =>
     language.encapsulate('misc.artistCommentary.entry', entryCapsule =>
       html.tags([
         html.tag('p', {class: 'commentary-entry-heading'},
@@ -107,6 +103,9 @@ export default {
             relations.colorStyle.clone()
               .slot('color', slots.color),
 
+          data.isWikiEditorCommentary &&
+            {class: 'wiki-commentary'},
+
           relations.bodyContent.slot('mode', 'multiline')),
       ])),
 };
diff --git a/src/content/dependencies/generateCommentaryEntryDate.js b/src/content/dependencies/generateCommentaryEntryDate.js
index f1cf5cb3..e924f244 100644
--- a/src/content/dependencies/generateCommentaryEntryDate.js
+++ b/src/content/dependencies/generateCommentaryEntryDate.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateTextWithTooltip', 'generateTooltip'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, _entry) => ({
     textWithTooltip:
       relation('generateTextWithTooltip'),
diff --git a/src/content/dependencies/generateCommentaryIndexPage.js b/src/content/dependencies/generateCommentaryIndexPage.js
index d68ba42e..8cc30913 100644
--- a/src/content/dependencies/generateCommentaryIndexPage.js
+++ b/src/content/dependencies/generateCommentaryIndexPage.js
@@ -1,13 +1,11 @@
+import multilingualWordCount from 'word-count';
+
 import {sortChronologically} from '#sort';
 import {accumulateSum, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generatePageLayout', 'linkAlbumCommentary'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
-  sprawl({albumData}) {
-    return {albumData};
-  },
+  sprawl: ({albumData}) =>
+    ({albumData}),
 
   query(sprawl) {
     const query = {};
@@ -21,44 +19,52 @@ export default {
           .filter(({commentary}) => commentary)
           .flatMap(({commentary}) => commentary));
 
-    query.wordCounts =
-      entries.map(entries =>
-        accumulateSum(
-          entries,
-          entry => entry.body.split(' ').length));
+    query.bodies =
+      entries.map(entries => entries.map(entry => entry.body));
 
     query.entryCounts =
       entries.map(entries => entries.length);
 
-    filterMultipleArrays(query.albums, query.wordCounts, query.entryCounts,
-      (album, wordCount, entryCount) => entryCount >= 1);
+    filterMultipleArrays(query.albums, query.bodies, query.entryCounts,
+      (album, bodies, entryCount) => entryCount >= 1);
 
     return query;
   },
 
-  relations(relation, query) {
-    return {
-      layout:
-        relation('generatePageLayout'),
+  relations: (relation, query) => ({
+    layout:
+      relation('generatePageLayout'),
 
-      albumLinks:
-        query.albums
-          .map(album => relation('linkAlbumCommentary', album)),
-    };
-  },
+    albumLinks:
+      query.albums
+        .map(album => relation('linkAlbumCommentary', album)),
 
-  data(query) {
-    return {
-      wordCounts: query.wordCounts,
-      entryCounts: query.entryCounts,
+    albumBodies:
+      query.bodies
+        .map(bodies => bodies
+          .map(body => relation('transformContent', body))),
+  }),
 
-      totalWordCount: accumulateSum(query.wordCounts),
-      totalEntryCount: accumulateSum(query.entryCounts),
-    };
-  },
+  data: (query) => ({
+    entryCounts: query.entryCounts,
+    totalEntryCount: accumulateSum(query.entryCounts),
+  }),
 
-  generate: (data, relations, {html, language}) =>
-    language.encapsulate('commentaryIndex', pageCapsule =>
+  generate(data, relations, {html, language}) {
+    const wordCounts =
+      relations.albumBodies.map(bodies =>
+        accumulateSum(bodies, body =>
+          multilingualWordCount(
+            html.resolve(
+              body.slot('mode', 'multiline'),
+              {normalize: 'plain'}))));
+
+    const totalWordCount =
+      accumulateSum(wordCounts);
+
+    const {entryCounts, totalEntryCount} = data;
+
+    return language.encapsulate('commentaryIndex', pageCapsule =>
       relations.layout.slots({
         title: language.$(pageCapsule, 'title'),
 
@@ -69,11 +75,11 @@ export default {
           html.tag('p', language.$(pageCapsule, 'infoLine', {
             words:
               html.tag('b',
-                language.formatWordCount(data.totalWordCount, {unit: true})),
+                language.formatWordCount(totalWordCount, {unit: true})),
 
             entries:
               html.tag('b',
-                  language.countCommentaryEntries(data.totalEntryCount, {unit: true})),
+                language.countCommentaryEntries(totalEntryCount, {unit: true})),
           })),
 
           language.encapsulate(pageCapsule, 'albumList', listCapsule => [
@@ -83,8 +89,8 @@ export default {
             html.tag('ul',
               stitchArrays({
                 albumLink: relations.albumLinks,
-                wordCount: data.wordCounts,
-                entryCount: data.entryCounts,
+                wordCount: wordCounts,
+                entryCount: entryCounts,
               }).map(({albumLink, wordCount, entryCount}) =>
                 html.tag('li',
                   language.$(listCapsule, 'item', {
@@ -100,5 +106,6 @@ export default {
           {auto: 'home'},
           {auto: 'current'},
         ],
-      })),
+      }));
+  },
 };
diff --git a/src/content/dependencies/generateContentContentHeading.js b/src/content/dependencies/generateContentContentHeading.js
new file mode 100644
index 00000000..9ed2d9f0
--- /dev/null
+++ b/src/content/dependencies/generateContentContentHeading.js
@@ -0,0 +1,73 @@
+export default {
+  relations: (relation, _thing) => ({
+    contentHeading:
+      relation('generateContentHeading'),
+  }),
+
+  data: (thing) => ({
+    name:
+      (thing
+        ? thing.name
+        : null),
+  }),
+
+  slots: {
+    attributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+
+    string: {
+      type: 'string',
+    },
+
+    summary: {
+      type: 'boolean',
+      default: false,
+    },
+  },
+
+  generate: (data, relations, slots, {html, language}) =>
+    relations.contentHeading.slots({
+      attributes: slots.attributes,
+
+      title:
+        (() => {
+          if (!slots.string) return html.blank();
+
+          const options = {};
+
+          if (slots.summary) {
+            options.cue =
+              html.tag('span', {class: 'cue'},
+                language.$(slots.string, 'cue'));
+          }
+
+          if (data.name) {
+            options.thing = html.tag('i', data.name);
+          }
+
+          if (slots.summary) {
+            return html.tags([
+              html.tag('span', {class: 'when-open'},
+                language.$(slots.string, options)),
+
+              html.tag('span', {class: 'when-collapsed'},
+                language.$(slots.string, 'collapsed', options)),
+            ]);
+          } else {
+            return language.$(slots.string, options);
+          }
+        })(),
+
+      stickyTitle:
+        (slots.string
+          ? language.$(slots.string, 'sticky')
+          : html.blank()),
+
+      tag:
+        (slots.summary
+          ? 'summary'
+          : 'p'),
+    }),
+};
diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js
index f52bc043..a7cf201f 100644
--- a/src/content/dependencies/generateContentHeading.js
+++ b/src/content/dependencies/generateContentHeading.js
@@ -1,7 +1,5 @@
 export default {
   extraDependencies: ['html'],
-  contentDependencies: ['generateColorStyleAttribute'],
-
   relations: (relation) => ({
     colorStyle: relation('generateColorStyleAttribute'),
   }),
diff --git a/src/content/dependencies/generateContributionList.js b/src/content/dependencies/generateContributionList.js
index d1c3de0f..4f68321f 100644
--- a/src/content/dependencies/generateContributionList.js
+++ b/src/content/dependencies/generateContributionList.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkContribution'],
-  extraDependencies: ['html'],
-
   relations: (relation, contributions) => ({
     contributionLinks:
       contributions
@@ -12,10 +9,14 @@ export default {
     chronologyKind: {type: 'string'},
   },
 
-  generate: (relations, slots, {html}) =>
+  generate: (relations, slots, {html, language}) =>
     html.tag('ul',
       {[html.onlyIfContent]: true},
 
+      relations.contributionLinks.length > 1 &&
+      language.$order('misc.artistLink.withContribution', 0) === 'ARTIST' &&
+        {class: 'offset-tooltips'},
+
       relations.contributionLinks
         .map(contributionLink =>
           html.tag('li',
diff --git a/src/content/dependencies/generateContributionTooltip.js b/src/content/dependencies/generateContributionTooltip.js
index 3a31014d..fd16371b 100644
--- a/src/content/dependencies/generateContributionTooltip.js
+++ b/src/content/dependencies/generateContributionTooltip.js
@@ -1,21 +1,79 @@
-export default {
-  contentDependencies: [
-    'generateContributionTooltipChronologySection',
-    'generateContributionTooltipExternalLinkSection',
-    'generateTooltip',
-  ],
+function compareReleaseContributions(a, b) {
+  if (a === b) {
+    return true;
+  }
+
+  const {previous: aPrev, next: aNext} = getSiblings(a);
+  const {previous: bPrev, next: bNext} = getSiblings(b);
+
+  const effective = contrib =>
+    (contrib?.thing.isAlbum && contrib.thing.style === 'single'
+      ? contrib.thing.tracks[0]
+      : contrib?.thing);
+
+  return (
+    effective(aPrev) === effective(bPrev) &&
+    effective(aNext) === effective(bNext)
+  );
+}
 
-  extraDependencies: ['html'],
+function getSiblings(contribution) {
+  let previous = contribution;
+  while (previous && previous.thing === contribution.thing) {
+    previous = previous.previousBySameArtist;
+  }
 
-  relations: (relation, contribution) => ({
+  let next = contribution;
+  while (next && next.thing === contribution.thing) {
+    next = next.nextBySameArtist;
+  }
+
+  return {previous, next};
+}
+
+export default {
+  query: (contribution) => ({
+    albumArtistContribution:
+      (contribution.thing.isTrack
+        ? contribution.thing.album.artistContribs
+            .find(artistContrib => artistContrib.artist === contribution.artist)
+        : null),
+  }),
+
+  relations: (relation, query, contribution) => ({
     tooltip:
       relation('generateTooltip'),
 
     externalLinkSection:
       relation('generateContributionTooltipExternalLinkSection', contribution),
 
-    chronologySection:
+    ownChronologySection:
       relation('generateContributionTooltipChronologySection', contribution),
+
+    artistReleaseChronologySection:
+      (query.albumArtistContribution
+        ? relation('generateContributionTooltipChronologySection',
+            query.albumArtistContribution)
+        : null),
+  }),
+
+  data: (query, contribution) => ({
+    artistName:
+      contribution.artist.name,
+
+    isAlbumArtistContribution:
+      contribution.thing.isAlbum &&
+      contribution.thingProperty === 'artistContribs',
+
+    isSingleTrackArtistContribution:
+      contribution.thing.isTrack &&
+      contribution.thingProperty === 'artistContribs' &&
+      contribution.thing.album.style === 'single',
+
+    artistReleaseChronologySectionDiffers:
+      (query.albumArtistContribution
+        ? !compareReleaseContributions(contribution, query.albumArtistContribution)
+        : null),
   }),
 
   slots: {
@@ -25,24 +83,64 @@ export default {
     chronologyKind: {type: 'string'},
   },
 
-  generate: (relations, slots, {html}) =>
-    relations.tooltip.slots({
-      attributes:
-        {class: 'contribution-tooltip'},
-
-      contentAttributes: {
-        [html.joinChildren]:
-          html.tag('span', {class: 'tooltip-divider'}),
-      },
-
-      content: [
-        slots.showExternalLinks &&
-          relations.externalLinkSection,
-
-        slots.showChronology &&
-          relations.chronologySection.slots({
-            kind: slots.chronologyKind,
-          }),
-      ],
-    }),
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('misc.artistLink', capsule =>
+      relations.tooltip.slots({
+        attributes:
+          {class: 'contribution-tooltip'},
+
+        contentAttributes: {
+          [html.joinChildren]:
+            html.tag('span', {class: 'tooltip-divider'}),
+        },
+
+        content: [
+          slots.showExternalLinks &&
+            relations.externalLinkSection,
+
+          slots.showChronology &&
+            language.encapsulate(capsule, 'chronology', capsule => {
+              const chronologySections = [];
+
+              if (data.isAlbumArtistContribution) {
+                relations.ownChronologySection.setSlots({
+                  kind: 'release',
+                  heading:
+                    language.$(capsule, 'heading.artistReleases', {
+                      artist: data.artistName,
+                    }),
+                });
+              } else {
+                relations.ownChronologySection.setSlot('kind', slots.chronologyKind);
+              }
+
+              if (
+                data.isSingleTrackArtistContribution &&
+                relations.artistReleaseChronologySection
+              ) {
+                relations.artistReleaseChronologySection.setSlot('kind', 'release');
+
+                relations.artistReleaseChronologySection.setSlot('heading',
+                  language.$(capsule, 'heading.artistReleases', {
+                    artist: data.artistName,
+                  }));
+
+                chronologySections.push(relations.artistReleaseChronologySection);
+
+                if (data.artistReleaseChronologySectionDiffers) {
+                  relations.ownChronologySection.setSlot('heading',
+                    language.$(capsule, 'heading.artistTracks', {
+                      artist: data.artistName,
+                    }));
+
+                  chronologySections.push(relations.ownChronologySection);
+                }
+              } else {
+                chronologySections.push(relations.ownChronologySection);
+              }
+
+              return chronologySections;
+            }),
+        ],
+      })),
 };
diff --git a/src/content/dependencies/generateContributionTooltipChronologySection.js b/src/content/dependencies/generateContributionTooltipChronologySection.js
index 378c0e1c..e4b9bfda 100644
--- a/src/content/dependencies/generateContributionTooltipChronologySection.js
+++ b/src/content/dependencies/generateContributionTooltipChronologySection.js
@@ -1,36 +1,33 @@
-import Thing from '#thing';
-
 function getName(thing) {
   if (!thing) {
     return null;
   }
 
-  const referenceType = thing.constructor[Thing.referenceType];
-
-  if (referenceType === 'artwork') {
+  if (thing.isArtwork) {
     return thing.thing.name;
   }
 
   return thing.name;
 }
 
-export default {
-  contentDependencies: ['linkAnythingMan'],
-  extraDependencies: ['html', 'language'],
+function getSiblings(contribution) {
+  let previous = contribution;
+  while (previous && previous.thing === contribution.thing) {
+    previous = previous.previousBySameArtist;
+  }
 
-  query(contribution) {
-    let previous = contribution;
-    while (previous && previous.thing === contribution.thing) {
-      previous = previous.previousBySameArtist;
-    }
+  let next = contribution;
+  while (next && next.thing === contribution.thing) {
+    next = next.nextBySameArtist;
+  }
 
-    let next = contribution;
-    while (next && next.thing === contribution.thing) {
-      next = next.nextBySameArtist;
-    }
+  return {previous, next};
+}
 
-    return {previous, next};
-  },
+export default {
+  query: (contribution) => ({
+    ...getSiblings(contribution),
+  }),
 
   relations: (relation, query, _contribution) => ({
     previousLink:
@@ -53,23 +50,19 @@ export default {
   }),
 
   slots: {
-    kind: {
-      validate: v =>
-        v.is(
-          'album',
-          'bannerArt',
-          'coverArt',
-          'flash',
-          'track',
-          'trackArt',
-          'trackContribution',
-          'wallpaperArt'),
-    },
+    heading: {type: 'html', mutable: false},
+    kind: {type: 'string'},
   },
 
   generate: (data, relations, slots, {html, language}) =>
     language.encapsulate('misc.artistLink.chronology', capsule =>
       html.tags([
+        html.tag('span', {class: 'chronology-heading'},
+          {[html.onlyIfContent]: true},
+          {[html.onlyIfSiblings]: true},
+
+          slots.heading),
+
         html.tags([
           relations.previousLink?.slots({
             attributes: {class: 'chronology-link'},
diff --git a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js
index 4f9a23ed..210db1e9 100644
--- a/src/content/dependencies/generateContributionTooltipExternalLinkSection.js
+++ b/src/content/dependencies/generateContributionTooltipExternalLinkSection.js
@@ -1,14 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateExternalHandle',
-    'generateExternalIcon',
-    'generateExternalPlatform',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, contribution) => ({
     icons:
       contribution.artist.urls
diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js
index c1a23bbd..616b3c95 100644
--- a/src/content/dependencies/generateCoverArtwork.js
+++ b/src/content/dependencies/generateCoverArtwork.js
@@ -1,15 +1,8 @@
 export default {
-  contentDependencies: [
-    'generateCoverArtworkArtTagDetails',
-    'generateCoverArtworkArtistDetails',
-    'generateCoverArtworkOriginDetails',
-    'generateCoverArtworkReferenceDetails',
-    'image',
-  ],
-
-  extraDependencies: ['html'],
-
   relations: (relation, artwork) => ({
+    colorStyleAttribute:
+      relation('generateColorStyleAttribute'),
+
     image:
       relation('image', artwork),
 
@@ -40,13 +33,17 @@ export default {
 
     dimensions:
       artwork.dimensions,
+
+    style:
+      artwork.style,
   }),
 
   slots: {
     alt: {type: 'string'},
 
     color: {
-      validate: v => v.isColor,
+      validate: v => v.anyOf(v.isBoolean, v.isColor),
+      default: false,
     },
 
     mode: {
@@ -68,10 +65,15 @@ export default {
   generate(data, relations, slots, {html}) {
     const {image} = relations;
 
-    image.setSlots({
-      color: slots.color ?? data.color,
-      alt: slots.alt,
-    });
+    const imgAttributes = html.attributes();
+
+    if (data.style) {
+      imgAttributes.add('style', data.style.split('\n').join(' '));
+    }
+
+    image.setSlot('imgAttributes', imgAttributes);
+
+    image.setSlot('alt', slots.alt);
 
     const square =
       (data.dimensions
@@ -84,6 +86,22 @@ export default {
       image.setSlot('dimensions', data.dimensions);
     }
 
+    const attributes = html.attributes();
+
+    let color = null;
+    if (typeof slots.color === 'boolean') {
+      if (slots.color) {
+        color = data.color;
+      }
+    } else if (slots.color) {
+      color = slots.color;
+    }
+
+    if (color) {
+      relations.colorStyleAttribute.setSlot('color', color);
+      attributes.add(relations.colorStyleAttribute);
+    }
+
     return html.tags([
       data.attachAbove &&
         html.tag('div', {class: 'cover-artwork-joiner'}),
@@ -96,12 +114,38 @@ export default {
         data.attachedArtworkIsMainArtwork &&
           {class: 'attached-artwork-is-main-artwork'},
 
+        attributes,
+
         (slots.mode === 'primary'
           ? [
               relations.image.slots({
                 thumb: 'medium',
                 reveal: true,
                 link: true,
+
+                responsiveThumb: true,
+                responsiveSizes:
+                  // No clamp(), min(), or max() here because Safari.
+                  // The boundaries here are mostly experimental, apart from
+                  // the ones which flat-out switch layouts.
+
+                  // Layout - Thin (phones)
+                  // Most of viewport width
+                  '(max-width: 600px) 90vw,\n' +
+
+                  // Layout - Medium
+                  // Sidebar is hidden; content area is by definition
+                  // most of the viewport
+                  '(max-width: 640px) 220px,\n' +
+                  '(max-width: 800px) 36vw,\n' +
+                  '(max-width: 850px) 280px,\n' +
+
+                  // Layout - Wide
+                  // Sidebar is visible; content area has its own maximum
+                  // Assume the sidebar is at minimum width
+                  '(max-width: 880px) 220px,\n' +
+                  '(max-width: 1050pz) calc(0.40 * (90vw - 150px - 10px)),\n' +
+                  '280px',
               }),
 
               slots.showOriginDetails &&
diff --git a/src/content/dependencies/generateCoverArtworkArtTagDetails.js b/src/content/dependencies/generateCoverArtworkArtTagDetails.js
index 4d908665..50571a4f 100644
--- a/src/content/dependencies/generateCoverArtworkArtTagDetails.js
+++ b/src/content/dependencies/generateCoverArtworkArtTagDetails.js
@@ -5,9 +5,6 @@ function linkable(tag) {
 }
 
 export default {
-  contentDependencies: ['linkArtTagGallery'],
-  extraDependencies: ['html', 'language'],
-
   query: (artwork) => ({
     linkableArtTags:
       artwork.artTags.filter(linkable),
diff --git a/src/content/dependencies/generateCoverArtworkArtistDetails.js b/src/content/dependencies/generateCoverArtworkArtistDetails.js
index 3ead80ab..2773c6fc 100644
--- a/src/content/dependencies/generateCoverArtworkArtistDetails.js
+++ b/src/content/dependencies/generateCoverArtworkArtistDetails.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkArtistGallery'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, artwork) => ({
     artistLinks:
       artwork.artistContribs
diff --git a/src/content/dependencies/generateCoverArtworkOriginDetails.js b/src/content/dependencies/generateCoverArtworkOriginDetails.js
index 3908414f..e489eea6 100644
--- a/src/content/dependencies/generateCoverArtworkOriginDetails.js
+++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js
@@ -1,19 +1,5 @@
-import Thing from '#thing';
-
 export default {
-  contentDependencies: [
-    'generateArtistCredit',
-    'generateAbsoluteDatetimestamp',
-    'linkAlbum',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'pagePath'],
-
   query: (artwork) => ({
-    artworkThingType:
-      artwork.thing.constructor[Thing.referenceType],
-
     attachedArtistContribs:
       (artwork.attachedArtwork
         ? artwork.attachedArtwork.artistContribs
@@ -29,15 +15,18 @@ export default {
     source:
       relation('transformContent', artwork.source),
 
+    originDetails:
+      relation('transformContent', artwork.originDetails),
+
     albumLink:
-      (query.artworkThingType === 'album'
+      (artwork.thing.isAlbum
         ? relation('linkAlbum', artwork.thing)
         : null),
 
     datetimestamp:
-      (artwork.date && artwork.date !== artwork.thing.date
-        ? relation('generateAbsoluteDatetimestamp', artwork.date)
-        : null),
+      relation('generateAbsoluteDatetimestamp',
+        artwork.date,
+        artwork.thing.date),
   }),
 
 
@@ -45,23 +34,26 @@ export default {
     label:
       artwork.label,
 
-    artworkThingType:
-      query.artworkThingType,
+    forAlbum:
+      artwork.thing.isAlbum,
+
+    forSingleStyleAlbum:
+      artwork.thing.isAlbum &&
+      artwork.thing.style === 'single',
+
+    showFilename:
+      artwork.showFilename,
   }),
 
   generate: (data, relations, {html, language, pagePath}) =>
     language.encapsulate('misc.coverArtwork', capsule =>
       html.tag('p', {class: 'image-details'},
         {[html.onlyIfContent]: true},
-        {[html.joinChildren]: html.tag('br')},
 
         {class: 'origin-details'},
 
         (() => {
-          relations.datetimestamp?.setSlots({
-            style: 'year',
-            tooltip: true,
-          });
+          relations.datetimestamp.setSlot('style', 'year-difference');
 
           const artworkBy =
             language.encapsulate(capsule, 'artworkBy', workingCapsule => {
@@ -72,7 +64,7 @@ export default {
                 workingOptions.label = data.label;
               }
 
-              if (relations.datetimestamp) {
+              if (!html.isBlank(relations.datetimestamp)) {
                 workingCapsule += '.withYear';
                 workingOptions.year = relations.datetimestamp;
               }
@@ -94,7 +86,8 @@ export default {
 
           const trackArtFromAlbum =
             pagePath[0] === 'track' &&
-            data.artworkThingType === 'album' &&
+            data.forAlbum &&
+            !data.forSingleStyleAlbum &&
               language.$(capsule, 'trackArtFromAlbum', {
                 album:
                   relations.albumLink.slot('color', false),
@@ -112,7 +105,7 @@ export default {
                 workingOptions.label = data.label;
               }
 
-              if (html.isBlank(artworkBy) && relations.datetimestamp) {
+              if (html.isBlank(artworkBy) && !html.isBlank(relations.datetimestamp)) {
                 workingCapsule += '.withYear';
                 workingOptions.year = relations.datetimestamp;
               }
@@ -129,7 +122,7 @@ export default {
                 label: data.label,
               };
 
-              if (relations.datetimestamp) {
+              if (!html.isBlank(relations.datetimestamp)) {
                 workingCapsule += '.withYear';
                 workingOptions.year = relations.datetimestamp;
               }
@@ -146,12 +139,35 @@ export default {
               year: relations.datetimestamp,
             });
 
+          const originDetailsLine =
+            html.tag('span', {class: 'origin-details-line'},
+              {[html.onlyIfContent]: true},
+
+              relations.originDetails.slots({
+                mode: 'inline',
+                absorbPunctuationFollowingExternalLinks: false,
+              }));
+
+          const filenameLine =
+            html.tag('span', {class: 'filename-line'},
+              {[html.onlyIfContent]: true},
+
+              html.tag('code', {class: 'filename'},
+                {[html.onlyIfContent]: true},
+
+                language.sanitize(data.showFilename)));
+
           return [
-            artworkBy,
-            trackArtFromAlbum,
-            source,
-            label,
-            year,
+            html.tags([
+              artworkBy,
+              trackArtFromAlbum,
+              source,
+              label,
+              year,
+            ], {[html.joinChildren]: html.tag('br')}),
+
+            originDetailsLine,
+            filenameLine,
           ];
         })())),
 };
diff --git a/src/content/dependencies/generateCoverArtworkReferenceDetails.js b/src/content/dependencies/generateCoverArtworkReferenceDetails.js
index 035ab586..d4e4e7e4 100644
--- a/src/content/dependencies/generateCoverArtworkReferenceDetails.js
+++ b/src/content/dependencies/generateCoverArtworkReferenceDetails.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkReferencedArtworks', 'linkReferencingArtworks'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, artwork) => ({
     referencedArtworksLink:
       relation('linkReferencedArtworks', artwork),
diff --git a/src/content/dependencies/generateCoverCarousel.js b/src/content/dependencies/generateCoverCarousel.js
index 0705d93e..1ffeff8e 100644
--- a/src/content/dependencies/generateCoverCarousel.js
+++ b/src/content/dependencies/generateCoverCarousel.js
@@ -2,8 +2,6 @@ import {empty, repeat, stitchArrays} from '#sugar';
 import {getCarouselLayoutForNumberOfItems} from '#wiki-data';
 
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     images: {validate: v => v.strictArrayOf(v.isHTML)},
     links: {validate: v => v.strictArrayOf(v.isHTML)},
diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js
index e4dfd905..091833a9 100644
--- a/src/content/dependencies/generateCoverGrid.js
+++ b/src/content/dependencies/generateCoverGrid.js
@@ -1,20 +1,22 @@
-import {stitchArrays} from '#sugar';
+import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: ['generateGridActionLinks'],
-  extraDependencies: ['html', 'language'],
+  relations: (relation) => ({
+    actionLinks:
+      relation('generateGridActionLinks'),
 
-  relations(relation) {
-    return {
-      actionLinks: relation('generateGridActionLinks'),
-    };
-  },
+    expando:
+      relation('generateGridExpando'),
+  }),
 
   slots: {
+    attributes: {type: 'attributes', mutable: false},
+
     images: {validate: v => v.strictArrayOf(v.isHTML)},
     links: {validate: v => v.strictArrayOf(v.isHTML)},
     names: {validate: v => v.strictArrayOf(v.isHTML)},
     info: {validate: v => v.strictArrayOf(v.isHTML)},
+    tab: {validate: v => v.strictArrayOf(v.isHTML)},
     notFromThisGroup: {validate: v => v.strictArrayOf(v.isBoolean)},
 
     // Differentiating from sparseArrayOf here - this list of classes should
@@ -30,45 +32,115 @@ export default {
               v.isString))),
     },
 
+    itemAttributes: {
+      validate: v =>
+        v.strictArrayOf(
+          v.optional(v.isAttributes)),
+    },
+
     lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)},
     actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)},
+
+    revealAllWarnings: {
+      validate: v => v.looseArrayOf(v.isString),
+    },
+
+    bottomCaption: {
+      type: 'html',
+      mutable: false,
+    },
+
+    cutIndex: {validate: v => v.isWholeNumber},
   },
 
   generate: (relations, slots, {html, language}) =>
     html.tag('div', {class: 'grid-listing'},
+      slots.attributes,
       {[html.onlyIfContent]: true},
 
       [
+        !empty((slots.revealAllWarnings ?? []).filter(Boolean)) &&
+          language.encapsulate('misc.coverGrid.revealAll', capsule =>
+            html.tag('div', {class: 'reveal-all-container'},
+              ((slots.tab ?? [])
+                .slice(0, 4)
+                .some(tab => tab && !html.isBlank(tab))) &&
+
+                {class: 'has-nearby-tab'},
+
+              html.tag('p', {class: 'reveal-all'}, [
+                html.tag('a', {href: '#'}, [
+                  html.tag('span', {class: 'reveal-label'},
+                    language.$(capsule, 'reveal')),
+
+                  html.tag('span', {class: 'conceal-label'},
+                    {style: 'display: none'},
+                    language.$(capsule, 'conceal')),
+                ]),
+
+                html.tag('br'),
+
+                html.tag('span', {class: 'warnings'},
+                  language.$(capsule, 'warnings', {
+                    warnings:
+                      language.formatUnitList(
+                        unique(slots.revealAllWarnings.filter(Boolean))
+                          .sort()
+                          .map(warning => html.tag('b', warning))),
+                  })),
+              ]))),
+
         stitchArrays({
           classes: slots.classes,
+          attributes: slots.itemAttributes,
           image: slots.images,
           link: slots.links,
           name: slots.names,
           info: slots.info,
+          tab: slots.tab,
 
           notFromThisGroup:
             slots.notFromThisGroup ??
             Array.from(slots.links).fill(null)
         }).map(({
             classes,
+            attributes,
             image,
             link,
             name,
             info,
+            tab,
             notFromThisGroup,
           }, index) =>
             link.slots({
               attributes: [
+                link.getSlotValue('attributes'),
+
                 {class: ['grid-item', 'box']},
 
+                tab &&
+                !html.isBlank(tab) &&
+                  {class: 'has-tab'},
+
+                attributes,
+
                 (classes
                   ? {class: classes}
                   : null),
+
+                slots.cutIndex >= 1 &&
+                index >= slots.cutIndex &&
+                  {class: 'hidden-by-expandable-cut'},
               ],
 
               colorContext: 'image-box',
 
               content: [
+                html.tag('span',
+                  {[html.onlyIfContent]: true},
+
+                  tab),
+
                 image.slots({
                   thumb: 'medium',
                   square: true,
@@ -106,5 +178,17 @@ export default {
 
         relations.actionLinks
           .slot('actionLinks', slots.actionLinks),
+
+        (slots.cutIndex >= 1 &&
+         slots.cutIndex < slots.links.length
+          ? relations.expando.slots({
+              caption: slots.bottomCaption,
+            })
+
+       : !html.isBlank(relations.bottomCaption)
+          ? html.tag('p', {class: 'grid-caption'},
+              slots.caption)
+
+          : html.blank()),
       ]),
 };
diff --git a/src/content/dependencies/generateDatetimestampTemplate.js b/src/content/dependencies/generateDatetimestampTemplate.js
index a92d15fc..56b2e595 100644
--- a/src/content/dependencies/generateDatetimestampTemplate.js
+++ b/src/content/dependencies/generateDatetimestampTemplate.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateTextWithTooltip'],
-  extraDependencies: ['html'],
-
   relations: (relation) => ({
     textWithTooltip:
       relation('generateTextWithTooltip'),
diff --git a/src/content/dependencies/generateDotSwitcherTemplate.js b/src/content/dependencies/generateDotSwitcherTemplate.js
index 22205922..561a44bc 100644
--- a/src/content/dependencies/generateDotSwitcherTemplate.js
+++ b/src/content/dependencies/generateDotSwitcherTemplate.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     attributes: {
       type: 'attributes',
diff --git a/src/content/dependencies/generateExpandableGallerySection.js b/src/content/dependencies/generateExpandableGallerySection.js
deleted file mode 100644
index 122ca4b1..00000000
--- a/src/content/dependencies/generateExpandableGallerySection.js
+++ /dev/null
@@ -1,92 +0,0 @@
-export default {
-  contentDependencies: ['generateContentHeading'],
-  extraDependencies: ['html', 'language'],
-
-  relations: (relation) => ({
-    contentHeading:
-      relation('generateContentHeading'),
-  }),
-
-  slots: {
-    title: {
-      type: 'html',
-      mutable: false,
-    },
-
-    contentAboveCut: {
-      type: 'html',
-      mutable: false,
-    },
-
-    contentBelowCut: {
-      type: 'html',
-      mutable: false,
-    },
-
-    caption: {
-      type: 'html',
-      mutable: false,
-    },
-
-    expandCue: {
-      type: 'html',
-      mutable: false,
-    },
-
-    collapseCue: {
-      type: 'html',
-      mutable: false,
-    },
-  },
-
-  generate: (relations, slots, {html, language}) =>
-    html.tag('section', {class: 'expandable-gallery-section'}, [
-      relations.contentHeading.slots({
-        tag: 'h2',
-        title: slots.title,
-      }),
-
-      html.tag('div', {class: 'section-content-above-cut'},
-        {[html.onlyIfContent]: true},
-
-        slots.contentAboveCut),
-
-      html.tag('div', {class: 'section-content-below-cut'},
-        {[html.onlyIfContent]: true},
-
-        !html.isBlank(slots.contentBelowCut) &&
-          {style: 'display: none'},
-
-        slots.contentBelowCut),
-
-      html.tag('div', {class: 'section-expando'},
-        {[html.onlyIfSiblings]: true},
-
-        html.tag('div', {class: 'section-expando-content'},
-          {[html.joinChildren]: html.tag('br')},
-
-          [
-            html.tag('span', {class: 'section-caption'},
-              slots.caption),
-
-            !html.isBlank(slots.contentBelowCut) &&
-              language.$('misc.coverGrid.expandCollapseCue', {
-                cue:
-                  html.tag('a', {class: 'section-expando-toggle'},
-                    {href: '#'},
-
-                    {[html.joinChildren]: ''},
-                    {[html.noEdgeWhitespace]: true},
-
-                    [
-                      html.tag('span', {class: 'section-expand-cue'},
-                        slots.expandCue),
-
-                      html.tag('span', {class: 'section-collapse-cue'},
-                        {style: 'display: none'},
-                        slots.collapseCue),
-                    ]),
-              }),
-          ])),
-    ]),
-};
diff --git a/src/content/dependencies/generateExternalHandle.js b/src/content/dependencies/generateExternalHandle.js
index 8c0368a4..8653b177 100644
--- a/src/content/dependencies/generateExternalHandle.js
+++ b/src/content/dependencies/generateExternalHandle.js
@@ -1,8 +1,6 @@
 import {isExternalLinkContext} from '#external-links';
 
 export default {
-  extraDependencies: ['html', 'language'],
-
   data: (url) => ({url}),
 
   slots: {
diff --git a/src/content/dependencies/generateExternalIcon.js b/src/content/dependencies/generateExternalIcon.js
index 637af658..03af643e 100644
--- a/src/content/dependencies/generateExternalIcon.js
+++ b/src/content/dependencies/generateExternalIcon.js
@@ -1,8 +1,6 @@
 import {isExternalLinkContext} from '#external-links';
 
 export default {
-  extraDependencies: ['html', 'language', 'to'],
-
   data: (url) => ({url}),
 
   slots: {
diff --git a/src/content/dependencies/generateExternalPlatform.js b/src/content/dependencies/generateExternalPlatform.js
index c4f63ecf..b2822d64 100644
--- a/src/content/dependencies/generateExternalPlatform.js
+++ b/src/content/dependencies/generateExternalPlatform.js
@@ -1,8 +1,6 @@
 import {isExternalLinkContext} from '#external-links';
 
 export default {
-  extraDependencies: ['html', 'language'],
-
   data: (url) => ({url}),
 
   slots: {
diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js
index 84ab549d..896ee224 100644
--- a/src/content/dependencies/generateFlashActGalleryPage.js
+++ b/src/content/dependencies/generateFlashActGalleryPage.js
@@ -1,19 +1,6 @@
 import striptags from 'striptags';
 
 export default {
-  contentDependencies: [
-    'generateCoverGrid',
-    'generateFlashActNavAccent',
-    'generateFlashActSidebar',
-    'generatePageLayout',
-    'image',
-    'linkFlash',
-    'linkFlashAct',
-    'linkFlashIndex',
-  ],
-
-  extraDependencies: ['language'],
-
   relations: (relation, act) => ({
     layout:
       relation('generatePageLayout'),
diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js
index c4ec77b8..7ad46051 100644
--- a/src/content/dependencies/generateFlashActNavAccent.js
+++ b/src/content/dependencies/generateFlashActNavAccent.js
@@ -1,15 +1,6 @@
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'generateNextLink',
-    'generatePreviousLink',
-    'linkFlashAct',
-  ],
-
-  extraDependencies: ['wikiData'],
-
   sprawl: ({flashActData}) =>
     ({flashActData}),
 
diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js
index 1421dde9..0d952077 100644
--- a/src/content/dependencies/generateFlashActSidebar.js
+++ b/src/content/dependencies/generateFlashActSidebar.js
@@ -1,10 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateFlashActSidebarCurrentActBox',
-    'generateFlashActSidebarSideMapBox',
-    'generatePageSidebar',
-  ],
-
   relations: (relation, act, flash) => ({
     sidebar:
       relation('generatePageSidebar'),
diff --git a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js
index 6d152c7c..e08582fe 100644
--- a/src/content/dependencies/generateFlashActSidebarCurrentActBox.js
+++ b/src/content/dependencies/generateFlashActSidebarCurrentActBox.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generatePageSidebarBox',
-    'linkFlash',
-    'linkFlashAct',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, act, _flash) => ({
     box:
       relation('generatePageSidebarBox'),
diff --git a/src/content/dependencies/generateFlashActSidebarSideMapBox.js b/src/content/dependencies/generateFlashActSidebarSideMapBox.js
index 7b26ef31..4b97f21d 100644
--- a/src/content/dependencies/generateFlashActSidebarSideMapBox.js
+++ b/src/content/dependencies/generateFlashActSidebarSideMapBox.js
@@ -1,15 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generatePageSidebarBox',
-    'linkFlashAct',
-    'linkFlashIndex',
-  ],
-
-  extraDependencies: ['html', 'wikiData'],
-
   sprawl: ({flashSideData}) => ({flashSideData}),
 
   relations: (relation, sprawl, _act, _flash) => ({
diff --git a/src/content/dependencies/generateFlashArtworkColumn.js b/src/content/dependencies/generateFlashArtworkColumn.js
index 5987df9e..207c3bf3 100644
--- a/src/content/dependencies/generateFlashArtworkColumn.js
+++ b/src/content/dependencies/generateFlashArtworkColumn.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generateCoverArtwork'],
-
   relations: (relation, flash) => ({
     coverArtwork:
       relation('generateCoverArtwork', flash.coverArtwork),
diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js
index 2788406c..1fb286c6 100644
--- a/src/content/dependencies/generateFlashIndexPage.js
+++ b/src/content/dependencies/generateFlashIndexPage.js
@@ -1,17 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateCoverGrid',
-    'generatePageLayout',
-    'image',
-    'linkFlash',
-    'linkFlashAct',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({flashActData}) => ({flashActData}),
 
   query(sprawl) {
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index 095e43c4..935ffdc6 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -1,22 +1,19 @@
 import {empty} from '#sugar';
 
-export default {
-  contentDependencies: [
-    'generateAdditionalNamesBox',
-    'generateCommentaryEntry',
-    'generateContentHeading',
-    'generateContributionList',
-    'generateFlashActSidebar',
-    'generateFlashArtworkColumn',
-    'generateFlashNavAccent',
-    'generatePageLayout',
-    'generateTrackList',
-    'linkExternal',
-    'linkFlashAct',
-  ],
-
-  extraDependencies: ['html', 'language'],
+function checkInterrupted(which, relations, {html}) {
+  if (
+    !html.isBlank(relations.contributorContributionList) ||
+    !html.isBlank(relations.featuredTracksList)
+  ) return true;
+
+  if (which === 'crediting-sources') {
+    if (!html.isBlank(relations.artistCommentaryEntries)) return true;
+  }
+
+  return false;
+}
 
+export default {
   query(flash) {
     const query = {};
 
@@ -53,6 +50,12 @@ export default {
     contentHeading:
       relation('generateContentHeading'),
 
+    commentaryContentHeading:
+      relation('generateCommentaryContentHeading', flash),
+
+    readCommentaryLine:
+      relation('generateReadCommentaryLine', flash),
+
     flashActLink:
       relation('linkFlashAct', flash.act),
 
@@ -60,7 +63,7 @@ export default {
       relation('generateFlashNavAccent', flash),
 
     featuredTracksList:
-      relation('generateTrackList', flash.featuredTracks),
+      relation('generateTrackList', flash.featuredTracks, []),
 
     contributorContributionList:
       relation('generateContributionList', flash.contributorContribs),
@@ -69,9 +72,10 @@ export default {
       flash.commentary
         .map(entry => relation('generateCommentaryEntry', entry)),
 
-    creditSourceEntries:
-      flash.commentary
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    creditingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        flash.creditingSources,
+        flash),
   }),
 
   data: (_query, flash) => ({
@@ -123,21 +127,16 @@ export default {
             {[html.joinChildren]: html.tag('br')},
 
             language.encapsulate('releaseInfo', capsule => [
-              !html.isBlank(relations.artistCommentaryEntries) &&
-                language.encapsulate(capsule, 'readCommentary', capsule =>
-                  language.$(capsule, {
-                    link:
-                      html.tag('a',
-                        {href: '#artist-commentary'},
-                        language.$(capsule, 'link')),
-                  })),
+              checkInterrupted('commentary', relations, {html}) &&
+                relations.readCommentaryLine,
 
-              !html.isBlank(relations.creditSourceEntries) &&
-                language.encapsulate(capsule, 'readCreditSources', capsule =>
+              checkInterrupted('crediting-sources', relations, {html}) &&
+              !html.isBlank(relations.creditingSourcesSection) &&
+                language.encapsulate(capsule, 'readCreditingSources', capsule =>
                   language.$(capsule, {
                     link:
                       html.tag('a',
-                        {href: '#credit-sources'},
+                        {href: '#crediting-sources'},
                         language.$(capsule, 'link')),
                   })),
             ])),
@@ -168,24 +167,14 @@ export default {
           ]),
 
           html.tags([
-            relations.contentHeading.clone()
-              .slots({
-                attributes: {id: 'artist-commentary'},
-                title: language.$('misc.artistCommentary'),
-              }),
-
+            relations.commentaryContentHeading,
             relations.artistCommentaryEntries,
           ]),
 
-          html.tags([
-            relations.contentHeading.clone()
-              .slots({
-                attributes: {id: 'credit-sources'},
-                title: language.$('misc.creditSources'),
-              }),
-
-            relations.creditSourceEntries,
-          ]),
+          relations.creditingSourcesSection.slots({
+            id: 'crediting-sources',
+            string: 'misc.creditingSources',
+          }),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js
index 0f5d2d6b..db9d3c1e 100644
--- a/src/content/dependencies/generateFlashNavAccent.js
+++ b/src/content/dependencies/generateFlashNavAccent.js
@@ -1,15 +1,6 @@
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'generateNextLink',
-    'generatePreviousLink',
-    'linkFlash',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({flashActData}) =>
     ({flashActData}),
 
diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js
index dfd83aef..efa1972a 100644
--- a/src/content/dependencies/generateFooterLocalizationLinks.js
+++ b/src/content/dependencies/generateFooterLocalizationLinks.js
@@ -2,15 +2,6 @@ import {sortByName} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  extraDependencies: [
-    'defaultLanguage',
-    'html',
-    'language',
-    'languages',
-    'pagePath',
-    'to',
-  ],
-
   generate({
     defaultLanguage,
     html,
diff --git a/src/content/dependencies/generateGridActionLinks.js b/src/content/dependencies/generateGridActionLinks.js
index 585a02b9..5b3f9c1e 100644
--- a/src/content/dependencies/generateGridActionLinks.js
+++ b/src/content/dependencies/generateGridActionLinks.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)},
   },
diff --git a/src/content/dependencies/generateGridExpando.js b/src/content/dependencies/generateGridExpando.js
new file mode 100644
index 00000000..5a0cbce5
--- /dev/null
+++ b/src/content/dependencies/generateGridExpando.js
@@ -0,0 +1,37 @@
+export default {
+  slots: {
+    caption: {type: 'html', mutable: false},
+  },
+
+  generate: (slots, {html, language}) =>
+    language.encapsulate('misc.coverGrid', capsule =>
+      html.tag('div', {class: 'grid-expando'},
+        {[html.onlyIfSiblings]: true},
+
+        html.tag('p', {class: 'grid-expando-content'},
+          {[html.joinChildren]: html.tag('br')},
+
+          [
+            html.tag('span', {class: 'grid-caption'},
+              slots.caption),
+
+            !html.isBlank(slots.contentBelowCut) &&
+              language.$(capsule, 'expandCollapseCue', {
+                cue:
+                  html.tag('a', {class: 'grid-expando-toggle'},
+                    {href: '#'},
+
+                    {[html.joinChildren]: ''},
+                    {[html.noEdgeWhitespace]: true},
+
+                    [
+                      html.tag('span', {class: 'grid-expand-cue'},
+                        language.$(capsule, 'expand')),
+
+                      html.tag('span', {class: 'grid-collapse-cue'},
+                        {style: 'display: none'},
+                        language.$(capsule, 'collapse')),
+                    ]),
+              }),
+          ]))),
+};
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index dfdad0e8..e378f8a2 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -2,22 +2,6 @@ import {sortChronologically} from '#sort';
 import {filterItemsForCarousel, getTotalDuration} from '#wiki-data';
 
 export default {
-  contentDependencies: [
-    'generateCoverCarousel',
-    'generateGroupGalleryPageAlbumsByDateView',
-    'generateGroupGalleryPageAlbumsBySeriesView',
-    'generateGroupNavLinks',
-    'generateGroupSecondaryNav',
-    'generateIntrapageDotSwitcher',
-    'generatePageLayout',
-    'generateQuickDescription',
-    'image',
-    'linkAlbum',
-    'linkListing',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) =>
     ({enableGroupUI: wikiInfo.enableGroupUI}),
 
@@ -183,7 +167,10 @@ export default {
                 }))),
           */
 
-          relations.albumsByDateView,
+          relations.albumsByDateView.slots({
+            showTitle:
+              !html.isBlank(relations.albumsBySeriesView),
+          }),
 
           relations.albumsBySeriesView.slots({
             attributes: [
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js
index 7d9aa2d2..37c1951d 100644
--- a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js
@@ -2,34 +2,51 @@ import {stitchArrays} from '#sugar';
 import {getTotalDuration} from '#wiki-data';
 
 export default {
-  contentDependencies: ['generateCoverGrid', 'image', 'linkAlbum'],
-  extraDependencies: ['language'],
+  query: (albums, _group) => ({
+    artworks:
+      albums.map(album =>
+        (album.hasCoverArt
+          ? album.coverArtworks[0]
+          : null)),
+  }),
 
-  relations: (relation, albums, _group) => ({
+  relations: (relation, query, albums, group) => ({
     coverGrid:
       relation('generateCoverGrid'),
 
     links:
-      albums.map(album =>
-        relation('linkAlbum', album)),
+      albums
+        .map(album => relation('linkAlbum', album)),
 
     images:
-      albums.map(album =>
-        (album.hasCoverArt
-          ? relation('image', album.coverArtworks[0])
-          : relation('image')))
+      query.artworks
+        .map(artwork => relation('image', artwork)),
+
+    tabs:
+      albums
+        .map(album =>
+          relation('generateGroupGalleryPageAlbumGridTab', album, group)),
   }),
 
-  data: (albums, group) => ({
+  data: (query, albums, group) => ({
     names:
       albums.map(album => album.name),
 
-    durations:
-      albums.map(album => getTotalDuration(album.tracks)),
+    styles:
+      albums.map(album => album.style),
 
     tracks:
       albums.map(album => album.tracks.length),
 
+    allWarnings:
+      query.artworks.flatMap(artwork => artwork?.contentWarnings),
+
+    durations:
+      albums.map(album =>
+        (album.hideDuration
+          ? null
+          : getTotalDuration(album.tracks))),
+
     notFromThisGroup:
       albums.map(album => !album.groups.includes(group)),
   }),
@@ -53,14 +70,28 @@ export default {
                   }),
               })),
 
+        itemAttributes:
+          data.styles.map(style => ({'data-style': style})),
+
+        tab: relations.tabs,
+
         info:
           stitchArrays({
+            style: data.styles,
             tracks: data.tracks,
             duration: data.durations,
-          }).map(({tracks, duration}) =>
-              language.$(capsule, 'details.albumLength', {
-                tracks: language.countTracks(tracks, {unit: true}),
-                time: language.formatDuration(duration),
-              })),
+          }).map(({style, tracks, duration}) =>
+              (style === 'single' && duration
+                ? language.$(capsule, 'details.albumLength.single', {
+                    time: language.formatDuration(duration),
+                  })
+             : duration
+                ? language.$(capsule, 'details.albumLength', {
+                    tracks: language.countTracks(tracks, {unit: true}),
+                    time: language.formatDuration(duration),
+                  })
+                : null)),
+
+        revealAllWarnings: data.allWarnings,
       })),
 };
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js b/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js
new file mode 100644
index 00000000..c3b860e4
--- /dev/null
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumGridTab.js
@@ -0,0 +1,79 @@
+import {empty} from '#sugar';
+
+export default {
+  query(album, group) {
+    if (album.groups.length > 1) {
+      const contextGroup = group;
+
+      const candidateGroupCategory =
+        album.groups
+          .filter(group => !group.excludeFromGalleryTabs)
+          .find(group => group.category !== contextGroup.category)
+          ?.category ??
+        null;
+
+      const candidateGroups =
+        album.groups
+          .filter(group => !group.excludeFromGalleryTabs)
+          .filter(group => group.category === candidateGroupCategory);
+
+      if (!empty(candidateGroups)) {
+        return {
+          mode: 'groups',
+          notedGroups: candidateGroups,
+        };
+      }
+    }
+
+    if (!empty(album.artistContribs)) {
+      if (
+        album.artistContribs.length === 1 &&
+        !empty(group.closelyLinkedArtists) &&
+        (album.artistContribs[0].artist.name ===
+         group.closelyLinkedArtists[0].artist.name)
+      ) {
+        return {mode: null};
+      }
+
+      return {
+        mode: 'artists',
+        notedArtistContribs: album.artistContribs,
+      };
+    }
+
+    return {mode: null};;
+  },
+
+  relations: (relation, query, _album, _group) => ({
+    artistCredit:
+      (query.mode === 'artists'
+        ? relation('generateArtistCredit', query.notedArtistContribs, [])
+        : null),
+  }),
+
+  data: (query, _album, _group) => ({
+    mode: query.mode,
+
+    groupNames:
+      (query.mode === 'groups'
+        ? query.notedGroups.map(group => group.name)
+        : null),
+  }),
+
+  generate: (data, relations, {language}) =>
+    language.encapsulate('misc.coverGrid.tab', capsule =>
+      (data.mode === 'groups'
+        ? language.$(capsule, 'groups', {
+            groups:
+              language.formatUnitList(data.groupNames),
+          })
+     : data.mode === 'artists'
+        ? relations.artistCredit.slots({
+            normalStringKey:
+              capsule + '.artists',
+
+            normalFeaturingStringKey:
+              capsule + '.artists.featuring',
+          })
+        : null)),
+};
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js
index b7d01eb5..75ef1048 100644
--- a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js
@@ -1,15 +1,17 @@
 import {sortChronologically} from '#sort';
 
 export default {
-  contentDependencies: ['generateGroupGalleryPageAlbumGrid'],
-  extraDependencies: ['html', 'language'],
-
   query: (group) => ({
     albums:
-      sortChronologically(group.albums, {latestFirst: true}),
+      sortChronologically(group.albums.slice(), {latestFirst: true}),
   }),
 
   relations: (relation, query, group) => ({
+    styleSelector:
+      (group.divideAlbumsByStyle
+        ? relation('generateGroupGalleryPageStyleSelector', group)
+        : null),
+
     albumGrid:
       relation('generateGroupGalleryPageAlbumGrid',
         query.albums,
@@ -17,6 +19,10 @@ export default {
   }),
 
   slots: {
+    showTitle: {
+      type: 'boolean',
+    },
+
     attributes: {
       type: 'attributes',
       mutable: false,
@@ -31,8 +37,11 @@ export default {
         {[html.onlyIfContent]: true},
 
         html.tag('section', [
-          html.tag('h2',
-            language.$(capsule, 'title')),
+          slots.showTitle &&
+            html.tag('h2',
+              language.$(capsule, 'title')),
+
+          relations.styleSelector,
 
           relations.albumGrid,
         ]))),
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js
index 0337275f..68cf249f 100644
--- a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateGroupGalleryPageSeriesSection'],
-  extraDependencies: ['html'],
-
   relations: (relation, group) => ({
     seriesSections:
       group.serieses
diff --git a/src/content/dependencies/generateGroupGalleryPageSeriesSection.js b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js
index 2ccead5d..1aa835d6 100644
--- a/src/content/dependencies/generateGroupGalleryPageSeriesSection.js
+++ b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js
@@ -1,22 +1,11 @@
 import {sortChronologically} from '#sort';
 
 export default {
-  contentDependencies: [
-    'generateExpandableGallerySection',
-    'generateGroupGalleryPageAlbumGrid',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query(series) {
     const query = {};
 
-    // Includes undated albums.
-    const albumsLatestFirst =
-      sortChronologically(series.albums, {latestFirst: true});
-
-    query.albumsAboveCut = albumsLatestFirst.slice(0, 4);
-    query.albumsBelowCut = albumsLatestFirst.slice(4);
+    query.albums =
+      sortChronologically(series.albums.slice(), {latestFirst: true});
 
     query.allAlbumsDated =
       series.albums.every(album => album.date);
@@ -25,13 +14,13 @@ export default {
       series.albums.some(album => !album.groups.includes(series.group));
 
     query.latestAlbum =
-      albumsLatestFirst
+      query.albums
         .filter(album => album.date)
         .at(0) ??
       null;
 
     query.earliestAlbum =
-      albumsLatestFirst
+      query.albums
         .filter(album => album.date)
         .at(-1) ??
       null;
@@ -40,17 +29,12 @@ export default {
   },
 
   relations: (relation, query, series) => ({
-    gallerySection:
-      relation('generateExpandableGallerySection'),
-
-    gridAboveCut:
-      relation('generateGroupGalleryPageAlbumGrid',
-        query.albumsAboveCut,
-        series.group),
+    contentHeading:
+      relation('generateContentHeading'),
 
-    gridBelowCut:
+    grid:
       relation('generateGroupGalleryPageAlbumGrid',
-        query.albumsBelowCut,
+        query.albums,
         series.group),
   }),
 
@@ -88,69 +72,67 @@ export default {
 
   generate: (data, relations, {html, language}) =>
     language.encapsulate('groupGalleryPage.albumSection', capsule =>
-      relations.gallerySection.slots({
-        title: data.name,
-
-        contentAboveCut: relations.gridAboveCut,
-        contentBelowCut: relations.gridBelowCut,
-
-        caption:
-          language.encapsulate(capsule, 'caption', captionCapsule =>
-            html.tags([
-              data.anyAlbumNotFromThisGroup &&
-                language.$(captionCapsule, 'seriesAlbumsNotFromGroup', {
-                  marker:
-                    language.$('misc.coverGrid.details.notFromThisGroup.marker'),
-
-                  series:
-                    html.tag('i', data.name),
-
-                  group: data.groupName,
-                }),
-
-              language.encapsulate(captionCapsule, workingCapsule => {
-                const workingOptions = {};
-
-                workingOptions.tracks =
-                  html.tag('b',
-                    language.countTracks(data.tracks, {unit: true}));
-
-                workingOptions.albums =
-                  html.tag('b',
-                    language.countAlbums(data.albums, {unit: true}));
-
-                if (data.allAlbumsDated) {
-                  const earliestDate = data.earliestAlbumDate;
-                  const latestDate = data.latestAlbumDate;
-
-                  const earliestYear = earliestDate.getFullYear();
-                  const latestYear = latestDate.getFullYear();
-
-                  if (earliestYear === latestYear) {
-                    if (data.albums === 1) {
-                      workingCapsule += '.withDate';
-                      workingOptions.date =
-                        language.formatDate(earliestDate);
+      html.tags([
+        relations.contentHeading.slots({
+          tag: 'h2',
+          title: language.sanitize(data.name),
+        }),
+
+        relations.grid.slots({
+          cutIndex: 4,
+
+          bottomCaption:
+            language.encapsulate(capsule, 'caption', captionCapsule =>
+              html.tags([
+                data.anyAlbumNotFromThisGroup &&
+                  language.$(captionCapsule, 'seriesAlbumsNotFromGroup', {
+                    marker:
+                      language.$('misc.coverGrid.details.notFromThisGroup.marker'),
+
+                    series:
+                      html.tag('i', data.name),
+
+                    group: data.groupName,
+                  }),
+
+                language.encapsulate(captionCapsule, workingCapsule => {
+                  const workingOptions = {};
+
+                  workingOptions.tracks =
+                    html.tag('b',
+                      language.countTracks(data.tracks, {unit: true}));
+
+                  workingOptions.albums =
+                    html.tag('b',
+                      language.countAlbums(data.albums, {unit: true}));
+
+                  if (data.allAlbumsDated) {
+                    const earliestDate = data.earliestAlbumDate;
+                    const latestDate = data.latestAlbumDate;
+
+                    const earliestYear = earliestDate.getFullYear();
+                    const latestYear = latestDate.getFullYear();
+
+                    if (earliestYear === latestYear) {
+                      if (data.albums === 1) {
+                        workingCapsule += '.withDate';
+                        workingOptions.date =
+                          language.formatDate(earliestDate);
+                      } else {
+                        workingCapsule += '.withYear';
+                        workingOptions.year =
+                          language.formatYear(earliestDate);
+                      }
                     } else {
-                      workingCapsule += '.withYear';
-                      workingOptions.year =
-                        language.formatYear(earliestDate);
+                      workingCapsule += '.withYearRange';
+                      workingOptions.yearRange =
+                        language.formatYearRange(earliestDate, latestDate);
                     }
-                  } else {
-                    workingCapsule += '.withYearRange';
-                    workingOptions.yearRange =
-                      language.formatYearRange(earliestDate, latestDate);
                   }
-                }
-
-                return language.$(workingCapsule, workingOptions);
-              }),
-            ], {[html.joinChildren]: html.tag('br')})),
 
-        expandCue:
-          language.$(capsule, 'expand'),
-
-        collapseCue:
-          language.$(capsule, 'collapse'),
-      })),
+                  return language.$(workingCapsule, workingOptions);
+                }),
+              ], {[html.joinChildren]: html.tag('br')})),
+        }),
+      ])),
 };
diff --git a/src/content/dependencies/generateGroupGalleryPageStyleSelector.js b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js
new file mode 100644
index 00000000..9342e50f
--- /dev/null
+++ b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js
@@ -0,0 +1,60 @@
+import {unique} from '#sugar';
+
+export default {
+  query: (group) => ({
+    styles:
+      unique(group.albums.map(album => album.style)),
+  }),
+
+  data: (query, group) => ({
+    albums:
+      group.albums.length,
+
+    styles:
+      query.styles,
+  }),
+
+  generate: (data, {html, language}) =>
+    language.encapsulate('groupGalleryPage', pageCapsule =>
+      (data.styles.length <= 1
+        ? html.blank()
+        : html.tag('p', {class: 'gallery-style-selector'},
+            {class: ['drop', 'shiny']},
+
+            language.encapsulate(pageCapsule, 'albumStyleSwitcher', capsule => [
+              html.tag('span',
+                language.$(capsule)),
+
+              html.tag('br'),
+
+              html.tag('span', {class: 'styles'},
+                data.styles.map(style =>
+                  html.tag('label', {'data-style': style}, [
+                    html.tag('input', {type: 'checkbox'},
+                      {checked: true}),
+
+                    html.tag('span',
+                      language.$(capsule, style)),
+                  ]))),
+
+              html.tag('br'),
+
+              html.tag('span', {class: ['count', 'all']},
+                language.$(capsule, 'count.all', {
+                  total: data.albums,
+                })),
+
+              html.tag('span', {class: ['count', 'filtered']},
+                {style: 'display: none'},
+
+                language.$(capsule, 'count.filtered', {
+                  count: html.tag('span'),
+                  total: data.albums,
+                })),
+
+              html.tag('span', {class: ['count', 'none']},
+                {style: 'display: none'},
+
+                language.$(capsule, 'count.none')),
+            ])))),
+};
diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js
index 7b9c2afa..0f3093b2 100644
--- a/src/content/dependencies/generateGroupInfoPage.js
+++ b/src/content/dependencies/generateGroupInfoPage.js
@@ -1,20 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateGroupInfoPageAlbumsSection',
-    'generateGroupNavLinks',
-    'generateGroupSecondaryNav',
-    'generateGroupSidebar',
-    'generatePageLayout',
-    'linkArtist',
-    'linkExternal',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     enableGroupUI:
       wikiInfo.enableGroupUI,
diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js
index df42598d..bd3f5dd5 100644
--- a/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js
+++ b/src/content/dependencies/generateGroupInfoPageAlbumsListByDate.js
@@ -1,10 +1,6 @@
 import {sortChronologically} from '#sort';
 
 export default {
-  contentDependencies: ['generateGroupInfoPageAlbumsListItem'],
-
-  extraDependencies: ['html'],
-
   query: (group) => ({
     // Typically, a latestFirst: false (default) chronological sort would be
     // appropriate here, but navigation between adjacent albums in a group is a
@@ -33,12 +29,16 @@ export default {
     },
   },
 
-  generate: (relations, slots, {html}) =>
+  generate: (relations, slots, {html, language}) =>
     html.tag('ul',
       {id: 'group-album-list-by-date'},
 
       slots.hidden && {style: 'display: none'},
 
+      relations.items.length > 1&&
+      language.$order('groupInfoPage.albumList.item.withYear', 0) === 'YEAR_ACCENT' &&
+        {class: 'offset-tooltips'},
+
       {[html.onlyIfContent]: true},
 
       relations.items
diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js
index bcd5d288..ddba0aec 100644
--- a/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js
+++ b/src/content/dependencies/generateGroupInfoPageAlbumsListBySeries.js
@@ -1,13 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateContentHeading',
-    'generateGroupInfoPageAlbumsListItem',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query: (group) => ({
     closelyLinkedArtists:
       group.closelyLinkedArtists
@@ -19,6 +12,10 @@ export default {
       group.serieses
         .map(() => relation('generateContentHeading')),
 
+    seriesDescriptions:
+      group.serieses
+        .map(series => relation('transformContent', series.description)),
+
     seriesItems:
       group.serieses
         .map(series => series.albums
@@ -51,17 +48,23 @@ export default {
         {id: 'group-album-list-by-series'},
         {class: 'group-series-list'},
 
+        relations.seriesItems.flat().length > 1 &&
+        language.$order(listCapsule, 'item.withYear', 0) === 'YEAR_ACCENT' &&
+          {class: 'offset-tooltips'},
+
         {[html.onlyIfContent]: true},
 
         stitchArrays({
           name: data.seriesNames,
           itemsShowArtists: data.seriesItemsShowArtists,
           heading: relations.seriesHeadings,
+          description: relations.seriesDescriptions,
           items: relations.seriesItems,
         }).map(({
             name,
             itemsShowArtists,
             heading,
+            description,
             items,
           }) =>
             html.tags([
@@ -73,7 +76,11 @@ export default {
                   }),
               }),
 
-              html.tag('dd',
+              html.tag('dd', [
+                html.tag('blockquote',
+                  {[html.onlyIfContent]: true},
+                  description),
+
                 html.tag('ul',
                   stitchArrays({
                     item: items,
@@ -82,6 +89,7 @@ export default {
                       item.slots({
                         accentMode:
                           (showArtists ? 'artists' : null),
-                      })))),
+                      }))),
+              ]),
             ])))),
 };
diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js
index 4680cb46..1211dfb8 100644
--- a/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js
+++ b/src/content/dependencies/generateGroupInfoPageAlbumsListItem.js
@@ -1,16 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateAbsoluteDatetimestamp',
-    'generateArtistCredit',
-    'generateColorStyleAttribute',
-    'linkAlbum',
-    'linkGroup',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query: (album, group) => {
     const otherCategory =
       album.groups
@@ -76,7 +66,7 @@ export default {
             workingOptions.yearAccent =
               language.$(yearCapsule, 'accent', {
                 year:
-                  relations.datetimestamp.slots({style: 'year', tooltip: true}),
+                  relations.datetimestamp.slot('style', 'year'),
               });
           }
 
@@ -127,9 +117,7 @@ export default {
             workingCapsule += '.withArtists';
             workingOptions.by =
               html.tag('span', {class: 'by'},
-                // TODO: This is obviously evil.
-                html.metatag('chunkwrap', {split: /,| (?=and)/},
-                  html.resolve(artistCredit)));
+                artistCredit);
           }
 
           return language.$(workingCapsule, workingOptions);
diff --git a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
index 0b678e9d..4470eb2f 100644
--- a/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
+++ b/src/content/dependencies/generateGroupInfoPageAlbumsSection.js
@@ -1,14 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateContentHeading',
-    'generateGroupInfoPageAlbumsListByDate',
-    'generateGroupInfoPageAlbumsListBySeries',
-    'generateIntrapageDotSwitcher',
-    'linkGroupGallery',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, group) => ({
     contentHeading:
       relation('generateContentHeading'),
diff --git a/src/content/dependencies/generateGroupNavAccent.js b/src/content/dependencies/generateGroupNavAccent.js
index 0e4ebe8a..18281bf0 100644
--- a/src/content/dependencies/generateGroupNavAccent.js
+++ b/src/content/dependencies/generateGroupNavAccent.js
@@ -1,14 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'linkGroup',
-    'linkGroupGallery',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, group) => ({
     switcher:
       relation('generateInterpageDotSwitcher'),
diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js
index bdc3ee4c..4f13e474 100644
--- a/src/content/dependencies/generateGroupNavLinks.js
+++ b/src/content/dependencies/generateGroupNavLinks.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateGroupNavAccent', 'linkGroup'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({groupCategoryData, wikiInfo}) => ({
     groupCategoryData,
     enableGroupUI: wikiInfo.enableGroupUI,
diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js
index c48f3142..6b4347dd 100644
--- a/src/content/dependencies/generateGroupSecondaryNav.js
+++ b/src/content/dependencies/generateGroupSecondaryNav.js
@@ -1,9 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateSecondaryNav',
-    'generateGroupSecondaryNavCategoryPart',
-  ],
-
   relations: (relation, group) => ({
     secondaryNav:
       relation('generateSecondaryNav'),
diff --git a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js
index b2adb9f8..df627c99 100644
--- a/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js
+++ b/src/content/dependencies/generateGroupSecondaryNavCategoryPart.js
@@ -1,15 +1,6 @@
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateSecondaryNavParentSiblingsPart',
-    'linkGroupDynamically',
-    'linkListing',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({listingSpec, wikiInfo}) => ({
     groupsByCategoryListing:
       (wikiInfo.enableListings
diff --git a/src/content/dependencies/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js
index 0888cbbe..1359eaca 100644
--- a/src/content/dependencies/generateGroupSidebar.js
+++ b/src/content/dependencies/generateGroupSidebar.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateGroupSidebarCategoryDetails',
-    'generatePageSidebar',
-    'generatePageSidebarBox',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({groupCategoryData}) => ({groupCategoryData}),
 
   relations: (relation, sprawl, group) => ({
diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
index 208ccd07..a7e1f240 100644
--- a/src/content/dependencies/generateGroupSidebarCategoryDetails.js
+++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
@@ -1,14 +1,6 @@
 import {empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'linkGroup',
-    'linkGroupGallery',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations(relation, category) {
     return {
       colorStyle:
diff --git a/src/content/dependencies/generateImageOverlay.js b/src/content/dependencies/generateImageOverlay.js
index cfb78a1b..006cfcce 100644
--- a/src/content/dependencies/generateImageOverlay.js
+++ b/src/content/dependencies/generateImageOverlay.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'language'],
-
   generate: ({html, language}) =>
     html.tag('div', {id: 'image-overlay-container'},
       html.tag('div', {id: 'image-overlay-content-container'}, [
diff --git a/src/content/dependencies/generateInterpageDotSwitcher.js b/src/content/dependencies/generateInterpageDotSwitcher.js
index 5a33444e..ddb7cb37 100644
--- a/src/content/dependencies/generateInterpageDotSwitcher.js
+++ b/src/content/dependencies/generateInterpageDotSwitcher.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateDotSwitcherTemplate'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation) => ({
     template:
       relation('generateDotSwitcherTemplate'),
diff --git a/src/content/dependencies/generateIntrapageDotSwitcher.js b/src/content/dependencies/generateIntrapageDotSwitcher.js
index 1d58367d..943d862c 100644
--- a/src/content/dependencies/generateIntrapageDotSwitcher.js
+++ b/src/content/dependencies/generateIntrapageDotSwitcher.js
@@ -1,9 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateDotSwitcherTemplate'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation) => ({
     template:
       relation('generateDotSwitcherTemplate'),
@@ -39,11 +36,32 @@ export default {
         stitchArrays({
           title: slots.titles,
           targetID: slots.targetIDs,
-        }).map(({title, targetID}) =>
-            html.tag('a', {href: '#'},
-              {'data-target-id': targetID},
-              {[html.onlyIfContent]: true},
+        }).map(({title, targetID}) => {
+            const {content} = html.smush(title);
+
+            const customCue =
+              content.find(item =>
+                item?.tagName === 'span' &&
+                item.attributes.has('class', 'dot-switcher-interaction-cue'));
+
+            const cue =
+              (customCue && !html.isBlank(customCue)
+                ? customCue.content
+                : language.sanitize(title));
+
+            const a =
+              html.tag('a', {href: '#'},
+                {'data-target-id': targetID},
+                {[html.onlyIfContent]: true},
+
+                cue);
 
-              language.sanitize(title))),
+            if (customCue) {
+              content.splice(content.indexOf(customCue), 1, a);
+              return html.tags(content, {[html.joinChildren]: ''});
+            } else {
+              return a;
+            }
+          }),
     }),
 };
diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js
index 0a929429..e381a745 100644
--- a/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js
+++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumChunk.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateListAllAdditionalFilesChunk'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, _album, additionalFiles) => ({
     chunk:
       relation('generateListAllAdditionalFilesChunk', additionalFiles),
diff --git a/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js
index a0af1375..0f14f12c 100644
--- a/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js
+++ b/src/content/dependencies/generateListAllAdditionalFilesAlbumSection.js
@@ -1,13 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateContentHeading',
-    'generateListAllAdditionalFilesAlbumChunk',
-    'generateListAllAdditionalFilesTrackChunk',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html'],
-
   relations: (relation, album, property) => ({
     heading:
       relation('generateContentHeading'),
diff --git a/src/content/dependencies/generateListAllAdditionalFilesChunk.js b/src/content/dependencies/generateListAllAdditionalFilesChunk.js
index df652efd..d68e3bc1 100644
--- a/src/content/dependencies/generateListAllAdditionalFilesChunk.js
+++ b/src/content/dependencies/generateListAllAdditionalFilesChunk.js
@@ -1,9 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['linkAdditionalFile'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, additionalFiles) => ({
     links:
       additionalFiles
diff --git a/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js
index b2e5addf..9ac79bb5 100644
--- a/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js
+++ b/src/content/dependencies/generateListAllAdditionalFilesTrackChunk.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateListAllAdditionalFilesChunk', 'linkTrack'],
-  extraDependencies: ['html'],
-
   relations: (relation, track, additionalFiles) => ({
     trackLink:
       relation('linkTrack', track),
diff --git a/src/content/dependencies/generateListRandomPageLinksAlbumLink.js b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js
index b3560aca..29e7b1c9 100644
--- a/src/content/dependencies/generateListRandomPageLinksAlbumLink.js
+++ b/src/content/dependencies/generateListRandomPageLinksAlbumLink.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkAlbum'],
-
   data: (album) =>
     ({directory: album.directory}),
 
diff --git a/src/content/dependencies/generateListingIndexList.js b/src/content/dependencies/generateListingIndexList.js
index 78622e6e..db494f37 100644
--- a/src/content/dependencies/generateListingIndexList.js
+++ b/src/content/dependencies/generateListingIndexList.js
@@ -1,9 +1,6 @@
 import {empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['linkListing'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({listingTargetSpec, wikiInfo}) {
     return {listingTargetSpec, wikiInfo};
   },
diff --git a/src/content/dependencies/generateListingPage.js b/src/content/dependencies/generateListingPage.js
index 5f9a99a9..987008eb 100644
--- a/src/content/dependencies/generateListingPage.js
+++ b/src/content/dependencies/generateListingPage.js
@@ -1,67 +1,36 @@
-import {bindOpts, empty, stitchArrays} from '#sugar';
+import {bindOpts, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateContentHeading',
-    'generateListingSidebar',
-    'generatePageLayout',
-    'linkListing',
-    'linkListingIndex',
-    'linkTemplate',
-  ],
+  relations: (relation, listing) => ({
+    layout:
+      relation('generatePageLayout'),
 
-  extraDependencies: ['html', 'language', 'wikiData'],
+    sidebar:
+      relation('generateListingSidebar', listing),
 
-  relations(relation, listing) {
-    const relations = {};
+    listingsIndexLink:
+      relation('linkListingIndex'),
 
-    relations.layout =
-      relation('generatePageLayout');
+    chunkHeading:
+      relation('generateContentHeading'),
 
-    relations.sidebar =
-      relation('generateListingSidebar', listing);
+    showSkipToSectionLinkTemplate:
+      relation('linkTemplate'),
 
-    relations.listingsIndexLink =
-      relation('linkListingIndex');
+    sameTargetListingsLine:
+      (listing.target.listings.length > 1
+        ? relation('generateListingPageSameTargetListingsLine', listing)
+        : null),
 
-    relations.chunkHeading =
-      relation('generateContentHeading');
+    seeAlsoLinks:
+      listing.seeAlso
+        .map(listing => relation('linkListing', listing)),
+  }),
 
-    relations.showSkipToSectionLinkTemplate =
-      relation('linkTemplate');
-
-    if (listing.target.listings.length > 1) {
-      relations.sameTargetListingLinks =
-        listing.target.listings
-          .map(listing => relation('linkListing', listing));
-    } else {
-      relations.sameTargetListingLinks = [];
-    }
-
-    relations.seeAlsoLinks =
-      (!empty(listing.seeAlso)
-        ? listing.seeAlso
-            .map(listing => relation('linkListing', listing))
-        : []);
-
-    return relations;
-  },
-
-  data(listing) {
-    return {
-      stringsKey: listing.stringsKey,
-
-      targetStringsKey: listing.target.stringsKey,
-
-      sameTargetListingStringsKeys:
-        listing.target.listings
-          .map(listing => listing.stringsKey),
-
-      sameTargetListingsCurrentIndex:
-        listing.target.listings
-          .indexOf(listing),
-    };
-  },
+  data: (listing) => ({
+    stringsKey:
+      listing.stringsKey,
+  }),
 
   slots: {
     type: {
@@ -169,29 +138,7 @@ export default {
       headingMode: 'sticky',
 
       mainContent: [
-        html.tag('p',
-          {[html.onlyIfContent]: true},
-          language.$('listingPage.listingsFor', {
-            [language.onlyIfOptions]: ['listings'],
-
-            target:
-              language.$('listingPage.target', data.targetStringsKey),
-
-            listings:
-              language.formatUnitList(
-                stitchArrays({
-                  link: relations.sameTargetListingLinks,
-                  stringsKey: data.sameTargetListingStringsKeys,
-                }).map(({link, stringsKey}, index) =>
-                    html.tag('span',
-                      index === data.sameTargetListingsCurrentIndex &&
-                        {class: 'current'},
-
-                      link.slots({
-                        attributes: {class: 'nowrap'},
-                        content: language.$('listingPage', stringsKey, 'title.short'),
-                      })))),
-          })),
+        relations.sameTargetListingsLine,
 
         html.tag('p',
           {[html.onlyIfContent]: true},
diff --git a/src/content/dependencies/generateListingPageSameTargetListingsLine.js b/src/content/dependencies/generateListingPageSameTargetListingsLine.js
new file mode 100644
index 00000000..2146b1eb
--- /dev/null
+++ b/src/content/dependencies/generateListingPageSameTargetListingsLine.js
@@ -0,0 +1,46 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  relations: (relation, listing) => ({
+    listingLinks:
+      listing.target.listings
+        .map(listing => relation('linkListing', listing)),
+  }),
+
+  data: (listing) => ({
+    targetStringsKey:
+      listing.target.stringsKey,
+
+    listingStringsKeys:
+      listing.target.listings.map(listing => listing.stringsKey),
+
+    currentIndex:
+      listing.target.listings.indexOf(listing),
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    html.tag('p',
+      {[html.onlyIfContent]: true},
+
+      language.$('listingPage.listingsFor', {
+        [language.onlyIfOptions]: ['listings'],
+
+        target:
+          language.$('listingPage.target', data.targetStringsKey),
+
+        listings:
+          language.formatUnitList(
+            stitchArrays({
+              link: relations.listingLinks,
+              stringsKey: data.listingStringsKeys,
+            }).map(({link, stringsKey}, index) =>
+                html.tag('span',
+                  index === data.currentIndex &&
+                    {class: 'current'},
+
+                  link.slots({
+                    attributes: {class: 'nowrap'},
+                    content: language.$('listingPage', stringsKey, 'title.short'),
+                  })))),
+      })),
+};
diff --git a/src/content/dependencies/generateListingSidebar.js b/src/content/dependencies/generateListingSidebar.js
index aeac05cf..2d9429cf 100644
--- a/src/content/dependencies/generateListingSidebar.js
+++ b/src/content/dependencies/generateListingSidebar.js
@@ -1,13 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateListingIndexList',
-    'generatePageSidebar',
-    'generatePageSidebarBox',
-    'linkListingIndex',
-  ],
-
-  extraDependencies: ['html'],
-
   relations: (relation, currentListing) => ({
     sidebar:
       relation('generatePageSidebar'),
diff --git a/src/content/dependencies/generateListingsIndexPage.js b/src/content/dependencies/generateListingsIndexPage.js
index b57ebe15..80963d12 100644
--- a/src/content/dependencies/generateListingsIndexPage.js
+++ b/src/content/dependencies/generateListingsIndexPage.js
@@ -1,20 +1,14 @@
 import {getTotalDuration} from '#wiki-data';
 
 export default {
-  contentDependencies: [
-    'generateListingIndexList',
-    'generateListingSidebar',
-    'generatePageLayout',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({albumData, trackData, wikiInfo}) {
     return {
       wikiName: wikiInfo.name,
       numTracks: trackData.length,
       numAlbums: albumData.length,
-      totalDuration: getTotalDuration(trackData),
+      totalDuration:
+        getTotalDuration(
+          trackData.filter(track => track.countInArtistTotals)),
     };
   },
 
diff --git a/src/content/dependencies/generateLyricsEntry.js b/src/content/dependencies/generateLyricsEntry.js
index 02fd3634..15f84b27 100644
--- a/src/content/dependencies/generateLyricsEntry.js
+++ b/src/content/dependencies/generateLyricsEntry.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkArtist', 'linkExternal', 'transformContent'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, entry) => ({
     content:
       relation('transformContent', entry.body),
@@ -17,6 +14,9 @@ export default {
     sourceLinks:
       entry.sourceURLs
         .map(url => relation('linkExternal', url)),
+
+    originDetails:
+      relation('transformContent', entry.originDetails),
   }),
 
   data: (entry) => ({
@@ -25,6 +25,30 @@ export default {
 
     hasSquareBracketAnnotations:
       entry.hasSquareBracketAnnotations,
+
+    numStanzas:
+      1 +
+
+      (Array.from(
+        entry.body
+          .matchAll(/\n\n|<br><br>/g))
+
+        .length) +
+
+      (entry.body.includes('<br')
+        ? entry.body.split('\n').length
+        : 0),
+
+    numLines:
+      1 +
+
+      (Array.from(
+        entry.body
+          .replaceAll(/(<br>){1,}/g, '\n')
+          .replaceAll(/\n{2,}/g, '\n')
+          .matchAll(/\n/g))
+
+        .length),
   }),
 
   slots: {
@@ -36,9 +60,16 @@ export default {
 
   generate: (data, relations, slots, {html, language}) =>
     language.encapsulate('misc.lyrics', capsule =>
-      html.tag('div', {class: 'lyrics-entry'},
+      html.tag('blockquote', {class: 'lyrics-entry'},
         slots.attributes,
 
+        {'data-stanzas': data.numStanzas},
+        {'data-lines': data.numLines},
+
+        (data.numStanzas > 1 ||
+         data.numLines > 8) &&
+          {class: 'long-lyrics'},
+
         [
           html.tag('p', {class: 'lyrics-details'},
             {[html.onlyIfContent]: true},
@@ -75,6 +106,14 @@ export default {
                 language.$(capsule, 'squareBracketAnnotations'),
             ]),
 
+          html.tag('p', {class: 'origin-details'},
+            {[html.onlyIfContent]: true},
+
+            relations.originDetails.slots({
+              mode: 'inline',
+              absorbPunctuationFollowingExternalLinks: false,
+            })),
+
           relations.content.slot('mode', 'lyrics'),
         ])),
 };
diff --git a/src/content/dependencies/generateLyricsSection.js b/src/content/dependencies/generateLyricsSection.js
index f6b719a9..bbc3a776 100644
--- a/src/content/dependencies/generateLyricsSection.js
+++ b/src/content/dependencies/generateLyricsSection.js
@@ -1,15 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateContentHeading',
-    'generateIntrapageDotSwitcher',
-    'generateLyricsEntry',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, entries) => ({
     heading:
       relation('generateContentHeading'),
@@ -21,10 +12,10 @@ export default {
       entries
         .map(entry => relation('generateLyricsEntry', entry)),
 
-    annotations:
+    annotationParts:
       entries
-        .map(entry => entry.annotation)
-        .map(annotation => relation('transformContent', annotation)),
+        .map(entry => entry.annotationParts
+          .map(part => relation('transformContent', part))),
   }),
 
   data: (entries) => ({
@@ -54,11 +45,24 @@ export default {
                 initialOptionIndex: 0,
 
                 titles:
-                  relations.annotations.map(annotation =>
-                    annotation.slots({
-                      mode: 'inline',
-                      textOnly: true,
-                    })),
+                  relations.annotationParts
+                    .map(([first, ...rest]) =>
+                      language.formatUnitList([
+                        html.tag('span',
+                          {class: 'dot-switcher-interaction-cue'},
+                          {[html.onlyIfContent]: true},
+
+                          first?.slots({
+                            mode: 'inline',
+                            textOnly: true,
+                          })),
+
+                        ...rest.map(part =>
+                          part.slots({
+                            mode: 'inline',
+                            textOnly: true,
+                          })),
+                      ])),
 
                 targetIDs:
                   data.ids,
diff --git a/src/content/dependencies/generateName.js b/src/content/dependencies/generateName.js
new file mode 100644
index 00000000..e0d0c6d3
--- /dev/null
+++ b/src/content/dependencies/generateName.js
@@ -0,0 +1,33 @@
+export default {
+  contentDependencies: ['transformContent'],
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, thing) => ({
+    customName:
+      (thing.nameText
+        ? relation('transformContent', thing.nameText)
+        : null),
+  }),
+
+  data: (thing) => ({
+    normalName:
+      thing.name,
+
+    shortName:
+      thing.nameShort,
+  }),
+
+  slots: {
+    preferShortName: {
+      type: 'boolean',
+      default: false,
+    },
+  },
+
+  generate: (data, relations, slots, {language}) =>
+    (relations.customName
+      ? relations.customName.slot('mode', 'inline')
+   : slots.preferShortName && data.shortName
+      ? language.sanitize(data.shortName)
+      : language.sanitize(data.normalName)),
+};
diff --git a/src/content/dependencies/generateNearbyTrackList.js b/src/content/dependencies/generateNearbyTrackList.js
new file mode 100644
index 00000000..56ab2df5
--- /dev/null
+++ b/src/content/dependencies/generateNearbyTrackList.js
@@ -0,0 +1,44 @@
+export default {
+  query: (tracks, contextTrack, _contextContributions) => ({
+    presentedTracks:
+      (contextTrack
+        ? tracks.map(track =>
+            track.otherReleases.find(({album}) => album === contextTrack.album) ??
+            track)
+        : tracks),
+  }),
+
+  relations: (relation, query, _tracks, _contextTrack, contextContributions) => ({
+    items:
+      query.presentedTracks
+        .map(track => relation('generateTrackListItem', track, contextContributions)),
+  }),
+
+  slots: {
+    showArtists: {
+      validate: v => v.is(true, false, 'auto'),
+      default: 'auto',
+    },
+
+    showDuration: {
+      type: 'boolean',
+      default: false,
+    },
+
+    colorMode: {
+      validate: v => v.is('none', 'track', 'line'),
+      default: 'track',
+    },
+  },
+
+  generate: (relations, slots, {html}) =>
+    html.tag('ul',
+      {[html.onlyIfContent]: true},
+
+      relations.items.map(item =>
+        item.slots({
+          showArtists: slots.showArtists,
+          showDuration: slots.showDuration,
+          colorMode: slots.colorMode,
+        }))),
+};
diff --git a/src/content/dependencies/generateNewsEntryNavAccent.js b/src/content/dependencies/generateNewsEntryNavAccent.js
index 5d168e41..05248eb3 100644
--- a/src/content/dependencies/generateNewsEntryNavAccent.js
+++ b/src/content/dependencies/generateNewsEntryNavAccent.js
@@ -1,11 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateInterpageDotSwitcher',
-    'generateNextLink',
-    'generatePreviousLink',
-    'linkNewsEntry',
-  ],
-
   relations: (relation, previousEntry, nextEntry) => ({
     switcher:
       relation('generateInterpageDotSwitcher'),
diff --git a/src/content/dependencies/generateNewsEntryPage.js b/src/content/dependencies/generateNewsEntryPage.js
index 4abd87d1..bbfb886d 100644
--- a/src/content/dependencies/generateNewsEntryPage.js
+++ b/src/content/dependencies/generateNewsEntryPage.js
@@ -2,16 +2,6 @@ import {sortChronologically} from '#sort';
 import {atOffset} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateNewsEntryNavAccent',
-    'generateNewsEntryReadAnotherLinks',
-    'generatePageLayout',
-    'linkNewsIndex',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({newsData}) {
     return {newsData};
   },
diff --git a/src/content/dependencies/generateNewsEntryReadAnotherLinks.js b/src/content/dependencies/generateNewsEntryReadAnotherLinks.js
index d978b0e4..50c23513 100644
--- a/src/content/dependencies/generateNewsEntryReadAnotherLinks.js
+++ b/src/content/dependencies/generateNewsEntryReadAnotherLinks.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateAbsoluteDatetimestamp',
-    'generateRelativeDatetimestamp',
-    'linkNewsEntry',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations(relation, currentEntry, previousEntry, nextEntry) {
     const relations = {};
 
@@ -57,10 +49,7 @@ export default {
       if (relations.previousEntryDatetimestamp) {
         parts.push('withDate');
         options.date =
-          relations.previousEntryDatetimestamp.slots({
-            style: 'full',
-            tooltip: true,
-          });
+          relations.previousEntryDatetimestamp.slot('style', 'full');
       }
 
       entryLines.push(language.$(...parts, options));
@@ -75,21 +64,22 @@ export default {
       if (relations.nextEntryDatetimestamp) {
         parts.push('withDate');
         options.date =
-          relations.nextEntryDatetimestamp.slots({
-            style: 'full',
-            tooltip: true,
-          });
+          relations.nextEntryDatetimestamp.slot('style', 'full');
       }
 
       entryLines.push(language.$(...parts, options));
     }
 
+    console.log(language.$order(prefix, 'previous.withDate', 0));
+
     return (
       html.tag('p', {class: 'read-another-links'},
         {[html.onlyIfContent]: true},
         {[html.joinChildren]: html.tag('br')},
 
         entryLines.length > 1 &&
+        language.$order(prefix, 'previous.withDate', 0) === 'DATE' &&
+        language.$order(prefix, 'next.withDate', 0) === 'DATE' &&
           {class: 'offset-tooltips'},
 
         entryLines));
diff --git a/src/content/dependencies/generateNewsIndexPage.js b/src/content/dependencies/generateNewsIndexPage.js
index 02964ce8..d88bfdba 100644
--- a/src/content/dependencies/generateNewsIndexPage.js
+++ b/src/content/dependencies/generateNewsIndexPage.js
@@ -2,14 +2,6 @@ import {sortChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generatePageLayout',
-    'linkNewsEntry',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({newsData}) {
     return {newsData};
   },
diff --git a/src/content/dependencies/generateNextLink.js b/src/content/dependencies/generateNextLink.js
index 2e48cd2b..2c497e12 100644
--- a/src/content/dependencies/generateNextLink.js
+++ b/src/content/dependencies/generateNextLink.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generatePreviousNextLink'],
-
   relations: (relation) => ({
     link:
       relation('generatePreviousNextLink'),
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 89fefb23..23d5932d 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -1,27 +1,9 @@
+import striptags from 'striptags';
+
 import {openAggregate} from '#aggregate';
 import {atOffset, empty, repeat} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateColorStyleRules',
-    'generateFooterLocalizationLinks',
-    'generateImageOverlay',
-    'generatePageSidebar',
-    'generateSearchSidebarBox',
-    'generateStickyHeadingContainer',
-    'transformContent',
-  ],
-
-  extraDependencies: [
-    'getColors',
-    'html',
-    'language',
-    'pagePath',
-    'pagePathStringFromRoot',
-    'to',
-    'wikiData',
-  ],
-
   sprawl: ({wikiInfo}) => ({
     enableSearch: wikiInfo.enableSearch,
     footerContent: wikiInfo.footerContent,
@@ -58,8 +40,14 @@ export default {
         relation('transformContent', sprawl.footerContent);
     }
 
-    relations.colorStyleRules =
-      relation('generateColorStyleRules');
+    relations.colorStyleTag =
+      relation('generateColorStyleTag');
+
+    relations.staticURLStyleTag =
+      relation('generateStaticURLStyleTag');
+
+    relations.wikiWallpaperStyleTag =
+      relation('generateWikiWallpaperStyleTag');
 
     relations.imageOverlay =
       relation('generateImageOverlay');
@@ -107,9 +95,9 @@ export default {
 
     color: {validate: v => v.isColor},
 
-    styleRules: {
-      validate: v => v.sparseArrayOf(v.isHTML),
-      default: [],
+    styleTags: {
+      type: 'html',
+      mutable: false,
     },
 
     mainClasses: {
@@ -288,12 +276,17 @@ export default {
     const titleContentsHTML =
       (html.isBlank(slots.title)
         ? null
-     : html.isBlank(slots.additionalNames)
-        ? language.sanitize(slots.title)
-        : html.tag('a', {
+
+     : (!html.isBlank(slots.additionalNames) &&
+        !html.resolve(slots.additionalNames, {slots: ['alwaysVisible']})
+          .getSlotValue('alwaysVisible'))
+
+        ? html.tag('a', {
             href: '#additional-names-box',
             title: language.$('misc.additionalNames.tooltip').toString(),
-          }, language.sanitize(slots.title)));
+          }, language.sanitize(slots.title))
+
+        : language.sanitize(slots.title));
 
     const titleHTML =
       (html.isBlank(slots.title)
@@ -581,29 +574,33 @@ export default {
               {id: 'additional-files', string: 'additionalFiles'},
               {id: 'commentary', string: 'commentary'},
               {id: 'artist-commentary', string: 'artistCommentary'},
-              {id: 'credit-sources', string: 'creditSources'},
+              {id: 'crediting-sources', string: 'creditingSources'},
+              {id: 'referencing-sources', string: 'referencingSources'},
             ])),
         ]);
 
-    const styleRulesCSS =
-      html.resolve(slots.styleRules, {normalize: 'string'});
+    const slottedStyleTags =
+      html.smush(slots.styleTags);
 
-    const fallbackBackgroundStyleRule =
-      (styleRulesCSS.match(/body::before[^}]*background-image:/)
-        ? ''
-        : `body::before {\n` +
-          `    background-image: url("${to('media.path', 'bg.jpg')}");\n` +
-          `}`);
+    const slottedWallpaperStyleTag =
+      slottedStyleTags.content
+        .find(tag => tag.attributes.has('class', 'wallpaper-style'));
 
-    const goshFrigginDarnitStyleRule =
-      `.image-media-link::after {\n` +
-      `    mask-image: url("${to('staticMisc.path', 'image.svg')}");\n` +
-      `}`;
+    const fallbackWallpaperStyleTag =
+      (slottedWallpaperStyleTag
+        ? html.blank()
+        : relations.wikiWallpaperStyleTag);
+
+    const usingWallpaperStyleTag =
+      (slottedWallpaperStyleTag
+        ? slottedWallpaperStyleTag
+        : html.resolve(fallbackWallpaperStyleTag, {normalize: 'tag'}));
 
     const numWallpaperParts =
-      html.resolve(slots.styleRules, {normalize: 'string'})
-        .match(/\.wallpaper-part:nth-child/g)
-        ?.length ?? 0;
+      (usingWallpaperStyleTag &&
+       usingWallpaperStyleTag.attributes.has('data-wallpaper-mode', 'parts')
+        ? parseInt(usingWallpaperStyleTag.attributes.get('data-num-wallpaper-parts'))
+        : 0);
 
     const wallpaperPartsHTML =
       html.tag('div', {class: 'wallpaper-parts'},
@@ -659,11 +656,25 @@ export default {
               language.encapsulate('misc.pageTitle', workingCapsule => {
                 const workingOptions = {};
 
-                workingOptions.title = slots.title;
+                // Slightly jank: The output of striptags is, of course, a string,
+                // and as far as language.formatString() is concerned, that means
+                // it needs to be sanitized - including turning ampersands into
+                // &amp;'s. But the title is already HTML that has implicitly been
+                // sanitized, however it got here, and includes HTML entities that
+                // are properly escaped. Those need to get included as they are,
+                // so we wrap the title in a tag and pass it off as good to go.
+                workingOptions.title =
+                  html.tags([
+                    striptags(slots.title.toString()),
+                  ]);
 
                 if (!html.isBlank(slots.subtitle)) {
+                  // Same shenanigans here, as far as wrapping striptags goes.
                   workingCapsule += '.withSubtitle';
-                  workingOptions.subtitle = slots.subtitle;
+                  workingOptions.subtitle =
+                    html.tags([
+                      striptags(slots.subtitle.toString()),
+                    ]);
                 }
 
                 const showWikiName =
@@ -745,14 +756,14 @@ export default {
               href: to('staticCSS.path', 'site.css'),
             }),
 
-            html.tag('style', [
-              relations.colorStyleRules
-                .slot('color', slots.color ?? data.wikiColor),
+            relations.colorStyleTag
+              .slot('color', slots.color ?? data.wikiColor),
 
-              fallbackBackgroundStyleRule,
-              goshFrigginDarnitStyleRule,
-              slots.styleRules,
-            ]),
+            relations.staticURLStyleTag,
+
+            fallbackWallpaperStyleTag,
+
+            slottedStyleTags,
 
             html.tag('script', {
               src: to('staticLib.path', 'chroma-js/chroma.min.js'),
diff --git a/src/content/dependencies/generatePageSidebar.js b/src/content/dependencies/generatePageSidebar.js
index d3b55580..dfe85632 100644
--- a/src/content/dependencies/generatePageSidebar.js
+++ b/src/content/dependencies/generatePageSidebar.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     // Attributes to apply to the whole sidebar. This be added to the
     // containing sidebar-column, arr - specify attributes on each section if
diff --git a/src/content/dependencies/generatePageSidebarBox.js b/src/content/dependencies/generatePageSidebarBox.js
index 26b30494..3133aa64 100644
--- a/src/content/dependencies/generatePageSidebarBox.js
+++ b/src/content/dependencies/generatePageSidebarBox.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     content: {
       type: 'html',
diff --git a/src/content/dependencies/generatePageSidebarConjoinedBox.js b/src/content/dependencies/generatePageSidebarConjoinedBox.js
index 7974c707..4ed0ff22 100644
--- a/src/content/dependencies/generatePageSidebarConjoinedBox.js
+++ b/src/content/dependencies/generatePageSidebarConjoinedBox.js
@@ -4,9 +4,6 @@
 // templates' resolved content), take care when slotting into this.
 
 export default {
-  contentDependencies: ['generatePageSidebarBox'],
-  extraDependencies: ['html'],
-
   relations: (relation) => ({
     box:
       relation('generatePageSidebarBox'),
diff --git a/src/content/dependencies/generatePreviousLink.js b/src/content/dependencies/generatePreviousLink.js
index 775367f9..29146a21 100644
--- a/src/content/dependencies/generatePreviousLink.js
+++ b/src/content/dependencies/generatePreviousLink.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generatePreviousNextLink'],
-
   relations: (relation) => ({
     link:
       relation('generatePreviousNextLink'),
diff --git a/src/content/dependencies/generatePreviousNextLink.js b/src/content/dependencies/generatePreviousNextLink.js
index afae1228..1e98358f 100644
--- a/src/content/dependencies/generatePreviousNextLink.js
+++ b/src/content/dependencies/generatePreviousNextLink.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html', 'language'],
-
   slots: {
     link: {
       type: 'html',
diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js
index e144503e..f67f9514 100644
--- a/src/content/dependencies/generateQuickDescription.js
+++ b/src/content/dependencies/generateQuickDescription.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['transformContent'],
-  extraDependencies: ['html', 'language'],
-
   query: (thing) => ({
     hasDescription:
       !!thing.description,
diff --git a/src/content/dependencies/generateReadCommentaryLine.js b/src/content/dependencies/generateReadCommentaryLine.js
new file mode 100644
index 00000000..05700536
--- /dev/null
+++ b/src/content/dependencies/generateReadCommentaryLine.js
@@ -0,0 +1,43 @@
+import {empty} from '#sugar';
+
+export default {
+  query: (thing) => ({
+    entries:
+      (thing.isTrack
+        ? [...thing.commentary, ...thing.commentaryFromMainRelease]
+        : thing.commentary),
+  }),
+
+  data: (query, _thing) => ({
+    hasWikiEditorCommentary:
+      query.entries.some(entry => entry.isWikiEditorCommentary),
+
+    onlyWikiEditorCommentary:
+      !empty(query.entries) &&
+      query.entries.every(entry => entry.isWikiEditorCommentary),
+
+    hasAnyCommentary:
+      !empty(query.entries),
+  }),
+
+  generate: (data, {html, language}) =>
+    language.encapsulate('releaseInfo.readCommentary', capsule =>
+      language.$(capsule, {
+        [language.onlyIfOptions]: ['link'],
+
+        link:
+          html.tag('a',
+            {[html.onlyIfContent]: true},
+
+            {href: '#artist-commentary'},
+
+            language.encapsulate(capsule, 'link', capsule =>
+              (data.onlyWikiEditorCommentary
+                ? language.$(capsule, 'onlyWikiCommentary')
+             : data.hasWikiEditorCommentary
+                ? language.$(capsule, 'withWikiCommentary')
+             : data.hasAnyCommentary
+                ? language.$(capsule)
+                : html.blank()))),
+      })),
+};
diff --git a/src/content/dependencies/generateReferencedArtworksPage.js b/src/content/dependencies/generateReferencedArtworksPage.js
index 154b4762..2f47b7a5 100644
--- a/src/content/dependencies/generateReferencedArtworksPage.js
+++ b/src/content/dependencies/generateReferencedArtworksPage.js
@@ -1,14 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateCoverArtwork',
-    'generateCoverGrid',
-    'generatePageLayout',
-    'image',
-    'linkAnythingMan',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, artwork) => ({
     layout:
       relation('generatePageLayout'),
@@ -47,7 +37,7 @@ export default {
   }),
 
   slots: {
-    styleRules: {type: 'html', mutable: false},
+    styleTags: {type: 'html', mutable: false},
 
     title: {type: 'html', mutable: false},
 
@@ -62,7 +52,7 @@ export default {
         subtitle: language.$(pageCapsule, 'subtitle'),
 
         color: data.color,
-        styleRules: slots.styleRules,
+        styleTags: slots.styleTags,
 
         artworkColumnContent:
           relations.cover.slots({
diff --git a/src/content/dependencies/generateReferencedTracksList.js b/src/content/dependencies/generateReferencedTracksList.js
new file mode 100644
index 00000000..1d566ce9
--- /dev/null
+++ b/src/content/dependencies/generateReferencedTracksList.js
@@ -0,0 +1,29 @@
+export default {
+  relations: (relation, track) => ({
+    previousProductionTrackList:
+      relation('generateNearbyTrackList',
+        track.previousProductionTracks,
+        track,
+        track.artistContribs),
+
+    referencedTrackList:
+      relation('generateNearbyTrackList',
+        track.referencedTracks,
+        track,
+        []),
+  }),
+
+  generate: (relations, {html, language}) =>
+    html.tag('ul', {[html.onlyIfContent]: true}, [
+      html.inside(relations.previousProductionTrackList)
+        .map(li => html.inside(li))
+        .map(label =>
+          html.tag('li',
+            language.$('trackList.item.previousProduction',
+              {track: label}))),
+
+      html.inside(relations.referencedTrackList),
+    ]),
+};
+
+
diff --git a/src/content/dependencies/generateReferencingArtworksPage.js b/src/content/dependencies/generateReferencingArtworksPage.js
index 55977b37..abb92732 100644
--- a/src/content/dependencies/generateReferencingArtworksPage.js
+++ b/src/content/dependencies/generateReferencingArtworksPage.js
@@ -1,14 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateCoverArtwork',
-    'generateCoverGrid',
-    'generatePageLayout',
-    'image',
-    'linkAnythingMan',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, artwork) => ({
     layout:
       relation('generatePageLayout'),
@@ -47,7 +37,7 @@ export default {
   }),
 
   slots: {
-    styleRules: {type: 'html', mutable: false},
+    styleTags: {type: 'html', mutable: false},
 
     title: {type: 'html', mutable: false},
 
@@ -62,7 +52,7 @@ export default {
         subtitle: language.$(pageCapsule, 'subtitle'),
 
         color: data.color,
-        styleRules: slots.styleRules,
+        styleTags: slots.styleTags,
 
         artworkColumnContent:
           relations.cover.slots({
diff --git a/src/content/dependencies/generateRelativeDatetimestamp.js b/src/content/dependencies/generateRelativeDatetimestamp.js
index a997de0e..1415564e 100644
--- a/src/content/dependencies/generateRelativeDatetimestamp.js
+++ b/src/content/dependencies/generateRelativeDatetimestamp.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateAbsoluteDatetimestamp',
-    'generateDatetimestampTemplate',
-    'generateTooltip',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   data: (currentDate, referenceDate) =>
     (currentDate.getTime() === referenceDate.getTime()
       ? {equal: true, date: currentDate}
@@ -28,19 +20,11 @@ export default {
       validate: v => v.is('full', 'year'),
       default: 'full',
     },
-
-    tooltip: {
-      type: 'boolean',
-      default: false,
-    },
   },
 
   generate(data, relations, slots, {language}) {
     if (data.equal) {
-      return relations.fallback.slots({
-        style: slots.style,
-        tooltip: slots.tooltip,
-      });
+      return relations.fallback.slot('style', slots.style);
     }
 
     return relations.template.slots({
@@ -52,15 +36,14 @@ export default {
           : null),
 
       tooltip:
-        slots.tooltip &&
-          relations.tooltip.slots({
-            content:
-              language.formatRelativeDate(data.currentDate, data.referenceDate, {
-                considerRoundingDays: true,
-                approximate: true,
-                absolute: slots.style === 'year',
-              }),
-          }),
+        relations.tooltip.slots({
+          content:
+            language.formatRelativeDate(data.currentDate, data.referenceDate, {
+              considerRoundingDays: true,
+              approximate: true,
+              absolute: slots.style === 'year',
+            }),
+        }),
 
       datetime:
         data.currentDate.toISOString(),
diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js
index 016e0a2c..4353ccf4 100644
--- a/src/content/dependencies/generateReleaseInfoContributionsLine.js
+++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js
@@ -1,16 +1,15 @@
 export default {
-  contentDependencies: ['generateArtistCredit'],
-  extraDependencies: ['html'],
-
-  relations: (relation, contributions) => ({
+  relations: (relation, contributions, formatText) => ({
     credit:
-      relation('generateArtistCredit', contributions, []),
+      relation('generateArtistCredit', contributions, [], formatText),
   }),
 
   slots: {
     stringKey: {type: 'string'},
     featuringStringKey: {type: 'string'},
 
+    additionalStringOptions: {validate: v => v.isObject},
+
     chronologyKind: {type: 'string'},
   },
 
@@ -27,5 +26,6 @@ export default {
 
       normalStringKey: slots.stringKey,
       normalFeaturingStringKey: slots.featuringStringKey,
+      additionalStringOptions: slots.additionalStringOptions,
     }),
 };
diff --git a/src/content/dependencies/generateReleaseInfoListenLine.js b/src/content/dependencies/generateReleaseInfoListenLine.js
new file mode 100644
index 00000000..97f248d6
--- /dev/null
+++ b/src/content/dependencies/generateReleaseInfoListenLine.js
@@ -0,0 +1,156 @@
+import {isExternalLinkContext} from '#external-links';
+import {empty, stitchArrays, unique} from '#sugar';
+
+function getReleaseContext(urlString, {
+  _artistURLs,
+  albumArtistURLs,
+}) {
+  const composerBandcampDomains =
+    albumArtistURLs
+      .filter(url => url.hostname.endsWith('.bandcamp.com'))
+      .map(url => url.hostname);
+
+  const url = new URL(urlString);
+
+  if (url.hostname === 'homestuck.bandcamp.com') {
+    return 'officialRelease';
+  }
+
+  if (composerBandcampDomains.includes(url.hostname)) {
+    return 'composerRelease';
+  }
+
+  return null;
+}
+
+export default {
+  query(thing) {
+    const query = {};
+
+    query.album =
+      (thing.album
+        ? thing.album
+        : thing);
+
+    query.urls =
+      (!empty(thing.urls)
+        ? thing.urls
+     : thing.album &&
+       thing.album.style === 'single' &&
+       thing.album.tracks[0] === thing
+        ? thing.album.urls
+        : []);
+
+    query.artists =
+      thing.artistContribs
+        .map(contrib => contrib.artist);
+
+    query.artistGroups =
+      query.artists
+        .flatMap(artist => artist.closelyLinkedGroups)
+        .map(({group}) => group);
+
+    query.albumArtists =
+      query.album.artistContribs
+        .map(contrib => contrib.artist);
+
+    query.albumArtistGroups =
+      query.albumArtists
+        .flatMap(artist => artist.closelyLinkedGroups)
+        .map(({group}) => group);
+
+    return query;
+  },
+
+  relations: (relation, query, _thing) => ({
+    links:
+      query.urls.map(url => relation('linkExternal', url)),
+  }),
+
+  data(query, thing) {
+    const data = {};
+
+    data.name = thing.name;
+
+    const artistURLs =
+      unique([
+        ...query.artists.flatMap(artist => artist.urls),
+        ...query.artistGroups.flatMap(group => group.urls),
+      ]).map(url => new URL(url));
+
+    const albumArtistURLs =
+      unique([
+        ...query.albumArtists.flatMap(artist => artist.urls),
+        ...query.albumArtistGroups.flatMap(group => group.urls),
+      ]).map(url => new URL(url));
+
+    const boundGetReleaseContext = urlString =>
+      getReleaseContext(urlString, {
+        artistURLs,
+        albumArtistURLs,
+      });
+
+    let releaseContexts =
+      query.urls.map(boundGetReleaseContext);
+
+    const albumReleaseContexts =
+      query.album.urls.map(boundGetReleaseContext);
+
+    const presentReleaseContexts =
+      unique(releaseContexts.filter(Boolean));
+
+    const presentAlbumReleaseContexts =
+      unique(albumReleaseContexts.filter(Boolean));
+
+    if (
+      presentReleaseContexts.length <= 1 &&
+      presentAlbumReleaseContexts.length <= 1
+    ) {
+      releaseContexts =
+        query.urls.map(() => null);
+    }
+
+    data.releaseContexts = releaseContexts;
+
+    return data;
+  },
+
+  slots: {
+    visibleWithoutLinks: {
+      type: 'boolean',
+      default: false,
+    },
+
+    context: {
+      validate: () => isExternalLinkContext,
+      default: 'generic',
+    },
+  },
+
+  generate: (data, relations, slots, {html, language}) =>
+    language.encapsulate('releaseInfo.listenOn', capsule =>
+      (empty(relations.links) && slots.visibleWithoutLinks
+        ? language.$(capsule, 'noLinks', {
+            name:
+              html.tag('i', data.name),
+          })
+
+        : language.$('releaseInfo.listenOn', {
+            [language.onlyIfOptions]: ['links'],
+
+            links:
+              language.formatDisjunctionList(
+                stitchArrays({
+                  link: relations.links,
+                  releaseContext: data.releaseContexts,
+                }).map(({link, releaseContext}) =>
+                    link.slot('context', [
+                      ...
+                      (Array.isArray(slots.context)
+                        ? slots.context
+                        : [slots.context]),
+
+                      releaseContext,
+                    ]))),
+          }))),
+};
diff --git a/src/content/dependencies/generateSearchSidebarBox.js b/src/content/dependencies/generateSearchSidebarBox.js
index 308a1105..701a01ac 100644
--- a/src/content/dependencies/generateSearchSidebarBox.js
+++ b/src/content/dependencies/generateSearchSidebarBox.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generatePageSidebarBox'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation) => ({
     sidebarBox:
       relation('generatePageSidebarBox'),
@@ -58,6 +55,23 @@ export default {
               language.$(capsule, 'artTag')),
           ]),
 
+          language.encapsulate(capsule, 'resultDisambiguator', capsule => [
+            html.tag('template', {class: 'wiki-search-group-result-disambiguator-string'},
+              language.$(capsule, 'group', {
+                disambiguator: html.tag('slot', {name: 'disambiguator'}),
+              })),
+
+            html.tag('template', {class: 'wiki-search-flash-result-disambiguator-string'},
+              language.$(capsule, 'flash', {
+                disambiguator: html.tag('slot', {name: 'disambiguator'}),
+              })),
+
+            html.tag('template', {class: 'wiki-search-track-result-disambiguator-string'},
+              language.$(capsule, 'track', {
+                disambiguator: html.tag('slot', {name: 'disambiguator'}),
+              })),
+          ]),
+
           language.encapsulate(capsule, 'resultFilter', capsule => [
             html.tag('template', {class: 'wiki-search-album-result-filter-string'},
               language.$(capsule, 'album')),
diff --git a/src/content/dependencies/generateSecondaryNav.js b/src/content/dependencies/generateSecondaryNav.js
index 9ce7ce9b..63b3839b 100644
--- a/src/content/dependencies/generateSecondaryNav.js
+++ b/src/content/dependencies/generateSecondaryNav.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     content: {
       type: 'html',
diff --git a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js
index f204f1fb..fe7c17ac 100644
--- a/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js
+++ b/src/content/dependencies/generateSecondaryNavParentSiblingsPart.js
@@ -1,15 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateInterpageDotSwitcher',
-    'generateNextLink',
-    'generatePreviousLink',
-    'linkAlbumDynamically',
-    'linkGroup',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation) => ({
     switcher:
       relation('generateInterpageDotSwitcher'),
diff --git a/src/content/dependencies/generateSocialEmbed.js b/src/content/dependencies/generateSocialEmbed.js
index 513ea518..5fa9376c 100644
--- a/src/content/dependencies/generateSocialEmbed.js
+++ b/src/content/dependencies/generateSocialEmbed.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['absoluteTo', 'html', 'language', 'wikiData'],
-
   sprawl({wikiInfo}) {
     return {
       canonicalBase: wikiInfo.canonicalBase,
diff --git a/src/content/dependencies/generateStaticPage.js b/src/content/dependencies/generateStaticPage.js
index 226152c7..485b802e 100644
--- a/src/content/dependencies/generateStaticPage.js
+++ b/src/content/dependencies/generateStaticPage.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generatePageLayout', 'transformContent'],
-  extraDependencies: ['html'],
-
   relations(relation, staticPage) {
     return {
       layout: relation('generatePageLayout'),
@@ -23,17 +20,19 @@ export default {
         title: data.name,
         headingMode: 'sticky',
 
-        styleRules:
-          (data.stylesheet
-            ? [data.stylesheet]
-            : []),
+        styleTags: [
+          html.tag('style', {class: 'static-page-style'},
+            {[html.onlyIfContent]: true},
+            data.stylesheet),
+        ],
 
         mainClasses: ['long-content'],
         mainContent: [
           relations.content,
 
-          data.script &&
-            html.tag('script', data.script),
+          html.tag('script',
+            {[html.onlyIfContent]: true},
+            data.script),
         ],
 
         navLinkStyle: 'hierarchical',
diff --git a/src/content/dependencies/generateStaticURLStyleTag.js b/src/content/dependencies/generateStaticURLStyleTag.js
new file mode 100644
index 00000000..443a4d08
--- /dev/null
+++ b/src/content/dependencies/generateStaticURLStyleTag.js
@@ -0,0 +1,20 @@
+export default {
+  relations: (relation) => ({
+    styleTag:
+      relation('generateStyleTag'),
+  }),
+
+  generate: (relations, {to}) =>
+    relations.styleTag.slots({
+      attributes: {class: 'static-url-style'},
+
+      rules: [
+        {
+          select: '.image-media-link::after',
+          declare: [
+            `mask-image: url("${to('staticMisc.path', 'image.svg')}");`
+          ],
+        },
+      ],
+    }),
+};
diff --git a/src/content/dependencies/generateStickyHeadingContainer.js b/src/content/dependencies/generateStickyHeadingContainer.js
index ec3062a3..f7388d60 100644
--- a/src/content/dependencies/generateStickyHeadingContainer.js
+++ b/src/content/dependencies/generateStickyHeadingContainer.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     rootAttributes: {
       type: 'attributes',
diff --git a/src/content/dependencies/generateStyleTag.js b/src/content/dependencies/generateStyleTag.js
new file mode 100644
index 00000000..cdeadcfe
--- /dev/null
+++ b/src/content/dependencies/generateStyleTag.js
@@ -0,0 +1,46 @@
+import {empty} from '#sugar';
+
+const indent = text =>
+  text
+    .split('\n')
+    .map(line => ' '.repeat(4) + line)
+    .join('\n');
+
+export default {
+  slots: {
+    attributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+
+    rules: {
+      validate: v =>
+        v.looseArrayOf(
+          v.validateProperties({
+            select: v.isString,
+            declare: v.looseArrayOf(v.isString),
+          })),
+    },
+  },
+
+  generate: (slots, {html}) =>
+    html.tag('style', slots.attributes,
+      {[html.onlyIfContent]: true},
+
+      slots.rules
+        .filter(Boolean)
+
+        .map(rule => ({
+          select: rule.select,
+          declare: rule.declare.filter(Boolean),
+        }))
+
+        .filter(rule => !empty(rule.declare))
+
+        .map(rule =>
+          `${rule.select} {\n` +
+          indent(rule.declare.join('\n')) + '\n' +
+          `}`)
+
+        .join('\n\n')),
+};
diff --git a/src/content/dependencies/generateTextWithTooltip.js b/src/content/dependencies/generateTextWithTooltip.js
index 49ce1f61..360cfebc 100644
--- a/src/content/dependencies/generateTextWithTooltip.js
+++ b/src/content/dependencies/generateTextWithTooltip.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     attributes: {
       type: 'attributes',
diff --git a/src/content/dependencies/generateTooltip.js b/src/content/dependencies/generateTooltip.js
index b09ee230..6f23af6d 100644
--- a/src/content/dependencies/generateTooltip.js
+++ b/src/content/dependencies/generateTooltip.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     attributes: {
       type: 'attributes',
diff --git a/src/content/dependencies/generateTrackArtistCommentarySection.js b/src/content/dependencies/generateTrackArtistCommentarySection.js
index e3041d3a..39a3e145 100644
--- a/src/content/dependencies/generateTrackArtistCommentarySection.js
+++ b/src/content/dependencies/generateTrackArtistCommentarySection.js
@@ -1,15 +1,6 @@
 import {empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateCommentaryEntry',
-    'generateContentHeading',
-    'linkAlbum',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   query: (track) => ({
     otherSecondaryReleasesWithCommentary:
       track.otherReleases
@@ -18,8 +9,8 @@ export default {
   }),
 
   relations: (relation, query, track) => ({
-    contentHeading:
-      relation('generateContentHeading'),
+    commentaryContentHeading:
+      relation('generateCommentaryContentHeading', track),
 
     mainReleaseTrackLink:
       (track.isSecondaryRelease
@@ -28,7 +19,7 @@ export default {
 
     mainReleaseArtistCommentaryEntries:
       (track.isSecondaryRelease
-        ? track.mainReleaseTrack.commentary
+        ? track.commentaryFromMainRelease
             .map(entry => relation('generateCommentaryEntry', entry))
         : null),
 
@@ -78,54 +69,40 @@ export default {
   generate: (data, relations, {html, language}) =>
     language.encapsulate('misc.artistCommentary', capsule =>
       html.tags([
-        relations.contentHeading.clone()
-          .slots({
-            attributes: {id: 'artist-commentary'},
-            title: language.$('misc.artistCommentary'),
-          }),
+        relations.commentaryContentHeading,
+        relations.artistCommentaryEntries,
 
         data.isSecondaryRelease &&
-          html.tags([
-            html.tag('p', {class: ['drop', 'commentary-drop']},
-              {[html.onlyIfSiblings]: true},
-
-              language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => {
-                const workingOptions = {};
-
-                workingOptions.album =
-                  relations.mainReleaseTrackLink.slots({
-                    content:
-                      data.mainReleaseAlbumName,
-
-                    color:
-                      data.mainReleaseAlbumColor,
-                  });
-
-                if (data.name !== data.mainReleaseName) {
-                  workingCapsule += '.namedDifferently';
-                  workingOptions.name =
-                    html.tag('i', data.mainReleaseName);
-                }
-
-                return language.$(workingCapsule, workingOptions);
-              })),
-
-            relations.mainReleaseArtistCommentaryEntries,
-          ]),
-
-        html.tags([
-          data.isSecondaryRelease &&
-          !html.isBlank(relations.mainReleaseArtistCommentaryEntries) &&
-            html.tag('p', {class: ['drop', 'commentary-drop']},
-              {[html.onlyIfSiblings]: true},
-
-              language.$(capsule, 'info.releaseSpecific', {
-                album:
-                  relations.thisReleaseAlbumLink,
-              })),
-
-          relations.artistCommentaryEntries,
-        ]),
+          html.tag('div', {class: 'inherited-commentary-section'},
+            {[html.onlyIfContent]: true},
+
+            [
+              html.tag('p', {class: ['drop', 'commentary-drop']},
+                {[html.onlyIfSiblings]: true},
+
+                language.encapsulate(capsule, 'info.fromMainRelease', workingCapsule => {
+                  const workingOptions = {};
+
+                  workingOptions.album =
+                    relations.mainReleaseTrackLink.slots({
+                      content:
+                        data.mainReleaseAlbumName,
+
+                      color:
+                        data.mainReleaseAlbumColor,
+                    });
+
+                  if (data.name !== data.mainReleaseName) {
+                    workingCapsule += '.namedDifferently';
+                    workingOptions.name =
+                      html.tag('i', data.mainReleaseName);
+                  }
+
+                  return language.$(workingCapsule, workingOptions);
+                })),
+
+              relations.mainReleaseArtistCommentaryEntries,
+            ]),
 
         html.tag('p', {class: ['drop', 'commentary-drop']},
           {[html.onlyIfContent]: true},
diff --git a/src/content/dependencies/generateTrackArtworkColumn.js b/src/content/dependencies/generateTrackArtworkColumn.js
index f06d735b..234586e0 100644
--- a/src/content/dependencies/generateTrackArtworkColumn.js
+++ b/src/content/dependencies/generateTrackArtworkColumn.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateCoverArtwork'],
-  extraDependencies: ['html'],
-
   relations: (relation, track) => ({
     albumCover:
       (!track.hasUniqueCoverArt && track.album.hasCoverArt
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index ab6ea1cb..d3c2d766 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,45 +1,46 @@
-export default {
-  contentDependencies: [
-    'generateAdditionalFilesList',
-    'generateAdditionalNamesBox',
-    'generateAlbumNavAccent',
-    'generateAlbumSecondaryNav',
-    'generateAlbumSidebar',
-    'generateAlbumStyleRules',
-    'generateCommentaryEntry',
-    'generateContentHeading',
-    'generateContributionList',
-    'generateLyricsSection',
-    'generatePageLayout',
-    'generateTrackArtistCommentarySection',
-    'generateTrackArtworkColumn',
-    'generateTrackInfoPageFeaturedByFlashesList',
-    'generateTrackInfoPageOtherReleasesList',
-    'generateTrackList',
-    'generateTrackListDividedByGroups',
-    'generateTrackNavLinks',
-    'generateTrackReleaseInfo',
-    'generateTrackSocialEmbed',
-    'linkAlbum',
-    'linkTrack',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language'],
+function checkInterrupted(which, relations, {html}) {
+  if (
+    !html.isBlank(relations.additionalFilesList) ||
+    !html.isBlank(relations.contributorContributionList) ||
+    !html.isBlank(relations.flashesThatFeatureList) ||
+    !html.isBlank(relations.lyricsSection) ||
+    !html.isBlank(relations.midiProjectFilesList) ||
+    !html.isBlank(relations.referencedByTracksList) ||
+    !html.isBlank(relations.referencedTracksList) ||
+    !html.isBlank(relations.sampledByTracksList) ||
+    !html.isBlank(relations.sampledTracksList) ||
+    !html.isBlank(relations.sheetMusicFilesList)
+  ) return true;
+
+  if (which === 'crediting-sources' || which === 'referencing-sources') {
+    if (!html.isBlank(relations.artistCommentarySection)) return true;
+  }
+
+  return false;
+}
 
+export default {
   query: (track) => ({
     mainReleaseTrack:
       (track.isMainRelease
         ? track
         : track.mainReleaseTrack),
+
+    singleTrackSingle:
+      track.album.style === 'single' &&
+      track.album.tracks.length === 1,
+
+    firstTrackInSingle:
+      track.album.style === 'single' &&
+      track === track.album.tracks[0],
   }),
 
   relations: (relation, query, track) => ({
     layout:
       relation('generatePageLayout'),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', track.album, track),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', track.album, track),
 
     socialEmbed:
       relation('generateTrackSocialEmbed', track),
@@ -47,6 +48,9 @@ export default {
     navLinks:
       relation('generateTrackNavLinks', track),
 
+    albumNavLink:
+      relation('linkAlbum', track.album),
+
     albumNavAccent:
       relation('generateAlbumNavAccent', track.album, track),
 
@@ -60,33 +64,46 @@ export default {
       relation('generateAdditionalNamesBox', track.additionalNames),
 
     artworkColumn:
-      relation('generateTrackArtworkColumn', track),
+      (query.firstTrackInSingle
+        ? relation('generateAlbumArtworkColumn', track.album)
+        : relation('generateTrackArtworkColumn', track)),
 
     contentHeading:
       relation('generateContentHeading'),
 
+    name:
+      relation('generateName', track),
+
     releaseInfo:
       relation('generateTrackReleaseInfo', track),
 
-    otherReleasesList:
-      relation('generateTrackInfoPageOtherReleasesList', track),
+    readCommentaryLine:
+      relation('generateReadCommentaryLine', track),
+
+    otherReleasesLine:
+      relation('generateTrackInfoPageOtherReleasesLine', track),
+
+    previousProductionLine:
+      relation('generateTrackInfoPagePreviousProductionLine', track),
 
     contributorContributionList:
       relation('generateContributionList', track.contributorContribs),
 
     referencedTracksList:
-      relation('generateTrackList', track.referencedTracks),
+      relation('generateReferencedTracksList', track),
 
     sampledTracksList:
-      relation('generateTrackList', track.sampledTracks),
+      relation('generateNearbyTrackList', track.sampledTracks, track, []),
 
     referencedByTracksList:
       relation('generateTrackListDividedByGroups',
-        query.mainReleaseTrack.referencedByTracks),
+        query.mainReleaseTrack.referencedByTracks,
+        track),
 
     sampledByTracksList:
       relation('generateTrackListDividedByGroups',
-        query.mainReleaseTrack.sampledByTracks),
+        query.mainReleaseTrack.sampledByTracks,
+        track),
 
     flashesThatFeatureList:
       relation('generateTrackInfoPageFeaturedByFlashesList', track),
@@ -106,17 +123,35 @@ export default {
     artistCommentarySection:
       relation('generateTrackArtistCommentarySection', track),
 
-    creditSourceEntries:
-      track.creditSources
-        .map(entry => relation('generateCommentaryEntry', entry)),
+    creditingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        track.creditingSources,
+        track),
+
+    referencingSourcesSection:
+      relation('generateCollapsedContentEntrySection',
+        track.referencingSources,
+        track),
   }),
 
-  data: (_query, track) => ({
+  data: (query, track) => ({
     name:
       track.name,
 
     color:
       track.color,
+
+    dateAlbumAddedToWiki:
+      track.album.dateAddedToWiki,
+
+    needsLyrics:
+      track.needsLyrics,
+
+    singleTrackSingle:
+      query.singleTrackSingle,
+
+    firstTrackInSingle:
+      query.firstTrackInSingle,
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -124,7 +159,7 @@ export default {
       relations.layout.slots({
         title:
           language.$(pageCapsule, 'title', {
-            track: data.name,
+            track: relations.name,
           }),
 
         headingMode: 'sticky',
@@ -132,7 +167,7 @@ export default {
         additionalNames: relations.additionalNamesBox,
 
         color: data.color,
-        styleRules: [relations.albumStyleRules],
+        styleTags: relations.albumStyleTags,
 
         artworkColumnContent:
           relations.artworkColumn,
@@ -172,26 +207,35 @@ export default {
                         language.$(capsule, 'link')),
                   })),
 
-              !html.isBlank(relations.artistCommentarySection) &&
-                language.encapsulate(capsule, 'readCommentary', capsule =>
+              checkInterrupted('commentary', relations, {html}) &&
+                relations.readCommentaryLine,
+
+              !html.isBlank(relations.creditingSourcesSection) &&
+              checkInterrupted('crediting-sources', relations, {html}) &&
+                language.encapsulate(capsule, 'readCreditingSources', capsule =>
                   language.$(capsule, {
                     link:
                       html.tag('a',
-                        {href: '#artist-commentary'},
+                        {href: '#crediting-sources'},
                         language.$(capsule, 'link')),
                   })),
 
-              !html.isBlank(relations.creditSourceEntries) &&
-                language.encapsulate(capsule, 'readCreditSources', capsule =>
+              !html.isBlank(relations.referencingSourcesSection) &&
+              checkInterrupted('referencing-sources', relations, {html}) &&
+                language.encapsulate(capsule, 'readReferencingSources', capsule =>
                   language.$(capsule, {
                     link:
                       html.tag('a',
-                        {href: '#credit-sources'},
+                        {href: '#referencing-sources'},
                         language.$(capsule, 'link')),
                   })),
             ])),
 
-          relations.otherReleasesList,
+          html.tag('p', {[html.onlyIfContent]: true},
+            relations.otherReleasesLine),
+
+          html.tag('p', {[html.onlyIfContent]: true},
+            relations.previousProductionLine),
 
           html.tags([
             relations.contentHeading.clone()
@@ -303,6 +347,25 @@ export default {
             relations.flashesThatFeatureList,
           ]),
 
+          data.firstTrackInSingle &&
+            html.tag('p',
+              {[html.onlyIfContent]: true},
+
+              language.$('releaseInfo.addedToWiki', {
+                [language.onlyIfOptions]: ['date'],
+                date: language.formatDate(data.dateAlbumAddedToWiki),
+              })),
+
+          data.firstTrackInSingle &&
+          (!html.isBlank(relations.lyricsSection) ||
+           !html.isBlank(relations.artistCommentarySection)) &&
+            html.tag('hr', {class: 'main-separator'}),
+
+          data.needsLyrics &&
+          html.isBlank(relations.lyricsSection) &&
+            html.tag('p',
+              language.$(pageCapsule, 'needsLyrics')),
+
           relations.lyricsSection,
 
           html.tags([
@@ -337,29 +400,40 @@ export default {
 
           relations.artistCommentarySection,
 
-          html.tags([
-            relations.contentHeading.clone()
-              .slots({
-                attributes: {id: 'credit-sources'},
-                title: language.$('misc.creditSources'),
-              }),
+          relations.creditingSourcesSection.slots({
+            id: 'crediting-sources',
+            string: 'misc.creditingSources',
+          }),
 
-            relations.creditSourceEntries,
-          ]),
+          relations.referencingSourcesSection.slots({
+            id: 'referencing-sources',
+            string: 'misc.referencingSources',
+          }),
         ],
 
         navLinkStyle: 'hierarchical',
-        navLinks: html.resolve(relations.navLinks),
+        navLinks:
+          (data.singleTrackSingle
+            ? [
+                {auto: 'home'},
+                {
+                  html: relations.albumNavLink,
+                  accent: language.$(pageCapsule, 'nav.singleAccent'),
+                },
+              ]
+            : html.resolve(relations.navLinks)),
 
         navBottomRowContent:
-          relations.albumNavAccent.slots({
-            showTrackNavigation: true,
-            showExtraLinks: false,
-          }),
+          (data.singleTrackSingle
+            ? null
+            : relations.albumNavAccent.slots({
+                showTrackNavigation: true,
+                showExtraLinks: false,
+              })),
 
         secondaryNav:
           relations.secondaryNav
-            .slot('mode', 'track'),
+            .slot('mode', data.firstTrackInSingle ? 'album' : 'track'),
 
         leftSidebar: relations.sidebar,
 
diff --git a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js
index 61654512..cd7bb014 100644
--- a/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js
+++ b/src/content/dependencies/generateTrackInfoPageFeaturedByFlashesList.js
@@ -2,9 +2,6 @@ import {sortFlashesChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['linkFlash', 'linkTrack'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     enableFlashesAndGames:
       wikiInfo.enableFlashesAndGames,
diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js
new file mode 100644
index 00000000..1793b73f
--- /dev/null
+++ b/src/content/dependencies/generateTrackInfoPageOtherReleasesLine.js
@@ -0,0 +1,80 @@
+import {onlyItem, stitchArrays} from '#sugar';
+
+export default {
+  query(track) {
+    const query = {};
+
+    query.singleSingle =
+      onlyItem(
+        track.otherReleases.filter(track => track.album.style === 'single'));
+
+    query.regularReleases =
+      (query.singleSingle
+        ? track.otherReleases.filter(track => track !== query.singleSingle)
+        : track.otherReleases);
+
+    return query;
+  },
+
+  relations: (relation, query, _track) => ({
+    singleLink:
+      (query.singleSingle
+        ? relation('linkTrack', query.singleSingle)
+        : null),
+
+    trackLinks:
+      query.regularReleases
+        .map(track => relation('linkTrack', track)),
+  }),
+
+  data: (query, _track) => ({
+    albumNames:
+      query.regularReleases
+        .map(track => track.album.name),
+
+    albumColors:
+      query.regularReleases
+        .map(track => track.album.color),
+  }),
+
+  generate: (data, relations, {html, language}) =>
+    language.encapsulate('releaseInfo.alsoReleased', capsule =>
+      language.encapsulate(capsule, workingCapsule => {
+        const workingOptions = {};
+
+        let any = false;
+
+        const albumList =
+          language.formatConjunctionList(
+            stitchArrays({
+              trackLink: relations.trackLinks,
+              albumName: data.albumNames,
+              albumColor: data.albumColors,
+            }).map(({trackLink, albumName, albumColor}) =>
+                trackLink.slots({
+                  content: language.sanitize(albumName),
+                  color: albumColor,
+                })));
+
+        if (!html.isBlank(albumList)) {
+          any = true;
+          workingCapsule += '.onAlbums';
+          workingOptions.albums = albumList;
+        }
+
+        if (relations.singleLink) {
+          any = true;
+          workingCapsule += '.asSingle';
+          workingOptions.single =
+            relations.singleLink.slots({
+              content: language.$(capsule, 'single'),
+            });
+        }
+
+        if (any) {
+          return language.$(workingCapsule, workingOptions);
+        } else {
+          return html.blank();
+        }
+      })),
+};
diff --git a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js b/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js
deleted file mode 100644
index ebd76577..00000000
--- a/src/content/dependencies/generateTrackInfoPageOtherReleasesList.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import {stitchArrays} from '#sugar';
-
-export default {
-  contentDependencies: ['linkTrack'],
-  extraDependencies: ['html', 'language'],
-
-  relations: (relation, track) => ({
-    trackLinks:
-      track.otherReleases
-        .map(track => relation('linkTrack', track)),
-  }),
-
-  data: (track) => ({
-    albumNames:
-      track.otherReleases
-        .map(track => track.album.name),
-
-    albumColors:
-      track.otherReleases
-        .map(track => track.album.color),
-  }),
-
-  generate: (data, relations, {html, language}) =>
-    html.tag('p',
-      {[html.onlyIfContent]: true},
-
-      language.$('releaseInfo.alsoReleasedOn', {
-        [language.onlyIfOptions]: ['albums'],
-
-        albums:
-          language.formatConjunctionList(
-            stitchArrays({
-              trackLink: relations.trackLinks,
-              albumName: data.albumNames,
-              albumColor: data.albumColors,
-            }).map(({trackLink, albumName, albumColor}) =>
-                trackLink.slots({
-                  content: language.sanitize(albumName),
-                  color: albumColor,
-                }))),
-      })),
-};
diff --git a/src/content/dependencies/generateTrackInfoPagePreviousProductionLine.js b/src/content/dependencies/generateTrackInfoPagePreviousProductionLine.js
new file mode 100644
index 00000000..f7f455c1
--- /dev/null
+++ b/src/content/dependencies/generateTrackInfoPagePreviousProductionLine.js
@@ -0,0 +1,38 @@
+import {stitchArrays} from '#sugar';
+import {getKebabCase} from '#wiki-data';
+
+export default {
+  relations: (relation, track) => ({
+    trackLinks:
+      track.followingProductionTracks
+        .map(track => relation('linkTrack', track)),
+
+    albumLinks:
+      track.followingProductionTracks
+        .map(following =>
+          (following.album !== track.album &&
+           getKebabCase(following.name) === getKebabCase(track.name)
+
+            ? relation('linkAlbum', following.album)
+            : null)),
+  }),
+
+  generate: (relations, {language}) =>
+    language.encapsulate('releaseInfo.previousProduction', capsule =>
+      language.$(capsule, {
+        [language.onlyIfOptions]: ['tracks'],
+
+        tracks:
+          language.formatConjunctionList(
+            stitchArrays({
+              trackLink: relations.trackLinks,
+              albumLink: relations.albumLinks,
+            }).map(({trackLink, albumLink}) =>
+                (albumLink
+                  ? language.$(capsule, 'trackOnAlbum', {
+                      track: trackLink,
+                      album: albumLink,
+                    })
+                  : trackLink))),
+      })),
+};
diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js
index 53a32536..c259c914 100644
--- a/src/content/dependencies/generateTrackList.js
+++ b/src/content/dependencies/generateTrackList.js
@@ -1,14 +1,21 @@
 export default {
-  contentDependencies: ['generateTrackListItem'],
-  extraDependencies: ['html'],
-
-  relations: (relation, tracks) => ({
+  relations: (relation, tracks, contextContributions) => ({
     items:
-      tracks
-        .map(track => relation('generateTrackListItem', track, [])),
+      tracks.map(track =>
+        relation('generateTrackListItem', track, contextContributions)),
   }),
 
   slots: {
+    showArtists: {
+      validate: v => v.is(true, false, 'auto'),
+      default: 'auto',
+    },
+
+    showDuration: {
+      type: 'boolean',
+      default: false,
+    },
+
     colorMode: {
       validate: v => v.is('none', 'track', 'line'),
       default: 'track',
@@ -21,8 +28,8 @@ export default {
 
       relations.items.map(item =>
         item.slots({
-          showArtists: true,
-          showDuration: false,
+          showArtists: slots.showArtists,
+          showDuration: slots.showDuration,
           colorMode: slots.colorMode,
         }))),
 };
diff --git a/src/content/dependencies/generateTrackListDividedByGroups.js b/src/content/dependencies/generateTrackListDividedByGroups.js
index 230868d6..419d7c0f 100644
--- a/src/content/dependencies/generateTrackListDividedByGroups.js
+++ b/src/content/dependencies/generateTrackListDividedByGroups.js
@@ -1,20 +1,12 @@
 import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateContentHeading',
-    'generateTrackList',
-    'linkGroup',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     divideTrackListsByGroups:
       wikiInfo.divideTrackListsByGroups,
   }),
 
-  query(sprawl, tracks) {
+  query(sprawl, tracks, _contextTrack) {
     const dividingGroups = sprawl.divideTrackListsByGroups;
 
     const groupings = new Map();
@@ -50,10 +42,10 @@ export default {
     return {groups, groupedTracks, ungroupedTracks};
   },
 
-  relations: (relation, query, sprawl, tracks) => ({
+  relations: (relation, query, sprawl, tracks, contextTrack) => ({
     flatList:
       (empty(sprawl.divideTrackListsByGroups)
-        ? relation('generateTrackList', tracks)
+        ? relation('generateNearbyTrackList', tracks, contextTrack, [])
         : null),
 
     contentHeading:
@@ -65,12 +57,12 @@ export default {
 
     groupedTrackLists:
       query.groupedTracks
-        .map(tracks => relation('generateTrackList', tracks)),
+        .map(tracks => relation('generateNearbyTrackList', tracks, contextTrack, [])),
 
     ungroupedTrackList:
       (empty(query.ungroupedTracks)
         ? null
-        : relation('generateTrackList', query.ungroupedTracks)),
+        : relation('generateNearbyTrackList', query.ungroupedTracks, contextTrack, [])),
   }),
 
   data: (query, _sprawl, _tracks) => ({
diff --git a/src/content/dependencies/generateTrackListItem.js b/src/content/dependencies/generateTrackListItem.js
index 3c850a18..c8c57534 100644
--- a/src/content/dependencies/generateTrackListItem.js
+++ b/src/content/dependencies/generateTrackListItem.js
@@ -1,21 +1,19 @@
 export default {
-  contentDependencies: [
-    'generateArtistCredit',
-    'generateColorStyleAttribute',
-    'generateTrackListMissingDuration',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, track, contextContributions) => ({
     trackLink:
       relation('linkTrack', track),
 
-    credit:
+    contextualCredit:
+      relation('generateArtistCredit',
+        track.artistContribs,
+        contextContributions,
+        track.artistTextInLists),
+
+    acontextualCredit:
       relation('generateArtistCredit',
         track.artistContribs,
-        contextContributions),
+        [],
+        track.artistTextInLists),
 
     colorStyle:
       relation('generateColorStyleAttribute', track.color),
@@ -35,12 +33,11 @@ export default {
   }),
 
   slots: {
-    // showArtists enables showing artists *at all.* It doesn't take precedence
-    // over behavior which automatically collapses (certain) artists because of
-    // provided context contributions.
+    // true always shows artists, false never does; 'auto' shows only if
+    // the track's artists differ from the given context contributions.
     showArtists: {
-      type: 'boolean',
-      default: true,
+      validate: v => v.is(true, false, 'auto'),
+      default: 'auto',
     },
 
     // If true and the track doesn't have a duration, a missing-duration cue
@@ -80,26 +77,33 @@ export default {
                 : relations.missingDuration);
           }
 
-          const artistCapsule = language.encapsulate(itemCapsule, 'withArtists');
-
-          relations.credit.setSlots({
-            normalStringKey:
-              artistCapsule + '.by',
-
-            featuringStringKey:
-              artistCapsule + '.featuring',
-
-            normalFeaturingStringKey:
-              artistCapsule + '.by.featuring',
-          });
-
-          if (!html.isBlank(relations.credit)) {
-            workingCapsule += '.withArtists';
-            workingOptions.by =
-              html.tag('span', {class: 'by'},
-                // TODO: This is obviously evil.
-                html.metatag('chunkwrap', {split: /,| (?=and)/},
-                  html.resolve(relations.credit)));
+          const chosenCredit =
+            (slots.showArtists === true
+              ? relations.acontextualCredit
+           : slots.showArtists === 'auto'
+              ? relations.contextualCredit
+              : null);
+
+          if (chosenCredit) {
+            const artistCapsule = language.encapsulate(itemCapsule, 'withArtists');
+
+            chosenCredit.setSlots({
+              normalStringKey:
+                artistCapsule + '.by',
+
+              featuringStringKey:
+                artistCapsule + '.featuring',
+
+              normalFeaturingStringKey:
+                artistCapsule + '.by.featuring',
+            });
+
+            if (!html.isBlank(chosenCredit)) {
+              workingCapsule += '.withArtists';
+              workingOptions.by =
+                html.tag('span', {class: 'by'},
+                  chosenCredit);
+            }
           }
 
           return language.$(workingCapsule, workingOptions);
diff --git a/src/content/dependencies/generateTrackListMissingDuration.js b/src/content/dependencies/generateTrackListMissingDuration.js
index b5917982..da3113a2 100644
--- a/src/content/dependencies/generateTrackListMissingDuration.js
+++ b/src/content/dependencies/generateTrackListMissingDuration.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateTextWithTooltip', 'generateTooltip'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation) => ({
     textWithTooltip:
       relation('generateTextWithTooltip'),
diff --git a/src/content/dependencies/generateTrackNavLinks.js b/src/content/dependencies/generateTrackNavLinks.js
index 6a8b7c64..d18e6cad 100644
--- a/src/content/dependencies/generateTrackNavLinks.js
+++ b/src/content/dependencies/generateTrackNavLinks.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkAlbum', 'linkTrack'],
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, track) => ({
     albumLink:
       relation('linkAlbum', track.album),
@@ -11,6 +8,9 @@ export default {
   }),
 
   data: (track) => ({
+    albumStyle:
+      track.album.style,
+
     hasTrackNumbers:
       track.album.hasTrackNumbers,
 
@@ -28,7 +28,13 @@ export default {
     language.encapsulate('trackPage.nav', navCapsule => [
       {auto: 'home'},
 
-      {html: relations.albumLink.slot('color', false)},
+      {
+        html: relations.albumLink.slot('color', false),
+        accent:
+          (data.albumStyle === 'single'
+            ? language.$(navCapsule, 'singleAccent')
+            : null),
+      },
 
       {
         html:
diff --git a/src/content/dependencies/generateTrackReferencedArtworksPage.js b/src/content/dependencies/generateTrackReferencedArtworksPage.js
index 93438c5b..a2612067 100644
--- a/src/content/dependencies/generateTrackReferencedArtworksPage.js
+++ b/src/content/dependencies/generateTrackReferencedArtworksPage.js
@@ -1,19 +1,10 @@
 export default {
-  contentDependencies: [
-    'generateAlbumStyleRules',
-    'generateBackToTrackLink',
-    'generateReferencedArtworksPage',
-    'generateTrackNavLinks',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, track) => ({
     page:
       relation('generateReferencedArtworksPage', track.trackArtworks[0]),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', track.album, track),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', track.album, track),
 
     navLinks:
       relation('generateTrackNavLinks', track),
@@ -35,7 +26,7 @@ export default {
             data.name,
         }),
 
-      styleRules: [relations.albumStyleRules],
+      styleTags: relations.albumStyleTags,
 
       navLinks:
         html.resolve(
diff --git a/src/content/dependencies/generateTrackReferencingArtworksPage.js b/src/content/dependencies/generateTrackReferencingArtworksPage.js
index e9818bad..be13dd79 100644
--- a/src/content/dependencies/generateTrackReferencingArtworksPage.js
+++ b/src/content/dependencies/generateTrackReferencingArtworksPage.js
@@ -1,19 +1,10 @@
 export default {
-  contentDependencies: [
-    'generateAlbumStyleRules',
-    'generateBackToTrackLink',
-    'generateReferencingArtworksPage',
-    'generateTrackNavLinks',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, track) => ({
     page:
       relation('generateReferencingArtworksPage', track.trackArtworks[0]),
 
-    albumStyleRules:
-      relation('generateAlbumStyleRules', track.album, track),
+    albumStyleTags:
+      relation('generateAlbumStyleTags', track.album, track),
 
     navLinks:
       relation('generateTrackNavLinks', track),
@@ -35,7 +26,7 @@ export default {
             data.name,
         }),
 
-      styleRules: [relations.albumStyleRules],
+      styleTags: relations.albumStyleTags,
 
       navLinks:
         html.resolve(
diff --git a/src/content/dependencies/generateTrackReleaseBox.js b/src/content/dependencies/generateTrackReleaseBox.js
index ef02e2b9..c880fe63 100644
--- a/src/content/dependencies/generateTrackReleaseBox.js
+++ b/src/content/dependencies/generateTrackReleaseBox.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generatePageSidebarBox',
-    'linkTrack',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, track) => ({
     box:
       relation('generatePageSidebarBox'),
diff --git a/src/content/dependencies/generateTrackReleaseInfo.js b/src/content/dependencies/generateTrackReleaseInfo.js
index 54e462c7..0207e574 100644
--- a/src/content/dependencies/generateTrackReleaseInfo.js
+++ b/src/content/dependencies/generateTrackReleaseInfo.js
@@ -1,24 +1,19 @@
-import {empty} from '#sugar';
+import {compareArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateReleaseInfoContributionsLine',
-    'linkExternal',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations(relation, track) {
     const relations = {};
 
-    relations.artistContributionLinks =
-      relation('generateReleaseInfoContributionsLine', track.artistContribs);
+    relations.artistContributionsLine =
+      relation('generateReleaseInfoContributionsLine',
+        track.artistContribs,
+        track.artistText);
 
-    if (!empty(track.urls)) {
-      relations.externalLinks =
-        track.urls.map(url =>
-          relation('linkExternal', url));
-    }
+    relations.listenLine =
+      relation('generateReleaseInfoListenLine', track);
+
+    relations.albumLink =
+      relation('linkAlbum', track.album);
 
     return relations;
   },
@@ -30,6 +25,16 @@ export default {
     data.date = track.date;
     data.duration = track.duration;
 
+    const {album} = track;
+
+    data.showAlbum =
+      album.showAlbumInTracksWithoutArtists &&
+      track.artistContribs.every(({annotation}) => !annotation) &&
+      compareArrays(
+        track.artistContribs.map(({artist}) => artist),
+        album.artistContribs.map(({artist}) => artist),
+        {checkOrder: true});
+
     if (
       track.hasUniqueCoverArt &&
       +track.coverArtDate !== +track.date
@@ -48,10 +53,21 @@ export default {
           {[html.joinChildren]: html.tag('br')},
 
           [
-            relations.artistContributionLinks.slots({
-              stringKey: capsule + '.by',
-              featuringStringKey: capsule + '.by.featuring',
-              chronologyKind: 'track',
+            language.encapsulate(capsule, 'by', capsule => {
+              const withAlbum =
+                (data.showAlbum ? '.withAlbum' : '');
+
+              const albumOptions =
+                (data.showAlbum ? {album: relations.albumLink} : {});
+
+              return relations.artistContributionsLine.slots({
+                stringKey: capsule + withAlbum,
+                featuringStringKey: capsule + '.featuring' + withAlbum,
+
+                additionalStringOptions: albumOptions,
+
+                chronologyKind: 'track',
+              });
             }),
 
             language.$(capsule, 'released', {
@@ -66,17 +82,9 @@ export default {
           ]),
 
         html.tag('p',
-          language.encapsulate(capsule, 'listenOn', capsule =>
-            (relations.externalLinks
-              ? language.$(capsule, {
-                  links:
-                    language.formatDisjunctionList(
-                      relations.externalLinks
-                        .map(link => link.slot('context', 'track'))),
-                })
-              : language.$(capsule, 'noLinks', {
-                  name:
-                    html.tag('i', data.name),
-                })))),
+          relations.listenLine.slots({
+            visibleWithoutLinks: true,
+            context: ['track'],
+          })),
       ])),
 };
diff --git a/src/content/dependencies/generateTrackSocialEmbed.js b/src/content/dependencies/generateTrackSocialEmbed.js
index 310816f3..94453f7d 100644
--- a/src/content/dependencies/generateTrackSocialEmbed.js
+++ b/src/content/dependencies/generateTrackSocialEmbed.js
@@ -1,11 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateSocialEmbed',
-    'generateTrackSocialEmbedDescription',
-  ],
-
-  extraDependencies: ['absoluteTo', 'language'],
-
   relations(relation, track) {
     return {
       socialEmbed:
diff --git a/src/content/dependencies/generateTrackSocialEmbedDescription.js b/src/content/dependencies/generateTrackSocialEmbedDescription.js
index 4706aa26..97a4017f 100644
--- a/src/content/dependencies/generateTrackSocialEmbedDescription.js
+++ b/src/content/dependencies/generateTrackSocialEmbedDescription.js
@@ -1,8 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  extraDependencies: ['html', 'language'],
-
   data: (track) => ({
     artistNames:
       track.artistContribs
diff --git a/src/content/dependencies/generateUnsafeMunchy.js b/src/content/dependencies/generateUnsafeMunchy.js
index c11aadc7..df8231ef 100644
--- a/src/content/dependencies/generateUnsafeMunchy.js
+++ b/src/content/dependencies/generateUnsafeMunchy.js
@@ -1,6 +1,4 @@
 export default {
-  extraDependencies: ['html'],
-
   slots: {
     contentSource: {type: 'string'},
   },
diff --git a/src/content/dependencies/generateWallpaperStyleTag.js b/src/content/dependencies/generateWallpaperStyleTag.js
new file mode 100644
index 00000000..b89f01c2
--- /dev/null
+++ b/src/content/dependencies/generateWallpaperStyleTag.js
@@ -0,0 +1,77 @@
+import {empty, stitchArrays} from '#sugar';
+
+export default {
+  relations: (relation) => ({
+    styleTag:
+      relation('generateStyleTag'),
+  }),
+
+  slots: {
+    singleWallpaperPath: {
+      validate: v => v.strictArrayOf(v.isString),
+    },
+
+    singleWallpaperStyle: {
+      validate: v => v.isString,
+    },
+
+    wallpaperPartPaths: {
+      validate: v =>
+        v.strictArrayOf(v.optional(v.strictArrayOf(v.isString))),
+    },
+
+    wallpaperPartStyles: {
+      validate: v =>
+        v.strictArrayOf(v.optional(v.isString)),
+    },
+  },
+
+  generate(relations, slots, {html, to}) {
+    const attributes = html.attributes();
+    const rules = [];
+
+    attributes.add('class', 'wallpaper-style');
+
+    if (empty(slots.wallpaperPartPaths)) {
+      attributes.set('data-wallpaper-mode', 'one');
+
+      rules.push({
+        select: 'body::before',
+        declare: [
+          `background-image: url("${to(...slots.singleWallpaperPath)}");`,
+          slots.singleWallpaperStyle,
+        ],
+      });
+    } else {
+      attributes.set('data-wallpaper-mode', 'parts');
+      attributes.set('data-num-wallpaper-parts', slots.wallpaperPartPaths.length);
+
+      stitchArrays({
+        path: slots.wallpaperPartPaths,
+        style: slots.wallpaperPartStyles,
+      }).forEach(({path, style}, index) => {
+          rules.push({
+            select: `.wallpaper-part:nth-child(${index + 1})`,
+            declare: [
+              path && `background-image: url("${to(...path)}");`,
+              style,
+            ],
+          });
+        });
+
+      rules.push({
+        select: 'body::before',
+        declare: [
+          'display: none;',
+        ],
+      });
+    }
+
+    relations.styleTag.setSlots({
+      attributes,
+      rules,
+    });
+
+    return relations.styleTag;
+  },
+};
diff --git a/src/content/dependencies/generateWikiHomepageActionsRow.js b/src/content/dependencies/generateWikiHomepageActionsRow.js
index 9f501099..5e3ff381 100644
--- a/src/content/dependencies/generateWikiHomepageActionsRow.js
+++ b/src/content/dependencies/generateWikiHomepageActionsRow.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generateGridActionLinks', 'transformContent'],
-
   relations: (relation, row) => ({
     template:
       relation('generateGridActionLinks'),
diff --git a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js
index b45bfc19..8f4b3400 100644
--- a/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js
+++ b/src/content/dependencies/generateWikiHomepageAlbumCarouselRow.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generateCoverCarousel', 'image', 'linkAlbum'],
-
   relations: (relation, row) => ({
     coverCarousel:
       relation('generateCoverCarousel'),
diff --git a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js
index a00136ba..eb3417d7 100644
--- a/src/content/dependencies/generateWikiHomepageAlbumGridRow.js
+++ b/src/content/dependencies/generateWikiHomepageAlbumGridRow.js
@@ -2,9 +2,6 @@ import {empty, stitchArrays} from '#sugar';
 import {getNewAdditions, getNewReleases} from '#wiki-data';
 
 export default {
-  contentDependencies: ['generateCoverGrid', 'image', 'linkAlbum'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}, row) {
     const sprawl = {};
 
@@ -21,8 +18,7 @@ export default {
         sprawl.albums =
           (row.sourceGroup
             ? row.sourceGroup.albums
-                .slice()
-                .reverse()
+                .toReversed()
                 .filter(album => album.isListedOnHomepage)
                 .slice(0, row.countAlbumsFromGroup)
             : []);
diff --git a/src/content/dependencies/generateWikiHomepageNewsBox.js b/src/content/dependencies/generateWikiHomepageNewsBox.js
index 83a27695..3a06a7c3 100644
--- a/src/content/dependencies/generateWikiHomepageNewsBox.js
+++ b/src/content/dependencies/generateWikiHomepageNewsBox.js
@@ -1,14 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generatePageSidebarBox',
-    'linkNewsEntry',
-    'transformContent',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({newsData}) => ({
     entries:
       newsData.slice(0, 3),
diff --git a/src/content/dependencies/generateWikiHomepagePage.js b/src/content/dependencies/generateWikiHomepagePage.js
index 8c09a007..9029131b 100644
--- a/src/content/dependencies/generateWikiHomepagePage.js
+++ b/src/content/dependencies/generateWikiHomepagePage.js
@@ -1,15 +1,4 @@
 export default {
-  contentDependencies: [
-    'generatePageLayout',
-    'generatePageSidebar',
-    'generatePageSidebarBox',
-    'generateWikiHomepageNewsBox',
-    'generateWikiHomepageSection',
-    'transformContent',
-  ],
-
-  extraDependencies: ['wikiData'],
-
   sprawl: ({wikiInfo}) => ({
     wikiName:
       wikiInfo.name,
diff --git a/src/content/dependencies/generateWikiHomepageSection.js b/src/content/dependencies/generateWikiHomepageSection.js
index 49a474da..5fc0c76f 100644
--- a/src/content/dependencies/generateWikiHomepageSection.js
+++ b/src/content/dependencies/generateWikiHomepageSection.js
@@ -1,13 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateWikiHomepageActionsRow',
-    'generateWikiHomepageAlbumCarouselRow',
-    'generateWikiHomepageAlbumGridRow',
-  ],
-
-  extraDependencies: ['html'],
-
   relations: (relation, homepageSection) => ({
     colorStyle:
       relation('generateColorStyleAttribute', homepageSection.color),
diff --git a/src/content/dependencies/generateWikiWallpaperStyleTag.js b/src/content/dependencies/generateWikiWallpaperStyleTag.js
new file mode 100644
index 00000000..be52bcc1
--- /dev/null
+++ b/src/content/dependencies/generateWikiWallpaperStyleTag.js
@@ -0,0 +1,35 @@
+export default {
+  sprawl: ({wikiInfo}) => ({wikiInfo}),
+
+  relations: (relation) => ({
+    wallpaperStyleTag:
+      relation('generateWallpaperStyleTag'),
+  }),
+
+  data: ({wikiInfo}) => ({
+    singleWallpaperPath: [
+      'media.path',
+      'bg.' + wikiInfo.wikiWallpaperFileExtension,
+    ],
+
+    singleWallpaperStyle:
+      wikiInfo.wikiWallpaperStyle,
+
+    wallpaperPartPaths:
+      wikiInfo.wikiWallpaperParts.map(part =>
+        (part.asset
+          ? ['media.path', part.asset]
+          : null)),
+
+    wallpaperPartStyles:
+      wikiInfo.wikiWallpaperParts.map(part => part.style),
+  }),
+
+  generate: (data, relations) =>
+    relations.wallpaperStyleTag.slots({
+      singleWallpaperPath: data.singleWallpaperPath,
+      singleWallpaperStyle: data.singleWallpaperStyle,
+      wallpaperPartPaths: data.wallpaperPartPaths,
+      wallpaperPartStyles: data.wallpaperPartStyles,
+    }),
+};
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index bf47b14f..aacf2fed 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -2,20 +2,6 @@ import {logWarn} from '#cli';
 import {empty} from '#sugar';
 
 export default {
-  extraDependencies: [
-    'checkIfImagePathHasCachedThumbnails',
-    'getDimensionsOfImagePath',
-    'getSizeOfMediaFile',
-    'getThumbnailEqualOrSmaller',
-    'getThumbnailsAvailableForDimensions',
-    'html',
-    'language',
-    'missingImagePaths',
-    'to',
-  ],
-
-  contentDependencies: ['generateColorStyleAttribute'],
-
   relations: (relation, _artwork) => ({
     colorStyle:
       relation('generateColorStyleAttribute'),
@@ -42,6 +28,8 @@ export default {
 
   slots: {
     thumb: {type: 'string'},
+    responsiveThumb: {type: 'boolean', default: false},
+    responsiveSizes: {type: 'string'},
 
     reveal: {type: 'boolean', default: true},
     lazy: {type: 'boolean', default: false},
@@ -60,6 +48,12 @@ export default {
       mutable: false,
     },
 
+    // Added to the <img>.
+    imgAttributes: {
+      type: 'attributes',
+      mutable: false,
+    },
+
     // Added to the <img> itself.
     alt: {type: 'string'},
 
@@ -114,12 +108,11 @@ export default {
     // src string directly when a parts-formed path *is* available seems wrong.
     // It should be possible to do urls.from(slots.path[0]).to(...slots.path),
     // for example, but will require reworking the control flow here a little.
-    let mediaSrc = null;
+    let mediaSrc = decodeURIComponent(originalSrc);
     if (originalSrc.startsWith(to('media.root'))) {
-      mediaSrc =
-        originalSrc
-          .slice(to('media.root').length)
-          .replace(/^\//, '');
+      mediaSrc = mediaSrc
+        .slice(to('media.root').length)
+        .replace(/^\//, '');
     }
 
     const isMissingImageFile =
@@ -141,6 +134,8 @@ export default {
     const imgAttributes = html.attributes([
       {class: 'image'},
 
+      slots.imgAttributes,
+
       slots.alt && {alt: slots.alt},
 
       dimensions &&
@@ -205,31 +200,29 @@ export default {
     // so it won't be set if thumbnails aren't available.
     let revealSrc = null;
 
+    let originalDimensions;
+    let availableThumbs;
+    let selectedThumbtack;
+
+    const getThumbSrc = (thumbtack) =>
+      to('thumb.path', mediaSrc.replace(/\.(png|jpg)$/, `.${thumbtack}.jpg`));
+
     // If thumbnails are available *and* being used, calculate thumbSrc,
     // and provide some attributes relevant to the large image overlay.
     if (hasThumbnails && slots.thumb) {
-      const selectedSize =
+      selectedThumbtack =
         getThumbnailEqualOrSmaller(slots.thumb, mediaSrc);
 
-      const mediaSrcJpeg =
-        mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`);
-
       displaySrc =
-        to('thumb.path', mediaSrcJpeg);
+        getThumbSrc(selectedThumbtack);
 
       if (willReveal) {
-        const miniSize =
-          getThumbnailEqualOrSmaller('mini', mediaSrc);
-
-        const mediaSrcJpeg =
-          mediaSrc.replace(/\.(png|jpg)$/, `.${miniSize}.jpg`);
-
         revealSrc =
-          to('thumb.path', mediaSrcJpeg);
+          getThumbSrc(getThumbnailEqualOrSmaller('mini', mediaSrc));
       }
 
-      const originalDimensions = getDimensionsOfImagePath(mediaSrc);
-      const availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions);
+      originalDimensions = getDimensionsOfImagePath(mediaSrc);
+      availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions);
 
       const fileSize =
         (willLink && mediaSrc
@@ -245,11 +238,54 @@ export default {
         !empty(availableThumbs) &&
           {'data-thumbs':
               availableThumbs
-                .map(([name, size]) => `${name}:${size}`)
+                .map(([tack, size]) => `${tack}:${size}`)
                 .join(' ')},
       ]);
     }
 
+    let displayStaticImg =
+      html.tag('img',
+        imgAttributes,
+        {src: displaySrc});
+
+    if (hasThumbnails && slots.responsiveThumb) responsive: {
+      if (slots.lazy) {
+        logWarn`${'responsiveThumb'} and ${'lazy'} are used together, but not compatible`;
+        break responsive;
+      }
+
+      if (!slots.thumb) {
+        logWarn`${'responsiveThumb'} must be used alongside a default ${'thumb'}`;
+        break responsive;
+      }
+
+      const srcset = [
+        // Never load the original source, which might be a very large
+        // uncompressed file. Bah!
+        /* [originalSrc, `${Math.min(...originalDimensions)}w`], */
+
+        ...availableThumbs.map(([tack, size]) =>
+          [getThumbSrc(tack), `${Math.floor(0.95 * size)}w`]),
+
+        // fallback
+        [displaySrc],
+      ].map(line => line.join(' ')).join(',\n');
+
+      displayStaticImg =
+        html.tag('img',
+          imgAttributes,
+
+          {sizes:
+            (slots.responsiveSizes.match(/(?=(?:,|^))\s*\S/)
+                // slot provided fallback size
+              ? slots.responsiveSizes
+                // default fallback size
+              : slots.responsiveSizes + ',\n' +
+                new Map(availableThumbs).get(selectedThumbtack) + 'px')},
+
+          {srcset});
+    }
+
     if (!displaySrc) {
       return (
         prepare(
@@ -258,10 +294,7 @@ export default {
     }
 
     const images = {
-      displayStatic:
-        html.tag('img',
-          imgAttributes,
-          {src: displaySrc}),
+      displayStatic: displayStaticImg,
 
       displayLazy:
         slots.lazy &&
diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js
index a5009804..cfa6346c 100644
--- a/src/content/dependencies/index.js
+++ b/src/content/dependencies/index.js
@@ -11,6 +11,11 @@ import {colors, logWarn} from '#cli';
 import contentFunction, {ContentFunctionSpecError} from '#content-function';
 import {annotateFunction} from '#sugar';
 
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const codeSrcPath = path.resolve(__dirname, '..');
+const codeRootPath = path.resolve(codeSrcPath, '..');
+
 function cachebust(filePath) {
   if (filePath in cachebust.cache) {
     cachebust.cache[filePath] += 1;
@@ -42,7 +47,9 @@ export function watchContentDependencies({
     close,
   });
 
-  const eslint = new ESLint();
+  const eslint = new ESLint({
+    cwd: codeRootPath,
+  });
 
   const metaPath = fileURLToPath(import.meta.url);
   const metaDirname = path.dirname(metaPath);
@@ -87,6 +94,8 @@ export function watchContentDependencies({
     const filePaths = files.map(file => path.join(watchPath, file));
     for (const filePath of filePaths) {
       if (filePath === metaPath) continue;
+      if (filePath.endsWith('.DS_Store')) continue;
+
       const functionName = getFunctionName(filePath);
       if (!isMocked(functionName)) {
         contentDependencies[functionName] = null;
@@ -98,8 +107,9 @@ export function watchContentDependencies({
     watcher.on('all', (event, filePath) => {
       if (!['add', 'change'].includes(event)) return;
       if (filePath === metaPath) return;
-      handlePathUpdated(filePath);
+      if (filePath.endsWith('.DS_Store')) return;
 
+      handlePathUpdated(filePath);
     });
 
     watcher.on('unlink', (filePath) => {
@@ -108,6 +118,8 @@ export function watchContentDependencies({
         return;
       }
 
+      if (filePath.endsWith('.DS_Store')) return;
+
       handlePathRemoved(filePath);
     });
 
diff --git a/src/content/dependencies/linkAdditionalFile.js b/src/content/dependencies/linkAdditionalFile.js
index a8a940b1..1b5e650f 100644
--- a/src/content/dependencies/linkAdditionalFile.js
+++ b/src/content/dependencies/linkAdditionalFile.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkTemplate'],
-
   query: (file, filename) => ({
     index:
       file.filenames.indexOf(filename),
diff --git a/src/content/dependencies/linkAlbum.js b/src/content/dependencies/linkAlbum.js
index 36b0d13a..085d5f62 100644
--- a/src/content/dependencies/linkAlbum.js
+++ b/src/content/dependencies/linkAlbum.js
@@ -1,8 +1,18 @@
 export default {
-  contentDependencies: ['linkThing'],
+  relations: (relation, album) => ({
+    link:
+      (album.style === 'single'
+        ? relation('linkTrack', album.tracks[0])
+        : relation('linkThing', 'localized.album', album)),
+  }),
 
-  relations: (relation, album) =>
-    ({link: relation('linkThing', 'localized.album', album)}),
+  data: (album) => ({
+    style: album.style,
+    name: album.name,
+  }),
 
-  generate: (relations) => relations.link,
+  generate: (data, relations, {language}) =>
+    (data.style === 'single'
+      ? relations.link.slot('content', language.sanitize(data.name))
+      : relations.link),
 };
diff --git a/src/content/dependencies/linkAlbumCommentary.js b/src/content/dependencies/linkAlbumCommentary.js
index ab519fd6..f1917345 100644
--- a/src/content/dependencies/linkAlbumCommentary.js
+++ b/src/content/dependencies/linkAlbumCommentary.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, album) =>
     ({link: relation('linkThing', 'localized.albumCommentary', album)}),
 
diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js
index 45f8c2a9..ba572c8d 100644
--- a/src/content/dependencies/linkAlbumDynamically.js
+++ b/src/content/dependencies/linkAlbumDynamically.js
@@ -1,14 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'linkAlbumCommentary',
-    'linkAlbumGallery',
-    'linkAlbum',
-  ],
-
-  extraDependencies: ['html', 'pagePath'],
-
   relations: (relation, album) => ({
     galleryLink:
       relation('linkAlbumGallery', album),
diff --git a/src/content/dependencies/linkAlbumGallery.js b/src/content/dependencies/linkAlbumGallery.js
index e3f30a29..efba66d1 100644
--- a/src/content/dependencies/linkAlbumGallery.js
+++ b/src/content/dependencies/linkAlbumGallery.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, album) =>
     ({link: relation('linkThing', 'localized.albumGallery', album)}),
 
diff --git a/src/content/dependencies/linkAlbumReferencedArtworks.js b/src/content/dependencies/linkAlbumReferencedArtworks.js
index ba51b5e3..411bd2ab 100644
--- a/src/content/dependencies/linkAlbumReferencedArtworks.js
+++ b/src/content/dependencies/linkAlbumReferencedArtworks.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, album) =>
     ({link: relation('linkThing', 'localized.albumReferencedArtworks', album)}),
 
diff --git a/src/content/dependencies/linkAlbumReferencingArtworks.js b/src/content/dependencies/linkAlbumReferencingArtworks.js
index 4d5e799d..3aee9a4b 100644
--- a/src/content/dependencies/linkAlbumReferencingArtworks.js
+++ b/src/content/dependencies/linkAlbumReferencingArtworks.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, album) =>
     ({link: relation('linkThing', 'localized.albumReferencingArtworks', album)}),
 
diff --git a/src/content/dependencies/linkAnythingMan.js b/src/content/dependencies/linkAnythingMan.js
index e408c1b2..cb22baee 100644
--- a/src/content/dependencies/linkAnythingMan.js
+++ b/src/content/dependencies/linkAnythingMan.js
@@ -1,24 +1,13 @@
 export default {
-  contentDependencies: [
-    'linkAlbum',
-    'linkArtwork',
-    'linkFlash',
-    'linkTrack',
-  ],
-
-  query: (thing) => ({
-    referenceType: thing.constructor[Symbol.for('Thing.referenceType')],
-  }),
-
-  relations: (relation, query, thing) => ({
+  relations: (relation, thing) => ({
     link:
-      (query.referenceType === 'album'
+      (thing.isAlbum
         ? relation('linkAlbum', thing)
-     : query.referenceType === 'artwork'
+     : thing.isArtwork
         ? relation('linkArtwork', thing)
-     : query.referenceType === 'flash'
+     : thing.isFlash
         ? relation('linkFlash', thing)
-     : query.referenceType === 'track'
+     : thing.isTrack
         ? relation('linkTrack', thing)
         : null),
   }),
diff --git a/src/content/dependencies/linkArtTagDynamically.js b/src/content/dependencies/linkArtTagDynamically.js
index 964258e1..4514b7c1 100644
--- a/src/content/dependencies/linkArtTagDynamically.js
+++ b/src/content/dependencies/linkArtTagDynamically.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkArtTagGallery', 'linkArtTagInfo'],
-  extraDependencies: ['pagePath'],
-
   relations: (relation, artTag) => ({
     galleryLink: relation('linkArtTagGallery', artTag),
     infoLink: relation('linkArtTagInfo', artTag),
diff --git a/src/content/dependencies/linkArtTagGallery.js b/src/content/dependencies/linkArtTagGallery.js
index a92b69c1..92ab1ed3 100644
--- a/src/content/dependencies/linkArtTagGallery.js
+++ b/src/content/dependencies/linkArtTagGallery.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, artTag) =>
     ({link: relation('linkThing', 'localized.artTagGallery', artTag)}),
 
diff --git a/src/content/dependencies/linkArtTagInfo.js b/src/content/dependencies/linkArtTagInfo.js
index 409cb3c0..5eb2ac56 100644
--- a/src/content/dependencies/linkArtTagInfo.js
+++ b/src/content/dependencies/linkArtTagInfo.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, artTag) =>
     ({link: relation('linkThing', 'localized.artTagInfo', artTag)}),
 
diff --git a/src/content/dependencies/linkArtist.js b/src/content/dependencies/linkArtist.js
index 718ee6fa..917ae6b6 100644
--- a/src/content/dependencies/linkArtist.js
+++ b/src/content/dependencies/linkArtist.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, artist) =>
     ({link: relation('linkThing', 'localized.artist', artist)}),
 
diff --git a/src/content/dependencies/linkArtistGallery.js b/src/content/dependencies/linkArtistGallery.js
index 66dc172d..001eec1f 100644
--- a/src/content/dependencies/linkArtistGallery.js
+++ b/src/content/dependencies/linkArtistGallery.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, artist) =>
     ({link: relation('linkThing', 'localized.artistGallery', artist)}),
 
diff --git a/src/content/dependencies/linkArtistRollingWindow.js b/src/content/dependencies/linkArtistRollingWindow.js
new file mode 100644
index 00000000..6ab516ac
--- /dev/null
+++ b/src/content/dependencies/linkArtistRollingWindow.js
@@ -0,0 +1,6 @@
+export default {
+  relations: (relation, artist) =>
+    ({link: relation('linkThing', 'localized.artistRollingWindow', artist)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkArtwork.js b/src/content/dependencies/linkArtwork.js
index 8cd6f359..fce89229 100644
--- a/src/content/dependencies/linkArtwork.js
+++ b/src/content/dependencies/linkArtwork.js
@@ -1,16 +1,9 @@
 export default {
-  contentDependencies: ['linkAlbum', 'linkTrack'],
-
-  query: (artwork) => ({
-    referenceType:
-      artwork.thing.constructor[Symbol.for('Thing.referenceType')],
-  }),
-
-  relations: (relation, query, artwork) => ({
+  relations: (relation, artwork) => ({
     link:
-      (query.referenceType === 'album'
+      (artwork.thing.isAlbum
         ? relation('linkAlbum', artwork.thing)
-     : query.referenceType === 'track'
+     : artwork.thing.isTrack
         ? relation('linkTrack', artwork.thing)
         : null),
   }),
diff --git a/src/content/dependencies/linkCommentaryIndex.js b/src/content/dependencies/linkCommentaryIndex.js
index 5568ff84..e59b3641 100644
--- a/src/content/dependencies/linkCommentaryIndex.js
+++ b/src/content/dependencies/linkCommentaryIndex.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkStationaryIndex'],
-
   relations: (relation) =>
     ({link:
         relation(
diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js
index c658d461..aa9bdef9 100644
--- a/src/content/dependencies/linkContribution.js
+++ b/src/content/dependencies/linkContribution.js
@@ -1,12 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateContributionTooltip',
-    'generateTextWithTooltip',
-    'linkArtist',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, contribution) => ({
     artistLink:
       relation('linkArtist', contribution.artist),
@@ -24,13 +16,15 @@ export default {
   }),
 
   slots: {
+    content: {type: 'html', mutable: false},
+
     showAnnotation: {type: 'boolean', default: false},
     showExternalLinks: {type: 'boolean', default: false},
     showChronology: {type: 'boolean', default: false},
 
     trimAnnotation: {type: 'boolean', default: false},
 
-    preventWrapping: {type: 'boolean', default: true},
+    preventWrapping: {type: 'boolean', default: false},
     preventTooltip: {type: 'boolean', default: false},
 
     chronologyKind: {type: 'string'},
@@ -46,6 +40,10 @@ export default {
       language.encapsulate('misc.artistLink', workingCapsule => {
         const workingOptions = {};
 
+        if (!html.isBlank(slots.content)) {
+          relations.artistLink.setSlot('content', slots.content);
+        }
+
         // Filling slots early is necessary to actually give the tooltip
         // content. Otherwise, the coming-up html.isBlank() always reports
         // the tooltip as blank!
diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js
index c132baaf..ad8d4f23 100644
--- a/src/content/dependencies/linkExternal.js
+++ b/src/content/dependencies/linkExternal.js
@@ -1,9 +1,20 @@
 import {isExternalLinkContext, isExternalLinkStyle} from '#external-links';
 
 export default {
-  extraDependencies: ['html', 'language', 'wikiData'],
+  sprawl: ({wikiInfo}) => ({
+    canonicalBase:
+      wikiInfo.canonicalBase,
 
-  data: (url) => ({url}),
+    canonicalMediaBase:
+      wikiInfo.canonicalMediaBase,
+  }),
+
+  data: (sprawl, url) => ({
+    url,
+
+    canonicalBase:
+      sprawl.canonicalBase,
+  }),
 
   slots: {
     content: {
@@ -50,19 +61,33 @@ export default {
     },
   },
 
-  generate(data, slots, {html, language}) {
+  generate(data, slots, {html, language, to}) {
+    const {url} = data;
+
     let urlIsValid;
     try {
-      new URL(data.url);
+      new URL(url);
       urlIsValid = true;
-    } catch (error) {
+    } catch {
       urlIsValid = false;
     }
 
+    let href;
+    if (urlIsValid) {
+      const {canonicalBase, canonicalMediaBase} = data;
+      if (canonicalMediaBase && url.startsWith(canonicalMediaBase)) {
+        href = to('media.path', url.slice(canonicalMediaBase.length));
+      } else if (canonicalBase && url.startsWith(canonicalBase)) {
+        href = to('shared.path', url.slice(canonicalBase.length));
+      } else {
+        href = url;
+      }
+    }
+
     let formattedLink;
     if (urlIsValid) {
       formattedLink =
-        language.formatExternalLink(data.url, {
+        language.formatExternalLink(url, {
           style: slots.style,
           context: slots.context,
         });
@@ -70,7 +95,7 @@ export default {
       // Fall back to platform if nothing matched the desired style.
       if (html.isBlank(formattedLink) && slots.style !== 'platform') {
         formattedLink =
-          language.formatExternalLink(data.url, {
+          language.formatExternalLink(url, {
             style: 'platform',
             context: slots.context,
           });
@@ -85,7 +110,7 @@ export default {
 
     let linkContent;
     if (urlIsValid) {
-      linkAttributes.set('href', data.url);
+      linkAttributes.set('href', href);
 
       if (html.isBlank(slots.content)) {
         linkContent = formattedLink;
diff --git a/src/content/dependencies/linkFlash.js b/src/content/dependencies/linkFlash.js
index 93dd5a28..cfc01079 100644
--- a/src/content/dependencies/linkFlash.js
+++ b/src/content/dependencies/linkFlash.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, flash) =>
     ({link: relation('linkThing', 'localized.flash', flash)}),
 
diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js
index 82c23325..069bedf4 100644
--- a/src/content/dependencies/linkFlashAct.js
+++ b/src/content/dependencies/linkFlashAct.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['generateUnsafeMunchy', 'linkThing'],
-
   relations: (relation, flashAct) => ({
     unsafeMunchy:
       relation('generateUnsafeMunchy'),
diff --git a/src/content/dependencies/linkFlashIndex.js b/src/content/dependencies/linkFlashIndex.js
index 6dd0710e..9c1b076e 100644
--- a/src/content/dependencies/linkFlashIndex.js
+++ b/src/content/dependencies/linkFlashIndex.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkStationaryIndex'],
-
   relations: (relation) =>
     ({link:
         relation(
diff --git a/src/content/dependencies/linkFlashSide.js b/src/content/dependencies/linkFlashSide.js
index b77ca65a..6407ef25 100644
--- a/src/content/dependencies/linkFlashSide.js
+++ b/src/content/dependencies/linkFlashSide.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkFlashAct'],
-
   relations: (relation, flashSide) => ({
     link:
       relation('linkFlashAct', flashSide.acts[0]),
diff --git a/src/content/dependencies/linkGroup.js b/src/content/dependencies/linkGroup.js
index ebab1b5b..10bec2fb 100644
--- a/src/content/dependencies/linkGroup.js
+++ b/src/content/dependencies/linkGroup.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, group) =>
     ({link: relation('linkThing', 'localized.groupInfo', group)}),
 
diff --git a/src/content/dependencies/linkGroupDynamically.js b/src/content/dependencies/linkGroupDynamically.js
index 90303ed1..0b5bd85c 100644
--- a/src/content/dependencies/linkGroupDynamically.js
+++ b/src/content/dependencies/linkGroupDynamically.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkGroupGallery', 'linkGroup'],
-  extraDependencies: ['pagePath'],
-
   relations: (relation, group) => ({
     galleryLink: relation('linkGroupGallery', group),
     infoLink: relation('linkGroup', group),
diff --git a/src/content/dependencies/linkGroupExtra.js b/src/content/dependencies/linkGroupExtra.js
index bc3c0580..1a6161c1 100644
--- a/src/content/dependencies/linkGroupExtra.js
+++ b/src/content/dependencies/linkGroupExtra.js
@@ -1,13 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'linkGroup',
-    'linkGroupGallery',
-  ],
-
-  extraDependencies: ['html'],
-
   relations(relation, group) {
     const relations = {};
 
diff --git a/src/content/dependencies/linkGroupGallery.js b/src/content/dependencies/linkGroupGallery.js
index 86c4a0f3..957756d8 100644
--- a/src/content/dependencies/linkGroupGallery.js
+++ b/src/content/dependencies/linkGroupGallery.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, group) =>
     ({link: relation('linkThing', 'localized.groupGallery', group)}),
 
diff --git a/src/content/dependencies/linkListing.js b/src/content/dependencies/linkListing.js
index ac66919a..4eb2dce6 100644
--- a/src/content/dependencies/linkListing.js
+++ b/src/content/dependencies/linkListing.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-  extraDependencies: ['language'],
-
   relations: (relation, listing) =>
     ({link: relation('linkThing', 'localized.listing', listing)}),
 
diff --git a/src/content/dependencies/linkListingIndex.js b/src/content/dependencies/linkListingIndex.js
index 1bfaf46e..209066a9 100644
--- a/src/content/dependencies/linkListingIndex.js
+++ b/src/content/dependencies/linkListingIndex.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkStationaryIndex'],
-
   relations: (relation) =>
     ({link:
         relation(
diff --git a/src/content/dependencies/linkNewsEntry.js b/src/content/dependencies/linkNewsEntry.js
index 1fb32dd9..9ef7ac0e 100644
--- a/src/content/dependencies/linkNewsEntry.js
+++ b/src/content/dependencies/linkNewsEntry.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, newsEntry) =>
     ({link: relation('linkThing', 'localized.newsEntry', newsEntry)}),
 
diff --git a/src/content/dependencies/linkNewsIndex.js b/src/content/dependencies/linkNewsIndex.js
index e911a384..4414afc6 100644
--- a/src/content/dependencies/linkNewsIndex.js
+++ b/src/content/dependencies/linkNewsIndex.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkStationaryIndex'],
-
   relations: (relation) =>
     ({link:
         relation(
diff --git a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js
index ec856631..5a16256e 100644
--- a/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js
+++ b/src/content/dependencies/linkOtherReleaseOnArtistInfoPage.js
@@ -3,9 +3,6 @@ import {sortAlbumsTracksChronologically, sortContributionsChronologically}
 import {chunkArtistTrackContributions} from '#wiki-data';
 
 export default {
-  contentDependencies: ['generateColorStyleAttribute'],
-  extraDependencies: ['html', 'language'],
-
   query(track, artist) {
     const relevantInfoPageChunkingContributions =
       track.allReleases
diff --git a/src/content/dependencies/linkPathFromMedia.js b/src/content/dependencies/linkPathFromMedia.js
index d71c69f8..344b7d2c 100644
--- a/src/content/dependencies/linkPathFromMedia.js
+++ b/src/content/dependencies/linkPathFromMedia.js
@@ -1,17 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: ['linkTemplate'],
-
-  extraDependencies: [
-    'checkIfImagePathHasCachedThumbnails',
-    'getDimensionsOfImagePath',
-    'getSizeOfMediaFile',
-    'getThumbnailsAvailableForDimensions',
-    'html',
-    'to',
-  ],
-
   relations: (relation) =>
     ({link: relation('linkTemplate')}),
 
diff --git a/src/content/dependencies/linkPathFromRoot.js b/src/content/dependencies/linkPathFromRoot.js
index dab3ac1f..b4a90c07 100644
--- a/src/content/dependencies/linkPathFromRoot.js
+++ b/src/content/dependencies/linkPathFromRoot.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkTemplate'],
-
   relations: (relation) =>
     ({link: relation('linkTemplate')}),
 
diff --git a/src/content/dependencies/linkPathFromSite.js b/src/content/dependencies/linkPathFromSite.js
index 64676465..67a43059 100644
--- a/src/content/dependencies/linkPathFromSite.js
+++ b/src/content/dependencies/linkPathFromSite.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkTemplate'],
-
   relations: (relation) =>
     ({link: relation('linkTemplate')}),
 
diff --git a/src/content/dependencies/linkReferencedArtworks.js b/src/content/dependencies/linkReferencedArtworks.js
index c456b808..f8b3f3c8 100644
--- a/src/content/dependencies/linkReferencedArtworks.js
+++ b/src/content/dependencies/linkReferencedArtworks.js
@@ -1,21 +1,9 @@
-import Thing from '#thing';
-
 export default {
-  contentDependencies: [
-    'linkAlbumReferencedArtworks',
-    'linkTrackReferencedArtworks',
-  ],
-
-  query: (artwork) => ({
-    referenceType:
-      artwork.thing.constructor[Thing.referenceType],
-  }),
-
-  relations: (relation, query, artwork) => ({
+  relations: (relation, artwork) => ({
     link:
-      (query.referenceType === 'album'
+      (artwork.thing.isAlbum
         ? relation('linkAlbumReferencedArtworks', artwork.thing)
-     : query.referenceType === 'track'
+     : artwork.thing.isTrack
         ? relation('linkTrackReferencedArtworks', artwork.thing)
         : null),
   }),
diff --git a/src/content/dependencies/linkReferencingArtworks.js b/src/content/dependencies/linkReferencingArtworks.js
index 0cfca4db..6b7e4f9a 100644
--- a/src/content/dependencies/linkReferencingArtworks.js
+++ b/src/content/dependencies/linkReferencingArtworks.js
@@ -1,21 +1,9 @@
-import Thing from '#thing';
-
 export default {
-  contentDependencies: [
-    'linkAlbumReferencingArtworks',
-    'linkTrackReferencingArtworks',
-  ],
-
-  query: (artwork) => ({
-    referenceType:
-      artwork.thing.constructor[Thing.referenceType],
-  }),
-
-  relations: (relation, query, artwork) => ({
+  relations: (relation, artwork) => ({
     link:
-      (query.referenceType === 'album'
+      (artwork.thing.isAlbum
         ? relation('linkAlbumReferencingArtworks', artwork.thing)
-     : query.referenceType === 'track'
+     : artwork.thing.isTrack
         ? relation('linkTrackReferencingArtworks', artwork.thing)
         : null),
   }),
diff --git a/src/content/dependencies/linkStaticPage.js b/src/content/dependencies/linkStaticPage.js
index 032af6c9..c3ac69fa 100644
--- a/src/content/dependencies/linkStaticPage.js
+++ b/src/content/dependencies/linkStaticPage.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, staticPage) =>
     ({link: relation('linkThing', 'localized.staticPage', staticPage)}),
 
diff --git a/src/content/dependencies/linkStationaryIndex.js b/src/content/dependencies/linkStationaryIndex.js
index d5506e60..10f8ba44 100644
--- a/src/content/dependencies/linkStationaryIndex.js
+++ b/src/content/dependencies/linkStationaryIndex.js
@@ -1,9 +1,6 @@
 // Not to be confused with "html.Stationery".
 
 export default {
-  contentDependencies: ['linkTemplate'],
-  extraDependencies: ['language'],
-
   relations(relation) {
     return {
       linkTemplate: relation('linkTemplate'),
diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js
index 4f853dc4..10466b43 100644
--- a/src/content/dependencies/linkTemplate.js
+++ b/src/content/dependencies/linkTemplate.js
@@ -3,13 +3,6 @@ import {empty} from '#sugar';
 import striptags from 'striptags';
 
 export default {
-  extraDependencies: [
-    'appendIndexHTML',
-    'html',
-    'language',
-    'to',
-  ],
-
   slots: {
     href: {type: 'string'},
     path: {validate: v => v.validateArrayItems(v.isString)},
diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js
index 3902f380..166a857d 100644
--- a/src/content/dependencies/linkThing.js
+++ b/src/content/dependencies/linkThing.js
@@ -1,13 +1,4 @@
 export default {
-  contentDependencies: [
-    'generateColorStyleAttribute',
-    'generateTextWithTooltip',
-    'generateTooltip',
-    'linkTemplate',
-  ],
-
-  extraDependencies: ['html', 'language'],
-
   relations: (relation, _pathKey, thing) => ({
     linkTemplate:
       relation('linkTemplate'),
@@ -20,11 +11,15 @@ export default {
 
     tooltip:
       relation('generateTooltip'),
+
+    name:
+      relation('generateName', thing),
   }),
 
   data: (pathKey, thing) => ({
     name: thing.name,
     nameShort: thing.nameShort ?? thing.shortName,
+    nameText: thing.nameText,
 
     path:
       (pathKey
@@ -75,22 +70,21 @@ export default {
     hash: {type: 'string'},
   },
 
-  generate(data, relations, slots, {html, language}) {
+  generate(data, relations, slots, {html}) {
     const path =
       slots.path ?? data.path;
 
     const linkAttributes = slots.attributes;
     const wrapperAttributes = html.attributes();
 
-    const showShortName =
-      (slots.preferShortName
-        ? data.nameShort && data.nameShort !== data.name
-        : false);
-
     const name =
-      (showShortName
-        ? data.nameShort
-        : data.name);
+      relations.name.slot('preferShortName', slots.preferShortName);
+
+    const showShortName =
+      slots.preferShortName &&
+     !data.nameText &&
+      data.nameShort &&
+      data.nameShort !== data.name;
 
     const showWikiTooltip =
       (slots.tooltipStyle === 'auto'
@@ -114,7 +108,7 @@ export default {
 
     const content =
       (html.isBlank(slots.content)
-        ? language.sanitize(name)
+        ? name
         : slots.content);
 
     if (slots.color !== false) {
diff --git a/src/content/dependencies/linkTrack.js b/src/content/dependencies/linkTrack.js
index d5d96726..8ee715f0 100644
--- a/src/content/dependencies/linkTrack.js
+++ b/src/content/dependencies/linkTrack.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, track) =>
     ({link: relation('linkThing', 'localized.track', track)}),
 
diff --git a/src/content/dependencies/linkTrackAsRelease.js b/src/content/dependencies/linkTrackAsRelease.js
new file mode 100644
index 00000000..7a114ad9
--- /dev/null
+++ b/src/content/dependencies/linkTrackAsRelease.js
@@ -0,0 +1,20 @@
+export default {
+  relations: (relation, track) => ({
+    trackLink:
+      relation('linkTrack', track),
+  }),
+
+  data: (track) => ({
+    albumName:
+      track.album.name,
+
+    albumColor:
+      track.album.color,
+  }),
+
+  generate: (data, relations, {language}) =>
+    relations.trackLink.slots({
+      content: language.sanitize(data.albumName),
+      color: data.albumColor,
+    }),
+};
diff --git a/src/content/dependencies/linkTrackDynamically.js b/src/content/dependencies/linkTrackDynamically.js
index bbcf1c34..088bbe09 100644
--- a/src/content/dependencies/linkTrackDynamically.js
+++ b/src/content/dependencies/linkTrackDynamically.js
@@ -1,9 +1,6 @@
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: ['linkTrack'],
-  extraDependencies: ['pagePath'],
-
   relations: (relation, track) => ({
     infoLink: relation('linkTrack', track),
   }),
diff --git a/src/content/dependencies/linkTrackReferencedArtworks.js b/src/content/dependencies/linkTrackReferencedArtworks.js
index b4cb08fe..6da6504e 100644
--- a/src/content/dependencies/linkTrackReferencedArtworks.js
+++ b/src/content/dependencies/linkTrackReferencedArtworks.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, track) =>
     ({link: relation('linkThing', 'localized.trackReferencedArtworks', track)}),
 
diff --git a/src/content/dependencies/linkTrackReferencingArtworks.js b/src/content/dependencies/linkTrackReferencingArtworks.js
index c9c9f4d1..4d113ba7 100644
--- a/src/content/dependencies/linkTrackReferencingArtworks.js
+++ b/src/content/dependencies/linkTrackReferencingArtworks.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['linkThing'],
-
   relations: (relation, track) =>
     ({link: relation('linkThing', 'localized.trackReferencingArtworks', track)}),
 
diff --git a/src/content/dependencies/linkWikiHomepage.js b/src/content/dependencies/linkWikiHomepage.js
index d8d3d0a0..91fbe410 100644
--- a/src/content/dependencies/linkWikiHomepage.js
+++ b/src/content/dependencies/linkWikiHomepage.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['linkTemplate'],
-  extraDependencies: ['wikiData'],
-
   sprawl({wikiInfo}) {
     return {wikiShortName: wikiInfo.nameShort};
   },
diff --git a/src/content/dependencies/listAlbumsByDate.js b/src/content/dependencies/listAlbumsByDate.js
index c83ffc97..eaf9eecf 100644
--- a/src/content/dependencies/listAlbumsByDate.js
+++ b/src/content/dependencies/listAlbumsByDate.js
@@ -2,9 +2,6 @@ import {sortChronologically} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listAlbumsByDateAdded.js b/src/content/dependencies/listAlbumsByDateAdded.js
index d462ad46..940da67d 100644
--- a/src/content/dependencies/listAlbumsByDateAdded.js
+++ b/src/content/dependencies/listAlbumsByDateAdded.js
@@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort';
 import {chunkByProperties} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listAlbumsByDuration.js b/src/content/dependencies/listAlbumsByDuration.js
index c60685ab..8de2bb84 100644
--- a/src/content/dependencies/listAlbumsByDuration.js
+++ b/src/content/dependencies/listAlbumsByDuration.js
@@ -3,16 +3,17 @@ import {filterByCount, stitchArrays} from '#sugar';
 import {getTotalDuration} from '#wiki-data';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
 
   query({albumData}, spec) {
-    const albums = sortAlphabetically(albumData.slice());
-    const durations = albums.map(album => getTotalDuration(album.tracks));
+    const albums =
+      sortAlphabetically(
+        albumData.filter(album => !album.hideDuration));
+
+    const durations =
+      albums.map(album => getTotalDuration(album.tracks));
 
     filterByCount(albums, durations);
     sortByCount(albums, durations, {greatestFirst: true});
diff --git a/src/content/dependencies/listAlbumsByName.js b/src/content/dependencies/listAlbumsByName.js
index 21419537..a7939292 100644
--- a/src/content/dependencies/listAlbumsByName.js
+++ b/src/content/dependencies/listAlbumsByName.js
@@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listAlbumsByTracks.js b/src/content/dependencies/listAlbumsByTracks.js
index 798e6c2e..b1f62a82 100644
--- a/src/content/dependencies/listAlbumsByTracks.js
+++ b/src/content/dependencies/listAlbumsByTracks.js
@@ -2,21 +2,25 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
 
   query({albumData}, spec) {
-    const albums = sortAlphabetically(albumData.slice());
-    const counts = albums.map(album => album.tracks.length);
+    const albums =
+      sortAlphabetically(
+        albumData.filter(album => !album.hideDuration));
+
+    const counts =
+      albums.map(album => album.tracks.length);
 
     filterByCount(albums, counts);
     sortByCount(albums, counts, {greatestFirst: true});
 
-    return {spec, albums, counts};
+    const styles =
+      albums.map(album => album.style);
+
+    return {spec, albums, counts, styles};
   },
 
   relations(relation, query) {
@@ -32,6 +36,7 @@ export default {
   data(query) {
     return {
       counts: query.counts,
+      styles: query.styles,
     };
   },
 
@@ -42,10 +47,19 @@ export default {
         stitchArrays({
           link: relations.albumLinks,
           count: data.counts,
-        }).map(({link, count}) => ({
-            album: link,
-            tracks: language.countTracks(count, {unit: true}),
-          })),
+          style: data.styles,
+        }).map(({link, count, style}) => {
+            const row = {
+              album: link,
+              tracks: language.countTracks(count, {unit: true}),
+            };
+
+            if (style === 'single') {
+              row.stringsKey = 'single';
+            }
+
+            return row;
+          }),
     });
   },
 };
diff --git a/src/content/dependencies/listAllAdditionalFiles.js b/src/content/dependencies/listAllAdditionalFiles.js
index a6e34b9a..2d338916 100644
--- a/src/content/dependencies/listAllAdditionalFiles.js
+++ b/src/content/dependencies/listAllAdditionalFiles.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['listAllAdditionalFilesTemplate'],
-
   relations: (relation, spec) =>
     ({page: relation('listAllAdditionalFilesTemplate', spec, 'additionalFiles')}),
 
diff --git a/src/content/dependencies/listAllAdditionalFilesTemplate.js b/src/content/dependencies/listAllAdditionalFilesTemplate.js
index 8ec69f1d..f298233c 100644
--- a/src/content/dependencies/listAllAdditionalFilesTemplate.js
+++ b/src/content/dependencies/listAllAdditionalFilesTemplate.js
@@ -1,13 +1,6 @@
 import {sortChronologically} from '#sort';
 
 export default {
-  contentDependencies: [
-    'generateListingPage',
-    'generateListAllAdditionalFilesAlbumSection',
-  ],
-
-  extraDependencies: ['html', 'wikiData'],
-
   sprawl: ({albumData}) => ({albumData}),
 
   query: (sprawl, spec, property) => ({
diff --git a/src/content/dependencies/listAllMidiProjectFiles.js b/src/content/dependencies/listAllMidiProjectFiles.js
index 31a70ef0..109cf2e7 100644
--- a/src/content/dependencies/listAllMidiProjectFiles.js
+++ b/src/content/dependencies/listAllMidiProjectFiles.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['listAllAdditionalFilesTemplate'],
-
   relations: (relation, spec) =>
     ({page: relation('listAllAdditionalFilesTemplate', spec, 'midiProjectFiles')}),
 
diff --git a/src/content/dependencies/listAllSheetMusicFiles.js b/src/content/dependencies/listAllSheetMusicFiles.js
index 166b2068..4f3bdb96 100644
--- a/src/content/dependencies/listAllSheetMusicFiles.js
+++ b/src/content/dependencies/listAllSheetMusicFiles.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['listAllAdditionalFilesTemplate'],
-
   relations: (relation, spec) =>
     ({page: relation('listAllAdditionalFilesTemplate', spec, 'sheetMusicFiles')}),
 
diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js
index 93dd4ce8..98f81019 100644
--- a/src/content/dependencies/listArtTagNetwork.js
+++ b/src/content/dependencies/listArtTagNetwork.js
@@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort';
 import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtTagInfo'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({artTagData}) {
     return {artTagData};
   },
diff --git a/src/content/dependencies/listArtTagsByName.js b/src/content/dependencies/listArtTagsByName.js
index 1df9dfff..10e9e873 100644
--- a/src/content/dependencies/listArtTagsByName.js
+++ b/src/content/dependencies/listArtTagsByName.js
@@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort';
 import {stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtTagGallery'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({artTagData}) {
     return {artTagData};
   },
diff --git a/src/content/dependencies/listArtTagsByUses.js b/src/content/dependencies/listArtTagsByUses.js
index eca7f1c6..5131580f 100644
--- a/src/content/dependencies/listArtTagsByUses.js
+++ b/src/content/dependencies/listArtTagsByUses.js
@@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays, unique} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtTagGallery'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl: ({artTagData}) =>
     ({artTagData}),
 
diff --git a/src/content/dependencies/listArtistsByCommentaryEntries.js b/src/content/dependencies/listArtistsByCommentaryEntries.js
index eff2dba3..ab7bde6c 100644
--- a/src/content/dependencies/listArtistsByCommentaryEntries.js
+++ b/src/content/dependencies/listArtistsByCommentaryEntries.js
@@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtist'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({artistData}) {
     return {artistData};
   },
diff --git a/src/content/dependencies/listArtistsByContributions.js b/src/content/dependencies/listArtistsByContributions.js
index 41944959..2f8d6391 100644
--- a/src/content/dependencies/listArtistsByContributions.js
+++ b/src/content/dependencies/listArtistsByContributions.js
@@ -1,18 +1,8 @@
 import {sortAlphabetically, sortByCount} from '#sort';
-
-import {
-  accumulateSum,
-  empty,
-  filterByCount,
-  filterMultipleArrays,
-  stitchArrays,
-  unique,
-} from '#sugar';
+import {empty, filterByCount, filterMultipleArrays, stitchArrays}
+  from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtist'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({artistData, wikiInfo}) {
     return {
       artistData,
@@ -41,37 +31,46 @@ export default {
       query[countsKey] = counts;
     };
 
+    const countContributions = (artist, keys) => {
+      const contribs =
+        keys
+          .flatMap(key => artist[key])
+          .filter(contrib => contrib.countInContributionTotals);
+
+      const things =
+        new Set(contribs.map(contrib => contrib.thing));
+
+      return things.size;
+    };
+
     queryContributionInfo(
       'artistsByTrackContributions',
       'countsByTrackContributions',
       artist =>
-        (unique(
-          ([
-            artist.trackArtistContributions,
-            artist.trackContributorContributions,
-          ]).flat()
-            .map(({thing}) => thing)
-        )).length);
+        countContributions(artist, [
+          'trackArtistContributions',
+          'trackContributorContributions',
+        ]));
 
     queryContributionInfo(
       'artistsByArtworkContributions',
       'countsByArtworkContributions',
       artist =>
-        accumulateSum(
-          [
-            artist.albumCoverArtistContributions,
-            artist.albumWallpaperArtistContributions,
-            artist.albumBannerArtistContributions,
-            artist.trackCoverArtistContributions,
-          ],
-          contribs => contribs.length));
+        countContributions(artist, [
+          'albumCoverArtistContributions',
+          'albumWallpaperArtistContributions',
+          'albumBannerArtistContributions',
+          'trackCoverArtistContributions',
+        ]));
 
     if (sprawl.enableFlashesAndGames) {
       queryContributionInfo(
         'artistsByFlashContributions',
         'countsByFlashContributions',
         artist =>
-          artist.flashContributorContributions.length);
+          countContributions(artist, [
+            'flashContributorContributions',
+          ]));
     }
 
     return query;
diff --git a/src/content/dependencies/listArtistsByDuration.js b/src/content/dependencies/listArtistsByDuration.js
index 6b2a18a0..1d550b26 100644
--- a/src/content/dependencies/listArtistsByDuration.js
+++ b/src/content/dependencies/listArtistsByDuration.js
@@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtist'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({artistData}) {
     return {artistData};
   },
diff --git a/src/content/dependencies/listArtistsByGroup.js b/src/content/dependencies/listArtistsByGroup.js
index 17096cfc..44564b4b 100644
--- a/src/content/dependencies/listArtistsByGroup.js
+++ b/src/content/dependencies/listArtistsByGroup.js
@@ -10,9 +10,6 @@ import {
 } from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({artistData, wikiInfo}) {
     return {artistData, wikiInfo};
   },
diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js
index 2a8d1b4c..dc7341cf 100644
--- a/src/content/dependencies/listArtistsByLatestContribution.js
+++ b/src/content/dependencies/listArtistsByLatestContribution.js
@@ -11,15 +11,6 @@ import {
 const {Album, Flash} = T;
 
 export default {
-  contentDependencies: [
-    'generateListingPage',
-    'linkAlbum',
-    'linkArtist',
-    'linkFlash',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({albumData, artistData, flashData, trackData, wikiInfo}) =>
     ({albumData, artistData, flashData, trackData,
       enableFlashesAndGames: wikiInfo.enableFlashesAndGames}),
diff --git a/src/content/dependencies/listArtistsByName.js b/src/content/dependencies/listArtistsByName.js
index 93218492..8bee4947 100644
--- a/src/content/dependencies/listArtistsByName.js
+++ b/src/content/dependencies/listArtistsByName.js
@@ -3,9 +3,6 @@ import {stitchArrays} from '#sugar';
 import {getArtistNumContributions} from '#wiki-data';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkArtist', 'linkGroup'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl: ({artistData, wikiInfo}) =>
     ({artistData, wikiInfo}),
 
diff --git a/src/content/dependencies/listGroupsByAlbums.js b/src/content/dependencies/listGroupsByAlbums.js
index 4adfb6d9..64814640 100644
--- a/src/content/dependencies/listGroupsByAlbums.js
+++ b/src/content/dependencies/listGroupsByAlbums.js
@@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkGroup'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({groupData}) {
     return {groupData};
   },
diff --git a/src/content/dependencies/listGroupsByCategory.js b/src/content/dependencies/listGroupsByCategory.js
index 43919bef..4c10a1e4 100644
--- a/src/content/dependencies/listGroupsByCategory.js
+++ b/src/content/dependencies/listGroupsByCategory.js
@@ -1,9 +1,6 @@
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({groupCategoryData}) {
     return {groupCategoryData};
   },
diff --git a/src/content/dependencies/listGroupsByDuration.js b/src/content/dependencies/listGroupsByDuration.js
index c79e1bc4..089915c2 100644
--- a/src/content/dependencies/listGroupsByDuration.js
+++ b/src/content/dependencies/listGroupsByDuration.js
@@ -3,9 +3,6 @@ import {filterByCount, stitchArrays} from '#sugar';
 import {getTotalDuration} from '#wiki-data';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkGroup'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({groupData}) {
     return {groupData};
   },
diff --git a/src/content/dependencies/listGroupsByLatestAlbum.js b/src/content/dependencies/listGroupsByLatestAlbum.js
index 48319314..2d83a354 100644
--- a/src/content/dependencies/listGroupsByLatestAlbum.js
+++ b/src/content/dependencies/listGroupsByLatestAlbum.js
@@ -2,15 +2,6 @@ import {compareDates, sortChronologically} from '#sort';
 import {filterMultipleArrays, sortMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateListingPage',
-    'linkAlbum',
-    'linkGroup',
-    'linkGroupGallery',
-  ],
-
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({groupData}) {
     return {groupData};
   },
diff --git a/src/content/dependencies/listGroupsByName.js b/src/content/dependencies/listGroupsByName.js
index 696a49bd..e3308158 100644
--- a/src/content/dependencies/listGroupsByName.js
+++ b/src/content/dependencies/listGroupsByName.js
@@ -2,9 +2,6 @@ import {sortAlphabetically} from '#sort';
 import {stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkGroup', 'linkGroupGallery'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({groupData}) {
     return {groupData};
   },
diff --git a/src/content/dependencies/listGroupsByTracks.js b/src/content/dependencies/listGroupsByTracks.js
index 0b5e4e97..c9d97614 100644
--- a/src/content/dependencies/listGroupsByTracks.js
+++ b/src/content/dependencies/listGroupsByTracks.js
@@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {accumulateSum, filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkGroup'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({groupData}) {
     return {groupData};
   },
diff --git a/src/content/dependencies/listRandomPageLinks.js b/src/content/dependencies/listRandomPageLinks.js
index 79bba441..81eca274 100644
--- a/src/content/dependencies/listRandomPageLinks.js
+++ b/src/content/dependencies/listRandomPageLinks.js
@@ -2,14 +2,6 @@ import {sortChronologically} from '#sort';
 import {empty} from '#sugar';
 
 export default {
-  contentDependencies: [
-    'generateListingPage',
-    'generateListRandomPageLinksAlbumLink',
-    'linkGroup',
-  ],
-
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl: ({albumData, wikiInfo}) => ({albumData, wikiInfo}),
 
   query(sprawl, spec) {
diff --git a/src/content/dependencies/listTracksByAlbum.js b/src/content/dependencies/listTracksByAlbum.js
index b2405034..f6858ada 100644
--- a/src/content/dependencies/listTracksByAlbum.js
+++ b/src/content/dependencies/listTracksByAlbum.js
@@ -1,7 +1,4 @@
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listTracksByDate.js b/src/content/dependencies/listTracksByDate.js
index dcfaeaf0..9d63f19b 100644
--- a/src/content/dependencies/listTracksByDate.js
+++ b/src/content/dependencies/listTracksByDate.js
@@ -2,9 +2,6 @@ import {sortAlbumsTracksChronologically} from '#sort';
 import {chunkByProperties, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl: ({trackData}) => ({trackData}),
 
   query({trackData}, spec) {
diff --git a/src/content/dependencies/listTracksByDuration.js b/src/content/dependencies/listTracksByDuration.js
index 64feb4f1..95fd28b2 100644
--- a/src/content/dependencies/listTracksByDuration.js
+++ b/src/content/dependencies/listTracksByDuration.js
@@ -2,9 +2,6 @@ import {sortAlphabetically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkTrack'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({trackData}) {
     return {trackData};
   },
diff --git a/src/content/dependencies/listTracksByDurationInAlbum.js b/src/content/dependencies/listTracksByDurationInAlbum.js
index c1ea32a1..ad44c7b2 100644
--- a/src/content/dependencies/listTracksByDurationInAlbum.js
+++ b/src/content/dependencies/listTracksByDurationInAlbum.js
@@ -2,9 +2,6 @@ import {sortByCount, sortChronologically} from '#sort';
 import {filterByCount, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listTracksByName.js b/src/content/dependencies/listTracksByName.js
index 773b0473..a9c2c504 100644
--- a/src/content/dependencies/listTracksByName.js
+++ b/src/content/dependencies/listTracksByName.js
@@ -1,9 +1,6 @@
 import {sortAlphabetically} from '#sort';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkTrack'],
-  extraDependencies: ['wikiData'],
-
   sprawl({trackData}) {
     return {trackData};
   },
diff --git a/src/content/dependencies/listTracksByTimesReferenced.js b/src/content/dependencies/listTracksByTimesReferenced.js
index 5838ded0..8a57e1a6 100644
--- a/src/content/dependencies/listTracksByTimesReferenced.js
+++ b/src/content/dependencies/listTracksByTimesReferenced.js
@@ -2,9 +2,6 @@ import {sortAlbumsTracksChronologically, sortByCount} from '#sort';
 import {filterByCount, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkTrack'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({trackData}) {
     return {trackData};
   },
diff --git a/src/content/dependencies/listTracksInFlashesByAlbum.js b/src/content/dependencies/listTracksInFlashesByAlbum.js
index 8ca0d993..db5472db 100644
--- a/src/content/dependencies/listTracksInFlashesByAlbum.js
+++ b/src/content/dependencies/listTracksInFlashesByAlbum.js
@@ -2,9 +2,6 @@ import {sortChronologically} from '#sort';
 import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'],
-  extraDependencies: ['language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listTracksInFlashesByFlash.js b/src/content/dependencies/listTracksInFlashesByFlash.js
index 6ab954ed..325b3cb5 100644
--- a/src/content/dependencies/listTracksInFlashesByFlash.js
+++ b/src/content/dependencies/listTracksInFlashesByFlash.js
@@ -2,9 +2,6 @@ import {sortFlashesChronologically} from '#sort';
 import {empty, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum', 'linkFlash', 'linkTrack'],
-  extraDependencies: ['wikiData'],
-
   sprawl({flashData}) {
     return {flashData};
   },
diff --git a/src/content/dependencies/listTracksNeedingLyrics.js b/src/content/dependencies/listTracksNeedingLyrics.js
new file mode 100644
index 00000000..d21fcd06
--- /dev/null
+++ b/src/content/dependencies/listTracksNeedingLyrics.js
@@ -0,0 +1,7 @@
+export default {
+  relations: (relation, spec) =>
+    ({page: relation('listTracksWithExtra', spec, 'needsLyrics', 'truthy')}),
+
+  generate: (relations) =>
+    relations.page,
+};
diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js
index c7f42f9d..09d8ee21 100644
--- a/src/content/dependencies/listTracksWithExtra.js
+++ b/src/content/dependencies/listTracksWithExtra.js
@@ -2,9 +2,6 @@ import {sortChronologically} from '#sort';
 import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
 
 export default {
-  contentDependencies: ['generateListingPage', 'linkAlbum', 'linkTrack'],
-  extraDependencies: ['html', 'language', 'wikiData'],
-
   sprawl({albumData}) {
     return {albumData};
   },
diff --git a/src/content/dependencies/listTracksWithLyrics.js b/src/content/dependencies/listTracksWithLyrics.js
index e6ab9d7d..79d76bf3 100644
--- a/src/content/dependencies/listTracksWithLyrics.js
+++ b/src/content/dependencies/listTracksWithLyrics.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['listTracksWithExtra'],
-
   relations: (relation, spec) =>
     ({page: relation('listTracksWithExtra', spec, 'lyrics', 'array')}),
 
diff --git a/src/content/dependencies/listTracksWithMidiProjectFiles.js b/src/content/dependencies/listTracksWithMidiProjectFiles.js
index 418af4c2..9a48f6ae 100644
--- a/src/content/dependencies/listTracksWithMidiProjectFiles.js
+++ b/src/content/dependencies/listTracksWithMidiProjectFiles.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['listTracksWithExtra'],
-
   relations: (relation, spec) =>
     ({page: relation('listTracksWithExtra', spec, 'midiProjectFiles', 'array')}),
 
diff --git a/src/content/dependencies/listTracksWithSheetMusicFiles.js b/src/content/dependencies/listTracksWithSheetMusicFiles.js
index 0c6761eb..f0ba4196 100644
--- a/src/content/dependencies/listTracksWithSheetMusicFiles.js
+++ b/src/content/dependencies/listTracksWithSheetMusicFiles.js
@@ -1,6 +1,4 @@
 export default {
-  contentDependencies: ['listTracksWithExtra'],
-
   relations: (relation, spec) =>
     ({page: relation('listTracksWithExtra', spec, 'sheetMusicFiles', 'array')}),
 
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index fcdc3aa4..8e902647 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -1,5 +1,6 @@
 import {basename} from 'node:path';
 
+import {logWarn} from '#cli';
 import {bindFind} from '#find';
 import {replacerSpec, parseContentNodes} from '#replacer';
 
@@ -28,14 +29,36 @@ const commonMarkedOptions = {
 
 const multilineMarked = new Marked({
   ...commonMarkedOptions,
+
+  renderer: {
+    code({text}) {
+      let lines = text
+        .replace(/^\n+/, '')
+        .replace(/\n+$/, '')
+        .split('\n');
+
+      lines = lines
+        .map(line => line
+          .replace(/^ +/, spaces => '&nbsp'.repeat(spaces.length))
+          .replaceAll(/ {2,}/g, spaces => '&nbsp'.repeat(spaces.length)));
+
+      return (
+        `<pre class="content-code"><span><code>` +
+        (lines.length > 1 ? '\n' : '') +
+        lines.join('<br>\n') +
+        (lines.length > 1 ? '\n' : '') +
+        `</pre></span></code>`
+      );
+    },
+  },
 });
 
 const inlineMarked = new Marked({
   ...commonMarkedOptions,
 
   renderer: {
-    paragraph(text) {
-      return text;
+    paragraph({tokens}) {
+      return this.parser.parseInline(tokens);
     },
   },
 });
@@ -57,27 +80,38 @@ function getArg(node, argKey) {
 }
 
 export default {
-  contentDependencies: [
-    ...(
-      Object.values(replacerSpec)
-        .map(description => description.link)
-        .filter(Boolean)),
-    'image',
-    'generateTextWithTooltip',
-    'generateTooltip',
-    'linkExternal',
-  ],
-
-  extraDependencies: ['html', 'language', 'to', 'wikiData'],
-
   sprawl(wikiData, content) {
-    const find = bindFind(wikiData);
+    const find =
+      bindFind(wikiData, {
+        mode: 'quiet',
+        fuzz: {
+          capitalization: true,
+          kebab: true,
+        },
+      });
 
-    const parsedNodes = parseContentNodes(content ?? '');
+    const {result: parsedNodes, error} =
+      parseContentNodes(content ?? '', {errorMode: 'return'});
 
     return {
+      error,
+
       nodes: parsedNodes
         .map(node => {
+          if (node.type === 'tooltip') {
+            return {
+              i: node.i,
+              iEnd: node.iEnd,
+              type: 'tooltip',
+              data: {
+                // No recursion yet. Sorry!
+                tooltip: node.data.content[0].data,
+                label: node.data.label[0].data,
+                link: null,
+              },
+            };
+          }
+
           if (node.type !== 'tag') {
             return node;
           }
@@ -98,7 +132,7 @@ export default {
           }
 
           if (spec.link) {
-            let data = {link: spec.link};
+            let data = {link: spec.link, replacerKey, replacerValue};
 
             determineData: {
               // No value at all: this is an index link.
@@ -137,9 +171,16 @@ export default {
 
             data.label =
               enteredLabel ??
-                (transformName && data.thing.name
-                  ? transformName(data.thing.name, node, content)
-                  : null);
+
+              (transformName && data.thing.name &&
+               replacerKeyImplied && replacerValue === data.thing.name
+
+                ? transformName(data.thing.name, node, content)
+                : null) ??
+
+              (replacerKeyImplied
+                ? replacerValue
+                : null);
 
             data.hash = enteredHash ?? null;
 
@@ -177,8 +218,8 @@ export default {
             ...node,
             data: {
               ...node.data,
-              replacerKey: node.data.replacerKey.data,
-              replacerValue: node.data.replacerValue[0].data,
+              replacerKey,
+              replacerValue,
             },
           };
         }),
@@ -189,25 +230,11 @@ export default {
     return {
       content,
 
+      error:
+        sprawl.error,
+
       nodes:
-        sprawl.nodes
-          .map(node => {
-            switch (node.type) {
-              // Replace internal link nodes with a stub. It'll be replaced
-              // (by position) with an item from relations.
-              //
-              // TODO: This should be where label and hash get passed through,
-              // rather than in relations... (in which case there's no need to
-              // handle it specially here, and we can really just return
-              // data.nodes = sprawl.nodes)
-              case 'internal-link':
-                return {type: 'internal-link'};
-
-              // Other nodes will get processed in generate.
-              default:
-                return node;
-            }
-          }),
+        sprawl.nodes,
     };
   },
 
@@ -299,9 +326,29 @@ export default {
       validate: v => v.is('small', 'medium', 'large'),
       default: 'large',
     },
+
+    substitute: {
+      validate: v =>
+        v.strictArrayOf(
+          v.validateProperties({
+            match: v.validateProperties({
+              replacerKey: v.isString,
+              replacerValue: v.isString,
+            }),
+
+            substitute: v.isHTML,
+
+            apply: v.optional(v.isFunction),
+          })),
+    },
   },
 
-  generate(data, relations, slots, {html, language, to}) {
+  generate(data, relations, slots, {html, language, niceShowAggregate, to}) {
+    if (data.error) {
+      logWarn`Error in content text.`;
+      niceShowAggregate(data.error);
+    }
+
     let imageIndex = 0;
     let internalLinkIndex = 0;
     let externalLinkIndex = 0;
@@ -309,6 +356,24 @@ export default {
 
     let offsetTextNode = 0;
 
+    const substitutions =
+      (slots.substitute
+        ? slots.substitute.slice()
+        : []);
+
+    const pickSubstitution = node => {
+      const index =
+        substitutions.findIndex(({match}) =>
+          match.replacerKey === node.data.replacerKey &&
+          match.replacerValue === node.data.replacerValue);
+
+      if (index === -1) {
+        return null;
+      }
+
+      return substitutions.splice(index, 1).at(0);
+    };
+
     const contentFromNodes =
       data.nodes.map((node, index) => {
         const nextNode = data.nodes[index + 1];
@@ -327,6 +392,25 @@ export default {
           }
         };
 
+        const substitution = pickSubstitution(node);
+
+        if (substitution) {
+          const source =
+            substitution.substitute;
+
+          let substitute = source;
+
+          if (substitution.apply) {
+            const result = substitution.apply(source, node);
+
+            if (result !== undefined) {
+              substitute = result;
+            }
+          }
+
+          return {type: 'substitution', data: substitute};
+        }
+
         switch (node.type) {
           case 'text': {
             const text = node.data.slice(offsetTextNode);
@@ -360,9 +444,8 @@ export default {
                   height && {height},
                   style && {style},
 
-                  align === 'center' &&
-                  !link &&
-                    {class: 'align-center'},
+                  align && !link &&
+                    {class: 'align-' + align},
 
                   pixelate &&
                     {class: 'pixelate'});
@@ -373,8 +456,8 @@ export default {
                     {href: link},
                     {target: '_blank'},
 
-                    align === 'center' &&
-                      {class: 'align-center'},
+                    align &&
+                      {class: 'align-' + align},
 
                     {title:
                       language.encapsulate('misc.external.opensInNewTab', capsule =>
@@ -424,8 +507,8 @@ export default {
               inline: false,
               data:
                 html.tag('div', {class: 'content-image-container'},
-                  align === 'center' &&
-                    {class: 'align-center'},
+                  align &&
+                    {class: 'align-' + align},
 
                   image),
             };
@@ -437,22 +520,31 @@ export default {
                 ? to('media.path', node.src.slice('media/'.length))
                 : node.src);
 
-            const {width, height, align, pixelate} = node;
+            const {width, height, align, inline, pixelate} = node;
 
-            const content =
-              html.tag('div', {class: 'content-video-container'},
-                align === 'center' &&
-                  {class: 'align-center'},
+            const video =
+              html.tag('video',
+                src && {src},
+                width && {width},
+                height && {height},
 
-                html.tag('video',
-                  src && {src},
-                  width && {width},
-                  height && {height},
+                {controls: true},
 
-                  {controls: true},
+                align && inline &&
+                  {class: 'align-' + align},
+
+                pixelate &&
+                  {class: 'pixelate'});
+
+            const content =
+              (inline
+                ? video
+                : html.tag('div', {class: 'content-video-container'},
+                    align &&
+                      {class: 'align-' + align},
+
+                    video));
 
-                  pixelate &&
-                    {class: 'pixelate'}));
 
             return {
               type: 'processed-video',
@@ -466,15 +558,14 @@ export default {
                 ? to('media.path', node.src.slice('media/'.length))
                 : node.src);
 
-            const {align, inline} = node;
+            const {align, inline, nameless} = node;
 
             const audio =
               html.tag('audio',
                 src && {src},
 
-                align === 'center' &&
-                inline &&
-                  {class: 'align-center'},
+                align && inline &&
+                  {class: 'align-' + align},
 
                 {controls: true});
 
@@ -482,13 +573,14 @@ export default {
               (inline
                 ? audio
                 : html.tag('div', {class: 'content-audio-container'},
-                    align === 'center' &&
-                      {class: 'align-center'},
+                    align &&
+                      {class: 'align-' + align},
 
                     [
-                      html.tag('a', {class: 'filename'},
-                        src && {href: src},
-                        language.sanitize(basename(node.src))),
+                      !nameless &&
+                        html.tag('a', {class: 'filename'},
+                          src && {href: src},
+                          language.sanitize(basename(node.src))),
 
                       audio,
                     ]));
@@ -537,7 +629,7 @@ export default {
             try {
               link.getSlotDescription('preferShortName');
               hasPreferShortNameSlot = true;
-            } catch (error) {
+            } catch {
               hasPreferShortNameSlot = false;
             }
 
@@ -550,7 +642,7 @@ export default {
             try {
               link.getSlotDescription('tooltipStyle');
               hasTooltipStyleSlot = true;
-            } catch (error) {
+            } catch {
               hasTooltipStyleSlot = false;
             }
 
@@ -574,9 +666,12 @@ export default {
           }
 
           case 'external-link': {
-            const {label} = node.data;
             const externalLink = relations.externalLinks[externalLinkIndex++];
 
+            const label =
+              node.data.label ??
+              node.data.href.replace(/^https?:\/\//, '');
+
             if (slots.textOnly) {
               return {type: 'text', data: label};
             }
@@ -794,20 +889,37 @@ export default {
     // This is separated into its own function just since we're gonna reuse
     // it in a minute if everything goes to heck in lyrics mode.
     const transformMultiline = () => {
-      const markedInput =
-        extractNonTextNodes()
-          // Compress multiple line breaks into single line breaks,
-          // except when they're preceding or following indented
-          // text (by at least two spaces).
-          .replace(/(?<!  .*)\n{2,}(?!^  )/gm, '\n') /* eslint-disable-line no-regex-spaces */
-          // Expand line breaks which don't follow a list, quote,
-          // or <br> / "  ", and which don't precede or follow
-          // indented text (by at least two spaces).
-          .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^  .*\n*|  $|<br>$)\n+(?!  |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */
-          // Expand line breaks which are at the end of a list.
-          .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n')
-          // Expand line breaks which are at the end of a quote.
-          .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n');
+      let fencedCode = [];
+
+      const fencedCodePlaceholder =
+        `<span class="INSERT-FENCED-CODE"></span>`;
+
+      let markedInput = extractNonTextNodes();
+
+      markedInput = markedInput
+        .replaceAll(/```(?:[\s\S](?!```))*\n```/g, (match) => {
+          fencedCode.push(match);
+          return fencedCodePlaceholder;
+        });
+
+      markedInput = markedInput
+        // Compress multiple line breaks into single line breaks,
+        // except when they're preceding or following indented
+        // text (by at least two spaces) or blockquotes.
+        .replace(/(?<!^  .*|^>.*)\n{2,}(?!^  |^>)/gm, '\n') /* eslint-disable-line no-regex-spaces */
+        // Expand line breaks which don't follow a list, quote,
+        // or <br> / "  ", and which don't precede or follow
+        // indented text (by at least two spaces).
+        .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^  .*\n*|  $|<br>$)\n+(?!  |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */
+        // Expand line breaks which are at the end of a list.
+        .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n')
+        // Expand line breaks which are at the end of a quote.
+        .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n');
+
+      fencedCode = fencedCode.reverse();
+
+      markedInput = markedInput
+        .replaceAll(fencedCodePlaceholder, () => fencedCode.pop());
 
       const markedOutput =
         multilineMarked.parse(markedInput);