« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static
diff options
context:
space:
mode:
Diffstat (limited to 'src/static')
-rw-r--r--src/static/client.js532
-rw-r--r--src/static/client3.js3483
-rw-r--r--src/static/icons.svg50
-rw-r--r--src/static/lazy-loading.js2
-rw-r--r--src/static/site2.css1232
-rw-r--r--src/static/site6.css2401
-rw-r--r--src/static/warning.svg93
7 files changed, 6019 insertions, 1774 deletions
diff --git a/src/static/client.js b/src/static/client.js
deleted file mode 100644
index ebe8604..0000000
--- a/src/static/client.js
+++ /dev/null
@@ -1,532 +0,0 @@
-/* eslint-env browser */
-
-// This is the JS file that gets loaded on the client! It's only really used for
-// the random track feature right now - the idea is we only use it for stuff
-// that cannot 8e done at static-site compile time, 8y its fundamentally
-// ephemeral nature.
-//
-// Upd8: As of 04/02/2021, it's now used for info cards too! Nice.
-
-import {getColors} from '../util/colors.js';
-
-import {getArtistNumContributions} from '../util/wiki-data.js';
-
-let albumData, artistData;
-let officialAlbumData, fandomAlbumData;
-
-let ready = false;
-
-// Localiz8tion nonsense ----------------------------------
-
-const language = document.documentElement.getAttribute('lang');
-
-let list;
-if (typeof Intl === 'object' && typeof Intl.ListFormat === 'function') {
-  const getFormat = (type) => {
-    const formatter = new Intl.ListFormat(language, {type});
-    return formatter.format.bind(formatter);
-  };
-
-  list = {
-    conjunction: getFormat('conjunction'),
-    disjunction: getFormat('disjunction'),
-    unit: getFormat('unit'),
-  };
-} else {
-  // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free.
-  // We use the same mock for every list 'cuz we don't have any of the
-  // necessary CLDR info to appropri8tely distinguish 8etween them.
-  const arbitraryMock = (array) => array.join(', ');
-
-  list = {
-    conjunction: arbitraryMock,
-    disjunction: arbitraryMock,
-    unit: arbitraryMock,
-  };
-}
-
-// Miscellaneous helpers ----------------------------------
-
-function rebase(href, rebaseKey = 'rebaseLocalized') {
-  const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/';
-  if (relative) {
-    return relative + href;
-  } else {
-    return href;
-  }
-}
-
-function pick(array) {
-  return array[Math.floor(Math.random() * array.length)];
-}
-
-function cssProp(el, key) {
-  return getComputedStyle(el).getPropertyValue(key).trim();
-}
-
-function getRefDirectory(ref) {
-  return ref.split(':')[1];
-}
-
-function getAlbum(el) {
-  const directory = cssProp(el, '--album-directory');
-  return albumData.find((album) => album.directory === directory);
-}
-
-// TODO: These should pro8a8ly access some shared urlSpec path. We'd need to
-// separ8te the tooling around that into common-shared code too.
-const getLinkHref = (type, directory) => rebase(`${type}/${directory}`);
-const openAlbum = (d) => rebase(`album/${d}`);
-const openTrack = (d) => rebase(`track/${d}`);
-const openArtist = (d) => rebase(`artist/${d}`);
-
-// TODO: This should also use urlSpec.
-function fetchData(type, directory) {
-  return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData')).then(
-    (res) => res.json()
-  );
-}
-
-// JS-based links -----------------------------------------
-
-for (const a of document.body.querySelectorAll('[data-random]')) {
-  a.addEventListener('click', (evt) => {
-    if (!ready) {
-      evt.preventDefault();
-      return;
-    }
-
-    setTimeout(() => {
-      a.href = rebase('js-disabled');
-    });
-    switch (a.dataset.random) {
-      case 'album':
-        return (a.href = openAlbum(pick(albumData).directory));
-      case 'album-in-fandom':
-        return (a.href = openAlbum(pick(fandomAlbumData).directory));
-      case 'album-in-official':
-        return (a.href = openAlbum(pick(officialAlbumData).directory));
-      case 'track':
-        return (a.href = openTrack(
-          getRefDirectory(
-            pick(
-              albumData.map((a) => a.tracks).reduce((a, b) => a.concat(b), [])
-            )
-          )
-        ));
-      case 'track-in-album':
-        return (a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks))));
-      case 'track-in-fandom':
-        return (a.href = openTrack(
-          getRefDirectory(
-            pick(
-              fandomAlbumData.reduce(
-                (acc, album) => acc.concat(album.tracks),
-                []
-              )
-            )
-          )
-        ));
-      case 'track-in-official':
-        return (a.href = openTrack(
-          getRefDirectory(
-            pick(
-              officialAlbumData.reduce(
-                (acc, album) => acc.concat(album.tracks),
-                []
-              )
-            )
-          )
-        ));
-      case 'artist':
-        return (a.href = openArtist(pick(artistData).directory));
-      case 'artist-more-than-one-contrib':
-        return (a.href = openArtist(
-          pick(
-            artistData.filter((artist) => getArtistNumContributions(artist) > 1)
-          ).directory
-        ));
-    }
-  });
-}
-
-const next = document.getElementById('next-button');
-const previous = document.getElementById('previous-button');
-const random = document.getElementById('random-button');
-
-const prependTitle = (el, prepend) => {
-  const existing = el.getAttribute('title');
-  if (existing) {
-    el.setAttribute('title', prepend + ' ' + existing);
-  } else {
-    el.setAttribute('title', prepend);
-  }
-};
-
-if (next) prependTitle(next, '(Shift+N)');
-if (previous) prependTitle(previous, '(Shift+P)');
-if (random) prependTitle(random, '(Shift+R)');
-
-document.addEventListener('keypress', (event) => {
-  if (event.shiftKey) {
-    if (event.charCode === 'N'.charCodeAt(0)) {
-      if (next) next.click();
-    } else if (event.charCode === 'P'.charCodeAt(0)) {
-      if (previous) previous.click();
-    } else if (event.charCode === 'R'.charCodeAt(0)) {
-      if (random && ready) random.click();
-    }
-  }
-});
-
-for (const reveal of document.querySelectorAll('.reveal')) {
-  reveal.addEventListener('click', (event) => {
-    if (!reveal.classList.contains('revealed')) {
-      reveal.classList.add('revealed');
-      event.preventDefault();
-      event.stopPropagation();
-    }
-  });
-}
-
-const elements1 = document.getElementsByClassName('js-hide-once-data');
-const elements2 = document.getElementsByClassName('js-show-once-data');
-
-for (const element of elements1) element.style.display = 'block';
-
-fetch(rebase('data.json', 'rebaseShared'))
-  .then((data) => data.json())
-  .then((data) => {
-    albumData = data.albumData;
-    artistData = data.artistData;
-
-    officialAlbumData = albumData.filter((album) =>
-      album.groups.includes('group:official')
-    );
-    fandomAlbumData = albumData.filter(
-      (album) => !album.groups.includes('group:official')
-    );
-
-    for (const element of elements1) element.style.display = 'none';
-    for (const element of elements2) element.style.display = 'block';
-
-    ready = true;
-  });
-
-// Data & info card ---------------------------------------
-
-const NORMAL_HOVER_INFO_DELAY = 750;
-const FAST_HOVER_INFO_DELAY = 250;
-const END_FAST_HOVER_DELAY = 500;
-const HIDE_HOVER_DELAY = 250;
-
-let fastHover = false;
-let endFastHoverTimeout = null;
-
-function colorLink(a, color) {
-  console.warn('Info card link colors temporarily disabled: chroma.js required, no dependency linking for client.js yet');
-  return;
-
-  // eslint-disable-next-line no-unreachable
-  const chroma = {};
-
-  if (color) {
-    const {primary, dim} = getColors(color, {chroma});
-    a.style.setProperty('--primary-color', primary);
-    a.style.setProperty('--dim-color', dim);
-  }
-}
-
-function link(a, type, {name, directory, color}) {
-  colorLink(a, color);
-  a.innerText = name;
-  a.href = getLinkHref(type, directory);
-}
-
-function joinElements(type, elements) {
-  // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on
-  // strings. So instead, we'll pass the element's outer HTML's (which means
-  // the entire HTML of that element).
-  //
-  // That does mean this function returns a string, so always 8e sure to
-  // set innerHTML when using it (not appendChild).
-
-  return list[type](elements.map((el) => el.outerHTML));
-}
-
-const infoCard = (() => {
-  const container = document.getElementById('info-card-container');
-
-  let cancelShow = false;
-  let hideTimeout = null;
-  let showing = false;
-
-  container.addEventListener('mouseenter', cancelHide);
-  container.addEventListener('mouseleave', readyHide);
-
-  function show(type, target) {
-    cancelShow = false;
-
-    fetchData(type, target.dataset[type]).then((data) => {
-      // Manual DOM 'cuz we're laaaazy.
-
-      if (cancelShow) {
-        return;
-      }
-
-      showing = true;
-
-      const rect = target.getBoundingClientRect();
-
-      container.style.setProperty('--primary-color', data.color);
-
-      container.style.top = window.scrollY + rect.bottom + 'px';
-      container.style.left = window.scrollX + rect.left + 'px';
-
-      // Use a short timeout to let a currently hidden (or not yet shown)
-      // info card teleport to the position set a8ove. (If it's currently
-      // shown, it'll transition to that position.)
-      setTimeout(() => {
-        container.classList.remove('hide');
-        container.classList.add('show');
-      }, 50);
-
-      // 8asic details.
-
-      const nameLink = container.querySelector('.info-card-name a');
-      link(nameLink, 'track', data);
-
-      const albumLink = container.querySelector('.info-card-album a');
-      link(albumLink, 'album', data.album);
-
-      const artistSpan = container.querySelector('.info-card-artists span');
-      artistSpan.innerHTML = joinElements(
-        'conjunction',
-        data.artists.map(({artist}) => {
-          const a = document.createElement('a');
-          a.href = getLinkHref('artist', artist.directory);
-          a.innerText = artist.name;
-          return a;
-        })
-      );
-
-      const coverArtistParagraph = container.querySelector(
-        '.info-card-cover-artists'
-      );
-      const coverArtistSpan = coverArtistParagraph.querySelector('span');
-      if (data.coverArtists.length) {
-        coverArtistParagraph.style.display = 'block';
-        coverArtistSpan.innerHTML = joinElements(
-          'conjunction',
-          data.coverArtists.map(({artist}) => {
-            const a = document.createElement('a');
-            a.href = getLinkHref('artist', artist.directory);
-            a.innerText = artist.name;
-            return a;
-          })
-        );
-      } else {
-        coverArtistParagraph.style.display = 'none';
-      }
-
-      // Cover art.
-
-      const [containerNoReveal, containerReveal] = [
-        container.querySelector('.info-card-art-container.no-reveal'),
-        container.querySelector('.info-card-art-container.reveal'),
-      ];
-
-      const [containerShow, containerHide] = data.cover.warnings.length
-        ? [containerReveal, containerNoReveal]
-        : [containerNoReveal, containerReveal];
-
-      containerHide.style.display = 'none';
-      containerShow.style.display = 'block';
-
-      const img = containerShow.querySelector('.info-card-art');
-      img.src = rebase(data.cover.paths.small, 'rebaseMedia');
-
-      const imgLink = containerShow.querySelector('a');
-      colorLink(imgLink, data.color);
-      imgLink.href = rebase(data.cover.paths.original, 'rebaseMedia');
-
-      if (containerShow === containerReveal) {
-        const cw = containerShow.querySelector('.info-card-art-warnings');
-        cw.innerText = list.unit(data.cover.warnings);
-
-        const reveal = containerShow.querySelector('.reveal');
-        reveal.classList.remove('revealed');
-      }
-    });
-  }
-
-  function hide() {
-    container.classList.remove('show');
-    container.classList.add('hide');
-    cancelShow = true;
-    showing = false;
-  }
-
-  function readyHide() {
-    if (!hideTimeout && showing) {
-      hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY);
-    }
-  }
-
-  function cancelHide() {
-    if (hideTimeout) {
-      clearTimeout(hideTimeout);
-      hideTimeout = null;
-    }
-  }
-
-  return {
-    show,
-    hide,
-    readyHide,
-    cancelHide,
-  };
-})();
-
-function makeInfoCardLinkHandlers(type) {
-  let hoverTimeout = null;
-
-  return {
-    mouseenter(evt) {
-      hoverTimeout = setTimeout(
-        () => {
-          fastHover = true;
-          infoCard.show(type, evt.target);
-        },
-        fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY);
-
-      clearTimeout(endFastHoverTimeout);
-      endFastHoverTimeout = null;
-
-      infoCard.cancelHide();
-    },
-
-    mouseleave() {
-      clearTimeout(hoverTimeout);
-
-      if (fastHover && !endFastHoverTimeout) {
-        endFastHoverTimeout = setTimeout(() => {
-          endFastHoverTimeout = null;
-          fastHover = false;
-        }, END_FAST_HOVER_DELAY);
-      }
-
-      infoCard.readyHide();
-    },
-  };
-}
-
-const infoCardLinkHandlers = {
-  track: makeInfoCardLinkHandlers('track'),
-};
-
-function addInfoCardLinkHandlers(type) {
-  for (const a of document.querySelectorAll(`a[data-${type}]`)) {
-    for (const [eventName, handler] of Object.entries(
-      infoCardLinkHandlers[type]
-    )) {
-      a.addEventListener(eventName, handler);
-    }
-  }
-}
-
-// Info cards are disa8led for now since they aren't quite ready for release,
-// 8ut you can try 'em out 8y setting this localStorage flag!
-//
-//     localStorage.tryInfoCards = true;
-//
-if (localStorage.tryInfoCards) {
-  addInfoCardLinkHandlers('track');
-}
-
-// Sticky content heading ---------------------------------
-
-const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky-heading-container'))
-  .map(stickyContainer => {
-    const {parentElement: contentContainer} = stickyContainer;
-    const stickySubheading = stickyContainer.querySelector('.content-sticky-subheading');
-    const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading'));
-
-    return {
-      contentContainer,
-      contentHeadings,
-      stickyContainer,
-      stickySubheading,
-      state: {
-        displayedHeading: null,
-      },
-    };
-  });
-
-const topOfViewInside = (el, scroll = window.scrollY) => (
-  scroll > el.offsetTop &&
-  scroll < el.offsetTop + el.offsetHeight
-);
-
-function updateStickyHeading() {
-  for (const {
-    contentContainer,
-    contentHeadings,
-    stickyContainer,
-    stickySubheading,
-    state,
-  } of stickyHeadingInfo) {
-    let closestHeading = null;
-
-    if (topOfViewInside(contentContainer)) {
-      if (stickySubheading.childNodes.length === 0) {
-        // &nbsp; to ensure correct basic line height
-        stickySubheading.appendChild(document.createTextNode('\xA0'));
-      }
-
-      const stickyRect = stickyContainer.getBoundingClientRect();
-      const subheadingRect = stickySubheading.getBoundingClientRect();
-      const stickyBottom = stickyRect.bottom + subheadingRect.height;
-
-      // This array is reversed so that we're starting from the bottom when
-      // iterating over it.
-      for (let i = contentHeadings.length - 1; i >= 0; i--) {
-        const heading = contentHeadings[i];
-        const headingRect = heading.getBoundingClientRect();
-        if (headingRect.y + headingRect.height / 1.5 < stickyBottom) {
-          closestHeading = heading;
-          break;
-        }
-      }
-    }
-
-    if (state.displayedHeading !== closestHeading) {
-      if (closestHeading) {
-        // Array.from needed to iterate over a live array with for..of
-        for (const child of Array.from(stickySubheading.childNodes)) {
-          child.remove();
-        }
-
-        for (const child of closestHeading.childNodes) {
-          if (child.tagName === 'A') {
-            for (const grandchild of child.childNodes) {
-              stickySubheading.appendChild(grandchild.cloneNode(true));
-            }
-          } else {
-            stickySubheading.appendChild(child.cloneNode(true));
-          }
-        }
-
-        stickySubheading.classList.add('visible');
-      } else {
-        stickySubheading.classList.remove('visible');
-      }
-
-      state.displayedHeading = closestHeading;
-    }
-  }
-}
-
-document.addEventListener('scroll', updateStickyHeading);
-
-updateStickyHeading();
diff --git a/src/static/client3.js b/src/static/client3.js
new file mode 100644
index 0000000..7d6544a
--- /dev/null
+++ b/src/static/client3.js
@@ -0,0 +1,3483 @@
+/* eslint-env browser */
+
+// This is the JS file that gets loaded on the client! It's only really used for
+// the random track feature right now - the idea is we only use it for stuff
+// that cannot 8e done at static-site compile time, 8y its fundamentally
+// ephemeral nature.
+
+import {accumulateSum, empty, filterMultipleArrays, stitchArrays}
+  from '../util/sugar.js';
+
+const clientInfo = window.hsmusicClientInfo = Object.create(null);
+
+const clientSteps = {
+  getPageReferences: [],
+  addInternalListeners: [],
+  mutatePageContent: [],
+  initializeState: [],
+  addPageListeners: [],
+};
+
+function initInfo(key, description) {
+  const object = {...description};
+
+  for (const obj of [
+    object,
+    object.state,
+    object.setting,
+    object.event,
+  ]) {
+    if (!obj) continue;
+    Object.preventExtensions(obj);
+  }
+
+  clientInfo[key] = object;
+
+  return object;
+}
+
+// Localiz8tion nonsense ----------------------------------
+
+/*
+const language = document.documentElement.getAttribute('lang');
+
+let list;
+if (typeof Intl === 'object' && typeof Intl.ListFormat === 'function') {
+  const getFormat = (type) => {
+    const formatter = new Intl.ListFormat(language, {type});
+    return formatter.format.bind(formatter);
+  };
+
+  list = {
+    conjunction: getFormat('conjunction'),
+    disjunction: getFormat('disjunction'),
+    unit: getFormat('unit'),
+  };
+} else {
+  // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free.
+  // We use the same mock for every list 'cuz we don't have any of the
+  // necessary CLDR info to appropri8tely distinguish 8etween them.
+  const arbitraryMock = (array) => array.join(', ');
+
+  list = {
+    conjunction: arbitraryMock,
+    disjunction: arbitraryMock,
+    unit: arbitraryMock,
+  };
+}
+*/
+
+// Miscellaneous helpers ----------------------------------
+
+function rebase(href, rebaseKey = 'rebaseLocalized') {
+  const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/';
+  if (relative) {
+    return relative + href;
+  } else {
+    return href;
+  }
+}
+
+function pick(array) {
+  return array[Math.floor(Math.random() * array.length)];
+}
+
+function cssProp(el, ...args) {
+  if (typeof args[0] === 'string' && args.length === 1) {
+    return getComputedStyle(el).getPropertyValue(args[0]).trim();
+  }
+
+  if (typeof args[0] === 'string' && args.length === 2) {
+    if (args[1] === null) {
+      el.style.removeProperty(args[0]);
+    } else {
+      el.style.setProperty(args[0], args[1]);
+    }
+    return;
+  }
+
+  if (typeof args[0] === 'object') {
+    for (const [property, value] of Object.entries(args[0])) {
+      cssProp(el, property, value);
+    }
+  }
+}
+
+// Curry-style, so multiple points can more conveniently be tested at once.
+function pointIsOverAnyOf(elements) {
+  return (clientX, clientY) => {
+    const element = document.elementFromPoint(clientX, clientY);
+    return elements.some(el => el.contains(element));
+  };
+}
+
+function getVisuallyContainingElement(child) {
+  let parent = child.parentElement;
+
+  while (parent) {
+    if (
+      cssProp(parent, 'overflow') === 'hidden' ||
+      cssProp(parent, 'contain') === 'paint'
+    ) {
+      return parent;
+    }
+
+    parent = parent.parentElement;
+  }
+
+  return null;
+}
+
+// TODO: These should pro8a8ly access some shared urlSpec path. We'd need to
+// separ8te the tooling around that into common-shared code too.
+
+/*
+const getLinkHref = (type, directory) => rebase(`${type}/${directory}`);
+*/
+
+const openAlbum = (d) => rebase(`album/${d}`);
+const openTrack = (d) => rebase(`track/${d}`);
+const openArtist = (d) => rebase(`artist/${d}`);
+
+// TODO: This should also use urlSpec.
+
+/*
+function fetchData(type, directory) {
+  return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData')).then(
+    (res) => res.json()
+  );
+}
+*/
+
+function dispatchInternalEvent(event, eventName, ...args) {
+  const [infoName] =
+    Object.entries(clientInfo)
+      .find(pair => pair[1].event === event);
+
+  if (!infoName) {
+    throw new Error(`Expected event to be stored on clientInfo`);
+  }
+
+  const {[eventName]: listeners} = event;
+
+  if (!listeners) {
+    throw new Error(`Event name "${eventName}" isn't stored on ${infoName}.event`);
+  }
+
+  let results = [];
+  for (const listener of listeners) {
+    try {
+      results.push(listener(...args));
+    } catch (error) {
+      console.warn(`Uncaught error in listener for ${infoName}.${eventName}`);
+      console.debug(error);
+      results.push(undefined);
+    }
+  }
+
+  return results;
+}
+
+// Rectangle math -----------------------------------------
+
+class WikiRect extends DOMRect {
+  // Useful constructors
+
+  static fromWindow() {
+    const {clientWidth: width, clientHeight: height} =
+      document.documentElement;
+
+    return Reflect.construct(this, [0, 0, width, height]);
+  }
+
+  static fromElement(element) {
+    return this.fromRect(element.getBoundingClientRect());
+  }
+
+  static fromMouse() {
+    const {clientX, clientY} = liveMousePositionInfo.state;
+
+    return WikiRect.fromRect({
+      x: clientX,
+      y: clientY,
+      width: 0,
+      height: 0,
+    });
+  }
+
+  static fromElementUnderMouse(element) {
+    const mouseRect = WikiRect.fromMouse();
+
+    const rects =
+      Array.from(element.getClientRects())
+        .map(rect => WikiRect.fromRect(rect));
+
+    const rectUnderMouse =
+      rects.find(rect => rect.contains(mouseRect));
+
+    if (rectUnderMouse) {
+      return rectUnderMouse;
+    } else {
+      return rects[0];
+    }
+  }
+
+  static leftOf(origin, offset = 0) {
+    // Returns a rectangle representing everywhere to the left of the provided
+    // point or rectangle (with no top or bottom bounds), towards negative x.
+    // If an offset is provided, this is added onto the origin.
+
+    return this.#past(origin, offset, {
+      origin: 'x',
+      extent: 'width',
+      edge: 'left',
+      direction: -Infinity,
+      construct: from =>
+        [from, -Infinity, -Infinity, Infinity],
+    });
+  }
+
+  static rightOf(origin, offset = 0) {
+    // Returns a rectangle representing everywhere to the right of the
+    // provided point or rectangle (with no top or bottom bounds), towards
+    // positive x. If an offset is provided, this is added onto the origin.
+
+    return this.#past(origin, offset, {
+      origin: 'x',
+      extent: 'width',
+      edge: 'right',
+      direction: Infinity,
+      construct: from =>
+        [from, -Infinity, Infinity, Infinity],
+    });
+  }
+
+  static above(origin, offset = 0) {
+    // Returns a rectangle representing everywhere above the provided point
+    // or rectangle (with no left or right bounds), towards negative y.
+    // If an offset is provided, this is added onto the origin.
+
+    return this.#past(origin, offset, {
+      origin: 'y',
+      extent: 'height',
+      edge: 'top',
+      direction: -Infinity,
+      construct: from =>
+        [-Infinity, from, Infinity, -Infinity],
+    });
+  }
+
+  static beneath(origin, offset = 0) {
+    // Returns a rectangle representing everywhere beneath the provided point
+    // or rectangle (with no left or right bounds), towards positive y.
+    // If an offset is provided, this is added onto the origin.
+
+    return this.#past(origin, offset, {
+      origin: 'y',
+      extent: 'height',
+      edge: 'bottom',
+      direction: Infinity,
+      construct: from =>
+        [-Infinity, from, Infinity, Infinity],
+    });
+  }
+
+  // Constructor helpers
+
+  static #past(origin, offset, opts) {
+    if (!isFinite(offset)) {
+      throw new TypeError(`Didn't expect infinite offset`);
+    }
+
+    const {direction, edge} = opts;
+
+    if (typeof origin === 'object') {
+      const {origin: originProperty, extent: extentProperty} = opts;
+
+      const normalized =
+        WikiRect.fromRect(origin).toNormalized();
+
+      if (normalized[extentProperty] === direction) {
+        throw new TypeError(`Provided rectangle already extends to ${edge} edge`);
+      }
+
+      if (normalized[extentProperty] === -direction) {
+        return this.#past(normalized[originProperty], offset, opts);
+      }
+
+      if (normalized.y === direction) {
+        throw new TypeError(`Provided rectangle already starts at ${edge} edge`);
+      }
+
+      return this.#past(normalized[edge], offset, opts);
+    }
+
+    const {construct} = opts;
+
+    if (origin === direction) {
+      throw new TypeError(`Provided point is already at ${edge} edge`);
+    }
+
+    return Reflect.construct(this, construct(origin + offset)).toNormalized();
+  }
+
+  // Predicates
+
+  static rejectInfiniteOriginNonZeroFiniteExtent({origin, extent}) {
+    // Indicate that, in this context, it's meaningless to provide
+    // a finite extent starting at an infinite origin and going towards
+    // or away from zero (i.e. a rectangle along a cardinal edge).
+
+    if (!isFinite(origin) && isFinite(extent) && extent !== 0) {
+      throw new TypeError(`Didn't expect infinite origin paired with finite extent`);
+    }
+  }
+
+  static rejectInfiniteOriginZeroExtent({origin, extent}) {
+    // Indicate that, in this context, it's meaningless to provide
+    // a zero extent at an infinite origin (i.e. a cardinal edge).
+
+    if (!isFinite(origin) && extent === 0) {
+      throw new TypeError(`Didn't expect infinite origin paired with zero extent`);
+    }
+  }
+
+  static rejectNonOpposingInfiniteOriginInfiniteExtent({origin, extent}) {
+    // Indicate that, in this context, it's meaningless to provide
+    // an infinite extent going in the same direction as its infinite
+    // origin (an area "infinitely past" a cardinal edge).
+
+    if (!isFinite(origin) && origin === extent) {
+      throw new TypeError(`Didn't expect non-opposing infinite origin and extent`);
+    }
+  }
+
+  // Transformations
+
+  static normalizeOriginExtent({origin, extent}) {
+    // Varying behavior based on inputs:
+    //
+    //  - For finite origin and finite extent, flip the orientation
+    //    (if necessary) so that extent is positive.
+    //  - For finite origin and infinite extent (i.e. an origin up to
+    //    a cardinal edge), leave as-is.
+    //  - For infinite origin and infinite extent, flip the orientation
+    //    (if necessary) so origin is negative and extent is positive.
+    //  - For infinite origin and zero extent (i.e. a cardinal edge),
+    //    leave as-is.
+    //  - For all other cases, error.
+    //
+
+    this.rejectInfiniteOriginNonZeroFiniteExtent({origin, extent});
+    this.rejectNonOpposingInfiniteOriginInfiniteExtent({origin, extent});
+
+    if (isFinite(origin) && isFinite(extent) && extent < 0) {
+      return {origin: origin + extent, extent: -extent};
+    }
+
+    if (!isFinite(origin) && !isFinite(extent)) {
+      return {origin: -Infinity, extent: Infinity};
+    }
+
+    return {origin, extent};
+  }
+
+  toNormalized() {
+    const {origin: newX, extent: newWidth} =
+      WikiRect.normalizeOriginExtent({
+        origin: this.x,
+        extent: this.width,
+      });
+
+    const {origin: newY, extent: newHeight} =
+      WikiRect.normalizeOriginExtent({
+        origin: this.y,
+        extent: this.height,
+      });
+
+    return Reflect.construct(this.constructor, [newX, newY, newWidth, newHeight]);
+  }
+
+  static intersectionFromOriginsExtents(...entries) {
+    // An intersection is the common subsection across two or more regions.
+
+    const [first, second, ...rest] = entries;
+
+    if (entries.length >= 3) {
+      return this.intersection(first, this.intersection(second, ...rest));
+    }
+
+    if (entries.length === 2) {
+      if (first === null || second === null) {
+        return null;
+      }
+
+      this.rejectInfiniteOriginZeroExtent(first);
+      this.rejectInfiniteOriginZeroExtent(second);
+
+      const {origin: origin1, extent: extent1} = this.normalizeOriginExtent(first);
+      const {origin: origin2, extent: extent2} = this.normalizeOriginExtent(second);
+
+      // After normalizing, *each* region will be one of these:
+      //
+      //  - Finite origin, finite extent
+      //    (a standard region, bounded on both sides)
+      //  - Finite origin, infinite extent
+      //    (everything to one direction of a given origin)
+      //  - Infinite origin, infinite extent
+      //    (everything everywhere)
+      //
+      // So we need to handle any *combination* of these kinds of regions.
+
+      // If either origin is infinite, that region represents everywhere,
+      // so it'll never limit the region of the other.
+
+      if (!isFinite(origin1)) {
+        return {origin: origin2, extent: extent2};
+      }
+
+      if (!isFinite(origin2)) {
+        return {origin: origin1, extent: extent1};
+      }
+
+      // If neither origin is infinite, both regions are bounded on at least
+      // one side, and may limit the other accordingly. Find the minimum and
+      // maximum points in each region, letting Infinity propagate through,
+      // which represents no boundary in that direction.
+
+      const minimum1 = Math.min(origin1, origin1 + extent1);
+      const minimum2 = Math.min(origin2, origin2 + extent2);
+      const maximum1 = Math.max(origin1, origin1 + extent1);
+      const maximum2 = Math.max(origin2, origin2 + extent2);
+
+      // Now get the maximum of the regions' minimums, and the minimum of the
+      // regions' maximums. These are the limits of the new region; computing
+      // with minimums and maximums in this way "polarizes" the limits, so we
+      // can perform specific polarized math in the following steps.
+      //
+      // Infinity will also propagate here, but with some important
+      // restricitons: only maxOfMinimums can be positive Infinity, and only
+      // minOfMaximums can be negative Infinity; and if either is Infinity,
+      // the other is not, since otherwise we'd be working with two everywhere
+      // regions, and would've just returned an everywhere region above.
+
+      const maxOfMinimums = Math.max(minimum1, minimum2);
+      const minOfMaximums = Math.min(maximum1, maximum2);
+
+      // Now check if the maximum of minimums is greater than the minimum of
+      // maximums. If so, the regions don't have any overlap - one region
+      // limits the overlap to end before the other region starts. This works
+      // because we've polarized the limits above!
+
+      if (maxOfMinimums > minOfMaximums) {
+        return null;
+      }
+
+      // Otherwise there's at least some overlap, even if it's just one point
+      // (i.e. one ends exactly where the other begins). We have to take care
+      // of infinities in particular, now. As mentioned above, only one of the
+      // points will be infinity (at most). So the origin is the non-infinite
+      // point, and the extent is in the direction of the infinite point.
+
+      if (minOfMaximums === -Infinity) {
+        return {origin: maxOfMinimums, extent: -Infinity};
+      }
+
+      if (maxOfMinimums === Infinity) {
+        return {origin: minOfMaximums, extent: Infinity};
+      }
+
+      // If neither point is infinity, we're working with two regions that are
+      // both bounded on both sides, so the overlapping region is just the
+      // region constrained by the limits above. Since these are polarized,
+      // start from maxOfMinimums and extend to minOfMaximums, resulting in
+      // a standard, already-normalized region.
+
+      return {
+        origin: maxOfMinimums,
+        extent: minOfMaximums - maxOfMinimums,
+      };
+    }
+
+    if (entries.length === 1) {
+      return first;
+    }
+
+    throw new TypeError(`Expected at least one {origin, extent} entry`);
+  }
+
+  intersectionWith(rect) {
+    const horizontalIntersection =
+      WikiRect.intersectionFromOriginsExtents(
+        {origin: this.x, extent: this.width},
+        {origin: rect.x, extent: rect.width});
+
+    const verticalIntersection =
+      WikiRect.intersectionFromOriginsExtents(
+        {origin: this.y, extent: this.height},
+        {origin: rect.y, extent: rect.height});
+
+    if (!horizontalIntersection) return null;
+    if (!verticalIntersection) return null;
+
+    const {origin: x, extent: width} = horizontalIntersection;
+    const {origin: y, extent: height} = verticalIntersection;
+
+    return Reflect.construct(this.constructor, [x, y, width, height]);
+  }
+
+  chopExtendingOutside(rect) {
+    this.intersectionWith(rect).writeOnto(this);
+  }
+
+  static insetOriginExtent({origin, extent, start = 0, end = 0}) {
+    const normalized =
+      this.normalizeOriginExtent({origin, extent});
+
+    // If this would crush the bounds past each other, just return
+    // the halfway point.
+    if (extent < start + end) {
+      return {origin: origin + (start + end) / 2, extent: 0};
+    }
+
+    return {
+      origin: normalized.origin + start,
+      extent: normalized.extent - start - end,
+    };
+  }
+
+  toInset(arg1, arg2) {
+    if (typeof arg1 === 'number' && typeof arg2 === 'number') {
+      return this.toInset({
+        left: arg2,
+        right: arg2,
+        top: arg1,
+        bottom: arg1,
+      });
+    } else if (typeof arg1 === 'number') {
+      return this.toInset({
+        left: arg1,
+        right: arg1,
+        top: arg1,
+        bottom: arg1,
+      });
+    }
+
+    const {top, left, bottom, right} = arg1;
+
+    const {origin: x, extent: width} =
+      WikiRect.insetOriginExtent({
+        origin: this.x,
+        extent: this.width,
+        start: left,
+        end: right,
+      });
+
+    const {origin: y, extent: height} =
+      WikiRect.insetOriginExtent({
+        origin: this.y,
+        extent: this.height,
+        start: top,
+        end: bottom,
+      });
+
+    return Reflect.construct(this.constructor, [x, y, width, height]);
+  }
+
+  static extendOriginExtent({origin, extent, start = 0, end = 0}) {
+    const normalized =
+      this.normalizeOriginExtent({origin, extent});
+
+    return {
+      origin: normalized.origin - start,
+      extent: normalized.extent + start + end,
+    };
+  }
+
+  toExtended(arg1, arg2) {
+    if (typeof arg1 === 'number' && typeof arg2 === 'number') {
+      return this.toExtended({
+        left: arg2,
+        right: arg2,
+        top: arg1,
+        bottom: arg1,
+      });
+    } else if (typeof arg1 === 'number') {
+      return this.toExtended({
+        left: arg1,
+        right: arg1,
+        top: arg1,
+        bottom: arg1,
+      });
+    }
+
+    const {top, left, bottom, right} = arg1;
+
+    const {origin: x, extent: width} =
+      WikiRect.extendOriginExtent({
+        origin: this.x,
+        extent: this.width,
+        start: left,
+        end: right,
+      });
+
+    const {origin: y, extent: height} =
+      WikiRect.extendOriginExtent({
+        origin: this.y,
+        extent: this.height,
+        start: top,
+        end: bottom,
+      });
+
+    return Reflect.construct(this.constructor, [x, y, width, height]);
+  }
+
+  // Comparisons
+
+  equals(rect) {
+    const rectNormalized = WikiRect.fromRect(rect).toNormalized();
+    const thisNormalized = this.toNormalized();
+
+    return (
+      rectNormalized.x === thisNormalized.x &&
+      rectNormalized.y === thisNormalized.y &&
+      rectNormalized.width === thisNormalized.width &&
+      rectNormalized.height === thisNormalized.height
+    );
+  }
+
+  contains(rect) {
+    return !!this.intersectionWith(rect)?.equals(rect);
+  }
+
+  containedWithin(rect) {
+    return !!this.intersectionWith(rect)?.equals(this);
+  }
+
+  fits(rect) {
+    const rectNormalized = WikiRect.fromRect(rect).toNormalized();
+    const thisNormalized = this.toNormalized();
+
+    return (
+      (!isFinite(this.width) || rectNormalized.width <= thisNormalized.width) &&
+      (!isFinite(this.height) || rectNormalized.height <= thisNormalized.height)
+    );
+  }
+
+  fitsWithin(rect) {
+    const rectNormalized = WikiRect.fromRect(rect).toNormalized();
+    const thisNormalized = this.toNormalized();
+
+    return (
+      (!isFinite(rect.width) || thisNormalized.width <= rectNormalized.width) &&
+      (!isFinite(rect.height) || thisNormalized.height <= rectNormalized.height)
+    );
+  }
+
+  // Interfacing utilities
+
+  static fromRect(rect) {
+    return Reflect.construct(this, [rect.x, rect.y, rect.width, rect.height]);
+  }
+
+  writeOnto(destination) {
+    Object.assign(destination, {
+      x: this.x,
+      y: this.y,
+      width: this.width,
+      height: this.height,
+    });
+  }
+}
+
+// CSS compatibility-assistant ----------------------------
+
+const cssCompatibilityAssistantInfo = clientInfo.cssCompatibilityAssistantInfo = {
+  coverArtContainer: null,
+  coverArtImageDetails: null,
+};
+
+function getCSSCompatibilityAssistantInfoReferences() {
+  const info = cssCompatibilityAssistantInfo;
+
+  info.coverArtContainer =
+    document.getElementById('cover-art-container');
+
+  info.coverArtImageDetails =
+    info.coverArtContainer?.querySelector('.image-details');
+}
+
+function mutateCSSCompatibilityContent() {
+  const info = cssCompatibilityAssistantInfo;
+
+  if (info.coverArtImageDetails) {
+    info.coverArtContainer.classList.add('has-image-details');
+  }
+}
+
+clientSteps.getPageReferences.push(getCSSCompatibilityAssistantInfoReferences);
+clientSteps.mutatePageContent.push(mutateCSSCompatibilityContent);
+
+// Ever-updating mouse position helper --------------------
+
+const liveMousePositionInfo = initInfo('liveMousePositionInfo', {
+  state: {
+    clientX: null,
+    clientY: null,
+  },
+});
+
+function addLiveMousePositionPageListeners() {
+  const info = liveMousePositionInfo;
+  const {state} = info;
+
+  document.body.addEventListener('mousemove', domEvent => {
+    Object.assign(state, {
+      clientX: domEvent.clientX,
+      clientY: domEvent.clientY,
+    });
+  });
+}
+
+clientSteps.addPageListeners.push(addLiveMousePositionPageListeners);
+
+// JS-based links -----------------------------------------
+
+const scriptedLinkInfo = initInfo('scriptedLinkInfo', {
+  randomLinks: null,
+  revealLinks: null,
+  revealContainers: null,
+
+  nextNavLink: null,
+  previousNavLink: null,
+  randomNavLink: null,
+
+  state: {
+    albumDirectories: null,
+    albumTrackDirectories: null,
+    artistDirectories: null,
+    artistNumContributions: null,
+  },
+});
+
+function getScriptedLinkReferences() {
+  scriptedLinkInfo.randomLinks =
+    document.querySelectorAll('[data-random]');
+
+  scriptedLinkInfo.revealLinks =
+    document.querySelectorAll('.reveal .image-outer-area > *');
+
+  scriptedLinkInfo.revealContainers =
+    Array.from(scriptedLinkInfo.revealLinks)
+      .map(link => link.closest('.reveal'));
+
+  scriptedLinkInfo.nextNavLink =
+    document.getElementById('next-button');
+
+  scriptedLinkInfo.previousNavLink =
+    document.getElementById('previous-button');
+
+  scriptedLinkInfo.randomNavLink =
+    document.getElementById('random-button');
+}
+
+function addRandomLinkListeners() {
+  for (const a of scriptedLinkInfo.randomLinks ?? []) {
+    a.addEventListener('click', domEvent => {
+      handleRandomLinkClicked(a, domEvent);
+    });
+  }
+}
+
+function handleRandomLinkClicked(a, domEvent) {
+  const href = determineRandomLinkHref(a);
+
+  if (!href) {
+    domEvent.preventDefault();
+    return;
+  }
+
+  setTimeout(() => {
+    a.href = '#'
+  });
+
+  a.href = href;
+}
+
+function determineRandomLinkHref(a) {
+  const {state} = scriptedLinkInfo;
+
+  const trackDirectoriesFromAlbumDirectories = albumDirectories =>
+    albumDirectories
+      .map(directory => state.albumDirectories.indexOf(directory))
+      .map(index => state.albumTrackDirectories[index])
+      .reduce((acc, trackDirectories) => acc.concat(trackDirectories, []));
+
+  switch (a.dataset.random) {
+    case 'album': {
+      const {albumDirectories} = state;
+      if (!albumDirectories) return null;
+
+      return openAlbum(pick(albumDirectories));
+    }
+
+    case 'track': {
+      const {albumDirectories} = state;
+      if (!albumDirectories) return null;
+
+      const trackDirectories =
+        trackDirectoriesFromAlbumDirectories(
+          albumDirectories);
+
+      return openTrack(pick(trackDirectories));
+    }
+
+    case 'album-in-group-dl': {
+      const albumLinks =
+        Array.from(a
+          .closest('dt')
+          .nextElementSibling
+          .querySelectorAll('li a'))
+
+      const listAlbumDirectories =
+        albumLinks
+          .map(a => cssProp(a, '--album-directory'));
+
+      return openAlbum(pick(listAlbumDirectories));
+    }
+
+    case 'track-in-group-dl': {
+      const {albumDirectories} = state;
+      if (!albumDirectories) return null;
+
+      const albumLinks =
+        Array.from(a
+          .closest('dt')
+          .nextElementSibling
+          .querySelectorAll('li a'))
+
+      const listAlbumDirectories =
+        albumLinks
+          .map(a => cssProp(a, '--album-directory'));
+
+      const trackDirectories =
+        trackDirectoriesFromAlbumDirectories(
+          listAlbumDirectories);
+
+      return openTrack(pick(trackDirectories));
+    }
+
+    case 'track-in-sidebar': {
+      // Note that the container for track links may be <ol> or <ul>, and
+      // they can't be identified by href, since links from one track to
+      // another don't include "track" in the href.
+      const trackLinks =
+        Array.from(document
+          .querySelector('.track-list-sidebar-box')
+          .querySelectorAll('li a'));
+
+      return pick(trackLinks).href;
+    }
+
+    case 'track-in-album': {
+      const {albumDirectories, albumTrackDirectories} = state;
+      if (!albumDirectories || !albumTrackDirectories) return null;
+
+      const albumDirectory = cssProp(a, '--album-directory');
+      const albumIndex = albumDirectories.indexOf(albumDirectory);
+      const trackDirectories = albumTrackDirectories[albumIndex];
+
+      return openTrack(pick(trackDirectories));
+    }
+
+    case 'artist': {
+      const {artistDirectories} = state;
+      if (!artistDirectories) return null;
+
+      return openArtist(pick(artistDirectories));
+    }
+
+    case 'artist-more-than-one-contrib': {
+      const {artistDirectories, artistNumContributions} = state;
+      if (!artistDirectories || !artistNumContributions) return null;
+
+      const filteredArtistDirectories =
+        artistDirectories
+          .filter((_artist, index) => artistNumContributions[index] > 1);
+
+      return openArtist(pick(filteredArtistDirectories));
+    }
+  }
+}
+
+function mutateNavigationLinkContent() {
+  const prependTitle = (el, prepend) =>
+    el?.setAttribute('title',
+      (el.hasAttribute('title')
+        ? prepend + ' ' + el.getAttribute('title')
+        : prepend));
+
+  prependTitle(scriptedLinkInfo.nextNavLink, '(Shift+N)');
+  prependTitle(scriptedLinkInfo.previousNavLink, '(Shift+P)');
+  prependTitle(scriptedLinkInfo.randomNavLink, '(Shift+R)');
+}
+
+function addNavigationKeyPressListeners() {
+  document.addEventListener('keypress', (event) => {
+    if (event.shiftKey) {
+      if (event.charCode === 'N'.charCodeAt(0)) {
+        scriptedLinkInfo.nextNavLink?.click();
+      } else if (event.charCode === 'P'.charCodeAt(0)) {
+        scriptedLinkInfo.previousNavLink?.click();
+      } else if (event.charCode === 'R'.charCodeAt(0)) {
+        scriptedLinkInfo.randomNavLink?.click();
+      }
+    }
+  });
+}
+
+function addRevealLinkClickListeners() {
+  const info = scriptedLinkInfo;
+
+  for (const {revealLink, revealContainer} of stitchArrays({
+    revealLink: Array.from(info.revealLinks ?? []),
+    revealContainer: Array.from(info.revealContainers ?? []),
+  })) {
+    revealLink.addEventListener('click', (event) => {
+      handleRevealLinkClicked(event, revealLink, revealContainer);
+    });
+  }
+}
+
+function handleRevealLinkClicked(domEvent, _revealLink, revealContainer) {
+  if (revealContainer.classList.contains('revealed')) {
+    return;
+  }
+
+  domEvent.preventDefault();
+  revealContainer.classList.add('revealed');
+  revealContainer.dispatchEvent(new CustomEvent('hsmusic-reveal'));
+}
+
+clientSteps.getPageReferences.push(getScriptedLinkReferences);
+clientSteps.addPageListeners.push(addRandomLinkListeners);
+clientSteps.addPageListeners.push(addNavigationKeyPressListeners);
+clientSteps.addPageListeners.push(addRevealLinkClickListeners);
+clientSteps.mutatePageContent.push(mutateNavigationLinkContent);
+
+if (
+  document.documentElement.dataset.urlKey === 'localized.listing' &&
+  document.documentElement.dataset.urlValue0 === 'random'
+) {
+  const dataLoadingLine = document.getElementById('data-loading-line');
+  const dataLoadedLine = document.getElementById('data-loaded-line');
+  const dataErrorLine = document.getElementById('data-error-line');
+
+  dataLoadingLine.style.display = 'block';
+
+  fetch(rebase('random-link-data.json', 'rebaseShared'))
+    .then(data => data.json())
+    .then(data => {
+      const {state} = scriptedLinkInfo;
+
+      Object.assign(state, {
+        albumDirectories: data.albumDirectories,
+        albumTrackDirectories: data.albumTrackDirectories,
+        artistDirectories: data.artistDirectories,
+        artistNumContributions: data.artistNumContributions,
+      });
+
+      dataLoadingLine.style.display = 'none';
+      dataLoadedLine.style.display = 'block';
+    }, () => {
+      dataLoadingLine.style.display = 'none';
+      dataErrorLine.style.display = 'block';
+    })
+    .then(() => {
+      const {randomLinks} = scriptedLinkInfo;
+      for (const a of randomLinks) {
+        const href = determineRandomLinkHref(a);
+        if (!href) {
+          a.removeAttribute('href');
+        }
+      }
+    });
+}
+
+// Tooltip-style hover (infrastructure) -------------------
+
+const hoverableTooltipInfo = initInfo('hoverableTooltipInfo', {
+  settings: {
+    // Hovering has two speed settings. The normal setting is used by default,
+    // and once a tooltip is displayed as a result of hover, the entire tooltip
+    // system will enter a "fast hover mode" - hovering will activate tooltips
+    // sooner. "Fast hover mode" is disabled after a sustained duration of not
+    // hovering over any hoverables; it's meant only to accelerate switching
+    // tooltips while still deciding, or getting a quick overview across more
+    // than one tooltip.
+    normalHoverInfoDelay: 400,
+    fastHoveringInfoDelay: 150,
+    endFastHoveringDelay: 500,
+
+    // Focusing has a single speed setting, which is how long it will take to
+    // enter a functional "focus mode" (though it's not actually implemented
+    // in terms of this state). As soon as "focus mode" is entered, the tooltip
+    // for the current hoverable is displayed, and focusing another hoverable
+    // will cause the current tooltip to be swapped for that one immediately.
+    // "Focus mode" ends as soon as anything apart from a tooltip or hoverable
+    // is focused, and it will be necessary to wait on this delay again.
+    focusInfoDelay: 750,
+
+    hideTooltipDelay: 500,
+
+    // If a tooltip that's transitioning to hidden is hovered during the grace
+    // period (or the corresponding hoverable is hovered at any point in the
+    // transition), it'll cancel out of this animation immediately.
+    transitionHiddenDuration: 300,
+    inertGracePeriod: 100,
+  },
+
+  state: {
+    // These maps store a record for each registered element and related state
+    // and registration info, if applicable.
+    registeredTooltips: new Map(),
+    registeredHoverables: new Map(),
+
+    // These are common across all tooltips, rather than stored individually,
+    // based on the principles that 1) only a single tooltip can be displayed
+    // at once, and 2) likewise, only a single hoverable can be hovered,
+    // focused, or otherwise active at once.
+    hoverTimeout: null,
+    focusTimeout: null,
+    touchTimeout: null,
+    hideTimeout: null,
+    transitionHiddenTimeout: null,
+    inertGracePeriodTimeout: null,
+    currentlyShownTooltip: null,
+    currentlyActiveHoverable: null,
+    currentlyTransitioningHiddenTooltip: null,
+    previouslyActiveHoverable: null,
+    tooltipWasJustHidden: false,
+    hoverableWasRecentlyTouched: false,
+
+    // Fast hovering is a global mode which is activated as soon as any tooltip
+    // is displayed and turns off after a delay of no hoverables being hovered.
+    // Note that fast hovering may be turned off while hovering a tooltip, but
+    // it will never be turned off while idling over a hoverable.
+    fastHovering: false,
+    endFastHoveringTimeout: false,
+
+    // These track the identifiers of current touches and a record of current
+    // identifiers that are "banished" by scrolling - that is, touches which
+    // existed while the page scrolled and were probably responsible for that
+    // scrolling. This is a bit loose (we can't actually tell which touches
+    // caused the page to scroll) but it's intended to keep scrolling the page
+    // from causing the current tooltip to be hidden.
+    currentTouchIdentifiers: new Set(),
+    touchIdentifiersBanishedByScrolling: new Set(),
+  },
+
+  event: {
+    whenTooltipShows: [],
+    whenTooltipHides: [],
+  },
+});
+
+// Adds DOM event listeners, so must be called during addPageListeners step.
+function registerTooltipElement(tooltip) {
+  const {state} = hoverableTooltipInfo;
+
+  if (!tooltip)
+    throw new Error(`Expected tooltip`);
+
+  if (state.registeredTooltips.has(tooltip))
+    throw new Error(`This tooltip is already registered`);
+
+  // No state or registration info here.
+  state.registeredTooltips.set(tooltip, {});
+
+  tooltip.addEventListener('mouseenter', () => {
+    handleTooltipMouseEntered(tooltip);
+  });
+
+  tooltip.addEventListener('mouseleave', () => {
+    handleTooltipMouseLeft(tooltip);
+  });
+
+  tooltip.addEventListener('focusin', event => {
+    handleTooltipReceivedFocus(tooltip, event.relatedTarget);
+  });
+
+  tooltip.addEventListener('focusout', event => {
+    // This event gets activated for tabbing *between* links inside the
+    // tooltip, which is no good and certainly doesn't represent the focus
+    // leaving the tooltip.
+    if (currentlyShownTooltipHasFocus(event.relatedTarget)) return;
+
+    handleTooltipLostFocus(tooltip, event.relatedTarget);
+  });
+}
+
+// Adds DOM event listeners, so must be called during addPageListeners step.
+function registerTooltipHoverableElement(hoverable, tooltip) {
+  const {state} = hoverableTooltipInfo;
+
+  if (!hoverable || !tooltip)
+    if (hoverable)
+      throw new Error(`Expected hoverable and tooltip, got only hoverable`);
+    else
+      throw new Error(`Expected hoverable and tooltip, got neither`);
+
+  if (!state.registeredTooltips.has(tooltip))
+    throw new Error(`Register tooltip before registering hoverable`);
+
+  if (state.registeredHoverables.has(hoverable))
+    throw new Error(`This hoverable is already registered`);
+
+  state.registeredHoverables.set(hoverable, {tooltip});
+
+  hoverable.addEventListener('mouseenter', () => {
+    handleTooltipHoverableMouseEntered(hoverable);
+  });
+
+  hoverable.addEventListener('mouseleave', () => {
+    handleTooltipHoverableMouseLeft(hoverable);
+  });
+
+  hoverable.addEventListener('focusin', event => {
+    handleTooltipHoverableReceivedFocus(hoverable, event);
+  });
+
+  hoverable.addEventListener('focusout', event => {
+    handleTooltipHoverableLostFocus(hoverable, event);
+  });
+
+  hoverable.addEventListener('touchend', event => {
+    handleTooltipHoverableTouchEnded(hoverable, event);
+  });
+
+  hoverable.addEventListener('click', event => {
+    handleTooltipHoverableClicked(hoverable, event);
+  });
+}
+
+function handleTooltipMouseEntered(tooltip) {
+  const {state} = hoverableTooltipInfo;
+
+  if (state.currentlyTransitioningHiddenTooltip) {
+    cancelTransitioningTooltipHidden(true);
+    return;
+  }
+
+  if (state.currentlyShownTooltip !== tooltip) return;
+
+  // Don't time out the current tooltip while hovering it.
+
+  if (state.hideTimeout) {
+    clearTimeout(state.hideTimeout);
+    state.hideTimeout = null;
+  }
+}
+
+function handleTooltipMouseLeft(tooltip) {
+  const {settings, state} = hoverableTooltipInfo;
+
+  if (state.currentlyShownTooltip !== tooltip) return;
+
+  // Start timing out the current tooltip when it's left. This could be
+  // canceled by mousing over a hoverable, or back over the tooltip again.
+  if (!state.hideTimeout) {
+    state.hideTimeout =
+      setTimeout(() => {
+        state.hideTimeout = null;
+        hideCurrentlyShownTooltip();
+      }, settings.hideTooltipDelay);
+  }
+}
+
+function handleTooltipReceivedFocus(_tooltip) {
+  const {state} = hoverableTooltipInfo;
+
+  // Cancel the tooltip-hiding timeout if it exists. The tooltip will never
+  // be hidden while it contains the focus anyway, but this ensures the timeout
+  // will be suitably reset when the tooltip loses focus.
+  if (state.hideTimeout) {
+    clearTimeout(state.hideTimeout);
+    state.hideTimeout = null;
+  }
+}
+
+function handleTooltipLostFocus(_tooltip) {
+  // Hide the current tooltip right away when it loses focus. Specify intent
+  // to replace - while we don't strictly know if another tooltip is going to
+  // immediately replace it, the mode of navigating with tab focus (once one
+  // tooltip has been activated) is a "switch focus immediately" kind of
+  // interaction in its nature.
+  hideCurrentlyShownTooltip(true);
+}
+
+function handleTooltipHoverableMouseEntered(hoverable) {
+  const {settings, state} = hoverableTooltipInfo;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  // If this tooltip was transitioning to hidden, hovering should cancel that
+  // animation and show it immediately.
+
+  if (tooltip === state.currentlyTransitioningHiddenTooltip) {
+    cancelTransitioningTooltipHidden(true);
+    return;
+  }
+
+  // Start a timer to show the corresponding tooltip, with the delay depending
+  // on whether fast hovering or not. This could be canceled by mousing out of
+  // the hoverable.
+
+  const hoverTimeoutDelay =
+    (state.fastHovering
+      ? settings.fastHoveringInfoDelay
+      : settings.normalHoverInfoDelay);
+
+  state.hoverTimeout =
+    setTimeout(() => {
+      state.hoverTimeout = null;
+      state.fastHovering = true;
+      showTooltipFromHoverable(hoverable);
+    }, hoverTimeoutDelay);
+
+  // Don't stop fast hovering while over any hoverable.
+  if (state.endFastHoveringTimeout) {
+    clearTimeout(state.endFastHoveringTimeout);
+    state.endFastHoveringTimeout = null;
+  }
+
+  // Don't time out the current tooltip while over any hoverable.
+  if (state.hideTimeout) {
+    clearTimeout(state.hideTimeout);
+    state.hideTimeout = null;
+  }
+}
+
+function handleTooltipHoverableMouseLeft(_hoverable) {
+  const {settings, state} = hoverableTooltipInfo;
+
+  // Don't show a tooltip when not over a hoverable!
+  if (state.hoverTimeout) {
+    clearTimeout(state.hoverTimeout);
+    state.hoverTimeout = null;
+  }
+
+  // Start timing out fast hovering (if active) when not over a hoverable.
+  // This will only be canceled by mousing over another hoverable.
+  if (state.fastHovering && !state.endFastHoveringTimeout) {
+    state.endFastHoveringTimeout =
+      setTimeout(() => {
+        state.endFastHoveringTimeout = null;
+        state.fastHovering = false;
+      }, settings.endFastHoveringDelay);
+  }
+
+  // Start timing out the current tooltip when mousing not over a hoverable.
+  // This could be canceled by mousing over another hoverable, or over the
+  // currently shown tooltip.
+  if (state.currentlyShownTooltip && !state.hideTimeout) {
+    state.hideTimeout =
+      setTimeout(() => {
+        state.hideTimeout = null;
+        hideCurrentlyShownTooltip();
+      }, settings.hideTooltipDelay);
+  }
+}
+
+function handleTooltipHoverableReceivedFocus(hoverable) {
+  const {settings, state} = hoverableTooltipInfo;
+
+  // By default, display the corresponding tooltip after a delay.
+
+  state.focusTimeout =
+    setTimeout(() => {
+      state.focusTimeout = null;
+      showTooltipFromHoverable(hoverable);
+    }, settings.focusInfoDelay);
+
+  // If a tooltip was just hidden - which is almost certainly a result of the
+  // focus changing - then display this tooltip immediately, canceling the
+  // above timeout.
+
+  if (state.tooltipWasJustHidden) {
+    clearTimeout(state.focusTimeout);
+    state.focusTimeout = null;
+
+    showTooltipFromHoverable(hoverable);
+  }
+}
+
+function handleTooltipHoverableLostFocus(hoverable, domEvent) {
+  const {state} = hoverableTooltipInfo;
+
+  // Don't show a tooltip from focusing a hoverable if it isn't focused
+  // anymore! If another hoverable is receiving focus, that will be evaluated
+  // and set its own focus timeout after we clear the previous one here.
+  if (state.focusTimeout) {
+    clearTimeout(state.focusTimeout);
+    state.focusTimeout = null;
+  }
+
+  // Unless focus is entering the tooltip itself, hide the tooltip immediately.
+  // This will set the tooltipWasJustHidden flag, which is detected by a newly
+  // focused hoverable, if applicable. Always specify intent to replace when
+  // navigating via tab focus. (Check `handleTooltipLostFocus` for details.)
+  if (!currentlyShownTooltipHasFocus(domEvent.relatedTarget)) {
+    hideCurrentlyShownTooltip(true);
+  }
+}
+
+function handleTooltipHoverableTouchEnded(hoverable, domEvent) {
+  const {state} = hoverableTooltipInfo;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  // Don't proceed if this hoverable's tooltip is already visible - in that
+  // case touching the hoverable again should behave just like a normal click.
+  if (state.currentlyShownTooltip === tooltip) {
+    // If the hoverable was *recently* touched - meaning that this is a second
+    // touchend in short succession - then just letting the click come through
+    // naturally would (depending on timing) not actually navigate anywhere,
+    // because we've deliberately banished the *first* touch from navigation.
+    // We do want the second touch to navigate, so clear that recently-touched
+    // state, allowing this touch's click to behave as normal.
+    if (state.hoverableWasRecentlyTouched) {
+      clearTimeout(state.touchTimeout);
+      state.touchTimeout = null;
+      state.hoverableWasRecentlyTouched = false;
+    }
+
+    // Otherwise, this is just a second touch after enough time has passed
+    // that the one which showed the tooltip is no longer "recent", and we're
+    // not in any special state. The link will navigate to its page just like
+    // normal.
+    return;
+  }
+
+  const touches = Array.from(domEvent.changedTouches);
+  const identifiers = touches.map(touch => touch.identifier);
+
+  // Don't process touch events that were "banished" because the page was
+  // scrolled while those touches were active, and most likely as a result of
+  // them.
+  filterMultipleArrays(touches, identifiers,
+    (_touch, identifier) =>
+      !state.touchIdentifiersBanishedByScrolling.has(identifier));
+
+  if (empty(touches)) return;
+
+  // Don't proceed if none of the (just-ended) touches ended over the
+  // hoverable.
+
+  const pointIsOverThisHoverable = pointIsOverAnyOf([hoverable]);
+
+  const anyTouchEndedOverHoverable =
+    touches.some(({clientX, clientY}) =>
+      pointIsOverThisHoverable(clientX, clientY));
+
+  if (!anyTouchEndedOverHoverable) {
+    return;
+  }
+
+  if (state.touchTimeout) {
+    clearTimeout(state.touchTimeout);
+    state.touchTimeout = null;
+  }
+
+  // Show the tooltip right away.
+  showTooltipFromHoverable(hoverable);
+
+  // Set a state, for a brief but not instantaneous period, indicating that a
+  // hoverable was recently touched. The touchend event may precede the click
+  // event by some time, and we don't want to navigate away from the page as
+  // a result of the click event which this touch precipitated.
+  state.hoverableWasRecentlyTouched = true;
+  state.touchTimeout =
+    setTimeout(() => {
+      state.touchTimeout = null;
+      state.hoverableWasRecentlyTouched = false;
+    }, 1200);
+}
+
+function handleTooltipHoverableClicked(hoverable) {
+  const {state} = hoverableTooltipInfo;
+
+  // Don't navigate away from the page if the this hoverable was recently
+  // touched (and had its tooltip activated). That flag won't be set if its
+  // tooltip was already open before the touch.
+  if (
+    state.currentlyActiveHoverable === hoverable &&
+    state.hoverableWasRecentlyTouched
+  ) {
+    event.preventDefault();
+  }
+}
+
+function currentlyShownTooltipHasFocus(focusElement = document.activeElement) {
+  const {state} = hoverableTooltipInfo;
+
+  const {
+    currentlyShownTooltip: tooltip,
+    currentlyActiveHoverable: hoverable,
+  } = state;
+
+  // If there's no tooltip, it can't possibly have focus.
+  if (!tooltip) return false;
+
+  // If the tooltip literally contains (or is) the focused element, then that's
+  // the principle condition we're looking for.
+  if (tooltip.contains(focusElement)) return true;
+
+  // If the hoverable *which opened the tooltip* is focused, then that also
+  // represents the tooltip being focused (in its currently shown state).
+  if (hoverable.contains(focusElement)) return true;
+
+  return false;
+}
+
+function beginTransitioningTooltipHidden(tooltip) {
+  const {settings, state} = hoverableTooltipInfo;
+
+  if (state.currentlyTransitioningHiddenTooltip) {
+    cancelTransitioningTooltipHidden();
+  }
+
+  cssProp(tooltip, {
+    'display': 'block',
+    'opacity': '0',
+
+    'transition-property': 'opacity',
+    'transition-timing-function':
+      `steps(${Math.ceil(settings.transitionHiddenDuration / 60)}, end)`,
+    'transition-duration':
+      `${settings.transitionHiddenDuration / 1000}s`,
+  });
+
+  state.currentlyTransitioningHiddenTooltip = tooltip;
+  state.transitionHiddenTimeout =
+    setTimeout(() => {
+      endTransitioningTooltipHidden();
+    }, settings.transitionHiddenDuration);
+}
+
+function cancelTransitioningTooltipHidden(andShow = false) {
+  const {state} = hoverableTooltipInfo;
+
+  endTransitioningTooltipHidden();
+
+  if (andShow) {
+    showTooltipFromHoverable(state.previouslyActiveHoverable);
+  }
+}
+
+function endTransitioningTooltipHidden() {
+  const {state} = hoverableTooltipInfo;
+  const {currentlyTransitioningHiddenTooltip: tooltip} = state;
+
+  if (!tooltip) return;
+
+  cssProp(tooltip, {
+    'display': null,
+    'opacity': null,
+    'transition-property': null,
+    'transition-timing-function': null,
+    'transition-duration': null,
+  });
+
+  state.currentlyTransitioningHiddenTooltip = null;
+
+  if (state.inertGracePeriodTimeout) {
+    clearTimeout(state.inertGracePeriodTimeout);
+    state.inertGracePeriodTimeout = null;
+  }
+
+  if (state.transitionHiddenTimeout) {
+    clearTimeout(state.transitionHiddenTimeout);
+    state.transitionHiddenTimeout = null;
+  }
+}
+
+function hideCurrentlyShownTooltip(intendingToReplace = false) {
+  const {settings, state, event} = hoverableTooltipInfo;
+  const {currentlyShownTooltip: tooltip} = state;
+
+  // If there was no tooltip to begin with, we're functionally in the desired
+  // state already, so return true.
+  if (!tooltip) return true;
+
+  // Never hide the tooltip if it's focused.
+  if (currentlyShownTooltipHasFocus()) return false;
+
+  state.currentlyActiveHoverable.classList.remove('has-visible-tooltip');
+
+  // If there's no intent to replace this tooltip, it's the last one currently
+  // apparent in the interaction, and should be hidden with a transition.
+  if (intendingToReplace) {
+    cssProp(tooltip, 'display', 'none');
+  } else {
+    beginTransitioningTooltipHidden(state.currentlyShownTooltip);
+  }
+
+  // Wait just a moment before making the tooltip inert. You might react
+  // (to the ghosting, or just to time passing) and realize you wanted
+  // to look at the tooltip after all - this delay gives a little buffer
+  // to second guess letting it disappear.
+  state.inertGracePeriodTimeout =
+    setTimeout(() => {
+      tooltip.inert = true;
+    }, settings.inertGracePeriod);
+
+  state.previouslyActiveHoverable = state.currentlyActiveHoverable;
+
+  state.currentlyShownTooltip = null;
+  state.currentlyActiveHoverable = null;
+
+  // Set this for one tick of the event cycle.
+  state.tooltipWasJustHidden = true;
+  setTimeout(() => {
+    state.tooltipWasJustHidden = false;
+  });
+
+  dispatchInternalEvent(event, 'whenTooltipHides', {
+    tooltip,
+  });
+
+  return true;
+}
+
+function showTooltipFromHoverable(hoverable) {
+  const {state, event} = hoverableTooltipInfo;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  if (!hideCurrentlyShownTooltip(true)) return false;
+
+  // Cancel out another tooltip that's transitioning hidden, if that's going
+  // on - it's a distraction that this tooltip is now replacing.
+  cancelTransitioningTooltipHidden();
+
+  hoverable.classList.add('has-visible-tooltip');
+
+  positionTooltipFromHoverableWithBrains(hoverable);
+
+  cssProp(tooltip, 'display', 'block');
+  tooltip.inert = false;
+
+  state.currentlyShownTooltip = tooltip;
+  state.currentlyActiveHoverable = hoverable;
+
+  state.tooltipWasJustHidden = false;
+
+  dispatchInternalEvent(event, 'whenTooltipShows', {
+    tooltip,
+  });
+
+  return true;
+}
+
+function peekTooltipClientRect(tooltip) {
+  const oldDisplayStyle = cssProp(tooltip, 'display');
+  cssProp(tooltip, 'display', 'block');
+
+  // Tooltips have a bit of padding that makes the interactive
+  // area wider, so that you're less likely to accidentally let
+  // the tooltip disappear (by hovering outside it). But this
+  // isn't visual at all, so for placement we only care about
+  // the content element.
+  const content =
+    tooltip.querySelector('.tooltip-content');
+
+  try {
+    return WikiRect.fromElement(content);
+  } finally {
+    cssProp(tooltip, 'display', oldDisplayStyle);
+  }
+}
+
+function positionTooltipFromHoverableWithBrains(hoverable) {
+  const {state} = hoverableTooltipInfo;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  // Reset before doing anything else. We're going to adapt to
+  // its natural placement, adjusted by CSS, which otherwise
+  // could be obscured by a placement we've previously provided.
+  resetDynamicTooltipPositioning(tooltip);
+
+  const opportunities =
+    getTooltipFromHoverablePlacementOpportunityAreas(hoverable);
+
+  const tooltipRect =
+    peekTooltipClientRect(tooltip);
+
+  // If the tooltip is already in the baseline containing area,
+  // prefer to keep it positioned naturally, adjusted by CSS
+  // instead of JavaScript.
+
+  const {numBaselineRects, idealBaseline: baselineRect} = opportunities;
+
+  if (baselineRect.contains(tooltipRect)) {
+    return;
+  }
+
+  let selectedRect = null;
+  for (let i = 0; i < numBaselineRects; i++) {
+    selectedRect = opportunities.right.down[i];
+    if (selectedRect) break;
+
+    selectedRect = opportunities.left.down[i];
+    if (selectedRect) break;
+
+    selectedRect = opportunities.right.up[i];
+    if (selectedRect) break;
+
+    selectedRect = opportunities.left.up[i];
+    if (selectedRect) break;
+  }
+
+  selectedRect ??= baselineRect;
+
+  positionTooltip(tooltip, selectedRect.x, selectedRect.y);
+}
+
+function positionTooltip(tooltip, x, y) {
+  // Imagine what it'd be like if the tooltip were positioned
+  // with zero left/top offset, and calculate its actual offsets
+  // based on that.
+
+  cssProp(tooltip, {
+    left: `0`,
+    top: `0`,
+  });
+
+  const tooltipRect =
+    peekTooltipClientRect(tooltip);
+
+  cssProp(tooltip, {
+    left: `${x - tooltipRect.x}px`,
+    top: `${y - tooltipRect.y}px`,
+  });
+}
+
+function resetDynamicTooltipPositioning(tooltip) {
+  cssProp(tooltip, {
+    left: null,
+    top: null,
+  });
+}
+
+function getTooltipFromHoverablePlacementOpportunityAreas(hoverable) {
+  const {state} = hoverableTooltipInfo;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  const baselineRects =
+    getTooltipBaselineOpportunityAreas(tooltip);
+
+  const hoverableRect =
+    WikiRect.fromElementUnderMouse(hoverable).toExtended(5, 10);
+
+  const tooltipRect =
+    peekTooltipClientRect(tooltip);
+
+  // Get placements relative to the hoverable. Make these available by key,
+  // allowing the caller to choose by preferred orientation. Each value is
+  // an array which corresponds to the baseline areas - placement closer to
+  // front of the array indicates stronger preference. Since not all relative
+  // placements cooperate with all baseline areas, any of these arrays may
+  // include (or be entirely made of) null.
+
+  const keepIfFits = (rect) =>
+    (rect?.fits(tooltipRect)
+      ? rect
+      : null);
+
+  const prepareRegionRects = (relationalRect, direct) =>
+    baselineRects
+      .map(rect => rect.intersectionWith(relationalRect))
+      .map(direct)
+      .map(keepIfFits);
+
+  const regionRects = {
+    left:
+      prepareRegionRects(
+        WikiRect.leftOf(hoverableRect),
+        rect => WikiRect.fromRect({
+          x: rect.right,
+          y: rect.y,
+          width: -rect.width,
+          height: rect.height,
+        })),
+
+    right:
+      prepareRegionRects(
+        WikiRect.rightOf(hoverableRect),
+        rect => rect),
+
+    top:
+      prepareRegionRects(
+        WikiRect.above(hoverableRect),
+        rect => WikiRect.fromRect({
+          x: rect.x,
+          y: rect.bottom,
+          width: rect.width,
+          height: -rect.height,
+        })),
+
+    bottom:
+      prepareRegionRects(
+        WikiRect.beneath(hoverableRect),
+        rect => rect),
+  };
+
+  const neededVerticalOverlap = 30;
+  const neededHorizontalOverlap = 30;
+
+  // Please don't ask us to make this but horizontal?
+  const prepareVerticalOrientationRects = (regionRects) => {
+    const orientations = {};
+
+    const upTopDown =
+      WikiRect.beneath(
+        hoverableRect.top + neededVerticalOverlap - tooltipRect.height);
+
+    const downBottomUp =
+      WikiRect.above(
+        hoverableRect.bottom - neededVerticalOverlap + tooltipRect.height);
+
+    const orientHorizontally = (rect, i) => {
+      if (!rect) return null;
+
+      const regionRect = regionRects[i];
+      if (regionRect.width > 0) {
+        return rect;
+      } else {
+        return WikiRect.fromRect({
+          x: regionRect.right - tooltipRect.width,
+          y: rect.y,
+          width: rect.width,
+          height: rect.height,
+        });
+      }
+    };
+
+    orientations.up =
+      regionRects
+        .map(rect => rect?.intersectionWith(upTopDown))
+        .map(orientHorizontally)
+        .map(keepIfFits);
+
+    orientations.down =
+      regionRects
+        .map(rect => rect?.intersectionWith(downBottomUp))
+        .map(rect =>
+          (rect
+            ? rect.intersectionWith(WikiRect.fromRect({
+                x: rect.x,
+                y: rect.bottom - tooltipRect.height,
+                width: rect.width,
+                height: tooltipRect.height,
+              }))
+            : null))
+        .map(orientHorizontally)
+        .map(keepIfFits);
+
+    const centerRect =
+      WikiRect.fromRect({
+        x: -Infinity, width: Infinity,
+        y: hoverableRect.top
+         + hoverableRect.height / 2
+         - tooltipRect.height / 2,
+        height: tooltipRect.height,
+      });
+
+    orientations.center =
+      regionRects
+        .map(rect => rect?.intersectionWith(centerRect))
+        .map(orientHorizontally)
+        .map(keepIfFits);
+
+    return orientations;
+  };
+
+  const orientationRects = {
+    left: prepareVerticalOrientationRects(regionRects.left),
+    right: prepareVerticalOrientationRects(regionRects.right),
+  };
+
+  return {
+    numBaselineRects: baselineRects.length,
+    idealBaseline: baselineRects[0],
+    ...orientationRects,
+  };
+}
+
+function getTooltipBaselineOpportunityAreas(tooltip) {
+  // Returns multiple basic areas in order of preference, with front of the
+  // array representing greater preference.
+
+  const {stickyContainers} = stickyHeadingInfo;
+  const results = [];
+
+  const windowRect =
+    WikiRect.fromWindow().toInset(10);
+
+  const workingRect =
+    WikiRect.fromRect(windowRect);
+
+  const tooltipRect =
+    peekTooltipClientRect(tooltip);
+
+  // As a baseline, always treat the window rect as fitting the tooltip.
+  results.unshift(WikiRect.fromRect(workingRect));
+
+  const containingParent =
+    getVisuallyContainingElement(tooltip);
+
+  if (containingParent) {
+    const containingRect =
+      WikiRect.fromElement(containingParent);
+
+    // Only respect a portion of the container's padding, giving
+    // the tooltip the impression of a "raised" element.
+    const padding = side =>
+      0.5 *
+      parseFloat(cssProp(containingParent, 'padding-' + side));
+
+    const insetContainingRect =
+      containingRect.toInset({
+        left: padding('left'),
+        right: padding('right'),
+        top: padding('top'),
+        bottom: padding('bottom'),
+      });
+
+    workingRect.chopExtendingOutside(insetContainingRect);
+
+    if (!workingRect.fits(tooltipRect)) {
+      return results;
+    }
+
+    results.unshift(WikiRect.fromRect(workingRect));
+  }
+
+  // This currently assumes a maximum of one sticky container
+  // per visually containing element.
+
+  const stickyContainer =
+    stickyContainers
+      .find(el => el.parentElement === containingParent);
+
+  if (stickyContainer) {
+    const stickyRect =
+      stickyContainer.getBoundingClientRect()
+
+    // Add some padding so the tooltip doesn't line up exactly
+    // with the edge of the sticky container.
+    const beneathStickyContainer =
+      WikiRect.beneath(stickyRect, 10);
+
+    workingRect.chopExtendingOutside(beneathStickyContainer);
+
+    if (!workingRect.fits(tooltipRect)) {
+      return results;
+    }
+
+    results.unshift(WikiRect.fromRect(workingRect));
+  }
+
+  return results;
+}
+
+function addHoverableTooltipPageListeners() {
+  const {state} = hoverableTooltipInfo;
+
+  const getTouchIdentifiers = domEvent =>
+    Array.from(domEvent.changedTouches)
+      .map(touch => touch.identifier)
+      .filter(identifier => typeof identifier !== 'undefined');
+
+  document.body.addEventListener('touchstart', domEvent => {
+    for (const identifier of getTouchIdentifiers(domEvent)) {
+      state.currentTouchIdentifiers.add(identifier);
+    }
+  });
+
+  window.addEventListener('scroll', () => {
+    for (const identifier of state.currentTouchIdentifiers) {
+      state.touchIdentifiersBanishedByScrolling.add(identifier);
+    }
+  });
+
+  document.body.addEventListener('touchend', domEvent => {
+    setTimeout(() => {
+      for (const identifier of getTouchIdentifiers(domEvent)) {
+        state.currentTouchIdentifiers.delete(identifier);
+        state.touchIdentifiersBanishedByScrolling.delete(identifier);
+      }
+    });
+  });
+
+  const getHoverablesAndTooltips = () => [
+    ...Array.from(state.registeredHoverables.keys()),
+    ...Array.from(state.registeredTooltips.keys()),
+  ];
+
+  document.body.addEventListener('touchend', domEvent => {
+    const touches = Array.from(domEvent.changedTouches);
+    const identifiers = touches.map(touch => touch.identifier);
+
+    // Don't process touch events that were "banished" because the page was
+    // scrolled while those touches were active, and most likely as a result of
+    // them.
+    filterMultipleArrays(touches, identifiers,
+      (_touch, identifier) =>
+        !state.touchIdentifiersBanishedByScrolling.has(identifier));
+
+    if (empty(touches)) return;
+
+    const pointIsOverHoverableOrTooltip =
+      pointIsOverAnyOf(getHoverablesAndTooltips());
+
+    const anyTouchOverAnyHoverableOrTooltip =
+      touches.some(({clientX, clientY}) =>
+        pointIsOverHoverableOrTooltip(clientX, clientY));
+
+    if (!anyTouchOverAnyHoverableOrTooltip) {
+      hideCurrentlyShownTooltip();
+    }
+  });
+
+  document.body.addEventListener('click', domEvent => {
+    const {clientX, clientY} = domEvent;
+
+    const pointIsOverHoverableOrTooltip =
+      pointIsOverAnyOf(getHoverablesAndTooltips());
+
+    if (!pointIsOverHoverableOrTooltip(clientX, clientY)) {
+      // Hide with "intent to replace" - we aren't actually going to replace
+      // the tooltip with a new one, but this intent indicates that it should
+      // be hidden right away, instead of showing. What we're really replacing,
+      // or rather removing, is the state of interacting with tooltips at all.
+      hideCurrentlyShownTooltip(true);
+
+      // Part of that state is fast hovering, which should be canceled out.
+      state.fastHovering = false;
+      if (state.endFastHoveringTimeout) {
+        clearTimeout(state.endFastHoveringTimeout);
+        state.endFastHoveringTimeout = null;
+      }
+
+      // Also cancel out of transitioning a tooltip hidden - this isn't caught
+      // by `hideCurrentlyShownTooltip` because a transitioning-hidden tooltip
+      // doesn't count as "shown" anymore.
+      cancelTransitioningTooltipHidden();
+    }
+  });
+}
+
+clientSteps.addPageListeners.push(addHoverableTooltipPageListeners);
+
+// Data & info card ---------------------------------------
+
+/*
+function colorLink(a, color) {
+  console.warn('Info card link colors temporarily disabled: chroma.js required, no dependency linking for client.js yet');
+  return;
+
+  // eslint-disable-next-line no-unreachable
+  const chroma = {};
+
+  if (color) {
+    const {primary, dim} = getColors(color, {chroma});
+    a.style.setProperty('--primary-color', primary);
+    a.style.setProperty('--dim-color', dim);
+  }
+}
+
+function link(a, type, {name, directory, color}) {
+  colorLink(a, color);
+  a.innerText = name;
+  a.href = getLinkHref(type, directory);
+}
+
+function joinElements(type, elements) {
+  // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on
+  // strings. So instead, we'll pass the element's outer HTML's (which means
+  // the entire HTML of that element).
+  //
+  // That does mean this function returns a string, so always 8e sure to
+  // set innerHTML when using it (not appendChild).
+
+  return list[type](elements.map((el) => el.outerHTML));
+}
+
+const infoCard = (() => {
+  const container = document.getElementById('info-card-container');
+
+  let cancelShow = false;
+  let hideTimeout = null;
+  let showing = false;
+
+  container.addEventListener('mouseenter', cancelHide);
+  container.addEventListener('mouseleave', readyHide);
+
+  function show(type, target) {
+    cancelShow = false;
+
+    fetchData(type, target.dataset[type]).then((data) => {
+      // Manual DOM 'cuz we're laaaazy.
+
+      if (cancelShow) {
+        return;
+      }
+
+      showing = true;
+
+      const rect = target.getBoundingClientRect();
+
+      container.style.setProperty('--primary-color', data.color);
+
+      container.style.top = window.scrollY + rect.bottom + 'px';
+      container.style.left = window.scrollX + rect.left + 'px';
+
+      // Use a short timeout to let a currently hidden (or not yet shown)
+      // info card teleport to the position set a8ove. (If it's currently
+      // shown, it'll transition to that position.)
+      setTimeout(() => {
+        container.classList.remove('hide');
+        container.classList.add('show');
+      }, 50);
+
+      // 8asic details.
+
+      const nameLink = container.querySelector('.info-card-name a');
+      link(nameLink, 'track', data);
+
+      const albumLink = container.querySelector('.info-card-album a');
+      link(albumLink, 'album', data.album);
+
+      const artistSpan = container.querySelector('.info-card-artists span');
+      artistSpan.innerHTML = joinElements(
+        'conjunction',
+        data.artists.map(({artist}) => {
+          const a = document.createElement('a');
+          a.href = getLinkHref('artist', artist.directory);
+          a.innerText = artist.name;
+          return a;
+        })
+      );
+
+      const coverArtistParagraph = container.querySelector(
+        '.info-card-cover-artists'
+      );
+      const coverArtistSpan = coverArtistParagraph.querySelector('span');
+      if (data.coverArtists.length) {
+        coverArtistParagraph.style.display = 'block';
+        coverArtistSpan.innerHTML = joinElements(
+          'conjunction',
+          data.coverArtists.map(({artist}) => {
+            const a = document.createElement('a');
+            a.href = getLinkHref('artist', artist.directory);
+            a.innerText = artist.name;
+            return a;
+          })
+        );
+      } else {
+        coverArtistParagraph.style.display = 'none';
+      }
+
+      // Cover art.
+
+      const [containerNoReveal, containerReveal] = [
+        container.querySelector('.info-card-art-container.no-reveal'),
+        container.querySelector('.info-card-art-container.reveal'),
+      ];
+
+      const [containerShow, containerHide] = data.cover.warnings.length
+        ? [containerReveal, containerNoReveal]
+        : [containerNoReveal, containerReveal];
+
+      containerHide.style.display = 'none';
+      containerShow.style.display = 'block';
+
+      const img = containerShow.querySelector('.info-card-art');
+      img.src = rebase(data.cover.paths.small, 'rebaseMedia');
+
+      const imgLink = containerShow.querySelector('a');
+      colorLink(imgLink, data.color);
+      imgLink.href = rebase(data.cover.paths.original, 'rebaseMedia');
+
+      if (containerShow === containerReveal) {
+        const cw = containerShow.querySelector('.info-card-art-warnings');
+        cw.innerText = list.unit(data.cover.warnings);
+
+        const reveal = containerShow.querySelector('.reveal');
+        reveal.classList.remove('revealed');
+      }
+    });
+  }
+
+  function hide() {
+    container.classList.remove('show');
+    container.classList.add('hide');
+    cancelShow = true;
+    showing = false;
+  }
+
+  function readyHide() {
+    if (!hideTimeout && showing) {
+      hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY);
+    }
+  }
+
+  function cancelHide() {
+    if (hideTimeout) {
+      clearTimeout(hideTimeout);
+      hideTimeout = null;
+    }
+  }
+
+  return {
+    show,
+    hide,
+    readyHide,
+    cancelHide,
+  };
+})();
+
+// Info cards are disa8led for now since they aren't quite ready for release,
+// 8ut you can try 'em out 8y setting this localStorage flag!
+//
+//     localStorage.tryInfoCards = true;
+//
+if (localStorage.tryInfoCards) {
+  addInfoCardLinkHandlers('track');
+}
+*/
+
+// Custom hash links --------------------------------------
+
+const hashLinkInfo = initInfo('hashLinkInfo', {
+  links: null,
+  hrefs: null,
+  targets: null,
+
+  state: {
+    highlightedTarget: null,
+    scrollingAfterClick: false,
+    concludeScrollingStateInterval: null,
+  },
+
+  event: {
+    beforeHashLinkScrolls: [],
+    whenHashLinkClicked: [],
+  },
+});
+
+function getHashLinkReferences() {
+  const info = hashLinkInfo;
+
+  info.links =
+    Array.from(document.querySelectorAll('a[href^="#"]:not([href="#"])'));
+
+  info.hrefs =
+    info.links
+      .map(link => link.getAttribute('href'));
+
+  info.targets =
+    info.hrefs
+      .map(href => document.getElementById(href.slice(1)));
+
+  filterMultipleArrays(
+    info.links,
+    info.hrefs,
+    info.targets,
+    (_link, _href, target) => target);
+}
+
+function processScrollingAfterHashLinkClicked() {
+  const {state} = hashLinkInfo;
+
+  if (state.concludeScrollingStateInterval) return;
+
+  let lastScroll = window.scrollY;
+  state.scrollingAfterClick = true;
+  state.concludeScrollingStateInterval = setInterval(() => {
+    if (Math.abs(window.scrollY - lastScroll) < 10) {
+      clearInterval(state.concludeScrollingStateInterval);
+      state.scrollingAfterClick = false;
+      state.concludeScrollingStateInterval = null;
+    } else {
+      lastScroll = window.scrollY;
+    }
+  }, 200);
+}
+
+function addHashLinkListeners() {
+  // Instead of defining a scroll offset (to account for the sticky heading)
+  // in JavaScript, we interface with the CSS property 'scroll-margin-top'.
+  // This lets the scroll offset be consolidated where it makes sense, and
+  // sets an appropriate offset when (re)loading a page with hash for free!
+
+  const info = hashLinkInfo;
+  const {state, event} = info;
+
+  for (const {hashLink, href, target} of stitchArrays({
+    hashLink: info.links,
+    href: info.hrefs,
+    target: info.targets,
+  })) {
+    hashLink.addEventListener('click', evt => {
+      if (evt.metaKey || evt.shiftKey || evt.ctrlKey || evt.altKey) {
+        return;
+      }
+
+      // Don't do anything if the target element isn't actually visible!
+      if (target.offsetParent === null) {
+        return;
+      }
+
+      // Allow event handlers to prevent scrolling.
+      const listenerResults =
+        dispatchInternalEvent(event, 'beforeHashLinkScrolls', {
+          link: hashLink,
+          target,
+        });
+
+      if (listenerResults.includes(false)) {
+        return;
+      }
+
+      // Hide skipper box right away, so the layout is updated on time for the
+      // math operations coming up next.
+      const skipper = document.getElementById('skippers');
+      skipper.style.display = 'none';
+      setTimeout(() => skipper.style.display = '');
+
+      const box = target.getBoundingClientRect();
+      const style = window.getComputedStyle(target);
+
+      const scrollY =
+          window.scrollY
+        + box.top
+        - style['scroll-margin-top'].replace('px', '');
+
+      evt.preventDefault();
+      history.pushState({}, '', href);
+      window.scrollTo({top: scrollY, behavior: 'smooth'});
+      target.focus({preventScroll: true});
+
+      const maxScroll =
+          document.body.scrollHeight
+        - window.innerHeight;
+
+      if (scrollY > maxScroll && target.classList.contains('content-heading')) {
+        if (state.highlightedTarget) {
+          state.highlightedTarget.classList.remove('highlight-hash-link');
+        }
+
+        target.classList.add('highlight-hash-link');
+        state.highlightedTarget = target;
+      }
+
+      processScrollingAfterHashLinkClicked();
+
+      dispatchInternalEvent(event, 'whenHashLinkClicked', {
+        link: hashLink,
+        target,
+      });
+    });
+  }
+
+  for (const target of info.targets) {
+    target.addEventListener('animationend', evt => {
+      if (evt.animationName !== 'highlight-hash-link') return;
+      target.classList.remove('highlight-hash-link');
+      if (target !== state.highlightedTarget) return;
+      state.highlightedTarget = null;
+    });
+  }
+}
+
+clientSteps.getPageReferences.push(getHashLinkReferences);
+clientSteps.addPageListeners.push(addHashLinkListeners);
+
+// Sticky content heading ---------------------------------
+
+const stickyHeadingInfo = initInfo('stickyHeadingInfo', {
+  stickyContainers: null,
+
+  stickySubheadingRows: null,
+  stickySubheadings: null,
+
+  stickyCoverContainers: null,
+  stickyCoverTextAreas: null,
+  stickyCovers: null,
+
+  contentContainers: null,
+  contentHeadings: null,
+  contentCovers: null,
+  contentCoversReveal: null,
+
+  state: {
+    displayedHeading: null,
+  },
+
+  event: {
+    whenDisplayedHeadingChanges: [],
+  },
+});
+
+function getStickyHeadingReferences() {
+  const info = stickyHeadingInfo;
+
+  info.stickyContainers =
+    Array.from(document.getElementsByClassName('content-sticky-heading-container'));
+
+  info.stickyCoverContainers =
+    info.stickyContainers
+      .map(el => el.querySelector('.content-sticky-heading-cover-container'));
+
+  info.stickyCovers =
+    info.stickyCoverContainers
+      .map(el => el?.querySelector('.content-sticky-heading-cover'));
+
+  info.stickyCoverTextAreas =
+    info.stickyCovers
+      .map(el => el?.querySelector('.image-text-area'));
+
+  info.stickySubheadingRows =
+    info.stickyContainers
+      .map(el => el.querySelector('.content-sticky-subheading-row'));
+
+  info.stickySubheadings =
+    info.stickySubheadingRows
+      .map(el => el.querySelector('h2'));
+
+  info.contentContainers =
+    info.stickyContainers
+      .map(el => el.parentElement);
+
+  info.contentCovers =
+    info.contentContainers
+      .map(el => el.querySelector('#cover-art-container'));
+
+  info.contentCoversReveal =
+    info.contentCovers
+      .map(el => el ? !!el.querySelector('.reveal') : null);
+
+  info.contentHeadings =
+    info.contentContainers
+      .map(el => Array.from(el.querySelectorAll('.content-heading')));
+}
+
+function removeTextPlaceholderStickyHeadingCovers() {
+  const info = stickyHeadingInfo;
+
+  const hasTextArea =
+    info.stickyCoverTextAreas.map(el => !!el);
+
+  const coverContainersWithTextArea =
+    info.stickyCoverContainers
+      .filter((_el, index) => hasTextArea[index]);
+
+  for (const el of coverContainersWithTextArea) {
+    el.remove();
+  }
+
+  info.stickyCoverContainers =
+    info.stickyCoverContainers
+      .map((el, index) => hasTextArea[index] ? null : el);
+
+  info.stickyCovers =
+    info.stickyCovers
+      .map((el, index) => hasTextArea[index] ? null : el);
+
+  info.stickyCoverTextAreas =
+    info.stickyCoverTextAreas
+      .slice()
+      .fill(null);
+}
+
+function addRevealClassToStickyHeadingCovers() {
+  const info = stickyHeadingInfo;
+
+  const stickyCoversWhichReveal =
+    info.stickyCovers
+      .filter((_el, index) => info.contentCoversReveal[index]);
+
+  for (const el of stickyCoversWhichReveal) {
+    el.classList.add('content-sticky-heading-cover-needs-reveal');
+  }
+}
+
+function addRevealListenersForStickyHeadingCovers() {
+  const info = stickyHeadingInfo;
+
+  const stickyCovers = info.stickyCovers.slice();
+  const contentCovers = info.contentCovers.slice();
+
+  filterMultipleArrays(
+    stickyCovers,
+    contentCovers,
+    (_stickyCover, _contentCover, index) => info.contentCoversReveal[index]);
+
+  for (const {stickyCover, contentCover} of stitchArrays({
+    stickyCover: stickyCovers,
+    contentCover: contentCovers,
+  })) {
+    // TODO: Janky - should use internal event instead of DOM event
+    contentCover.querySelector('.reveal').addEventListener('hsmusic-reveal', () => {
+      stickyCover.classList.remove('content-sticky-heading-cover-needs-reveal');
+    });
+  }
+}
+
+function topOfViewInside(el, scroll = window.scrollY) {
+  return (
+    scroll > el.offsetTop &&
+    scroll < el.offsetTop + el.offsetHeight);
+}
+
+function updateStickyCoverVisibility(index) {
+  const info = stickyHeadingInfo;
+
+  const stickyCoverContainer = info.stickyCoverContainers[index];
+  const contentCover = info.contentCovers[index];
+
+  if (contentCover && stickyCoverContainer) {
+    if (contentCover.getBoundingClientRect().bottom < 4) {
+      stickyCoverContainer.classList.add('visible');
+    } else {
+      stickyCoverContainer.classList.remove('visible');
+    }
+  }
+}
+
+function getContentHeadingClosestToStickySubheading(index) {
+  const info = stickyHeadingInfo;
+
+  const contentContainer = info.contentContainers[index];
+
+  if (!topOfViewInside(contentContainer)) {
+    return null;
+  }
+
+  const stickySubheading = info.stickySubheadings[index];
+
+  if (stickySubheading.childNodes.length === 0) {
+    // Supply a non-breaking space to ensure correct basic line height.
+    stickySubheading.appendChild(document.createTextNode('\xA0'));
+  }
+
+  const stickyContainer = info.stickyContainers[index];
+  const stickyRect = stickyContainer.getBoundingClientRect();
+
+  // TODO: Should this compute with the subheading row instead of h2?
+  const subheadingRect = stickySubheading.getBoundingClientRect();
+
+  const stickyBottom = stickyRect.bottom + subheadingRect.height;
+
+  // Iterate from bottom to top of the content area.
+  const contentHeadings = info.contentHeadings[index];
+  for (const heading of contentHeadings.slice().reverse()) {
+    const headingRect = heading.getBoundingClientRect();
+    if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) {
+      return heading;
+    }
+  }
+
+  return null;
+}
+
+function updateStickySubheadingContent(index) {
+  const info = stickyHeadingInfo;
+  const {event, state} = info;
+
+  const closestHeading = getContentHeadingClosestToStickySubheading(index);
+
+  if (state.displayedHeading === closestHeading) return;
+
+  const stickySubheadingRow = info.stickySubheadingRows[index];
+
+  if (closestHeading) {
+    const stickySubheading = info.stickySubheadings[index];
+
+    // Array.from needed to iterate over a live array with for..of
+    for (const child of Array.from(stickySubheading.childNodes)) {
+      child.remove();
+    }
+
+    const textContainer =
+      closestHeading.querySelector('.content-heading-main-title')
+        // Just for compatibility with older builds of the site.
+        ?? closestHeading;
+
+    for (const child of textContainer.childNodes) {
+      if (child.tagName === 'A') {
+        for (const grandchild of child.childNodes) {
+          stickySubheading.appendChild(grandchild.cloneNode(true));
+        }
+      } else {
+        stickySubheading.appendChild(child.cloneNode(true));
+      }
+    }
+
+    stickySubheadingRow.classList.add('visible');
+  } else {
+    stickySubheadingRow.classList.remove('visible');
+  }
+
+  const oldDisplayedHeading = state.displayedHeading;
+
+  state.displayedHeading = closestHeading;
+
+  dispatchInternalEvent(event, 'whenDisplayedHeadingChanges', index, {
+    oldHeading: oldDisplayedHeading,
+    newHeading: closestHeading,
+  });
+}
+
+function updateStickyHeadings(index) {
+  updateStickyCoverVisibility(index);
+  updateStickySubheadingContent(index);
+}
+
+function initializeStateForStickyHeadings() {
+  for (let i = 0; i < stickyHeadingInfo.stickyContainers.length; i++) {
+    updateStickyHeadings(i);
+  }
+}
+
+function addScrollListenerForStickyHeadings() {
+  document.addEventListener('scroll', () => {
+    for (let i = 0; i < stickyHeadingInfo.stickyContainers.length; i++) {
+      updateStickyHeadings(i);
+    }
+  });
+}
+
+clientSteps.getPageReferences.push(getStickyHeadingReferences);
+clientSteps.mutatePageContent.push(removeTextPlaceholderStickyHeadingCovers);
+clientSteps.mutatePageContent.push(addRevealClassToStickyHeadingCovers);
+clientSteps.initializeState.push(initializeStateForStickyHeadings);
+clientSteps.addPageListeners.push(addRevealListenersForStickyHeadingCovers);
+clientSteps.addPageListeners.push(addScrollListenerForStickyHeadings);
+
+// Image overlay ------------------------------------------
+
+// TODO: Update to clientSteps style.
+
+function addImageOverlayClickHandlers() {
+  const container = document.getElementById('image-overlay-container');
+
+  if (!container) {
+    console.warn(`#image-overlay-container missing, image overlay module disabled.`);
+    return;
+  }
+
+  for (const link of document.querySelectorAll('.image-link')) {
+    if (link.closest('.no-image-preview')) {
+      continue;
+    }
+
+    link.addEventListener('click', handleImageLinkClicked);
+  }
+
+  const actionContainer = document.getElementById('image-overlay-action-container');
+
+  container.addEventListener('click', handleContainerClicked);
+  document.body.addEventListener('keydown', handleKeyDown);
+
+  function handleContainerClicked(evt) {
+    // Only hide the image overlay if actually clicking the background.
+    if (evt.target !== container) {
+      return;
+    }
+
+    // If you clicked anything close to or beneath the action bar, don't hide
+    // the image overlay.
+    const rect = actionContainer.getBoundingClientRect();
+    if (evt.clientY >= rect.top - 40) {
+      return;
+    }
+
+    container.classList.remove('visible');
+  }
+
+  function handleKeyDown(evt) {
+    if (evt.key === 'Escape' || evt.key === 'Esc' || evt.keyCode === 27) {
+      container.classList.remove('visible');
+    }
+  }
+}
+
+function handleImageLinkClicked(evt) {
+  if (evt.metaKey || evt.shiftKey || evt.altKey) {
+    return;
+  }
+
+  evt.preventDefault();
+
+  // Don't show the overlay if the image still needs to be revealed.
+  if (evt.target.closest('.reveal:not(.revealed)')) {
+    return;
+  }
+
+  const container = document.getElementById('image-overlay-container');
+  container.classList.add('visible');
+  container.classList.remove('loaded');
+  container.classList.remove('errored');
+
+  const allViewOriginal = document.getElementsByClassName('image-overlay-view-original');
+  const mainImage = document.getElementById('image-overlay-image');
+  const thumbImage = document.getElementById('image-overlay-image-thumb');
+
+  const {href: originalSrc} = evt.target.closest('a');
+
+  const {
+    src: embeddedSrc,
+    dataset: {
+      originalSize: originalFileSize,
+      thumbs: availableThumbList,
+    },
+  } = evt.target.closest('a').querySelector('img');
+
+  updateFileSizeInformation(originalFileSize);
+
+  let mainSrc = null;
+  let thumbSrc = null;
+
+  if (availableThumbList) {
+    const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList);
+    const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList);
+    mainSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${mainThumb}.jpg`);
+    thumbSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${smallThumb}.jpg`);
+    // Show the thumbnail size on each <img> element's data attributes.
+    // Y'know, just for debugging convenience.
+    mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`;
+    thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`;
+  } else {
+    mainSrc = originalSrc;
+    thumbSrc = null;
+    mainImage.dataset.displayingThumb = '';
+    thumbImage.dataset.displayingThumb = '';
+  }
+
+  if (thumbSrc) {
+    thumbImage.src = thumbSrc;
+    thumbImage.style.display = null;
+  } else {
+    thumbImage.src = '';
+    thumbImage.style.display = 'none';
+  }
+
+  for (const viewOriginal of allViewOriginal) {
+    viewOriginal.href = originalSrc;
+  }
+
+  mainImage.addEventListener('load', handleMainImageLoaded);
+  mainImage.addEventListener('error', handleMainImageErrored);
+
+  container.style.setProperty('--download-progress', '0%');
+  loadImage(mainSrc, progress => {
+    container.style.setProperty('--download-progress', (20 + 0.8 * progress) + '%');
+  }).then(
+    blobUrl => {
+      mainImage.src = blobUrl;
+      container.style.setProperty('--download-progress', '100%');
+    },
+    handleMainImageErrored);
+
+  function handleMainImageLoaded() {
+    mainImage.removeEventListener('load', handleMainImageLoaded);
+    mainImage.removeEventListener('error', handleMainImageErrored);
+    container.classList.add('loaded');
+  }
+
+  function handleMainImageErrored() {
+    mainImage.removeEventListener('load', handleMainImageLoaded);
+    mainImage.removeEventListener('error', handleMainImageErrored);
+    container.classList.add('errored');
+  }
+}
+
+function parseThumbList(availableThumbList) {
+  // Parse all the available thumbnail sizes! These are provided by the actual
+  // content generation on each image.
+  const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250'
+  const availableSizes =
+    (availableThumbList || defaultThumbList)
+      .split(' ')
+      .map(part => part.split(':'))
+      .map(([thumb, length]) => ({thumb, length: parseInt(length)}))
+      .sort((a, b) => a.length - b.length);
+
+  return availableSizes;
+}
+
+function getPreferredThumbSize(availableThumbList) {
+  // Assuming a square, the image will be constrained to the lesser window
+  // dimension. Coefficient here matches CSS dimensions for image overlay.
+  const constrainedLength = Math.floor(Math.min(
+    0.80 * window.innerWidth,
+    0.80 * window.innerHeight));
+
+  // Match device pixel ratio, which is 2x for "retina" displays and certain
+  // device configurations.
+  const visualLength = window.devicePixelRatio * constrainedLength;
+
+  const availableSizes = parseThumbList(availableThumbList);
+
+  // Starting from the smallest dimensions, find (and return) the first
+  // available length which hits a "good enough" threshold - it's got to be
+  // at least that percent of the way to the actual displayed dimensions.
+  const goodEnoughThreshold = 0.90;
+
+  // (The last item is skipped since we'd be falling back to it anyway.)
+  for (const {thumb, length} of availableSizes.slice(0, -1)) {
+    if (Math.floor(visualLength * goodEnoughThreshold) <= length) {
+      return {thumb, length};
+    }
+  }
+
+  // If none of the items in the list were big enough to hit the "good enough"
+  // threshold, just use the largest size available.
+  return availableSizes[availableSizes.length - 1];
+}
+
+function getSmallestThumbSize(availableThumbList) {
+  // Just snag the smallest size. This'll be used for displaying the "preview"
+  // as the bigger one is loading.
+  const availableSizes = parseThumbList(availableThumbList);
+  return availableSizes[0];
+}
+
+function updateFileSizeInformation(fileSize) {
+  const fileSizeWarningThreshold = 8 * 10 ** 6;
+
+  const actionContentWithoutSize = document.getElementById('image-overlay-action-content-without-size');
+  const actionContentWithSize = document.getElementById('image-overlay-action-content-with-size');
+
+  if (!fileSize) {
+    actionContentWithSize.classList.remove('visible');
+    actionContentWithoutSize.classList.add('visible');
+    return;
+  }
+
+  actionContentWithoutSize.classList.remove('visible');
+  actionContentWithSize.classList.add('visible');
+
+  const megabytesContainer = document.getElementById('image-overlay-file-size-megabytes');
+  const kilobytesContainer = document.getElementById('image-overlay-file-size-kilobytes');
+  const megabytesContent = megabytesContainer.querySelector('.image-overlay-file-size-count');
+  const kilobytesContent = kilobytesContainer.querySelector('.image-overlay-file-size-count');
+  const fileSizeWarning = document.getElementById('image-overlay-file-size-warning');
+
+  fileSize = parseInt(fileSize);
+  const round = (exp) => Math.round(fileSize / 10 ** (exp - 1)) / 10;
+
+  if (fileSize > fileSizeWarningThreshold) {
+    fileSizeWarning.classList.add('visible');
+  } else {
+    fileSizeWarning.classList.remove('visible');
+  }
+
+  if (fileSize > 10 ** 6) {
+    megabytesContainer.classList.add('visible');
+    kilobytesContainer.classList.remove('visible');
+    megabytesContent.innerText = round(6);
+  } else {
+    megabytesContainer.classList.remove('visible');
+    kilobytesContainer.classList.add('visible');
+    kilobytesContent.innerText = round(3);
+  }
+
+  void fileSizeWarning;
+}
+
+addImageOverlayClickHandlers();
+
+/**
+ * Credits: Parziphal, Feb 13, 2017
+ * https://stackoverflow.com/a/42196770
+ *
+ * Loads an image with progress callback.
+ *
+ * The `onprogress` callback will be called by XMLHttpRequest's onprogress
+ * event, and will receive the loading progress ratio as an whole number.
+ * However, if it's not possible to compute the progress ratio, `onprogress`
+ * will be called only once passing -1 as progress value. This is useful to,
+ * for example, change the progress animation to an undefined animation.
+ *
+ * @param  {string}   imageUrl   The image to load
+ * @param  {Function} onprogress
+ * @return {Promise}
+ */
+function loadImage(imageUrl, onprogress) {
+  return new Promise((resolve, reject) => {
+    var xhr = new XMLHttpRequest();
+    var notifiedNotComputable = false;
+
+    xhr.open('GET', imageUrl, true);
+    xhr.responseType = 'arraybuffer';
+
+    xhr.onprogress = function(ev) {
+      if (ev.lengthComputable) {
+        onprogress(parseInt((ev.loaded / ev.total) * 1000) / 10);
+      } else {
+        if (!notifiedNotComputable) {
+          notifiedNotComputable = true;
+          onprogress(-1);
+        }
+      }
+    }
+
+    xhr.onloadend = function() {
+      if (!xhr.status.toString().match(/^2/)) {
+        reject(xhr);
+      } else {
+        if (!notifiedNotComputable) {
+          onprogress(100);
+        }
+
+        var options = {}
+        var headers = xhr.getAllResponseHeaders();
+        var m = headers.match(/^Content-Type:\s*(.*?)$/mi);
+
+        if (m && m[1]) {
+          options.type = m[1];
+        }
+
+        var blob = new Blob([this.response], options);
+
+        resolve(window.URL.createObjectURL(blob));
+      }
+    }
+
+    xhr.send();
+  });
+}
+
+// "Additional names" box ---------------------------------
+
+const additionalNamesBoxInfo = initInfo('additionalNamesBox', {
+  box: null,
+  links: null,
+  mainContentContainer: null,
+
+  state: {
+    visible: false,
+  },
+});
+
+function getAdditionalNamesBoxReferences() {
+  const info = additionalNamesBoxInfo;
+
+  info.box =
+    document.getElementById('additional-names-box');
+
+  info.links =
+    document.querySelectorAll('a[href="#additional-names-box"]');
+
+  info.mainContentContainer =
+    document.querySelector('#content .main-content-container');
+}
+
+function addAdditionalNamesBoxInternalListeners() {
+  const info = additionalNamesBoxInfo;
+
+  hashLinkInfo.event.beforeHashLinkScrolls.push(({target}) => {
+    if (target === info.box) {
+      return false;
+    }
+  });
+}
+
+function addAdditionalNamesBoxListeners() {
+  const info = additionalNamesBoxInfo;
+
+  for (const link of info.links) {
+    link.addEventListener('click', domEvent => {
+      handleAdditionalNamesBoxLinkClicked(domEvent);
+    });
+  }
+}
+
+function handleAdditionalNamesBoxLinkClicked(domEvent) {
+  const info = additionalNamesBoxInfo;
+  const {state} = info;
+
+  domEvent.preventDefault();
+
+  if (!info.box || !info.mainContentContainer) return;
+
+  const margin =
+    +(cssProp(info.box, 'scroll-margin-top').replace('px', ''));
+
+  const {top} =
+    (state.visible
+      ? info.box.getBoundingClientRect()
+      : info.mainContentContainer.getBoundingClientRect());
+
+  if (top + 20 < margin || top > 0.4 * window.innerHeight) {
+    if (!state.visible) {
+      toggleAdditionalNamesBox();
+    }
+
+    window.scrollTo({
+      top: window.scrollY + top - margin,
+      behavior: 'smooth',
+    });
+  } else {
+    toggleAdditionalNamesBox();
+  }
+}
+
+function toggleAdditionalNamesBox() {
+  const info = additionalNamesBoxInfo;
+  const {state} = info;
+
+  state.visible = !state.visible;
+  info.box.style.display =
+    (state.visible
+      ? 'block'
+      : 'none');
+}
+
+clientSteps.getPageReferences.push(getAdditionalNamesBoxReferences);
+clientSteps.addInternalListeners.push(addAdditionalNamesBoxInternalListeners);
+clientSteps.addPageListeners.push(addAdditionalNamesBoxListeners);
+
+// Group contributions table ------------------------------
+
+// TODO: Update to clientSteps style.
+
+const groupContributionsTableInfo =
+  Array.from(document.querySelectorAll('#content dl'))
+    .filter(dl => dl.querySelector('a.group-contributions-sort-button'))
+    .map(dl => ({
+      sortingByCountLink: dl.querySelector('dt.group-contributions-sorted-by-count a.group-contributions-sort-button'),
+      sortingByDurationLink: dl.querySelector('dt.group-contributions-sorted-by-duration a.group-contributions-sort-button'),
+      sortingByCountElements: dl.querySelectorAll('.group-contributions-sorted-by-count'),
+      sortingByDurationElements: dl.querySelectorAll('.group-contributions-sorted-by-duration'),
+    }));
+
+function sortGroupContributionsTableBy(info, sort) {
+  const [showThese, hideThese] =
+    (sort === 'count'
+      ? [info.sortingByCountElements, info.sortingByDurationElements]
+      : [info.sortingByDurationElements, info.sortingByCountElements]);
+
+  for (const element of showThese) element.classList.add('visible');
+  for (const element of hideThese) element.classList.remove('visible');
+}
+
+for (const info of groupContributionsTableInfo) {
+  info.sortingByCountLink.addEventListener('click', evt => {
+    evt.preventDefault();
+    sortGroupContributionsTableBy(info, 'duration');
+  });
+
+  info.sortingByDurationLink.addEventListener('click', evt => {
+    evt.preventDefault();
+    sortGroupContributionsTableBy(info, 'count');
+  });
+}
+
+// Generic links with tooltips ----------------------------
+
+const textWithTooltipInfo = initInfo('textWithTooltipInfo', {
+  hoverables: null,
+  tooltips: null,
+});
+
+function getTextWithTooltipReferences() {
+  const info = textWithTooltipInfo;
+
+  const spans =
+    Array.from(document.querySelectorAll('.text-with-tooltip'));
+
+  info.hoverables =
+    spans.map(span => span.children[0]);
+
+  info.tooltips =
+    spans.map(span => span.children[1]);
+}
+
+function addTextWithTooltipPageListeners() {
+  const info = textWithTooltipInfo;
+
+  for (const {hoverable, tooltip} of stitchArrays({
+    hoverable: info.hoverables,
+    tooltip: info.tooltips,
+  })) {
+    registerTooltipElement(tooltip);
+    registerTooltipHoverableElement(hoverable, tooltip);
+  }
+}
+
+clientSteps.getPageReferences.push(getTextWithTooltipReferences);
+clientSteps.addPageListeners.push(addTextWithTooltipPageListeners);
+
+// Datetimestamp tooltips ---------------------------------
+
+const datetimestampTooltipInfo = initInfo('datetimestampTooltipInfo', {
+  hoverables: null,
+  tooltips: null,
+});
+
+function getDatestampTooltipReferences() {
+  const info = datetimestampTooltipInfo;
+
+  const spans =
+    Array.from(document.querySelectorAll('span.datetimestamp.has-tooltip'));
+
+  info.hoverables =
+    spans.map(span => span.querySelector('time'));
+
+  info.tooltips =
+    spans.map(span => span.querySelector('span.datetimestamp-tooltip'));
+}
+
+function addDatestampTooltipPageListeners() {
+  const info = datetimestampTooltipInfo;
+
+  for (const {hoverable, tooltip} of stitchArrays({
+    hoverable: info.hoverables,
+    tooltip: info.tooltips,
+  })) {
+    registerTooltipElement(tooltip);
+    registerTooltipHoverableElement(hoverable, tooltip);
+  }
+}
+
+clientSteps.getPageReferences.push(getDatestampTooltipReferences);
+clientSteps.addPageListeners.push(addDatestampTooltipPageListeners);
+
+// Artist external link tooltips --------------------------
+
+// These don't need to have tooltip events specially added as
+// they're implemented with "text with tooltip" components.
+
+const artistExternalLinkTooltipInfo = initInfo('artistExternalLinkTooltipInfo', {
+  tooltips: null,
+  tooltipRows: null,
+
+  settings: {
+    // This is the maximum distance, in CSS pixels, that the mouse
+    // can appear to be moving per second while still considered
+    // "idle". A greater value means higher tolerance for small
+    // movements.
+    maximumIdleSpeed: 40,
+
+    // Leaving the mouse idle for this amount of time, over a single
+    // row of the tooltip, will cause a column of supplemental info
+    // to display.
+    mouseIdleShowInfoDelay: 1000,
+
+    // If none of these tooltips are visible for this amount of time,
+    // the supplemental info column is hidden. It'll never disappear
+    // while a tooltip is actually visible.
+    hideInfoAfterTooltipHiddenDelay: 2250,
+  },
+
+  state: {
+    // This is shared by all tooltips.
+    showingTooltipInfo: false,
+
+    mouseIdleTimeout: null,
+    hideInfoTimeout: null,
+
+    mouseMovementPositions: [],
+    mouseMovementTimestamps: [],
+  },
+});
+
+function getArtistExternalLinkTooltipPageReferences() {
+  const info = artistExternalLinkTooltipInfo;
+
+  info.tooltips =
+    Array.from(document.getElementsByClassName('icons-tooltip'));
+
+  info.tooltipRows =
+    info.tooltips.map(tooltip =>
+      Array.from(tooltip.getElementsByClassName('icon')));
+}
+
+function addArtistExternalLinkTooltipInternalListeners() {
+  const info = artistExternalLinkTooltipInfo;
+
+  hoverableTooltipInfo.event.whenTooltipShows.push(({tooltip}) => {
+    const {state} = info;
+
+    if (info.tooltips.includes(tooltip)) {
+      clearTimeout(state.hideInfoTimeout);
+      state.hideInfoTimeout = null;
+    }
+  });
+
+  hoverableTooltipInfo.event.whenTooltipHides.push(() => {
+    const {settings, state} = info;
+
+    if (state.showingTooltipInfo) {
+      state.hideInfoTimeout =
+        setTimeout(() => {
+          state.hideInfoTimeout = null;
+          hideArtistExternalLinkTooltipInfo();
+        }, settings.hideInfoAfterTooltipHiddenDelay);
+    } else {
+      clearTimeout(state.mouseIdleTimeout);
+      state.mouseIdleTimeout = null;
+    }
+  });
+}
+
+function addArtistExternalLinkTooltipPageListeners() {
+  const info = artistExternalLinkTooltipInfo;
+
+  for (const tooltip of info.tooltips) {
+    tooltip.addEventListener('mousemove', domEvent => {
+      handleArtistExternalLinkTooltipMouseMoved(domEvent);
+    });
+
+    tooltip.addEventListener('mouseout', () => {
+      const {state} = info;
+
+      clearTimeout(state.mouseIdleTimeout);
+      state.mouseIdleTimeout = null;
+    });
+  }
+
+  for (const tooltipRow of info.tooltipRows.flat()) {
+    tooltipRow.addEventListener('mouseover', () => {
+      const {state} = info;
+
+      clearTimeout(state.mouseIdleTimeout);
+      state.mouseIdleTimeout = null;
+    });
+  }
+}
+
+function handleArtistExternalLinkTooltipMouseMoved(domEvent) {
+  const info = artistExternalLinkTooltipInfo;
+  const {settings, state} = info;
+
+  if (state.showingTooltipInfo) {
+    return;
+  }
+
+  // Clean out expired mouse movements
+
+  const expiryTime = 1000;
+
+  if (!empty(state.mouseMovementTimestamps)) {
+    const firstRecentMovementIndex =
+      state.mouseMovementTimestamps
+        .findIndex(value => Date.now() - value <= expiryTime);
+
+    if (firstRecentMovementIndex === -1) {
+      state.mouseMovementTimestamps.splice(0);
+      state.mouseMovementPositions.splice(0);
+    } else if (firstRecentMovementIndex > 0) {
+      state.mouseMovementTimestamps.splice(0, firstRecentMovementIndex - 1);
+      state.mouseMovementPositions.splice(0, firstRecentMovementIndex - 1);
+    }
+  }
+
+  const currentMovementDistance =
+    Math.sqrt(domEvent.movementX ** 2 + domEvent.movementY ** 2);
+
+  state.mouseMovementTimestamps.push(Date.now());
+  state.mouseMovementPositions.push([domEvent.screenX, domEvent.screenY]);
+
+  // We can't really compute speed without having
+  // at least two data points!
+  if (state.mouseMovementPositions.length < 2) {
+    return;
+  }
+
+  const movementTravelDistances =
+    state.mouseMovementPositions.map((current, index, array) => {
+      if (index === 0) return 0;
+
+      const previous = array[index - 1];
+      const deltaX = current[0] - previous[0];
+      const deltaY = current[1] - previous[1];
+      return Math.sqrt(deltaX ** 2 + deltaY ** 2);
+    });
+
+  const totalTravelDistance =
+    accumulateSum(movementTravelDistances);
+
+  // In seconds rather than milliseconds.
+  const timeSinceFirstMovement =
+    (Date.now() - state.mouseMovementTimestamps[0]) / 1000;
+
+  const averageSpeed =
+    Math.floor(totalTravelDistance / timeSinceFirstMovement);
+
+  if (averageSpeed > settings.maximumIdleSpeed) {
+    clearTimeout(state.mouseIdleTimeout);
+    state.mouseIdleTimeout = null;
+  }
+
+  if (state.mouseIdleTimeout) {
+    return;
+  }
+
+  state.mouseIdleTimeout =
+    setTimeout(() => {
+      state.mouseIdleTimeout = null;
+      showArtistExternalLinkTooltipInfo();
+    }, settings.mouseIdleShowInfoDelay);
+}
+
+function showArtistExternalLinkTooltipInfo() {
+  const info = artistExternalLinkTooltipInfo;
+  const {state} = info;
+
+  state.showingTooltipInfo = true;
+
+  for (const tooltip of info.tooltips) {
+    tooltip.classList.add('show-info');
+  }
+}
+
+function hideArtistExternalLinkTooltipInfo() {
+  const info = artistExternalLinkTooltipInfo;
+  const {state} = info;
+
+  state.showingTooltipInfo = false;
+
+  for (const tooltip of info.tooltips) {
+    tooltip.classList.remove('show-info');
+  }
+}
+
+clientSteps.getPageReferences.push(getArtistExternalLinkTooltipPageReferences);
+clientSteps.addInternalListeners.push(addArtistExternalLinkTooltipInternalListeners);
+clientSteps.addPageListeners.push(addArtistExternalLinkTooltipPageListeners);
+
+// Sticky commentary sidebar ------------------------------
+
+const albumCommentarySidebarInfo = initInfo('albumCommentarySidebarInfo', {
+  sidebar: null,
+  sidebarHeading: null,
+
+  sidebarTrackLinks: null,
+  sidebarTrackDirectories: null,
+
+  sidebarTrackSections: null,
+  sidebarTrackSectionStartIndices: null,
+
+  state: {
+    currentTrackSection: null,
+    currentTrackLink: null,
+    justChangedTrackSection: false,
+  },
+});
+
+function getAlbumCommentarySidebarReferences() {
+  const info = albumCommentarySidebarInfo;
+
+  info.sidebar =
+    document.getElementById('sidebar-left');
+
+  info.sidebarHeading =
+    info.sidebar.querySelector('h1');
+
+  info.sidebarTrackLinks =
+    Array.from(info.sidebar.querySelectorAll('li a'));
+
+  info.sidebarTrackDirectories =
+    info.sidebarTrackLinks
+      .map(el => el.getAttribute('href')?.slice(1) ?? null);
+
+  info.sidebarTrackSections =
+    Array.from(info.sidebar.getElementsByTagName('details'));
+
+  info.sidebarTrackSectionStartIndices =
+    info.sidebarTrackSections
+      .map(details => details.querySelector('ol, ul'))
+      .reduce(
+        (accumulator, _list, index, array) =>
+          (empty(accumulator)
+            ? [0]
+            : [
+              ...accumulator,
+              (accumulator[accumulator.length - 1] +
+                array[index - 1].querySelectorAll('li a').length),
+            ]),
+        []);
+}
+
+function scrollAlbumCommentarySidebar() {
+  const info = albumCommentarySidebarInfo;
+  const {state} = info;
+  const {currentTrackLink, currentTrackSection} = state;
+
+  if (!currentTrackLink) {
+    return;
+  }
+
+  const {sidebar, sidebarHeading} = info;
+
+  const scrollTop = sidebar.scrollTop;
+
+  const headingRect = sidebarHeading.getBoundingClientRect();
+  const sidebarRect = sidebar.getBoundingClientRect();
+
+  const stickyPadding = headingRect.height;
+  const sidebarViewportHeight = sidebarRect.height - stickyPadding;
+
+  const linkRect = currentTrackLink.getBoundingClientRect();
+  const sectionRect = currentTrackSection.getBoundingClientRect();
+
+  const sectionTopEdge =
+    sectionRect.top - (sidebarRect.top - scrollTop);
+
+  const sectionHeight =
+    sectionRect.height;
+
+  const sectionScrollTop =
+    sectionTopEdge - stickyPadding - 10;
+
+  const linkTopEdge =
+    linkRect.top - (sidebarRect.top - scrollTop);
+
+  const linkBottomEdge =
+    linkRect.bottom - (sidebarRect.top - scrollTop);
+
+  const linkScrollTop =
+    linkTopEdge - stickyPadding - 5;
+
+  const linkVisibleFromTopOfSection =
+    linkBottomEdge - sectionTopEdge > sidebarViewportHeight;
+
+  const linkScrollBottom =
+    linkScrollTop - sidebarViewportHeight + linkRect.height + 20;
+
+  const maxScrollInViewport =
+    scrollTop + stickyPadding + sidebarViewportHeight;
+
+  const minScrollInViewport =
+    scrollTop + stickyPadding;
+
+  if (linkBottomEdge > maxScrollInViewport) {
+    if (linkVisibleFromTopOfSection) {
+      sidebar.scrollTo({top: linkScrollBottom, behavior: 'smooth'});
+    } else {
+      sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'});
+    }
+  } else if (linkTopEdge < minScrollInViewport) {
+    if (linkVisibleFromTopOfSection) {
+      sidebar.scrollTo({top: linkScrollTop, behavior: 'smooth'});
+    } else {
+      sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'});
+    }
+  } else if (state.justChangedTrackSection) {
+    if (sectionHeight < sidebarViewportHeight) {
+      sidebar.scrollTo({top: sectionScrollTop, behavior: 'smooth'});
+    }
+  }
+}
+
+function markDirectoryAsCurrentForAlbumCommentary(trackDirectory) {
+  const info = albumCommentarySidebarInfo;
+  const {state} = info;
+
+  const trackIndex =
+    (trackDirectory
+      ? info.sidebarTrackDirectories
+          .indexOf(trackDirectory)
+      : -1);
+
+  const sectionIndex =
+    (trackIndex >= 0
+      ? info.sidebarTrackSectionStartIndices
+          .findIndex((start, index, array) =>
+            (index === array.length - 1
+              ? true
+              : trackIndex < array[index + 1]))
+      : -1);
+
+  const sidebarTrackLink =
+    (trackIndex >= 0
+      ? info.sidebarTrackLinks[trackIndex]
+      : null);
+
+  const sidebarTrackSection =
+    (sectionIndex >= 0
+      ? info.sidebarTrackSections[sectionIndex]
+      : null);
+
+  state.currentTrackLink?.classList?.remove('current');
+  state.currentTrackLink = sidebarTrackLink;
+  state.currentTrackLink?.classList?.add('current');
+
+  if (sidebarTrackSection !== state.currentTrackSection) {
+    if (sidebarTrackSection && !sidebarTrackSection.open) {
+      if (state.currentTrackSection) {
+        state.currentTrackSection.open = false;
+      }
+
+      sidebarTrackSection.open = true;
+    }
+
+    state.currentTrackSection?.classList?.remove('current');
+    state.currentTrackSection = sidebarTrackSection;
+    state.currentTrackSection?.classList?.add('current');
+    state.justChangedTrackSection = true;
+  } else {
+    state.justChangedTrackSection = false;
+  }
+}
+
+function addAlbumCommentaryInternalListeners() {
+  const info = albumCommentarySidebarInfo;
+
+  const mainContentIndex =
+    (stickyHeadingInfo.contentContainers ?? [])
+      .findIndex(({id}) => id === 'content');
+
+  if (mainContentIndex === -1) return;
+
+  stickyHeadingInfo.event.whenDisplayedHeadingChanges.push((index, {newHeading}) => {
+    if (index !== mainContentIndex) return;
+    if (hashLinkInfo.state.scrollingAfterClick) return;
+
+    const trackDirectory =
+      (newHeading
+        ? newHeading.id
+        : null);
+
+    markDirectoryAsCurrentForAlbumCommentary(trackDirectory);
+    scrollAlbumCommentarySidebar();
+  });
+
+  hashLinkInfo.event.whenHashLinkClicked.push(({link}) => {
+    const hash = link.getAttribute('href').slice(1);
+    if (!info.sidebarTrackDirectories.includes(hash)) return;
+    markDirectoryAsCurrentForAlbumCommentary(hash);
+  });
+}
+
+if (document.documentElement.dataset.urlKey === 'localized.albumCommentary') {
+  clientSteps.getPageReferences.push(getAlbumCommentarySidebarReferences);
+  clientSteps.addInternalListeners.push(addAlbumCommentaryInternalListeners);
+}
+
+// Run setup steps ----------------------------------------
+
+for (const [key, steps] of Object.entries(clientSteps)) {
+  for (const step of steps) {
+    try {
+      step();
+    } catch (error) {
+      console.warn(`During ${key}, failed to run ${step.name}`);
+      console.debug(error);
+    }
+  }
+}
diff --git a/src/static/icons.svg b/src/static/icons.svg
index 1e4351b..8c9a80a 100644
--- a/src/static/icons.svg
+++ b/src/static/icons.svg
@@ -1,11 +1,43 @@
 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" display="none" width="0" height="0">
-	<symbol id="icon-globe" viewBox="0 0 40 40"><path d="M20,3C10.6,3,3,10.6,3,20s7.6,17,17,17s17-7.6,17-17S29.4,3,20,3z M34.8,18.9h-6.2c-0.1-2.2-0.3-4.2-0.8-6.1 c1.5-0.4,3-0.9,4.2-1.5c0.6,0.9,1.2,1.8,1.6,2.9C34.3,15.7,34.7,17.3,34.8,18.9z M25.7,26.7c-1.5-0.3-3.1-0.4-4.6-0.5v-5.1h5.4 c-0.1,1.8-0.3,3.5-0.6,5.1C25.8,26.3,25.8,26.5,25.7,26.7z M14.2,26.2c-0.3-1.6-0.6-3.3-0.6-5.1h5.4v5.1c-1.6,0-3.2,0.2-4.6,0.5 C14.2,26.5,14.2,26.3,14.2,26.2z M14.3,13.3c1.5,0.3,3.1,0.4,4.6,0.5v5.1h-5.4c0.1-1.8,0.3-3.5,0.6-5.1 C14.2,13.7,14.2,13.5,14.3,13.3z M21.1,5.4C21.4,5.6,21.7,5.7,22,6c0.8,0.7,1.6,1.7,2.2,3c0.4,0.7,0.7,1.5,0.9,2.3 c-1.3,0.2-2.7,0.4-4,0.4V5.4z M18,6c0.3-0.3,0.6-0.4,0.9-0.6v6.2c-1.4,0-2.8-0.2-4-0.4c0.3-0.8,0.6-1.6,0.9-2.3 C16.5,7.7,17.2,6.7,18,6z M18.9,28.4v6.2c-0.3-0.1-0.6-0.3-0.9-0.6c-0.8-0.7-1.6-1.7-2.2-3c-0.4-0.7-0.7-1.5-0.9-2.3 C16.2,28.6,17.5,28.4,18.9,28.4z M22,34c-0.3,0.3-0.6,0.4-0.9,0.6v-6.2c1.4,0,2.8,0.2,4,0.4c-0.3,0.8-0.6,1.6-0.9,2.3 C23.5,32.3,22.8,33.3,22,34z M21.1,18.9v-5.1c1.6,0,3.2-0.2,4.6-0.5c0,0.2,0.1,0.4,0.1,0.5c0.3,1.6,0.6,3.3,0.6,5.1H21.1z M30.5,9.5 c0,0,0.1,0.1,0.1,0.1c-1,0.4-2.2,0.8-3.4,1.1c-0.6-1.9-1.4-3.5-2.4-4.8c0.3,0.1,0.6,0.2,0.9,0.3C27.5,7.1,29.1,8.1,30.5,9.5z M14.2,6.3c0.3-0.1,0.6-0.2,0.9-0.3c-0.9,1.3-1.7,2.9-2.4,4.8c-1.2-0.3-2.3-0.7-3.4-1.1c0,0,0.1-0.1,0.1-0.1 C10.9,8.1,12.5,7.1,14.2,6.3z M7.9,11.4c1.3,0.6,2.7,1.1,4.2,1.5c-0.4,1.9-0.7,3.9-0.8,6.1H5.2c0.1-1.6,0.5-3.2,1.1-4.7 C6.8,13.2,7.3,12.3,7.9,11.4z M5.2,21.1h6.2c0.1,2.2,0.3,4.2,0.8,6.1c-1.5,0.4-3,0.9-4.2,1.5c-0.6-0.9-1.2-1.8-1.6-2.9 C5.7,24.3,5.3,22.7,5.2,21.1z M9.5,30.5c0,0-0.1-0.1-0.1-0.1c1-0.4,2.2-0.8,3.4-1.1c0.6,1.9,1.4,3.5,2.4,4.8 c-0.3-0.1-0.6-0.2-0.9-0.3C12.5,32.9,10.9,31.9,9.5,30.5z M25.8,33.7c-0.3,0.1-0.6,0.2-0.9,0.3c0.9-1.3,1.7-2.9,2.4-4.8 c1.2,0.3,2.3,0.7,3.4,1.1c0,0-0.1,0.1-0.1,0.1C29.1,31.9,27.5,32.9,25.8,33.7z M32.1,28.6c-1.3-0.6-2.7-1.1-4.2-1.5 c0.4-1.9,0.7-3.9,0.8-6.1h6.2c-0.1,1.6-0.5,3.2-1.1,4.7C33.2,26.8,32.7,27.7,32.1,28.6z"/></symbol>
-	<symbol id="icon-bandcamp" viewBox="0 0 40 40"><path d="M7.1,13.3c5.6,0,11.1,0,16.7,0c0,1.5,0,3.1,0,4.6c0.7-0.7,1.5-1.5,3.2-1.3c2.6,0.3,3.8,3,3.6,5.6c-0.1,1.1-0.5,2.4-1.3,3.1 c-0.9,0.9-2.9,1.4-4.6,0.5c-0.4-0.2-0.7-0.6-1-1.1c0,0.4,0,0.8,0,1.3c-0.6,0-1.3,0-1.9,0c0-4.2,0-8.3,0-12.5 c-2.3,3.9-4.6,8.4-6.9,12.5c-4.9,0-9.8,0-14.7,0C2.5,21.7,4.8,17.5,7.1,13.3L7.1,13.3z M24.3,19c-1.4,1.9-0.4,6.7,2.8,5.5 c2.4-0.9,2-6.6-1.2-6.3C25.2,18.3,24.7,18.5,24.3,19L24.3,19z"/> <path d="M39.7,19.9c-0.6,0-1.3,0-2,0c0-1.6-1.9-2-3.1-1.5c-2.3,1.1-1.8,7.1,1.6,6.2c0.8-0.2,1.2-0.9,1.4-2c0.6,0,1.3,0,2,0 c-0.1,2.4-2.1,3.9-4.4,3.7c-2.1-0.1-3.8-1.8-4-4.2c-0.2-2.9,1.3-5.9,5-5.5C38.3,16.8,39.6,17.9,39.7,19.9z"/></symbol>
-	<symbol id="icon-deviantart" viewBox="0 0 40 40"><path d="M30,9.2L24,20.9l0.5,0.6H30v8.3H19.9L19,30.5l-2.8,5.5l-0.6,0.6h-6v-6.1l6.1-11.7l-0.5-0.6H9.5V9.8h10.2l0.9-0.6l2.8-5.5 L24,3.2h6C30,3.2,30,9.2,30,9.2z"/></symbol>
-	<symbol id="icon-soundcloud" viewBox="0 0 40 40"><path d="M13.8,27.4l0.3-4.2L13.8,14c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1C13.1,13.8,13,13.9,13,14 l-0.2,9.2l0.2,4.2c0,0.1,0.1,0.2,0.1,0.3c0.1,0.1,0.2,0.1,0.3,0.1C13.7,27.8,13.8,27.7,13.8,27.4z M18.8,26.9l0.2-3.7l-0.2-10.3 c0-0.2-0.1-0.3-0.2-0.4c-0.1-0.1-0.2-0.1-0.3-0.1s-0.2,0-0.3,0.1c-0.1,0.1-0.2,0.2-0.2,0.4l0,0.1l-0.2,10.1c0,0,0.1,1.4,0.2,4.1v0 c0,0.1,0,0.2,0.1,0.3c0.1,0.1,0.2,0.2,0.4,0.2c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.2-0.2,0.2-0.4L18.8,26.9z M1.2,20.9l0.3,2.2 l-0.3,2.2c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.1-0.1-0.2-0.2l-0.3-2.2l0.3-2.2c0-0.1,0.1-0.2,0.2-0.2S1.2,20.8,1.2,20.9z M2.7,19.5 l0.4,3.6l-0.4,3.6c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L2,23.2l0.4-3.6c0-0.1,0.1-0.2,0.2-0.2C2.6,19.4,2.7,19.4,2.7,19.5z M4.2,18.9l0.4,4.3l-0.4,4.2c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2l-0.4-4.2l0.4-4.3c0-0.1,0.1-0.2,0.2-0.2 C4.2,18.7,4.2,18.7,4.2,18.9z M5.8,18.8l0.4,4.4l-0.4,4.3c0,0.2-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L5,23.2l0.4-4.4 c0-0.2,0.1-0.2,0.2-0.2C5.7,18.5,5.8,18.6,5.8,18.8z M7.4,19.1l0.4,4.1l-0.4,4.3c0,0.2-0.1,0.3-0.3,0.3c-0.1,0-0.1,0-0.2-0.1 c-0.1-0.1-0.1-0.1-0.1-0.2l-0.3-4.3l0.3-4.1c0-0.1,0-0.1,0.1-0.2C7,18.8,7,18.8,7.1,18.8C7.3,18.8,7.4,18.9,7.4,19.1L7.4,19.1z M9,16.5l0.4,6.7L9,27.5c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.1,0.1-0.2,0.1c-0.2,0-0.3-0.1-0.3-0.3l-0.3-4.3l0.3-6.7 c0-0.2,0.1-0.3,0.3-0.3c0.1,0,0.1,0,0.2,0.1S9,16.4,9,16.5z M10.5,15l0.3,8.2l-0.3,4.3c0,0.1,0,0.2-0.1,0.2 c-0.1,0.1-0.1,0.1-0.2,0.1c-0.2,0-0.3-0.1-0.3-0.3l-0.3-4.3L9.9,15c0-0.2,0.1-0.3,0.3-0.3c0.1,0,0.2,0,0.2,0.1 C10.5,14.8,10.5,14.9,10.5,15z M12.2,14.3l0.3,8.9l-0.3,4.2c0,0.2-0.1,0.4-0.4,0.4c-0.2,0-0.3-0.1-0.4-0.4l-0.3-4.2l0.3-8.9 c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0.1C12.1,14.1,12.2,14.2,12.2,14.3z M18.8,27.3L18.8,27.3L18.8,27.3z M15.4,14.2l0.3,8.9l-0.3,4.2c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.1-0.3,0.1c-0.1,0-0.2,0-0.3-0.1s-0.1-0.2-0.1-0.3l-0.2-4.2 l0.2-8.9c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1C15.4,14,15.4,14.1,15.4,14.2L15.4,14.2z M17.1,14.6 l0.2,8.6l-0.2,4.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.1-0.3,0.1c-0.1,0-0.2,0-0.3-0.1c-0.1-0.1-0.1-0.2-0.2-0.3L16,23.2l0.2-8.6 c0-0.1,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1C17.1,14.3,17.1,14.4,17.1,14.6z M20.7,23.2l-0.2,4 c0,0.2-0.1,0.3-0.2,0.4c-0.1,0.1-0.2,0.2-0.4,0.2c-0.1,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.2-0.2-0.4l-0.1-2l-0.1-2L19.4,12V12 c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1c0.2,0.1,0.2,0.2,0.3,0.5L20.7,23.2z M39.4,22.9 c0,1.4-0.5,2.5-1.4,3.5c-0.9,1-2,1.4-3.4,1.4H21.4c-0.1,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.2-0.2-0.4V11.5c0-0.3,0.2-0.5,0.5-0.6 c1-0.4,2-0.6,3-0.6c2.2,0,4.1,0.8,5.7,2.3c1.6,1.5,2.5,3.4,2.7,5.7c0.6-0.3,1.2-0.4,1.8-0.4c1.3,0,2.4,0.5,3.4,1.5 C38.9,20.3,39.4,21.5,39.4,22.9L39.4,22.9z"/></symbol>
-	<symbol id="icon-tumblr" viewBox="0 0 40 40"><path d="M26.8,29.7l1.6,4.6c-0.3,0.5-1,0.9-2.2,1.3s-2.3,0.6-3.4,0.6c-1.4,0-2.6-0.1-3.7-0.5s-2.1-0.8-2.8-1.4 c-0.7-0.6-1.3-1.3-1.9-2.1c-0.5-0.8-0.9-1.6-1.1-2.3c-0.2-0.8-0.3-1.5-0.3-2.3V16.9H9.7v-4.2c0.9-0.3,1.8-0.8,2.5-1.4 s1.3-1.1,1.8-1.8s0.8-1.3,1.1-2c0.3-0.7,0.5-1.4,0.7-1.9S16,4.6,16.1,4c0-0.1,0-0.1,0.1-0.2s0.1-0.1,0.1-0.1h4.8V12h6.5v4.9h-6.5V27 c0,0.4,0,0.8,0.1,1.1c0.1,0.3,0.2,0.7,0.4,1s0.5,0.6,1,0.8c0.4,0.2,1,0.3,1.6,0.3C25.2,30.2,26.1,30,26.8,29.7L26.8,29.7z"/></symbol>
-	<symbol id="icon-twitter" viewBox="0 0 40 40"><path d="M36.3,10.2c-1,1.3-2.1,2.5-3.4,3.5c0,0.2,0,0.4,0,1c0,1.7-0.2,3.6-0.9,5.3c-0.6,1.7-1.2,3.5-2.4,5.1 c-1.1,1.5-2.3,3.1-3.7,4.3c-1.4,1.2-3.3,2.3-5.3,3c-2.1,0.8-4.2,1.2-6.6,1.2c-3.6,0-7-1-10.2-3c0.4,0,1.1,0.1,1.5,0.1 c3.1,0,5.9-1,8.2-2.9c-1.4,0-2.7-0.4-3.8-1.3c-1.2-1-1.9-2-2.2-3.3c0.4,0.1,1,0.1,1.2,0.1c0.6,0,1.2-0.1,1.7-0.2 c-1.4-0.3-2.7-1.1-3.7-2.3s-1.4-2.6-1.4-4.2v-0.1c1,0.6,2,0.9,3,0.9c-1-0.6-1.5-1.3-2.2-2.4c-0.6-1-0.9-2.1-0.9-3.3s0.3-2.3,1-3.4 c1.5,2.1,3.6,3.6,6,4.9s4.9,2,7.6,2.1c-0.1-0.6-0.1-1.1-0.1-1.4c0-1.8,0.8-3.5,2-4.7c1.2-1.2,2.9-2,4.7-2c2,0,3.6,0.8,4.8,2.1 c1.4-0.3,2.9-0.9,4.2-1.5c-0.4,1.5-1.4,2.7-2.9,3.6C33.8,11.2,35.1,10.9,36.3,10.2L36.3,10.2z"/></symbol>
-	<symbol id="icon-youtube" viewBox="0 0 40 40"><path d="M24.3,27v4.2c0,0.9-0.3,1.3-0.8,1.3c-0.3,0-0.6-0.1-0.9-0.4v-6c0.3-0.3,0.6-0.4,0.9-0.4C24,25.6,24.3,26.1,24.3,27L24.3,27z M31.1,27v0.9h-1.8V27c0-0.9,0.3-1.4,0.9-1.4C30.8,25.6,31.1,26.1,31.1,27L31.1,27z M11.7,22.6h2.1v-1.9H7.6v1.9h2.1v11.4h2 L11.7,22.6L11.7,22.6z M17.5,34.1h1.8v-9.9h-1.8v7.6c-0.4,0.6-0.8,0.8-1.1,0.8c-0.2,0-0.4-0.1-0.4-0.4c0,0,0-0.3,0-0.7v-7.3h-1.8V32 c0,0.7,0.1,1.1,0.2,1.5c0.2,0.5,0.5,0.7,1.2,0.7c0.6,0,1.3-0.4,2-1.2L17.5,34.1L17.5,34.1z M26.1,31.1v-4c0-1-0.1-1.6-0.2-2 c-0.2-0.7-0.7-1.1-1.4-1.1c-0.7,0-1.3,0.4-1.9,1.1v-4.4h-1.8v13.3h1.8v-1c0.6,0.7,1.2,1.1,1.9,1.1c0.7,0,1.2-0.4,1.4-1.1 C26,32.7,26.1,32.1,26.1,31.1L26.1,31.1z M32.9,30.9v-0.3H31c0,0.7,0,1.1,0,1.2c-0.1,0.5-0.4,0.7-0.8,0.7c-0.6,0-0.9-0.5-0.9-1.4 v-1.7h3.6v-2.1c0-1.1-0.2-1.8-0.5-2.3c-0.5-0.7-1.2-1-2.1-1c-0.9,0-1.6,0.3-2.1,1c-0.4,0.5-0.6,1.3-0.6,2.3v3.5 c0,1.1,0.2,1.8,0.6,2.3c0.5,0.7,1.2,1,2.2,1c1,0,1.7-0.4,2.2-1.1c0.2-0.4,0.4-0.7,0.4-1.1C32.9,31.9,32.9,31.5,32.9,30.9L32.9,30.9z M20.7,12.5V8.3c0-0.9-0.3-1.4-0.9-1.4c-0.6,0-0.9,0.5-0.9,1.4v4.2c0,0.9,0.3,1.4,0.9,1.4C20.4,14,20.7,13.5,20.7,12.5z M35.1,27.6 c0,3.1-0.2,5.5-0.5,7c-0.2,0.8-0.6,1.5-1.2,2c-0.6,0.5-1.3,0.8-2,0.9c-2.5,0.3-6.2,0.4-11.1,0.4s-8.7-0.1-11.1-0.4 c-0.8-0.1-1.5-0.4-2.1-0.9c-0.6-0.5-1-1.2-1.2-2c-0.3-1.5-0.5-3.8-0.5-7c0-3.1,0.2-5.5,0.5-7c0.2-0.8,0.6-1.5,1.2-2 c0.6-0.5,1.3-0.9,2.1-0.9c2.5-0.3,6.2-0.4,11.1-0.4s8.7,0.1,11.1,0.4c0.8,0.1,1.5,0.4,2.1,0.9c0.6,0.5,1,1.2,1.2,2 C34.9,22.1,35.1,24.4,35.1,27.6z M15.1,2h2l-2.4,8v5.4h-2V10c-0.2-1-0.6-2.4-1.2-4.3c-0.5-1.4-0.9-2.6-1.3-3.8h2.1l1.4,5.3L15.1,2z M22.5,8.7v3.5c0,1.1-0.2,1.9-0.6,2.4c-0.5,0.7-1.2,1-2.1,1c-0.9,0-1.6-0.3-2.1-1c-0.4-0.5-0.6-1.3-0.6-2.4V8.7 c0-1.1,0.2-1.9,0.6-2.3c0.5-0.7,1.2-1,2.1-1c0.9,0,1.6,0.3,2.1,1C22.3,6.8,22.5,7.6,22.5,8.7z M29.2,5.4v10h-1.8v-1.1 c-0.7,0.8-1.4,1.2-2.1,1.2c-0.6,0-1-0.2-1.2-0.7C24,14.5,24,14,24,13.4V5.4h1.8v7.4c0,0.4,0,0.7,0,0.7c0,0.3,0.2,0.4,0.4,0.4 c0.4,0,0.7-0.3,1.1-0.9V5.4C27.4,5.4,29.2,5.4,29.2,5.4z"/></symbol>
-    <symbol id="icon-instagram" viewBox="0 0 40 40"><path d="M20,7c4.2,0,4.7,0,6.3,0.1c1.5,0.1,2.3,0.3,3,0.5C30,8,30.5,8.3,31.1,8.9c0.5,0.5,0.9,1.1,1.2,1.8c0.2,0.5,0.5,1.4,0.5,3 C33,15.3,33,15.8,33,20s0,4.7-0.1,6.3c-0.1,1.5-0.3,2.3-0.5,3c-0.3,0.7-0.6,1.2-1.2,1.8c-0.5,0.5-1.1,0.9-1.8,1.2 c-0.5,0.2-1.4,0.5-3,0.5C24.7,33,24.2,33,20,33s-4.7,0-6.3-0.1c-1.5-0.1-2.3-0.3-3-0.5C10,32,9.5,31.7,8.9,31.1 C8.4,30.6,8,30,7.7,29.3c-0.2-0.5-0.5-1.4-0.5-3C7,24.7,7,24.2,7,20s0-4.7,0.1-6.3c0.1-1.5,0.3-2.3,0.5-3C8,10,8.3,9.5,8.9,8.9 C9.4,8.4,10,8,10.7,7.7c0.5-0.2,1.4-0.5,3-0.5C15.3,7.1,15.8,7,20,7z M20,4.3c-4.3,0-4.8,0-6.5,0.1c-1.6,0-2.8,0.3-3.8,0.7 C8.7,5.5,7.8,6,6.9,6.9C6,7.8,5.5,8.7,5.1,9.7c-0.4,1-0.6,2.1-0.7,3.8c-0.1,1.7-0.1,2.2-0.1,6.5s0,4.8,0.1,6.5 c0,1.6,0.3,2.8,0.7,3.8c0.4,1,0.9,1.9,1.8,2.8c0.9,0.9,1.7,1.4,2.8,1.8c1,0.4,2.1,0.6,3.8,0.7c1.6,0.1,2.2,0.1,6.5,0.1 s4.8,0,6.5-0.1c1.6-0.1,2.9-0.3,3.8-0.7c1-0.4,1.9-0.9,2.8-1.8c0.9-0.9,1.4-1.7,1.8-2.8c0.4-1,0.6-2.1,0.7-3.8 c0.1-1.6,0.1-2.2,0.1-6.5s0-4.8-0.1-6.5c-0.1-1.6-0.3-2.9-0.7-3.8c-0.4-1-0.9-1.9-1.8-2.8c-0.9-0.9-1.7-1.4-2.8-1.8 c-1-0.4-2.1-0.6-3.8-0.7C24.8,4.3,24.3,4.3,20,4.3L20,4.3L20,4.3z"/><path d="M20,11.9c-4.5,0-8.1,3.7-8.1,8.1s3.7,8.1,8.1,8.1s8.1-3.7,8.1-8.1S24.5,11.9,20,11.9z M20,25.2c-2.9,0-5.2-2.3-5.2-5.2 s2.3-5.2,5.2-5.2s5.2,2.3,5.2,5.2S22.9,25.2,20,25.2z"/><path d="M30.6,11.6c0,1-0.8,1.9-1.9,1.9c-1,0-1.9-0.8-1.9-1.9s0.8-1.9,1.9-1.9C29.8,9.7,30.6,10.5,30.6,11.6z"/></symbol>
-    <symbol id="icon-mastodon" viewBox="-20 -20 237 255"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z"/></symbol>
+  <symbol id="icon-globe" viewBox="0 0 40 40"><path d="M20,3C10.6,3,3,10.6,3,20s7.6,17,17,17s17-7.6,17-17S29.4,3,20,3z M34.8,18.9h-6.2c-0.1-2.2-0.3-4.2-0.8-6.1 c1.5-0.4,3-0.9,4.2-1.5c0.6,0.9,1.2,1.8,1.6,2.9C34.3,15.7,34.7,17.3,34.8,18.9z M25.7,26.7c-1.5-0.3-3.1-0.4-4.6-0.5v-5.1h5.4 c-0.1,1.8-0.3,3.5-0.6,5.1C25.8,26.3,25.8,26.5,25.7,26.7z M14.2,26.2c-0.3-1.6-0.6-3.3-0.6-5.1h5.4v5.1c-1.6,0-3.2,0.2-4.6,0.5 C14.2,26.5,14.2,26.3,14.2,26.2z M14.3,13.3c1.5,0.3,3.1,0.4,4.6,0.5v5.1h-5.4c0.1-1.8,0.3-3.5,0.6-5.1 C14.2,13.7,14.2,13.5,14.3,13.3z M21.1,5.4C21.4,5.6,21.7,5.7,22,6c0.8,0.7,1.6,1.7,2.2,3c0.4,0.7,0.7,1.5,0.9,2.3 c-1.3,0.2-2.7,0.4-4,0.4V5.4z M18,6c0.3-0.3,0.6-0.4,0.9-0.6v6.2c-1.4,0-2.8-0.2-4-0.4c0.3-0.8,0.6-1.6,0.9-2.3 C16.5,7.7,17.2,6.7,18,6z M18.9,28.4v6.2c-0.3-0.1-0.6-0.3-0.9-0.6c-0.8-0.7-1.6-1.7-2.2-3c-0.4-0.7-0.7-1.5-0.9-2.3 C16.2,28.6,17.5,28.4,18.9,28.4z M22,34c-0.3,0.3-0.6,0.4-0.9,0.6v-6.2c1.4,0,2.8,0.2,4,0.4c-0.3,0.8-0.6,1.6-0.9,2.3 C23.5,32.3,22.8,33.3,22,34z M21.1,18.9v-5.1c1.6,0,3.2-0.2,4.6-0.5c0,0.2,0.1,0.4,0.1,0.5c0.3,1.6,0.6,3.3,0.6,5.1H21.1z M30.5,9.5 c0,0,0.1,0.1,0.1,0.1c-1,0.4-2.2,0.8-3.4,1.1c-0.6-1.9-1.4-3.5-2.4-4.8c0.3,0.1,0.6,0.2,0.9,0.3C27.5,7.1,29.1,8.1,30.5,9.5z M14.2,6.3c0.3-0.1,0.6-0.2,0.9-0.3c-0.9,1.3-1.7,2.9-2.4,4.8c-1.2-0.3-2.3-0.7-3.4-1.1c0,0,0.1-0.1,0.1-0.1 C10.9,8.1,12.5,7.1,14.2,6.3z M7.9,11.4c1.3,0.6,2.7,1.1,4.2,1.5c-0.4,1.9-0.7,3.9-0.8,6.1H5.2c0.1-1.6,0.5-3.2,1.1-4.7 C6.8,13.2,7.3,12.3,7.9,11.4z M5.2,21.1h6.2c0.1,2.2,0.3,4.2,0.8,6.1c-1.5,0.4-3,0.9-4.2,1.5c-0.6-0.9-1.2-1.8-1.6-2.9 C5.7,24.3,5.3,22.7,5.2,21.1z M9.5,30.5c0,0-0.1-0.1-0.1-0.1c1-0.4,2.2-0.8,3.4-1.1c0.6,1.9,1.4,3.5,2.4,4.8 c-0.3-0.1-0.6-0.2-0.9-0.3C12.5,32.9,10.9,31.9,9.5,30.5z M25.8,33.7c-0.3,0.1-0.6,0.2-0.9,0.3c0.9-1.3,1.7-2.9,2.4-4.8 c1.2,0.3,2.3,0.7,3.4,1.1c0,0-0.1,0.1-0.1,0.1C29.1,31.9,27.5,32.9,25.8,33.7z M32.1,28.6c-1.3-0.6-2.7-1.1-4.2-1.5 c0.4-1.9,0.7-3.9,0.8-6.1h6.2c-0.1,1.6-0.5,3.2-1.1,4.7C33.2,26.8,32.7,27.7,32.1,28.6z"/></symbol>
+
+  <symbol id="icon-appleMusic" viewBox="-20 -20 401 401"><path d="M 112.60938 0 C 108.30938 0 104.01093 -0.00046873 99.710938 0.01953125 C 96.090941 0.03953123 92.469606 0.0796876 88.849609 0.1796875 C 80.959617 0.39968728 72.999211 0.85953266 65.199219 2.2695312 C 57.279227 3.6895298 49.920462 6.0196912 42.730469 9.6796875 C 35.660476 13.279684 29.190073 17.979849 23.580078 23.589844 C 17.970084 29.199838 13.269918 35.660476 9.6699219 42.730469 C 6.0099255 49.930462 3.6797642 57.300711 2.2597656 65.220703 C 0.85976703 73.020695 0.38968729 80.979383 0.1796875 88.859375 C 0.0796876 92.479371 0.03953123 96.100707 0.01953125 99.720703 C -0.00046873 104.0107 0 108.30938 0 112.60938 L 0 247.38086 C 0 251.68086 -0.00046873 255.9793 0.01953125 260.2793 C 0.03953123 263.89929 0.0796876 267.52063 0.1796875 271.14062 C 0.38968729 279.03062 0.85976702 286.9793 2.2597656 294.7793 C 3.6797642 302.69929 6.0099255 310.06954 9.6699219 317.26953 C 13.269918 324.33952 17.970084 330.80016 23.580078 336.41016 C 29.190073 342.02015 35.660476 346.72032 42.730469 350.32031 C 49.920462 353.98031 57.289227 356.30047 65.199219 357.73047 C 72.999211 359.13047 80.959617 359.60055 88.849609 359.81055 C 92.469606 359.91055 96.090941 359.9507 99.710938 359.9707 C 104.01093 360.0007 108.30938 359.99023 112.60938 359.99023 L 247.38086 359.99023 C 251.68086 359.99023 255.9793 359.9907 260.2793 359.9707 C 263.89929 359.9507 267.52063 359.91055 271.14062 359.81055 C 279.03062 359.60055 286.98907 359.13047 294.78906 357.73047 C 302.70905 356.31047 310.06977 353.98031 317.25977 350.32031 C 324.32976 346.72032 330.80016 342.02015 336.41016 336.41016 C 342.02015 330.80016 346.72032 324.33952 350.32031 317.26953 C 353.98031 310.06954 356.31047 302.69929 357.73047 294.7793 C 359.13047 286.9793 359.60055 279.02062 359.81055 271.14062 C 359.91055 267.52063 359.9507 263.89929 359.9707 260.2793 C 360.0007 255.9793 359.99023 251.68086 359.99023 247.38086 L 359.99023 112.60938 L 360 112.60938 C 360 108.30938 360.00047 104.01093 359.98047 99.710938 C 359.96047 96.090941 359.92031 92.469606 359.82031 88.849609 C 359.61031 80.959617 359.14023 73.01093 357.74023 65.210938 C 356.32024 57.290945 353.99007 49.920696 350.33008 42.720703 C 346.73008 35.65071 342.02992 29.190073 336.41992 23.580078 C 330.80993 17.970084 324.33952 13.269918 317.26953 9.6699219 C 310.07954 6.0099255 302.71077 3.6897642 294.80078 2.2597656 C 287.00079 0.85976703 279.04038 0.38968729 271.15039 0.1796875 C 267.53039 0.0796876 263.90906 0.03953123 260.28906 0.01953125 C 255.98907 -0.00046873 251.69062 0 247.39062 0 L 112.60938 0 z M 254.5 55 C 260.28999 54.500001 263.53953 58.300944 263.51953 64.460938 L 263.51953 234.26953 C 263.51953 238.82953 263.47953 242.9593 262.51953 247.5293 C 261.58953 251.95929 259.89929 256.13086 257.2793 259.88086 C 254.6693 263.62086 251.32968 266.69024 247.42969 268.99023 C 243.48969 271.32023 239.34968 272.64906 234.92969 273.53906 C 226.6297 275.20906 220.94914 275.58953 215.61914 274.51953 C 210.47915 273.47953 206.12086 271.11992 202.63086 267.91992 C 197.46086 263.18993 194.23906 256.7896 193.53906 250.09961 C 192.71906 242.25962 195.32094 233.8907 201.21094 227.7207 C 204.18093 224.60071 207.91063 222.14094 212.89062 220.21094 C 218.10062 218.19094 223.84946 216.97922 232.68945 215.19922 L 239.67969 213.78906 C 242.74968 213.16906 245.37024 212.39078 247.49023 209.80078 C 249.63023 207.20078 249.66016 204.02086 249.66016 200.88086 L 249.66016 121.58984 C 249.66016 115.51985 246.93062 113.87047 241.14062 114.98047 C 236.99063 115.79047 148.05078 133.73047 148.05078 133.73047 C 143.03079 134.95047 141.26953 136.59055 141.26953 142.81055 L 141.26953 258.96094 C 141.26953 263.52093 141.04008 267.65071 140.08008 272.2207 C 139.15008 276.6507 137.45984 280.82032 134.83984 284.57031 C 132.22985 288.31031 128.89023 291.37969 124.99023 293.67969 C 121.05024 296.00969 116.91023 297.39906 112.49023 298.28906 C 104.19024 299.96906 98.509682 300.33953 93.179688 299.26953 C 88.039693 298.23953 83.67945 295.80937 80.189453 292.60938 C 75.019458 287.87938 72.010546 281.47906 71.310547 274.78906 C 70.490548 266.94907 72.879537 258.58015 78.769531 252.41016 C 81.739528 249.29016 85.469224 246.83039 90.449219 244.90039 C 95.659214 242.88039 101.41001 241.67062 110.25 239.89062 L 117.24023 238.48047 C 120.31023 237.86047 122.93078 237.08023 125.05078 234.49023 C 127.17078 231.90024 127.41992 228.86047 127.41992 225.73047 L 127.41992 91.810547 C 127.41992 90.010549 127.57016 88.789453 127.66016 88.189453 C 128.09016 85.369456 129.21977 82.950233 131.25977 81.240234 C 132.94976 79.820236 135.13969 78.830234 137.92969 78.240234 L 137.9707 78.230469 L 244.9707 56.640625 C 245.9007 56.450625 253.63 55.08 254.5 55 z " /></symbol>
+  <symbol id="icon-artstation" viewBox="45 35 118.8 118.8"><path d="M 92.900391 51.5 L 146.59961 144.5 L 155.09961 129.80078 C 156.69961 127.00078 157.19922 125.80039 157.19922 123.40039 C 157.19922 121.30039 156.6 119.29961 155.5 117.59961 L 120.69922 57.199219 C 118.89922 53.799222 115.40078 51.5 111.30078 51.5 L 92.900391 51.5 z M 84.199219 66.599609 L 60.199219 108.09961 L 108.09961 108.09961 L 84.199219 66.599609 z M 51.400391 123.30078 L 60.300781 138.69922 C 62.100779 142.19922 65.700785 144.59961 69.800781 144.59961 L 129.09961 144.59961 L 116.80078 123.30078 L 51.400391 123.30078 z " /></symbol>
+  <symbol id="icon-bandcamp" viewBox="-55 -145 440 440"><path d="M 87.222998,512.00002 H 274.006 l -87.225,-161.01 H 0 Z" transform="matrix(1.3333333,0,0,-1.3333333,0,682.66667)" /></symbol>
+
+  <symbol id="icon-carrd" viewBox="20 20 600 600">
+    <!-- Carrd Brand Asset: Symbol (Dark) | (c) Carrd Inc. | "Carrd" is a registered trademark of Carrd Inc. -->
+    <path d="M529.2,465.1L269.1,590c-1.6,0.8-3.4,1.2-5.2,1.2c-2.2,0-4.4-0.6-6.4-1.8c-3.5-2.2-5.6-6-5.6-10.2V455.5 l-140.5-58.8c-4.5-1.9-7.4-6.2-7.4-11.1V60.8c0-4.1,2.1-8,5.6-10.2c3.5-2.2,7.9-2.4,11.6-0.7l270.4,129.8l127.3-61.1 c3.7-1.8,8.1-1.5,11.6,0.7c3.5,2.2,5.6,6,5.6,10.2v324.8C536,458.9,533.4,463.1,529.2,465.1z M128,79.9v297.7l123.9,51.9v-59.8 l-77.9-31.4c-6.1-2.5-9.1-9.5-6.7-15.6c2.5-6.2,9.5-9.1,15.6-6.7l69,27.8v-49.1l-77.9-31.4c-6.1-2.5-9.1-9.5-6.7-15.6 c2.5-6.2,9.5-9.1,15.6-6.7l69,27.8v-14.3c0-4.6,2.6-8.8,6.8-10.8l17.8-8.6l-103.1-46.9c-6-2.7-8.7-9.9-6-15.9c2.7-6,9.9-8.7,15.9-6 l121.3,55.3l59.1-28.4L128,79.9z M512,148.6L275.9,262v24.3c0,0,0,0,0,0.1v74.9c0,0,0,0,0,0.1v198.8L512,446.7V148.6z M321,303.8 l135.3-65.4c6-2.9,13.1-0.4,16,5.6c2.9,6,0.4,13.2-5.6,16l-135.3,65.4c-1.7,0.8-3.5,1.2-5.2,1.2c-4.5,0-8.7-2.5-10.8-6.8 C312.6,313.9,315.1,306.7,321,303.8z M321,378.8l135.3-65.4c6-2.9,13.1-0.4,16,5.6c2.9,6,0.4,13.2-5.6,16l-135.3,65.4 c-1.7,0.8-3.5,1.2-5.2,1.2c-4.5,0-8.7-2.5-10.8-6.8C312.6,388.8,315.1,381.7,321,378.8z M321,453.7l135.3-65.4 c6-2.9,13.1-0.4,16,5.6c2.9,6,0.4,13.2-5.6,16l-135.3,65.4c-1.7,0.8-3.5,1.2-5.2,1.2c-4.5,0-8.7-2.5-10.8-6.8 C312.6,463.8,315.1,456.6,321,453.7z"/>
+  </symbol>
+
+  <symbol id="icon-bluesky" viewBox="0 0 600 530"><path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/></symbol>
+  <symbol id="icon-cohost" viewBox="0 -40 180 180"><path fill-rule="evenodd" clip-rule="evenodd" d="M142.814 106.403C131.705 113.206 118.897 118.552 104.39 122.439C88.2779 126.756 73.0919 128.487 58.8317 127.631C44.5716 126.775 32.4222 123.055 22.3834 116.471C12.3446 109.887 5.57634 100.068 2.07868 87.0142C-1.43905 73.886 -0.492012 61.9799 4.9198 51.2958C10.3316 40.6118 19.0083 31.3714 30.95 23.5747C42.8917 15.7779 56.9185 9.72092 73.0304 5.40379C89.0677 1.1066 104.193 -0.627685 118.406 0.200922C127.955 0.757684 136.568 2.6028 144.246 5.73626C147.995 7.26657 151.521 9.10414 154.824 11.249C164.89 17.7858 171.672 27.581 175.17 40.6346C178.667 53.6882 177.697 65.5807 172.258 76.312C171.498 77.8112 170.675 79.2823 169.789 80.7261C169.163 77.9074 167.906 75.4497 166.018 73.353C165.091 72.3236 164.061 71.3784 162.926 70.5172C160.603 68.7538 157.845 67.3429 154.652 66.2845C149.898 64.7092 144.602 63.9216 138.763 63.9216C132.896 63.9216 127.58 64.7024 122.813 66.2641C118.046 67.8259 114.257 70.1752 111.446 73.3122C108.635 76.4492 107.23 80.4078 107.23 85.188C107.23 89.9411 108.635 93.8928 111.446 97.0434C114.257 100.194 118.046 102.564 122.813 104.153C127.58 105.741 132.896 106.536 138.763 106.536C140.143 106.536 141.493 106.492 142.814 106.403ZM91.9944 97.9397C90.8808 99.1348 88.9185 100.404 86.1074 101.749C83.2963 103.093 79.9081 104.227 75.9427 105.151C71.9773 106.074 67.7132 106.536 63.1502 106.536C59.1577 106.536 55.2466 106.149 51.417 105.375C47.5875 104.601 44.1245 103.372 41.0283 101.688C37.932 100.004 35.4672 97.8039 33.6339 95.0879C31.8006 92.3719 30.8839 89.0719 30.8839 85.188C30.8839 81.2498 31.8006 77.9227 33.6339 75.2066C35.4672 72.4906 37.932 70.3042 41.0283 68.6475C44.1245 66.9907 47.5875 65.7888 51.417 65.0419C55.2466 64.295 59.1577 63.9216 63.1502 63.9216C67.7403 63.9216 71.9773 64.329 75.8612 65.1438C79.7451 65.9586 83.079 67.0246 85.8629 68.3419C88.6469 69.6592 90.6635 71.0647 91.9129 72.5585L81.4834 79.4028C79.9624 77.7461 77.6538 76.4221 74.5575 75.4307C71.4613 74.4394 67.6996 73.9437 63.2725 73.9437C61.0997 73.9437 58.9065 74.1135 56.6929 74.4529C54.4793 74.7925 52.4491 75.3696 50.6022 76.1844C48.7554 76.9992 47.2683 78.1399 46.1412 79.6066C45.014 81.0732 44.4504 82.9337 44.4504 85.188C44.4504 87.4151 45.014 89.2553 46.1412 90.7083C47.2683 92.1614 48.7554 93.3157 50.6022 94.1712C52.4491 95.0268 54.4793 95.6311 56.6929 95.9842C58.9065 96.3373 61.0997 96.5138 63.2725 96.5138C67.6181 96.5138 71.3866 95.9706 74.5779 94.8842C77.7692 93.7978 80.0167 92.5484 81.3204 91.1361L91.9944 97.9397ZM138.763 96.3508C144.439 96.3508 148.839 95.3323 151.963 93.2953C155.086 91.2583 156.648 88.5559 156.648 85.188C156.648 81.7386 155.079 79.0294 151.942 77.0603C148.805 75.0912 144.412 74.1066 138.763 74.1066C133.086 74.1066 128.666 75.0912 125.502 77.0603C122.338 79.0294 120.756 81.7386 120.756 85.188C120.756 88.6102 122.338 91.3262 125.502 93.3361C128.666 95.3459 133.086 96.3508 138.763 96.3508Z"/></symbol>
+  <symbol id="icon-deviantart" viewBox="0 0 40 40"><path d="M30,9.2L24,20.9l0.5,0.6H30v8.3H19.9L19,30.5l-2.8,5.5l-0.6,0.6h-6v-6.1l6.1-11.7l-0.5-0.6H9.5V9.8h10.2l0.9-0.6l2.8-5.5 L24,3.2h6C30,3.2,30,9.2,30,9.2z"/></symbol>
+  <symbol id="icon-facebook" viewBox="100 100 733 733"><path d="m 0,0 c 0,138.071 -111.929,250 -250,250 -138.071,0 -250,-111.929 -250,-250 0,-117.245 80.715,-215.622 189.606,-242.638 v 166.242 h -51.552 V 0 h 51.552 v 32.919 c 0,85.092 38.508,124.532 122.048,124.532 15.838,0 43.167,-3.105 54.347,-6.211 V 81.986 c -5.901,0.621 -16.149,0.932 -28.882,0.932 -40.993,0 -56.832,-15.528 -56.832,-55.9 V 0 h 81.659 l -14.028,-76.396 h -67.631 V -248.169 C -95.927,-233.218 0,-127.818 0,0" transform="matrix(1.3333333,0,0,-1.3333333,799.99998,466.66668)"/></symbol>
+  <symbol id="icon-kofi" viewBox="100 60 700 760"><path d="M 207.0293 281.82031 C 181.39936 281.82031 167.74023 305.44029 167.74023 322.49023 L 167.74023 327.68945 C 167.73023 329.70945 166.47977 530.46002 169.00977 638.17969 C 169.00977 638.38969 169.01906 638.61031 169.03906 638.82031 C 171.56906 679.76019 194.721 699.32909 213.71094 708.53906 C 233.32088 718.04903 252.52008 718.16969 253.33008 718.17969 L 253.41016 718.17969 C 261.93013 718.17969 463.3196 718.16992 562.0293 716.91992 C 567.75927 716.91992 573.18042 716.91969 580.40039 715.17969 L 580.65039 715.11914 C 610.6803 707.43917 632.08003 689.87056 644.25 662.89062 C 649.99997 650.14068 653.47987 635.77072 654.83984 619.05078 C 682.38978 618.35078 708.30022 612.90958 731.91016 602.84961 C 756.78007 592.24964 777.84029 577.07064 794.49023 557.7207 C 827.19014 519.72082 839.42919 469.40891 828.94922 416.03906 L 828.93945 416.03906 L 828.93945 416.01953 L 828.91992 415.91992 L 828.90039 415.80078 C 817.58042 359.06096 786.439 328.33075 762.28906 312.55078 C 733.99915 292.75084 698.67996 281.83984 662.83008 281.83984 L 207.0293 281.82031 z M 656.9707 392.17969 L 670.67969 392.17969 C 686.12963 392.17969 699.87089 398.51003 709.38086 410 L 709.64062 410.31055 C 717.58062 419.44052 721.43945 431.00925 721.43945 445.69922 C 721.43945 479.18913 709.16007 497.62956 680.41016 507.26953 C 672.92016 508.04953 665.0507 508.48984 656.9707 508.58984 L 656.9707 392.17969 z M 476.60742 393.72461 C 485.18645 393.7029 494.3015 395.30658 503.74023 399.28906 C 552.80015 420.42902 551.42025 477.44944 522.32031 511.85938 C 522.31746 511.86266 522.3134 511.86584 522.31055 511.86914 C 520.24235 514.26239 518.02021 516.7657 515.66992 519.35352 C 514.72786 520.39083 513.68242 521.49514 512.70117 522.55859 C 511.23104 524.15182 509.80205 525.71777 508.25391 527.36133 C 508.2503 527.36515 508.24774 527.36922 508.24414 527.37305 C 506.12619 529.6214 503.7259 532.03298 501.48828 534.35352 C 498.27088 537.69012 495.15981 540.96685 491.77148 544.38867 C 486.2458 549.96915 480.54963 555.60713 474.88477 561.15039 C 474.66348 561.36692 474.44579 561.58848 474.22461 561.80469 C 474.22132 561.8079 474.21813 561.81124 474.21484 561.81445 C 468.33146 567.56551 462.48624 573.2008 456.90039 578.53711 C 456.89719 578.54017 456.89383 578.54381 456.89062 578.54688 C 445.71718 589.22101 435.57811 598.69928 428.23242 605.50781 C 428.22984 605.5102 428.22523 605.51519 428.22266 605.51758 C 420.88542 612.31814 416.33008 616.46094 416.33008 616.46094 C 416.33008 616.46094 416.31111 616.476 416.31055 616.47656 C 416.25736 616.52936 413.7392 619.01347 410.82031 618.42969 L 410.81055 618.42969 C 409.53055 618.22969 408.34914 617.54016 407.36914 616.66016 C 407.14434 616.45713 405.99082 615.40947 405.69727 615.14453 C 405.69644 615.16685 405.68945 615.17525 405.68945 615.19922 C 393.78949 604.57924 316.9109 535.74932 299.71094 511.85938 C 281.70924 486.23263 272.47566 443.08094 294.81055 416.36328 C 295.52774 415.5049 296.27745 414.6632 297.06055 413.83984 C 297.06308 413.83718 297.06583 413.83469 297.06836 413.83203 L 297.07031 413.83008 C 297.85371 413.00684 298.66556 412.20625 299.50391 411.42969 C 299.50727 411.42658 299.51031 411.42303 299.51367 411.41992 C 300.35226 410.64352 301.21872 409.89266 302.10938 409.16406 C 328.93957 387.21554 378.74326 387.20684 412.30078 424.44922 C 412.30862 424.44038 412.94176 423.72533 414.1582 422.50391 C 415.37822 421.27829 417.17743 419.55137 419.49805 417.54102 C 419.50123 417.53826 419.50463 417.53401 419.50781 417.53125 C 420.66455 416.52948 421.9529 415.45886 423.35938 414.3457 C 434.64596 405.41287 453.72216 393.78252 476.60742 393.72461 z " /></symbol>
+  <symbol id="icon-instagram" viewBox="-50 -50 1100 1100"><path class="cls-1" d="M295.42,6c-53.2,2.51-89.53,11-121.29,23.48-32.87,12.81-60.73,30-88.45,57.82S40.89,143,28.17,175.92c-12.31,31.83-20.65,68.19-23,121.42S2.3,367.68,2.56,503.46,3.42,656.26,6,709.6c2.54,53.19,11,89.51,23.48,121.28,12.83,32.87,30,60.72,57.83,88.45S143,964.09,176,976.83c31.8,12.29,68.17,20.67,121.39,23s70.35,2.87,206.09,2.61,152.83-.86,206.16-3.39S799.1,988,830.88,975.58c32.87-12.86,60.74-30,88.45-57.84S964.1,862,976.81,829.06c12.32-31.8,20.69-68.17,23-121.35,2.33-53.37,2.88-70.41,2.62-206.17s-.87-152.78-3.4-206.1-11-89.53-23.47-121.32c-12.85-32.87-30-60.7-57.82-88.45S862,40.87,829.07,28.19c-31.82-12.31-68.17-20.7-121.39-23S637.33,2.3,501.54,2.56,348.75,3.4,295.42,6m5.84,903.88c-48.75-2.12-75.22-10.22-92.86-17-23.36-9-40-19.88-57.58-37.29s-28.38-34.11-37.5-57.42c-6.85-17.64-15.1-44.08-17.38-92.83-2.48-52.69-3-68.51-3.29-202s.22-149.29,2.53-202c2.08-48.71,10.23-75.21,17-92.84,9-23.39,19.84-40,37.29-57.57s34.1-28.39,57.43-37.51c17.62-6.88,44.06-15.06,92.79-17.38,52.73-2.5,68.53-3,202-3.29s149.31.21,202.06,2.53c48.71,2.12,75.22,10.19,92.83,17,23.37,9,40,19.81,57.57,37.29s28.4,34.07,37.52,57.45c6.89,17.57,15.07,44,17.37,92.76,2.51,52.73,3.08,68.54,3.32,202s-.23,149.31-2.54,202c-2.13,48.75-10.21,75.23-17,92.89-9,23.35-19.85,40-37.31,57.56s-34.09,28.38-57.43,37.5c-17.6,6.87-44.07,15.07-92.76,17.39-52.73,2.48-68.53,3-202.05,3.29s-149.27-.25-202-2.53m407.6-674.61a60,60,0,1,0,59.88-60.1,60,60,0,0,0-59.88,60.1M245.77,503c.28,141.8,115.44,256.49,257.21,256.22S759.52,643.8,759.25,502,643.79,245.48,502,245.76,245.5,361.22,245.77,503m90.06-.18a166.67,166.67,0,1,1,167,166.34,166.65,166.65,0,0,1-167-166.34" transform="translate(-2.5 -2.5)"/></symbol>
+  <symbol id="icon-itch" viewBox="-20 -20 265 241"><path d="M31.99 1.365C21.287 7.72.2 31.945 0 38.298v10.516C0 62.144 12.46 73.86 23.773 73.86c13.584 0 24.902-11.258 24.903-24.62 0 13.362 10.93 24.62 24.515 24.62 13.586 0 24.165-11.258 24.165-24.62 0 13.362 11.622 24.62 25.207 24.62h.246c13.586 0 25.208-11.258 25.208-24.62 0 13.362 10.58 24.62 24.164 24.62 13.585 0 24.515-11.258 24.515-24.62 0 13.362 11.32 24.62 24.903 24.62 11.313 0 23.773-11.714 23.773-25.046V38.298c-.2-6.354-21.287-30.58-31.988-36.933C180.118.197 157.056-.005 122.685 0c-34.37.003-81.228.54-90.697 1.365zm65.194 66.217a28.025 28.025 0 0 1-4.78 6.155c-5.128 5.014-12.157 8.122-19.906 8.122a28.482 28.482 0 0 1-19.948-8.126c-1.858-1.82-3.27-3.766-4.563-6.032l-.006.004c-1.292 2.27-3.092 4.215-4.954 6.037a28.5 28.5 0 0 1-19.948 8.12c-.934 0-1.906-.258-2.692-.528-1.092 11.372-1.553 22.24-1.716 30.164l-.002.045c-.02 4.024-.04 7.333-.06 11.93.21 23.86-2.363 77.334 10.52 90.473 19.964 4.655 56.7 6.775 93.555 6.788h.006c36.854-.013 73.59-2.133 93.554-6.788 12.883-13.14 10.31-66.614 10.52-90.474-.022-4.596-.04-7.905-.06-11.93l-.003-.045c-.162-7.926-.623-18.793-1.715-30.165-.786.27-1.757.528-2.692.528a28.5 28.5 0 0 1-19.948-8.12c-1.862-1.822-3.662-3.766-4.955-6.037l-.006-.004c-1.294 2.266-2.705 4.213-4.563 6.032a28.48 28.48 0 0 1-19.947 8.125c-7.748 0-14.778-3.11-19.906-8.123a28.025 28.025 0 0 1-4.78-6.155 27.99 27.99 0 0 1-4.736 6.155 28.49 28.49 0 0 1-19.95 8.124c-.27 0-.54-.012-.81-.02h-.007c-.27.008-.54.02-.813.02a28.49 28.49 0 0 1-19.95-8.123 27.992 27.992 0 0 1-4.736-6.155zm-20.486 26.49l-.002.01h.015c8.113.017 15.32 0 24.25 9.746 7.028-.737 14.372-1.105 21.722-1.094h.006c7.35-.01 14.694.357 21.723 1.094 8.93-9.747 16.137-9.73 24.25-9.746h.014l-.002-.01c3.833 0 19.166 0 29.85 30.007L210 165.244c8.504 30.624-2.723 31.373-16.727 31.4-20.768-.773-32.267-15.855-32.267-30.935-11.496 1.884-24.907 2.826-38.318 2.827h-.006c-13.412 0-26.823-.943-38.318-2.827 0 15.08-11.5 30.162-32.267 30.935-14.004-.027-25.23-.775-16.726-31.4L46.85 124.08C57.534 94.073 72.867 94.073 76.7 94.073zm45.985 23.582v.006c-.02.02-21.863 20.08-25.79 27.215l14.304-.573v12.474c0 .584 5.74.346 11.486.08h.006c5.744.266 11.485.504 11.485-.08v-12.474l14.304.573c-3.928-7.135-25.79-27.215-25.79-27.215v-.006l-.003.002z" color="#000"/></symbol>
+  <symbol id="icon-linktree" viewBox="0 0 24 24"><path d="m13.511 5.853 4.005-4.117 2.325 2.381-4.201 4.005h5.909v3.305h-5.937l4.229 4.108-2.325 2.334-5.741-5.769-5.741 5.769-2.325-2.325 4.229-4.108H2V8.122h5.909L3.708 4.117l2.325-2.381 4.005 4.117V0h3.473v5.853zM10.038 16.16h3.473v7.842h-3.473V16.16z"/></symbol>
+  <symbol id="icon-mastodon" viewBox="-20 -20 237 255"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z"/></symbol>
+  <symbol id="icon-patreon" viewBox="-80 -80 1160 1160"><path d="M1033.05,324.45c-0.19-137.9-107.59-250.92-233.6-291.7c-156.48-50.64-362.86-43.3-512.28,27.2 C106.07,145.41,49.18,332.61,47.06,519.31c-1.74,153.5,13.58,557.79,241.62,560.67c169.44,2.15,194.67-216.18,273.07-321.33 c55.78-74.81,127.6-95.94,216.01-117.82C929.71,603.22,1033.27,483.3,1033.05,324.45z"/></symbol>
+  <symbol id="icon-spotify" viewBox="0 0 41 40"><path d="m 40.738,21.322 c -0.015,0.221 -0.029,0.441 -0.051,0.659 -0.013,0.13 -0.032,0.258 -0.047,0.386 -0.024,0.197 -0.047,0.393 -0.076,0.587 -0.021,0.136 -0.046,0.271 -0.07,0.406 -0.032,0.185 -0.063,0.37 -0.1,0.553 -0.028,0.138 -0.06,0.274 -0.091,0.411 -0.04,0.178 -0.08,0.355 -0.125,0.531 -0.035,0.138 -0.074,0.274 -0.112,0.411 -0.048,0.172 -0.096,0.344 -0.149,0.515 a 19.186,19.186 0 0 1 -0.304,0.907 20.859,20.859 0 0 1 -0.515,1.278 26.182,26.182 0 0 1 -0.404,0.857 c -0.08,0.159 -0.163,0.315 -0.246,0.471 -0.066,0.122 -0.131,0.243 -0.199,0.364 a 21.5,21.5 0 0 1 -0.271,0.463 c -0.07,0.116 -0.139,0.232 -0.211,0.347 -0.097,0.155 -0.198,0.307 -0.3,0.459 -0.073,0.109 -0.144,0.219 -0.219,0.326 -0.108,0.156 -0.221,0.308 -0.334,0.461 -0.074,0.1 -0.145,0.2 -0.221,0.299 -0.124,0.162 -0.253,0.319 -0.383,0.477 -0.069,0.085 -0.136,0.171 -0.206,0.255 -0.159,0.188 -0.324,0.371 -0.489,0.554 -0.046,0.049 -0.088,0.101 -0.134,0.15 -0.215,0.231 -0.434,0.457 -0.66,0.678 -0.04,0.04 -0.084,0.078 -0.125,0.118 a 19.93,19.93 0 0 1 -0.562,0.525 c -0.081,0.073 -0.165,0.141 -0.246,0.212 -0.156,0.135 -0.311,0.27 -0.471,0.4 -0.097,0.079 -0.196,0.154 -0.294,0.231 -0.15,0.117 -0.299,0.234 -0.452,0.348 -0.107,0.079 -0.216,0.154 -0.324,0.231 l -0.339,0.237 c 0.032,-0.023 0.067,-0.042 0.1,-0.064 -0.038,0.026 -0.077,0.048 -0.115,0.074 -0.032,0.022 -0.063,0.045 -0.095,0.066 -0.115,0.077 -0.231,0.151 -0.347,0.225 a 19.368,19.368 0 0 1 -0.599,0.371 l -0.07,0.04 c 0.015,-0.008 0.031,-0.016 0.045,-0.025 l -0.209,0.121 c -0.148,0.086 -0.296,0.17 -0.446,0.252 l -0.197,0.104 c -0.115,0.061 -0.232,0.121 -0.349,0.18 l -0.061,0.03 c -0.09,0.045 -0.178,0.091 -0.269,0.135 -0.122,0.058 -0.244,0.116 -0.368,0.173 l -0.1,0.044 c -0.11,0.05 -0.22,0.099 -0.331,0.147 l -0.048,0.021 c -0.097,0.042 -0.196,0.082 -0.294,0.123 a 15.8,15.8 0 0 1 -0.516,0.203 c -0.117,0.044 -0.233,0.087 -0.35,0.129 l -0.151,0.055 c -0.129,0.045 -0.26,0.088 -0.391,0.13 -0.107,0.035 -0.214,0.07 -0.322,0.103 l -0.043,0.013 c -0.116,0.036 -0.232,0.07 -0.349,0.103 l -0.334,0.094 c -0.143,0.038 -0.285,0.077 -0.429,0.113 l -0.073,0.016 -0.18,0.044 c 0.032,-0.007 0.062,-0.017 0.094,-0.024 l -0.208,0.047 c 0.037,-0.009 0.076,-0.014 0.114,-0.023 -0.088,0.021 -0.177,0.037 -0.265,0.057 l -0.013,0.003 c -0.143,0.031 -0.285,0.064 -0.429,0.092 -0.149,0.029 -0.299,0.054 -0.449,0.079 -0.088,0.015 -0.175,0.034 -0.263,0.048 0.041,-0.007 0.081,-0.017 0.122,-0.024 l -0.217,0.036 c 0.032,-0.005 0.064,-0.007 0.095,-0.012 l -0.147,0.021 -0.118,0.02 c -0.199,0.029 -0.4,0.051 -0.601,0.075 -0.13,0.015 -0.26,0.034 -0.392,0.046 l -0.03,0.004 h -0.019 c -0.054,0.005 -0.11,0.007 -0.164,0.012 -0.124,0.011 -0.248,0.018 -0.372,0.027 -0.421,0.03 -0.845,0.049 -1.273,0.053 -0.038,0 -0.074,0.005 -0.111,0.005 h -0.153 c -0.036,0 -0.07,-0.004 -0.106,-0.005 A 21.083,21.083 0 0 1 18.9,39.931 C 18.781,39.922 18.661,39.915 18.542,39.905 18.494,39.9 18.445,39.899 18.396,39.894 h -0.018 a 19.603,19.603 0 0 1 -1.065,-0.138 c 0.051,0.008 0.103,0.012 0.154,0.019 -0.127,-0.019 -0.257,-0.03 -0.383,-0.05 l -0.18,-0.032 c 0.048,0.008 0.095,0.021 0.143,0.029 -0.086,-0.014 -0.17,-0.034 -0.256,-0.049 l -0.03,-0.005 c -0.053,-0.009 -0.104,-0.022 -0.156,-0.031 a 21.03,21.03 0 0 1 -1.011,-0.206 l 0.096,0.019 -0.158,-0.036 0.062,0.017 c -0.145,-0.034 -0.286,-0.077 -0.43,-0.115 l 0.023,0.006 C 15.172,39.319 15.158,39.314 15.143,39.31 l 0.021,0.006 c -0.291,-0.075 -0.582,-0.15 -0.868,-0.237 L 14.26,39.069 C 14.221,39.057 14.183,39.043 14.144,39.03 l -0.018,-0.006 c -0.161,-0.051 -0.319,-0.11 -0.479,-0.165 l 0.208,0.071 c -0.11,-0.037 -0.219,-0.076 -0.328,-0.115 l 0.12,0.044 c -0.245,-0.084 -0.49,-0.168 -0.73,-0.261 l -0.121,-0.047 c -0.017,-0.007 -0.033,-0.015 -0.05,-0.021 a 21.151,21.151 0 0 1 -0.491,-0.205 c 0.107,0.046 0.215,0.089 0.322,0.133 -0.159,-0.065 -0.317,-0.134 -0.474,-0.202 0.051,0.022 0.101,0.047 0.152,0.069 -0.168,-0.072 -0.338,-0.142 -0.504,-0.219 l -0.036,-0.015 c -0.022,-0.01 -0.045,-0.019 -0.067,-0.03 -0.079,-0.036 -0.154,-0.08 -0.232,-0.117 -0.16,-0.078 -0.32,-0.154 -0.478,-0.235 0.135,0.069 0.273,0.133 0.41,0.2 a 21.302,21.302 0 0 1 -0.584,-0.296 c 0.059,0.031 0.115,0.065 0.174,0.096 -0.133,-0.069 -0.265,-0.137 -0.396,-0.208 L 10.494,37.476 C 10.445,37.45 10.397,37.421 10.349,37.394 10.286,37.36 10.227,37.321 10.165,37.285 l -0.053,-0.03 0.027,0.015 C 9.922,37.146 9.708,37.02 9.497,36.889 9.56,36.928 9.62,36.972 9.684,37.011 9.581,36.949 9.481,36.883 9.38,36.82 L 9.371,36.814 9.217,36.717 C 9.139,36.667 9.065,36.612 8.988,36.561 8.823,36.452 8.654,36.349 8.492,36.235 8.613,36.32 8.742,36.395 8.865,36.477 8.649,36.333 8.431,36.192 8.221,36.039 l 0.044,0.033 C 8.25,36.062 8.236,36.05 8.221,36.039 8.025,35.895 7.834,35.745 7.643,35.595 L 7.634,35.588 7.632,35.587 C 7.548,35.52 7.462,35.456 7.378,35.388 L 7.377,35.387 7.344,35.359 C 7.112,35.169 6.882,34.975 6.659,34.775 L 6.641,34.76 6.627,34.747 C 6.545,34.674 6.469,34.597 6.389,34.522 6.231,34.376 6.073,34.23 5.92,34.079 L 5.901,34.061 5.864,34.026 5.901,34.061 C 5.838,33.999 5.778,33.934 5.716,33.871 5.766,33.922 5.813,33.976 5.864,34.026 5.813,33.975 5.765,33.921 5.714,33.869 L 5.632,33.783 C 5.493,33.639 5.353,33.497 5.218,33.349 L 5.216,33.346 C 5.139,33.262 5.064,33.176 4.989,33.091 L 4.931,33.024 A 21.99,21.99 0 0 1 4.335,32.314 C 4.315,32.29 4.296,32.264 4.277,32.239 A 19.844,19.844 0 0 1 3.729,31.519 C 3.707,31.488 3.687,31.456 3.665,31.426 A 22.618,22.618 0 0 1 3.334,30.947 C 3.279,30.865 3.222,30.783 3.168,30.699 3.143,30.661 3.121,30.622 3.097,30.584 A 18.36,18.36 0 0 1 2.799,30.102 C 2.787,30.08 2.773,30.059 2.76,30.038 2.723,29.977 2.685,29.917 2.649,29.855 2.622,29.809 2.598,29.761 2.572,29.714 A 20.391,20.391 0 0 1 2.298,29.212 C 2.255,29.133 2.21,29.056 2.169,28.976 2.143,28.926 2.12,28.875 2.095,28.825 A 16.057,16.057 0 0 1 1.839,28.289 C 1.806,28.22 1.771,28.151 1.739,28.081 L 1.687,27.96 C 1.594,27.752 1.507,27.541 1.422,27.329 L 1.352,27.16 1.323,27.081 A 18.694,18.694 0 0 1 1.044,26.308 L 1.01,26.212 1,26.181 A 19.533,19.533 0 0 1 0.394,23.914 L 0.386,23.874 C 0.37,23.795 0.359,23.715 0.344,23.637 A 19.434,19.434 0 0 1 0.224,22.955 L 0.209,22.867 C 0.193,22.755 0.183,22.641 0.169,22.528 A 17.848,17.848 0 0 1 0.096,21.912 C 0.094,21.892 0.09,21.872 0.089,21.851 0.086,21.824 0.087,21.796 0.085,21.768 A 19.6,19.6 0 0 1 0,19.995 c 0,-0.257 0.01,-0.513 0.02,-0.768 -0.004,0.116 -0.014,0.231 -0.016,0.348 0.002,-0.117 0.012,-0.232 0.016,-0.348 0.006,-0.145 0.012,-0.29 0.02,-0.435 0.005,-0.075 0.005,-0.151 0.011,-0.226 -0.006,0.075 -0.006,0.151 -0.011,0.226 0.012,-0.193 0.028,-0.384 0.045,-0.575 -0.011,0.116 -0.026,0.232 -0.034,0.349 0.008,-0.118 0.024,-0.234 0.034,-0.352 0.013,-0.144 0.026,-0.287 0.042,-0.43 0.022,-0.19 0.047,-0.38 0.074,-0.569 0.02,-0.142 0.04,-0.283 0.063,-0.423 V 16.791 C 0.295,16.604 0.33,16.418 0.366,16.233 V 16.231 C 0.393,16.092 0.42,15.953 0.45,15.815 v -10e-4 a 19.77,19.77 0 0 1 0.13,-0.55 C 0.613,15.128 0.647,14.991 0.684,14.856 V 14.854 C 0.733,14.674 0.785,14.497 0.839,14.32 L 0.84,14.316 c 0.04,-0.134 0.08,-0.269 0.123,-0.401 l 0.001,-0.003 c 0.057,-0.175 0.118,-0.348 0.18,-0.521 L 1.146,13.386 C 1.193,13.255 1.239,13.124 1.289,12.994 L 1.29,12.991 C 1.355,12.82 1.424,12.652 1.493,12.484 L 1.496,12.477 C 1.549,12.349 1.602,12.22 1.657,12.093 L 1.659,12.09 a 21.895,21.895 0 0 1 0.231,-0.5 c 0.059,-0.125 0.117,-0.251 0.179,-0.374 l 0.002,-0.004 c 0.08,-0.161 0.164,-0.319 0.249,-0.477 l 0.005,-0.01 C 2.39,10.604 2.454,10.481 2.522,10.361 L 2.524,10.357 C 2.611,10.202 2.703,10.049 2.794,9.896 L 2.801,9.885 C 2.872,9.767 2.942,9.648 3.015,9.531 L 3.018,9.527 C 3.112,9.377 3.21,9.23 3.308,9.083 L 3.316,9.07 C 3.393,8.956 3.468,8.841 3.547,8.728 L 3.55,8.723 C 3.65,8.579 3.755,8.438 3.859,8.296 L 3.87,8.282 C 3.952,8.171 4.032,8.06 4.116,7.951 L 4.12,7.946 C 4.226,7.808 4.337,7.673 4.448,7.537 L 4.46,7.522 C 4.547,7.415 4.633,7.308 4.722,7.203 A 0.018,0.018 0 0 1 4.726,7.198 C 4.838,7.066 4.955,6.936 5.071,6.807 L 5.086,6.791 C 5.178,6.688 5.268,6.585 5.362,6.484 L 5.367,6.48 C 5.485,6.353 5.608,6.23 5.729,6.107 L 5.746,6.09 C 5.843,5.992 5.938,5.893 6.037,5.797 A 0.018,0.018 0 0 0 6.041,5.792 L 6.077,5.756 6.043,5.791 C 6.15,5.687 6.261,5.587 6.37,5.485 6.28,5.569 6.187,5.65 6.098,5.735 6.203,5.634 6.313,5.537 6.42,5.438 L 6.439,5.421 C 6.541,5.327 6.641,5.232 6.744,5.141 L 6.749,5.137 6.802,5.088 6.749,5.137 C 6.86,5.038 6.976,4.944 7.09,4.847 6.994,4.928 6.896,5.005 6.802,5.088 6.914,4.99 7.03,4.897 7.143,4.802 A 0.425,0.425 0 0 1 7.165,4.784 C 7.27,4.696 7.374,4.606 7.481,4.52 L 7.486,4.516 7.538,4.473 7.487,4.515 C 7.603,4.422 7.722,4.333 7.84,4.243 7.74,4.32 7.637,4.394 7.538,4.473 7.655,4.379 7.777,4.29 7.897,4.199 L 7.92,4.181 C 8.03,4.098 8.137,4.014 8.248,3.933 L 8.252,3.93 8.303,3.892 8.255,3.928 C 8.376,3.84 8.499,3.757 8.621,3.672 8.516,3.746 8.407,3.816 8.303,3.892 8.426,3.802 8.554,3.718 8.68,3.632 L 8.706,3.614 C 8.818,3.536 8.929,3.457 9.043,3.382 L 9.047,3.379 9.096,3.346 9.052,3.376 C 9.177,3.294 9.306,3.217 9.432,3.137 9.321,3.207 9.206,3.274 9.096,3.346 9.226,3.261 9.359,3.182 9.491,3.1 L 9.52,3.083 C 9.635,3.011 9.749,2.937 9.866,2.868 L 9.87,2.866 A 0.739,0.739 0 0 1 9.917,2.837 l -0.04,0.025 c 0.13,-0.077 0.263,-0.149 0.395,-0.223 -0.118,0.066 -0.239,0.129 -0.355,0.198 0.135,-0.08 0.275,-0.154 0.412,-0.23 L 10.361,2.589 C 10.479,2.524 10.595,2.456 10.714,2.392 A 0.008,0.008 0 0 0 10.718,2.39 l 0.045,-0.024 -0.036,0.02 C 10.863,2.314 11.003,2.247 11.14,2.178 11.015,2.241 10.887,2.3 10.763,2.366 10.905,2.291 11.05,2.223 11.193,2.151 a 0.378,0.378 0 0 0 0.035,-0.017 c 0.12,-0.059 0.238,-0.121 0.36,-0.178 l 0.003,-0.002 0.043,-0.021 -0.031,0.016 c 0.143,-0.067 0.289,-0.129 0.434,-0.193 -0.134,0.06 -0.27,0.115 -0.403,0.177 0.148,-0.069 0.299,-0.131 0.449,-0.197 L 12.121,1.719 C 12.242,1.666 12.362,1.61 12.485,1.56 l 0.003,-0.002 a 0.684,0.684 0 0 0 0.04,-0.017 L 12.502,1.553 C 12.654,1.49 12.809,1.434 12.963,1.375 12.818,1.43 12.672,1.483 12.528,1.541 12.682,1.478 12.84,1.422 12.996,1.362 L 13.039,1.346 C 13.161,1.299 13.281,1.25 13.405,1.205 h 0.002 A 0.426,0.426 0 0 1 13.445,1.191 L 13.423,1.199 C 13.588,1.14 13.757,1.088 13.924,1.033 13.764,1.086 13.603,1.135 13.445,1.191 13.606,1.134 13.77,1.084 13.932,1.03 l 0.05,-0.016 c 0.122,-0.04 0.241,-0.083 0.363,-0.12 h 0.002 L 14.382,0.883 14.365,0.889 C 14.539,0.836 14.716,0.79 14.891,0.742 l -0.027,0.008 0.07,-0.02 0.061,-0.017 c 0.104,-0.028 0.206,-0.059 0.311,-0.086 h 0.001 l 0.031,-0.008 -0.01,0.003 c 0.18,-0.045 0.363,-0.083 0.545,-0.123 L 15.809,0.513 C 15.901,0.492 15.992,0.47 16.085,0.451 l 0.088,-0.02 c 0.038,-0.008 0.075,-0.018 0.112,-0.025 l 0.001,-0.001 0.027,-0.004 -0.004,0.001 c 0.188,-0.038 0.379,-0.068 0.57,-0.1 L 16.777,0.319 C 16.947,0.29 17.115,0.256 17.285,0.231 L 17.286,0.23 17.303,0.228 h 0.004 C 17.459,0.205 17.614,0.19 17.767,0.17 17.664,0.183 17.559,0.195 17.456,0.209 17.608,0.188 17.762,0.17 17.915,0.152 L 17.767,0.17 C 17.944,0.148 18.12,0.122 18.299,0.104 H 18.3 l 0.021,-0.002 h 10e-4 c 0.151,-0.015 0.304,-0.023 0.456,-0.034 l -0.149,0.011 c 0.126,-0.01 0.251,-0.02 0.377,-0.028 l -0.228,0.017 c 0.183,-0.014 0.365,-0.032 0.549,-0.041 h 10e-4 L 19.351,0.026 C 19.503,0.018 19.657,0.018 19.809,0.014 L 19.724,0.016 C 19.947,0.009 20.169,0 20.394,0 c 11.264,0 20.394,8.951 20.394,19.995 0,0.335 -0.01,0.668 -0.026,0.999 -0.006,0.11 -0.017,0.219 -0.024,0.328 z M 8.562,26.391 c 0.155,0.67 0.838,1.09 1.522,0.936 7.098,-1.59 13.126,-0.942 17.914,1.928 0.6,0.358 1.383,0.175 1.749,-0.416 A 1.231,1.231 0 0 0 29.325,27.127 C 23.949,23.905 17.286,23.156 9.517,24.898 A 1.242,1.242 0 0 0 8.562,26.391 Z M 7.748,20.54 c 0.256,0.823 1.144,1.287 1.986,1.038 6.484,-1.929 14.841,-0.973 20.321,2.331 0.747,0.449 1.727,0.22 2.187,-0.512 A 1.543,1.543 0 0 0 31.72,21.252 C 25.424,17.461 16.217,16.392 8.809,18.596 A 1.557,1.557 0 0 0 7.748,20.54 Z M 34.404,14.509 C 26.836,10.104 14.861,9.69 7.657,11.835 a 1.864,1.864 0 0 0 -1.271,2.332 c 0.305,0.989 1.37,1.546 2.379,1.246 6.275,-1.867 17.118,-1.514 23.693,2.313 a 1.927,1.927 0 0 0 2.613,-0.655 1.848,1.848 0 0 0 -0.667,-2.562 z M 20.34,0 h 0.054 C 20.198,0 20.004,0.009 19.809,0.014 19.986,0.009 20.162,0 20.34,0 Z"/></symbol>
+
+  <symbol id="icon-toyhouse" viewBox="0 0 197 190">
+    <!-- Via: https://logos.fandom.com/wiki/File:Toyhouse.svg -->
+    <path d="M 92.377573 54.858845 L -6.2923729 154.22229 L 25.027679 154.05951 L 25.027679 245.16092 L 159.06549 245.16092 L 159.06549 153.9484 L 191.43458 153.9484 L 152.83642 115.35024 L 152.83642 69.076582 L 121.69051 69.076582 L 121.69051 84.093236 L 92.377573 54.858845 z M 108.84479 170.6156 L 108.62206 196.7153 L 135.0499 196.7153 L 135.0499 170.99543 L 144.0168 170.99543 L 144.0168 232.6604 L 134.73519 232.6604 L 134.73519 203.89264 L 108.70061 203.89264 L 108.70061 233.01438 L 99.73423 233.01438 L 99.73423 181.1023 C 99.73423 178.74267 99.733714 178.97633 99.733714 178.97633 L 81.812844 178.97633 L 81.812844 232.98079 L 70.800596 232.98079 L 70.800596 179.03214 L 54.338035 179.03214 L 54.338035 170.63369 L 108.84479 170.6156 z " transform="translate(6.2923729,-54.858845)"/>
+  </symbol>
+
+  <symbol id="icon-internetArchive" viewBox="0 0 27 30"><path d="M 26.666667,28.604651 V 30 H 0 l 2.8368794e-4,-1.395349 z m -1.052632,-2.093023 v 1.744186 H 1.0526316 V 26.511628 Z M 3.624692,7.6744186 3.9174691,7.8215328 4.0639977,10.173954 4.2105263,13.996393 v 3.676169 L 4.0639977,22.255044 4.039623,25.342193 3.624692,25.465116 H 2.1602464 L 1.7209407,25.342193 1.5503175,22.255044 1.4035088,17.697034 V 14.021147 L 1.5503175,10.173954 1.6842385,7.8088748 1.9896232,7.6744186 Z m 21.052795,0 0.293116,0.1471142 0.146277,2.3524212 0.146278,3.822439 v 3.676169 l -0.146278,4.582482 -0.0241,3.087149 -0.415294,0.122923 h -1.464458 l -0.439393,-0.122923 -0.171218,-3.087149 -0.146278,-4.55801 v -3.675887 l 0.146278,-3.847193 0.134508,-2.3650792 0.305166,-0.1344562 z m -14.737064,0 0.292806,0.1471142 0.146544,2.3524212 0.146543,3.822439 v 3.676169 l -0.146543,4.582482 -0.0241,3.087149 -0.415253,0.122923 H 8.4758312 L 8.0362015,25.342193 7.8655613,22.255044 7.7192982,17.697034 V 14.021147 L 7.8655613,10.173954 8.000056,7.8088748 8.3049108,7.6744186 Z m 8.070175,0 0.292807,0.1471142 0.146543,2.3524212 0.146543,3.822439 v 3.676169 l -0.146543,4.582482 -0.0241,3.087149 -0.415253,0.122923 h -1.464591 l -0.43935,-0.122923 -0.17092,-3.087149 -0.146263,-4.55801 v -3.675887 l 0.146263,-3.847193 0.134494,-2.3650792 0.305135,-0.1344562 z M 25.614035,4.5348837 V 6.9767442 H 1.0526316 V 4.5348837 Z M 13.080676,0 25.964912,2.9333134 25.448414,3.8372093 H 0.77192525 L 0,3.1041615 Z"/></symbol>
+
+  <symbol id="icon-newgrounds" viewbox="0 0 40">
+    <!-- Thanks for clicking a convert button for the greater good, @PeterShaggyNoble! https://github.com/simple-icons/simple-icons/issues/1974#issuecomment-637606360 -->
+    <path d="M 21.67012,2.5595806 C 21.281556,2.6425058 21.182046,2.7041073 20.355164,3.3769857 20.042418,3.6304997 19.575668,4.0072168 19.319784,4.2109758 18.793802,4.6327094 18.656383,4.7724975 18.535549,5.0165344 L 18.452624,5.1894925 17.308257,5.9926819 C 16.678026,6.4357391 16.092811,6.8479955 16.005148,6.9095971 L 15.848774,7.0209537 15.69714,6.9190742 15.545505,6.8148255 13.022211,6.4428469 C 11.633807,6.239088 10.475225,6.0661298 10.449162,6.059022 10.408885,6.0519141 10.378084,5.9997897 10.321221,5.8434166 10.226449,5.585164 10.001367,5.1468453 9.9326572,5.0852438 9.8639478,5.0212729 9.3237497,4.7014188 9.2858411,4.7014188 9.2479324,4.7014188 9.2479324,4.7037881 9.2811025,4.6066472 9.3024261,4.5474149 9.3355962,4.5189835 9.4327371,4.4763363 9.5725252,4.4123654 9.5843716,4.3886725 9.6341267,4.0190633 9.6578196,3.8484744 9.6554503,3.8129351 9.6246495,3.76318 9.5938488,3.7181635 9.5891102,3.6684084 9.6009566,3.5072966 L 9.6175417,3.305907 9.5322472,3.2656291 C 9.4777536,3.2395669 9.4351063,3.1945504 9.4066749,3.1329488 9.3403347,2.9836836 9.1365758,2.7917711 8.9541405,2.7041073 8.7977673,2.6306593 8.7835516,2.6282901 8.4850211,2.6282901 8.2007063,2.6282901 8.1675362,2.6330286 8.0395945,2.6922609 7.7837112,2.8130947 7.5491515,3.0926709 7.5112429,3.3201227 7.4993964,3.3983093 7.4828114,3.4243715 7.430687,3.4456951 7.2885296,3.5049274 7.2766831,3.5404667 7.2766831,3.8792752 7.2766831,4.2346687 7.2885296,4.2725773 7.430687,4.3389174 7.4828114,4.3626103 7.5254586,4.3934111 7.5254586,4.4099961 7.5254586,4.4242119 7.5301972,4.4526434 7.5373051,4.4692284 7.5467822,4.4952906 7.4614878,4.5284606 7.1724344,4.6042779 6.9639369,4.6611409 6.7649165,4.7251117 6.7270079,4.7488046 6.6748835,4.7796054 6.6346056,4.8506841 6.5493111,5.071028 6.3858301,5.4785459 6.3052742,5.7770765 6.246042,6.1917022 6.1702247,6.712946 6.2010255,6.9356593 6.3905687,7.2223434 6.4308466,7.2839449 6.4616474,7.3360693 6.4569088,7.3408079 6.4521702,7.3455464 6.2081334,7.5113967 5.9143414,7.7104171 5.2106623,8.1890137 4.0354944,9.0680203 3.9738929,9.1627919 3.8909677,9.2907335 3.7535489,11.00373 3.7203789,12.370811 L 3.7037938,12.993934 3.3104917,13.344589 C 2.7157999,13.870571 2.2229876,14.344429 1.5548478,15.022046 0.79193642,15.796804 0.78008997,15.81102 0.73744275,16.017148 0.69716482,16.21143 0.64504044,16.801383 0.65925618,16.924586 0.66873334,16.998034 0.64977902,17.038312 0.51946807,17.227855 0.05982581,17.888887 -0.09180875,18.675491 0.05271794,19.620838 0.19013676,20.51406 0.77772068,21.321988 1.3842589,21.44993 1.4742919,21.468884 3.966785,21.475992 10.354391,21.475992 H 19.196581 L 19.362431,21.378851 C 19.748626,21.146661 20.293562,20.663326 20.646587,20.241592 21.184415,19.597145 21.698551,18.587827 21.961543,17.661435 22.205579,16.803752 22.309828,16.038471 22.321675,14.995984 L 22.331152,14.379968 22.66996,14.135932 C 23.07274,13.844509 23.108279,13.813708 23.127233,13.728414 23.148557,13.626534 23.10354,13.493854 22.913997,13.095813 22.473309,12.174159 21.947327,11.607899 21.288664,11.342539 20.926163,11.195643 19.736779,11.032162 18.739308,10.989514 18.29862,10.97056 18.33179,10.994253 18.327052,10.695723 18.324682,10.463532 18.286774,10.321375 18.210956,10.266881 18.168309,10.23608 18.056953,10.212387 17.83187,10.183956 17.656543,10.162632 17.509647,10.143678 17.504908,10.141309 17.502539,10.13657 17.488323,10.044168 17.476477,9.9351804 17.443307,9.6698199 17.348535,9.2788871 17.237178,8.9448172 17.185054,8.7955519 17.144776,8.6699796 17.144776,8.6676103 17.144776,8.6628717 17.258502,8.5989009 17.400659,8.5254529 17.540447,8.4496356 18.128031,8.13452 18.706138,7.824143 L 19.760472,7.260252 H 19.959493 C 20.06848,7.260252 20.222484,7.243667 20.30304,7.2223434 20.419135,7.1915426 21.146507,6.8906428 22.890304,6.1514243 23.018246,6.0969306 23.243328,5.9476653 23.394963,5.8197237 23.641369,5.6088569 23.859344,5.2558327 23.949377,4.9170242 24.010978,4.6824645 24.018086,4.2180836 23.961223,3.9858932 23.781157,3.2466747 23.129603,2.6496137 22.395123,2.5477342 22.151086,2.5121948 21.859663,2.5169334 21.67012,2.5595806 Z M 22.357214,2.9268206 C 22.786055,2.99553 23.179358,3.2703676 23.418656,3.6636698 23.631892,4.0214326 23.688755,4.5331992 23.553705,4.9146549 23.40444,5.3387578 23.006399,5.7273214 22.594143,5.8505245 22.414077,5.9050181 22.018406,5.9239724 21.831232,5.8884331 21.317096,5.7865536 20.838499,5.3671893 20.672649,4.874377 20.644217,4.7843439 20.615786,4.6232322 20.606309,4.490552 L 20.592093,4.2631001 20.53523,4.4052576 C 20.504429,4.4834441 20.471259,4.6137551 20.464151,4.6966802 L 20.449936,4.8435762 19.620684,5.4145751 18.791433,5.9832047 18.772478,5.8813252 C 18.717985,5.5899026 18.810387,5.2202933 18.99993,4.9928415 19.158672,4.800929 20.33621,3.8105658 21.018565,3.2964298 21.475838,2.9481442 21.854925,2.8438954 22.357214,2.9268206 Z M 8.9778334,3.2822141 C 9.2147624,3.3177534 9.4303678,3.3746164 9.4493221,3.4054172 9.4801229,3.452803 9.4706457,3.6447155 9.4303678,3.7750264 9.3948284,3.8982295 9.3948284,3.8982295 9.3071647,3.8911216 9.2289781,3.8840138 9.2171317,3.8745366 9.1815923,3.7773957 9.1436837,3.6778855 9.1342065,3.6684084 9.032327,3.649454 8.8878004,3.6233919 8.8238295,3.6423462 8.7717051,3.7300099 8.7480122,3.7702878 8.7029957,3.8129351 8.6698257,3.8271508 8.6034856,3.8508437 8.3404944,3.8555823 8.2078141,3.8342587 8.1296276,3.8200429 8.1154118,3.8081965 8.0940882,3.7323792 8.0822418,3.6849934 8.0703953,3.5760061 8.0703953,3.4883423 8.0703953,3.2585212 8.0751339,3.2561519 8.4755439,3.2561519 8.6579792,3.2561519 8.8854311,3.2679984 8.9778334,3.2822141 Z M 9.0773436,3.943246 C 9.0962979,4.000109 9.1270986,4.0380176 9.179223,4.0593412 9.269256,4.0996191 9.269256,4.1043577 9.2052852,4.310486 9.1602687,4.4550126 9.1484223,4.4715977 9.0583892,4.5142449 8.925709,4.5734771 8.7574894,4.5711079 8.6129627,4.5095063 8.4755439,4.4502741 8.4281581,4.3768261 8.4092038,4.1920215 L 8.394988,4.0688184 8.5466226,4.0522333 C 8.7385351,4.0285404 8.8309374,3.9906318 8.8735846,3.9100759 8.9020161,3.8603209 8.9233397,3.8484744 8.9802027,3.853213 9.0370656,3.8579516 9.0560199,3.8769059 9.0773436,3.943246 Z M 7.8287277,5.0497044 C 8.3120629,5.5306703 9.0844514,5.6207033 9.326119,5.2274012 9.3971977,5.111306 9.4019363,5.1089367 9.4753843,5.1302603 9.6791432,5.1871233 9.6815125,5.1894925 9.6815125,5.3624507 9.6815125,5.5093467 9.7123133,5.7794458 9.743114,5.8979103 9.7549605,5.9500346 9.7502219,5.9500346 9.4753843,5.9642504 9.2976875,5.9737275 9.1318372,5.9974204 9.0181113,6.0305905 8.9209704,6.059022 8.5466226,6.2248723 8.1912291,6.3978304 7.6747239,6.6489752 7.5349358,6.7105767 7.5136121,6.6868838 7.4662263,6.6347594 7.210343,6.2153951 7.2198202,6.2059179 7.2269281,6.2011794 7.2885296,6.2343494 7.3596083,6.2793659 L 7.4899192,6.3622911 7.4970271,6.1419471 C 7.5088736,5.8647402 7.4685956,5.5140853 7.3927784,5.2250319 7.3619776,5.1018288 7.340654,4.9952108 7.3477618,4.9881029 7.357239,4.980995 7.4330563,4.9620407 7.52072,4.947825 7.6083837,4.9312399 7.6818317,4.9193935 7.6865703,4.9170242 7.6889396,4.9170242 7.7529104,4.9762564 7.8287277,5.0497044 Z M 18.542657,5.7249521 C 18.542657,5.895541 18.59952,6.1419471 18.668229,6.2864738 L 18.722723,6.4049383 16.448205,7.790973 C 15.19485,8.5515151 14.16184,9.1675305 14.152363,9.155684 14.140516,9.1462068 14.126301,9.0727589 14.119193,8.992203 14.102608,8.8216141 14.149993,8.6747181 14.251873,8.5775772 14.320582,8.5112371 18.495271,5.6064876 18.526072,5.6041183 18.535549,5.601749 18.542657,5.658612 18.542657,5.7249521 Z M 12.595739,6.7982405 C 14.100238,7.0138458 15.353593,7.1939119 15.379655,7.2010198 15.405717,7.2057583 15.445995,7.2247127 15.469688,7.2412977 15.507597,7.2697292 15.476796,7.2981606 15.166419,7.5161353 L 14.822872,7.7601722 13.25914,7.5445668 C 12.273516,7.4095173 11.662239,7.3360693 11.610115,7.3455464 11.517712,7.3621315 11.328169,7.480596 11.306845,7.5350896 11.299738,7.554044 11.3258,7.6914628 11.363708,7.8407281 11.456111,8.1984908 11.52482,8.6841953 11.543774,9.1130368 11.560359,9.4471067 11.55799,9.4636917 11.515343,9.4636917 11.437156,9.4636917 10.195648,9.2978414 10.183802,9.2883642 10.179063,9.2812564 10.160109,9.1296218 10.141155,8.9519251 10.079553,8.3193246 9.9326572,7.6914628 9.6886204,7.0019994 9.6175417,6.7982405 9.5535708,6.6015894 9.546463,6.56605 9.5275087,6.4618013 9.5914795,6.4073076 9.7383755,6.4073076 9.8047156,6.4073076 11.09124,6.5826351 12.595739,6.7982405 Z M 13.247294,7.8881139 C 13.870417,7.9734083 14.384553,8.0468563 14.389292,8.0515949 14.39403,8.0563334 14.292151,8.1321507 14.164209,8.2198145 14.02916,8.3122168 13.915434,8.4046191 13.898849,8.4425277 13.839616,8.5681001 13.711675,8.9400786 13.602687,9.2978414 13.541086,9.4992311 13.484223,9.6721892 13.474746,9.6816664 13.467638,9.6911435 13.164369,9.6650814 12.801867,9.6248034 12.230869,9.5584633 12.145574,9.5442476 12.15979,9.5134468 12.169267,9.4921232 12.211914,9.3736587 12.256931,9.2480863 12.42515,8.7576433 12.380134,8.3027396 12.121881,7.9023296 12.069757,7.8217737 12.02711,7.750695 12.02711,7.7459565 12.02711,7.7246328 12.117143,7.73411 13.247294,7.8881139 Z M 16.846245,9.0111573 C 16.933909,9.2338706 17.040527,9.6698199 17.073697,9.9328111 17.087913,10.053645 17.09739,10.157894 17.092652,10.160263 17.085544,10.16974 16.405558,10.086815 16.322632,10.067861 16.272877,10.056014 16.272877,10.053645 16.310786,9.9446576 16.339217,9.8617324 16.348695,9.7290522 16.351064,9.4423681 L 16.353433,9.0561738 16.542976,8.9519251 C 16.644856,8.8950621 16.741997,8.8476763 16.758582,8.8476763 16.772797,8.8476763 16.813075,8.9211243 16.846245,9.0111573 Z M 15.983824,9.6224341 C 15.971977,9.7290522 15.950654,9.8617324 15.934069,9.9138568 15.910376,9.9920434 15.89616,10.008628 15.844036,10.008628 15.737418,10.006259 14.938967,9.8949025 14.922382,9.8783175 14.884473,9.8427781 14.986353,9.776438 15.46258,9.5252932 L 15.971977,9.2575635 15.988562,9.3428579 C 15.99804,9.3902437 15.99567,9.5158161 15.983824,9.6224341 Z M 14.06233,10.162632 C 16.152043,10.416146 17.872148,10.624644 17.883994,10.624644 17.893472,10.624644 17.902949,10.693353 17.902949,10.778648 V 10.932652 H 17.635219 C 17.29878,10.932652 17.23007,10.965822 17.038158,11.209858 16.943386,11.333062 16.888893,11.382817 16.853353,11.382817 16.824922,11.382817 16.052533,11.30463 15.140357,11.207489 L 13.477115,11.034531 13.339696,10.911328 C 12.915593,10.532241 12.406196,10.297682 11.768857,10.186325 11.337646,10.112877 10.366237,10.008628 10.103246,10.008628 10.065337,10.008628 10.060599,9.9849355 10.060599,9.8404088 V 9.6745585 L 10.162478,9.6887743 C 10.216972,9.6958821 11.972616,9.9091182 14.06233,10.162632 Z M 10.084292,10.420885 C 11.261829,10.501441 11.59116,10.541719 12.055541,10.660183 12.380134,10.740739 12.522291,10.802341 12.716573,10.93739 13.081444,11.190904 13.446314,11.6387 13.66192,12.098342 13.749583,12.285516 13.853832,12.5722 13.841986,12.584047 13.82777,12.600632 10.003736,12.259454 9.9895202,12.242869 9.980043,12.233392 9.9445037,12.13862 9.9065951,12.034371 9.7146826,11.479957 9.3877205,11.046377 9.015742,10.849726 8.8190909,10.747847 8.8214602,10.747847 8.378403,10.890004 7.8026655,11.077178 6.956829,11.43968 6.1749633,11.835351 5.8172005,12.015417 5.6134416,12.122035 4.8718538,12.522445 4.7462814,12.591155 4.7439121,12.591155 4.8126215,12.531922 4.9002853,12.456105 5.5636865,12.008309 5.8669556,11.816397 6.776763,11.247767 7.7766033,10.762063 8.6508714,10.465901 L 8.9588791,10.361653 9.2550403,10.373499 C 9.4185213,10.380607 9.7904998,10.401931 10.084292,10.420885 Z M 18.779586,11.382817 C 19.575668,11.425464 20.70345,11.574729 21.042258,11.683716 21.478207,11.823505 21.883356,12.157574 22.212687,12.648017 22.33589,12.832822 22.577558,13.256925 22.615467,13.356435 22.63916,13.415667 22.63679,13.420406 22.575189,13.420406 22.489894,13.420406 19.556713,13.13846 19.551975,13.131353 19.549605,13.126614 19.523543,13.029473 19.495112,12.911009 19.334,12.266562 18.893312,11.745318 18.251234,11.43968 18.151724,11.392294 18.068799,11.352016 18.068799,11.347277 18.068799,11.344908 18.118554,11.344908 18.182525,11.349647 18.244127,11.354385 18.511856,11.368601 18.779586,11.382817 Z M 15.384394,11.643439 C 16.990772,11.797442 17.050004,11.80692 17.424352,11.991724 17.886364,12.216807 18.234649,12.541399 18.409977,12.90627 18.549765,13.197693 18.594781,13.403821 18.608997,13.832662 L 18.620844,14.185687 18.516595,14.171471 C 18.459732,14.164363 17.33195,14.060114 16.014625,13.939281 L 13.614534,13.718937 V 13.562563 13.40619 L 13.946235,13.091075 C 14.247134,12.80676 14.277935,12.768851 14.277935,12.695403 14.277935,12.517707 14.031529,11.875629 13.846724,11.577098 L 13.799339,11.501281 H 13.858571 C 13.891741,11.501281 14.578835,11.565252 15.384394,11.643439 Z M 9.0394349,12.8731 C 9.4114134,13.598103 9.5132929,14.531603 9.3237497,15.469842 9.1531608,16.31094 8.6840414,17.092805 8.124889,17.462415 7.8239891,17.663804 7.6486617,17.718298 7.3003761,17.720667 7.0658163,17.720667 6.9899991,17.71119 6.8596881,17.666174 6.5469418,17.559556 6.2531499,17.313149 6.0185902,16.960125 5.7011053,16.483898 5.5399936,15.960285 5.4997157,15.27556 L 5.4831306,15.019677 5.6205494,14.924905 C 5.6963667,14.872781 5.7674454,14.825395 5.7769226,14.823026 5.7863997,14.818287 5.7958769,14.927274 5.7958769,15.062324 5.7958769,15.199743 5.8029848,15.327684 5.8100926,15.349008 5.8219391,15.379809 5.8645863,15.386917 6.0209595,15.386917 6.1275775,15.386917 6.2649963,15.394025 6.3242286,15.401132 L 6.4308466,15.415348 6.4687553,15.574091 C 6.4877096,15.659385 6.5137718,15.756526 6.5208796,15.787327 L 6.5374647,15.84419 6.2270877,15.832343 C 5.878802,15.818127 5.8906485,15.81102 5.9427729,15.981608 L 5.9688351,16.074011 H 6.2957971 6.6227591 L 6.7080536,16.24223 C 6.8691653,16.564454 7.0895092,16.820337 7.3216997,16.950648 L 7.4377949,17.019358 7.3453926,17.038312 C 7.210343,17.066743 6.8644267,17.059635 6.7672858,17.026465 6.6914685,17.000403 6.6890992,17.002772 6.7293772,17.033573 6.8170409,17.099913 7.0397541,17.170992 7.2056045,17.180469 7.3998862,17.194685 7.684201,17.128345 7.8524206,17.031204 7.994578,16.948279 8.2386149,16.713719 8.3760337,16.528914 8.9138625,15.801542 9.1128829,14.64059 8.8617382,13.702352 8.8048752,13.491485 8.6485021,13.133722 8.5466226,12.979718 L 8.4968675,12.90627 8.6816721,12.77359 C 8.7811823,12.700142 8.8759539,12.64091 8.8925389,12.63854 8.9067547,12.63854 8.9730948,12.745158 9.0394349,12.8731 Z M 11.778334,13.396713 C 12.581523,13.448837 13.25914,13.491485 13.285203,13.491485 13.32785,13.491485 13.330219,13.512808 13.330219,13.977189 V 14.465263 L 13.218862,14.451047 C 13.154892,14.443939 12.941656,14.427354 12.740266,14.413139 12.446474,14.391815 12.373026,14.394184 12.356441,14.417877 12.346964,14.434462 12.330379,14.526864 12.320902,14.624005 12.309055,14.759055 12.313794,14.806441 12.337487,14.830134 12.365918,14.853826 13.022211,14.913059 13.268618,14.913059 13.311265,14.913059 13.313634,14.927274 13.299418,15.202112 13.282833,15.47695 13.1928,16.287247 13.173846,16.308571 13.169107,16.313309 12.955871,16.303832 12.699988,16.289616 12.318532,16.265923 12.22613,16.265923 12.202437,16.291985 12.185852,16.308571 12.157421,16.400973 12.140836,16.493375 12.117143,16.630794 12.117143,16.671072 12.140836,16.694765 12.162159,16.716088 12.323271,16.735043 12.619432,16.751628 12.868208,16.765843 13.074336,16.782429 13.079074,16.787167 13.098029,16.806121 12.92744,17.483738 12.818452,17.820178 L 12.707096,18.158986 12.562569,18.156617 C 12.484383,18.156617 12.264039,18.14714 12.074495,18.135293 11.820981,18.121077 11.719102,18.123447 11.688301,18.142401 11.633807,18.17794 11.503497,18.47884 11.520082,18.526226 11.539036,18.573612 11.603007,18.58072 12.081603,18.599674 12.311424,18.606782 12.500968,18.623367 12.500968,18.632844 12.500968,18.687338 12.140836,19.355477 11.984462,19.592406 L 11.799658,19.871983 11.25709,19.867244 10.712154,19.862506 10.593689,20.009401 C 10.363868,20.293716 10.370976,20.312671 10.686091,20.317409 10.804556,20.319778 11.008315,20.326886 11.136257,20.336364 L 11.373186,20.350579 11.200227,20.499845 C 10.998838,20.672803 10.759539,20.836284 10.508395,20.976072 L 10.328329,21.073213 8.6911493,21.068474 7.0516006,21.061366 7.2624674,20.900255 C 8.4708053,19.983339 9.4635378,18.17794 9.8686864,16.156936 9.9989974,15.50775 10.053491,14.995984 10.08903,14.053006 L 10.117462,13.301942 H 10.216972 C 10.273835,13.301942 10.975145,13.344589 11.778334,13.396713 Z M 20.608678,14.216487 21.880987,14.330213 21.892833,14.424985 C 21.899941,14.479479 21.909418,14.631113 21.916526,14.763793 L 21.926003,15.00783 H 21.817016 C 21.755414,15.00783 21.556394,14.995984 21.376328,14.981768 21.110967,14.962814 21.039889,14.962814 21.018565,14.988876 20.973549,15.036262 20.952225,15.351377 20.990134,15.384547 21.006719,15.396394 21.222324,15.422456 21.46873,15.439041 L 21.914157,15.467473 21.897572,15.735202 C 21.883356,16.005301 21.833601,16.419927 21.783846,16.671072 L 21.757784,16.808491 H 21.589564 C 21.497162,16.806121 21.279187,16.796644 21.10386,16.784798 20.928532,16.775321 20.772159,16.770582 20.755574,16.77769 20.715296,16.791906 20.630002,17.118868 20.653694,17.166253 20.672649,17.206531 20.722404,17.21127 21.381067,17.249179 21.584825,17.261025 21.646427,17.272872 21.646427,17.298934 21.646427,17.355797 21.373959,18.161355 21.298141,18.324836 L 21.229432,18.47884 20.933271,18.462255 C 20.772159,18.455147 20.53523,18.44567 20.407288,18.44567 L 20.177467,18.443301 20.089803,18.618628 C 20.042418,18.7134 20.011617,18.808171 20.018725,18.829495 20.032941,18.867404 20.115866,18.87925 20.70345,18.910051 L 21.025673,18.929005 20.859823,19.21332 C 20.76979,19.369693 20.613417,19.61373 20.513906,19.755887 L 20.331471,20.011771 19.786534,20.009401 19.241598,20.007032 19.104179,20.177621 C 19.028362,20.270023 18.969129,20.362426 18.973868,20.383749 18.980976,20.41455 19.040208,20.424027 19.348216,20.438243 19.549605,20.44772 19.758103,20.459567 19.807858,20.461936 L 19.90263,20.469044 19.70124,20.637263 C 19.592253,20.732035 19.419294,20.867084 19.315046,20.940532 L 19.130241,21.073213 H 17.829501 16.53113 L 16.941017,20.663326 C 17.376966,20.229745 17.639958,19.902783 17.917165,19.44551 18.53318,18.433824 18.969129,17.116498 19.144457,15.74231 19.196581,15.334792 19.236859,14.671391 19.227382,14.358645 L 19.217905,14.081438 19.277137,14.093284 C 19.310307,14.100392 19.909737,14.154886 20.608678,14.216487 Z M 4.9997955,16.074011 C 5.0211191,16.168782 5.0329655,16.254077 5.0282269,16.261185 5.0187498,16.2754 4.7249578,16.47679 3.6895781,17.1781 L 3.2654752,17.464784 3.1446414,17.303672 C 3.0783013,17.213639 2.9740525,17.102283 2.9100817,17.052528 2.8484801,17.002772 2.7963558,16.950648 2.7939865,16.938802 2.7916172,16.924586 3.2725831,16.5763 3.860167,16.164044 L 4.931086,15.412979 4.9476711,15.657016 C 4.9571482,15.792065 4.9808411,15.979239 4.9997955,16.074011 Z M 2.5309953,17.481369 C 2.6589369,17.642481 2.822418,17.983659 2.8840195,18.218218 2.985899,18.618628 2.9645753,19.237013 2.8318951,19.684809 2.6897377,20.161036 2.3296056,20.639633 1.9884279,20.803114 1.8770712,20.855238 1.8083618,20.871823 1.6377729,20.878931 1.5193084,20.883669 1.3795203,20.876562 1.3250267,20.862346 1.1710228,20.819699 0.95304814,20.660956 0.82984506,20.497475 0.59054677,20.17999 0.45075866,19.78195 0.42469647,19.343631 0.41285002,19.125656 0.44602008,18.637583 0.47919014,18.554657 0.4886673,18.526226 0.68294908,18.393546 0.69479553,18.405392 0.69953411,18.410131 0.68768766,18.481209 0.66636405,18.564135 0.62608612,18.737093 0.59291606,19.234644 0.61660896,19.329415 0.6308247,19.388648 0.63556328,19.391017 0.82747577,19.391017 H 1.0217575 L 1.0596662,19.533174 1.0975748,19.675332 H 0.88670802 C 0.6782105,19.675332 0.67584121,19.675332 0.69005695,19.727456 0.71611914,19.81275 0.73033488,19.817489 0.94594027,19.834074 L 1.1520685,19.84829 1.2255165,19.98097 C 1.3084416,20.125497 1.5358935,20.376641 1.6046029,20.397965 1.6306651,20.407442 1.6496194,20.421658 1.6496194,20.433504 1.6496194,20.466674 1.3937361,20.487998 1.2942259,20.461936 1.1615457,20.428766 1.1994543,20.469044 1.343981,20.51643 1.5027234,20.570923 1.7467603,20.554338 1.8960256,20.483259 2.0310751,20.416919 2.2419419,20.208422 2.3627757,20.025987 2.7252771,19.462095 2.7797707,18.58072 2.4859788,17.971812 2.4054229,17.803592 2.1874482,17.550078 2.0666144,17.483738 2.0287058,17.462415 2.038183,17.448199 2.161386,17.360535 L 2.3011741,17.263394 2.3675143,17.31078 C 2.4054229,17.336842 2.4788709,17.41266 2.5309953,17.481369 Z M 4.7154807,17.502693 4.8268373,17.640111 4.4785517,17.805962 C 4.2866392,17.895995 4.1255274,17.967074 4.1231582,17.962335 4.0994653,17.941011 4.0686645,17.696974 4.0852495,17.680389 4.1089424,17.656697 4.5970162,17.355797 4.6017547,17.360535 4.604124,17.362905 4.6538791,17.426875 4.7154807,17.502693 Z M 6.6867299,18.296405 C 6.8762731,18.393546 7.0563392,18.670753 7.136895,18.988237 7.1961273,19.21332 7.1890194,19.630315 7.1250486,19.900414 7.0113227,20.376641 6.7222693,20.798375 6.3976766,20.957117 6.262627,21.023458 6.2318263,21.030565 6.0612374,21.023458 5.8148312,21.011611 5.6868896,20.935794 5.5328857,20.710711 5.3575582,20.457197 5.3101725,20.277131 5.3101725,19.864875 5.3101725,19.44551 5.3528197,19.239382 5.4997157,18.94559 5.682151,18.585458 5.9143414,18.348529 6.172594,18.265604 6.3218593,18.218218 6.5611576,18.232434 6.6867299,18.296405 Z M 4.4003651,18.827126 C 4.6491405,18.966914 4.8007751,19.31283 4.8007751,19.739302 4.8007751,20.172882 4.6799413,20.533015 4.440643,20.798375 4.1658054,21.106383 3.7819804,21.139553 3.54979,20.871823 3.3744625,20.675172 3.2844295,20.395596 3.2844295,20.063895 3.2844295,19.616099 3.4171097,19.260706 3.6872088,18.983499 3.860167,18.808171 3.9241378,18.77974 4.1586975,18.777371 4.2724234,18.775001 4.3316557,18.786848 4.4003651,18.827126 Z"/>
+  </symbol>
+
+  <symbol id="icon-soundcloud" viewBox="0 0 40 40"><path d="M13.8,27.4l0.3-4.2L13.8,14c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1C13.1,13.8,13,13.9,13,14 l-0.2,9.2l0.2,4.2c0,0.1,0.1,0.2,0.1,0.3c0.1,0.1,0.2,0.1,0.3,0.1C13.7,27.8,13.8,27.7,13.8,27.4z M18.8,26.9l0.2-3.7l-0.2-10.3 c0-0.2-0.1-0.3-0.2-0.4c-0.1-0.1-0.2-0.1-0.3-0.1s-0.2,0-0.3,0.1c-0.1,0.1-0.2,0.2-0.2,0.4l0,0.1l-0.2,10.1c0,0,0.1,1.4,0.2,4.1v0 c0,0.1,0,0.2,0.1,0.3c0.1,0.1,0.2,0.2,0.4,0.2c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.2-0.2,0.2-0.4L18.8,26.9z M1.2,20.9l0.3,2.2 l-0.3,2.2c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.1-0.1-0.2-0.2l-0.3-2.2l0.3-2.2c0-0.1,0.1-0.2,0.2-0.2S1.2,20.8,1.2,20.9z M2.7,19.5 l0.4,3.6l-0.4,3.6c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L2,23.2l0.4-3.6c0-0.1,0.1-0.2,0.2-0.2C2.6,19.4,2.7,19.4,2.7,19.5z M4.2,18.9l0.4,4.3l-0.4,4.2c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2l-0.4-4.2l0.4-4.3c0-0.1,0.1-0.2,0.2-0.2 C4.2,18.7,4.2,18.7,4.2,18.9z M5.8,18.8l0.4,4.4l-0.4,4.3c0,0.2-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L5,23.2l0.4-4.4 c0-0.2,0.1-0.2,0.2-0.2C5.7,18.5,5.8,18.6,5.8,18.8z M7.4,19.1l0.4,4.1l-0.4,4.3c0,0.2-0.1,0.3-0.3,0.3c-0.1,0-0.1,0-0.2-0.1 c-0.1-0.1-0.1-0.1-0.1-0.2l-0.3-4.3l0.3-4.1c0-0.1,0-0.1,0.1-0.2C7,18.8,7,18.8,7.1,18.8C7.3,18.8,7.4,18.9,7.4,19.1L7.4,19.1z M9,16.5l0.4,6.7L9,27.5c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.1,0.1-0.2,0.1c-0.2,0-0.3-0.1-0.3-0.3l-0.3-4.3l0.3-6.7 c0-0.2,0.1-0.3,0.3-0.3c0.1,0,0.1,0,0.2,0.1S9,16.4,9,16.5z M10.5,15l0.3,8.2l-0.3,4.3c0,0.1,0,0.2-0.1,0.2 c-0.1,0.1-0.1,0.1-0.2,0.1c-0.2,0-0.3-0.1-0.3-0.3l-0.3-4.3L9.9,15c0-0.2,0.1-0.3,0.3-0.3c0.1,0,0.2,0,0.2,0.1 C10.5,14.8,10.5,14.9,10.5,15z M12.2,14.3l0.3,8.9l-0.3,4.2c0,0.2-0.1,0.4-0.4,0.4c-0.2,0-0.3-0.1-0.4-0.4l-0.3-4.2l0.3-8.9 c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0.1C12.1,14.1,12.2,14.2,12.2,14.3z M18.8,27.3L18.8,27.3L18.8,27.3z M15.4,14.2l0.3,8.9l-0.3,4.2c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.1-0.3,0.1c-0.1,0-0.2,0-0.3-0.1s-0.1-0.2-0.1-0.3l-0.2-4.2 l0.2-8.9c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1C15.4,14,15.4,14.1,15.4,14.2L15.4,14.2z M17.1,14.6 l0.2,8.6l-0.2,4.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.1-0.3,0.1c-0.1,0-0.2,0-0.3-0.1c-0.1-0.1-0.1-0.2-0.2-0.3L16,23.2l0.2-8.6 c0-0.1,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1C17.1,14.3,17.1,14.4,17.1,14.6z M20.7,23.2l-0.2,4 c0,0.2-0.1,0.3-0.2,0.4c-0.1,0.1-0.2,0.2-0.4,0.2c-0.1,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.2-0.2-0.4l-0.1-2l-0.1-2L19.4,12V12 c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1c0.2,0.1,0.2,0.2,0.3,0.5L20.7,23.2z M39.4,22.9 c0,1.4-0.5,2.5-1.4,3.5c-0.9,1-2,1.4-3.4,1.4H21.4c-0.1,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.2-0.2-0.4V11.5c0-0.3,0.2-0.5,0.5-0.6 c1-0.4,2-0.6,3-0.6c2.2,0,4.1,0.8,5.7,2.3c1.6,1.5,2.5,3.4,2.7,5.7c0.6-0.3,1.2-0.4,1.8-0.4c1.3,0,2.4,0.5,3.4,1.5 C38.9,20.3,39.4,21.5,39.4,22.9L39.4,22.9z"/></symbol>
+  <symbol id="icon-tiktok" viewBox="0 0 5 5.292"><path fill-rule="evenodd" clip-rule="evenodd" d="M 3.0056593,3.7402047 C 2.9917683,4.1032048 2.686077,4.394531 2.3113304,4.394531 c -0.08567,0 -0.1676888,-0.015266 -0.2434637,-0.043152 0.075775,0.027886 0.1578202,0.043152 0.2434902,0.043152 0.3747466,0 0.6804387,-0.2913262 0.6943554,-0.6542999 l 0.00132,-3.24141643 h 0.6058809 c 0.058393,0.30815327 0.2455538,0.57257183 0.5048391,0.73777393 7.93e-5,1.058e-4 1.852e-4,2.117e-4 2.645e-4,3.175e-4 0.1804944,0.1149589 0.3957012,0.1820292 0.6267297,0.1820292 v 0.1800449 c 0,0 0,0 2.65e-5,2.65e-5 V 2.227616 c -0.4291437,0 -0.8268027,-0.1341672 -1.1513855,-0.3618624 v 1.6436603 c 0,0.8208777 -0.6833226,1.4886974 -1.5232747,1.4886974 -0.3245565,0 -0.6255391,-0.1000367 -0.8729448,-0.269816 C 1.1970357,4.728163 1.1969034,4.7280043 1.1967446,4.727872 0.80416835,4.4583206 0.54689375,4.0127459 0.54689375,3.5092552 c 0,-0.8208513 0.68332265,-1.4886974 1.52327485,-1.4886974 0.069689,0 0.1380032,0.00561 0.2052587,0.014526 V 2.22669 C 1.5091597,2.2441785 0.88092205,2.817015 0.79752745,3.5485978 0.88100145,2.8170944 1.5091863,2.2443373 2.2754008,2.2268487 v 0.6340861 c -0.06498,-0.01987 -0.1336642,-0.031432 -0.2052851,-0.031432 -0.3835836,0 -0.6956521,0.3050312 -0.6956521,0.6799109 0,0.2610585 0.1515497,0.4878806 0.3729741,0.6017548 0,2.64e-5 0,2.64e-5 0,2.64e-5 0.096544,0.049661 0.2062111,0.078103 0.3226514,0.078103 0.3747467,0 0.6804387,-0.2913261 0.6943554,-0.6542997 l 0.00132,-3.24144299 H 3.593361 c 0,0.0701124 0.00691,0.13863846 0.019525,0.20525906 H 3.0070087 Z" /></symbol>
+  <symbol id="icon-tumblr" viewBox="6 2 40 34"><path d="m 27.827893,29.747564 c 0,2.493786 1.258711,3.356328 3.263194,3.356328 h 2.843742 v 6.339769 h -5.384094 c -4.847873,0 -8.461022,-2.494139 -8.461022,-8.461022 v -9.555691 h -4.405136 v -5.174192 c 4.847872,-1.258711 6.875638,-5.43066 7.109177,-9.04381 h 5.034139 v 8.204552 h 5.873397 v 6.01345 h -5.873397 v 8.320616"/></symbol>
+  <symbol id="icon-twitch" viewBox="0 0 2400 2800"><g><path d="M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600 V1300z"/><rect x="1700" y="550" width="200" height="600"/><rect x="1150" y="550" width="200" height="600"/></g></symbol>
+  <symbol id="icon-twitter" viewBox="0 0 40 40"><path d="M36.3,10.2c-1,1.3-2.1,2.5-3.4,3.5c0,0.2,0,0.4,0,1c0,1.7-0.2,3.6-0.9,5.3c-0.6,1.7-1.2,3.5-2.4,5.1 c-1.1,1.5-2.3,3.1-3.7,4.3c-1.4,1.2-3.3,2.3-5.3,3c-2.1,0.8-4.2,1.2-6.6,1.2c-3.6,0-7-1-10.2-3c0.4,0,1.1,0.1,1.5,0.1 c3.1,0,5.9-1,8.2-2.9c-1.4,0-2.7-0.4-3.8-1.3c-1.2-1-1.9-2-2.2-3.3c0.4,0.1,1,0.1,1.2,0.1c0.6,0,1.2-0.1,1.7-0.2 c-1.4-0.3-2.7-1.1-3.7-2.3s-1.4-2.6-1.4-4.2v-0.1c1,0.6,2,0.9,3,0.9c-1-0.6-1.5-1.3-2.2-2.4c-0.6-1-0.9-2.1-0.9-3.3s0.3-2.3,1-3.4 c1.5,2.1,3.6,3.6,6,4.9s4.9,2,7.6,2.1c-0.1-0.6-0.1-1.1-0.1-1.4c0-1.8,0.8-3.5,2-4.7c1.2-1.2,2.9-2,4.7-2c2,0,3.6,0.8,4.8,2.1 c1.4-0.3,2.9-0.9,4.2-1.5c-0.4,1.5-1.4,2.7-2.9,3.6C33.8,11.2,35.1,10.9,36.3,10.2L36.3,10.2z"/></symbol>
+  <symbol id="icon-youtube" viewBox="0 -30 256 256"><g transform="matrix(1.3333333,0,0,-1.3333333,0,256)"><path d="M 0,0 V 52.338 L 46,26.168 Z M 102.322,68.806 C 100.298,76.428 94.334,82.43 86.762,84.467 73.037,88.169 18,88.169 18,88.169 c 0,0 -55.037,0 -68.762,-3.702 C -58.334,82.43 -64.298,76.428 -66.322,68.806 -70,54.992 -70,26.169 -70,26.169 c 0,0 0,-28.822 3.678,-42.637 2.024,-7.622 7.988,-13.624 15.56,-15.662 13.725,-3.701 68.762,-3.701 68.762,-3.701 0,0 55.037,0 68.762,3.701 7.572,2.038 13.536,8.04 15.56,15.662 C 106,-2.653 106,26.169 106,26.169 c 0,0 0,28.823 -3.678,42.637" transform="translate(78,69.8311)"/></g></symbol>
 </svg>
diff --git a/src/static/lazy-loading.js b/src/static/lazy-loading.js
index b04ad7c..1df56f0 100644
--- a/src/static/lazy-loading.js
+++ b/src/static/lazy-loading.js
@@ -37,7 +37,7 @@ function lazyLoadMain() {
   if (window.IntersectionObserver) {
     observer = new IntersectionObserver(lazyLoad, {
       rootMargin: '200px',
-      threshold: 1.0,
+      threshold: 0,
     });
     for (i = 0; i < lazyElements.length; i++) {
       observer.observe(lazyElements[i]);
diff --git a/src/static/site2.css b/src/static/site2.css
deleted file mode 100644
index 26c7ae3..0000000
--- a/src/static/site2.css
+++ /dev/null
@@ -1,1232 +0,0 @@
-/* A frontend file! Wow.
- * This file is just loaded statically 8y <link>s in the HTML files, so there's
- * no need to re-run upd8.js when tweaking values here. Handy!
- */
-
-:root {
-  --primary-color: #0088ff;
-}
-
-/* Layout - Common
- *
- */
-
-body {
-  margin: 10px;
-  overflow-y: scroll;
-}
-
-body::before {
-  content: "";
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  z-index: -1;
-}
-
-#page-container {
-  max-width: 1100px;
-  margin: 10px auto 50px;
-  padding: 15px 0;
-}
-
-#page-container > * {
-  margin-left: 15px;
-  margin-right: 15px;
-}
-
-#skippers:focus-within {
-  position: static;
-  width: unset;
-  height: unset;
-}
-
-#banner {
-  margin: 10px 0;
-  width: 100%;
-  position: relative;
-}
-
-#banner::after {
-  content: "";
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-}
-
-#banner img {
-  display: block;
-  width: 100%;
-  height: auto;
-}
-
-#skippers {
-  position: absolute;
-  left: -10000px;
-  top: auto;
-  width: 1px;
-  height: 1px;
-}
-
-.layout-columns {
-  display: flex;
-  align-items: stretch;
-}
-
-#header,
-#secondary-nav,
-#skippers,
-#footer {
-  padding: 5px;
-}
-
-#header,
-#secondary-nav,
-#skippers {
-  margin-bottom: 10px;
-}
-
-#footer {
-  margin-top: 10px;
-}
-
-#header {
-  display: grid;
-}
-
-#header.nav-has-main-links.nav-has-content {
-  grid-template-columns: 2.5fr 3fr;
-  grid-template-rows: min-content 1fr;
-  grid-template-areas:
-    "main-links content"
-    "bottom-row content";
-}
-
-#header.nav-has-main-links:not(.nav-has-content) {
-  grid-template-columns: 1fr;
-  grid-template-areas:
-    "main-links"
-    "bottom-row";
-}
-
-.nav-main-links {
-  grid-area: main-links;
-  margin-right: 20px;
-}
-
-.nav-content {
-  grid-area: content;
-}
-
-.nav-bottom-row {
-  grid-area: bottom-row;
-  align-self: start;
-}
-
-.sidebar-column {
-  flex: 1 1 20%;
-  min-width: 150px;
-  max-width: 250px;
-  flex-basis: 250px;
-  align-self: flex-start;
-}
-
-.sidebar-multiple {
-  display: flex;
-  flex-direction: column;
-}
-
-.sidebar-multiple .sidebar:not(:first-child) {
-  margin-top: 15px;
-}
-
-.sidebar {
-  --content-padding: 5px;
-  padding: var(--content-padding);
-}
-
-#sidebar-left {
-  margin-right: 10px;
-}
-
-#sidebar-right {
-  margin-left: 10px;
-}
-
-.sidebar.wide {
-  max-width: 350px;
-  flex-basis: 300px;
-  flex-shrink: 0;
-  flex-grow: 1;
-}
-
-#content {
-  --content-padding: 20px;
-  box-sizing: border-box;
-  padding: var(--content-padding);
-  flex-grow: 1;
-  flex-shrink: 3;
-}
-
-.footer-content {
-  margin: 5px 12%;
-}
-
-.footer-content > :first-child {
-  margin-top: 0;
-}
-
-.footer-content > :last-child {
-  margin-bottom: 0;
-}
-
-.footer-localization-links {
-  margin: 5px 12%;
-}
-
-#cover-art-container {
-  float: right;
-  width: 40%;
-  max-width: 400px;
-  margin: -5px 0 10px 10px;
-}
-
-/* Layout - Wide (most computers) */
-
-@media not all and (max-width: 900px) {
-  #secondary-nav:not(.no-hide) {
-    display: none;
-  }
-}
-
-/* Layout - Medium (tablets, some landscape mobiles)
- *
- * Note: Rules defined here are exclusive to "medium" width, i.e. they don't
- * additionally apply to "thin". Use the later section which applies to both
- * if so desired.
- */
-
-@media not all and (min-width: 900px), (max-width: 800px) {
-  body {
-    background: blue !important;
-  }
-}
-
-/* Layout - Medium or Thin */
-
-@media (max-width: 900px) {
-  .sidebar-column:not(.no-hide) {
-    display: none;
-  }
-
-  #secondary-nav {
-    display: block;
-  }
-
-  .layout-columns.vertical-when-thin {
-    flex-direction: column;
-  }
-
-  .layout-columns.vertical-when-thin > *:not(:last-child) {
-    margin-bottom: 10px;
-  }
-
-  .sidebar-column.no-hide {
-    max-width: unset !important;
-    flex-basis: unset !important;
-    margin-right: 0 !important;
-    margin-left: 0 !important;
-  }
-
-  .sidebar .news-entry:not(.first-news-entry) {
-    display: none;
-  }
-}
-
-/* Layout - Thin (phones) */
-
-@media (max-width: 600px) {
-  .content-columns {
-    columns: 1;
-  }
-
-  #cover-art-container {
-    float: none;
-    margin: 0 0 40px 0;
-    width: 100%;
-    max-width: unset;
-  }
-
-  /* Disable grid features, just line header children up vertically */
-
-  #header {
-    display: block;
-  }
-
-  #header > div:not(:first-child) {
-    margin-top: 0.5em;
-  }
-}
-
-/* Design & Appearance - Layout elements */
-
-body {
-  background: black;
-}
-
-body::before {
-  background-image: url("../media/bg.jpg");
-  background-position: center;
-  background-size: cover;
-  opacity: 0.5;
-}
-
-#page-container {
-  background-color: var(--bg-color, rgba(35, 35, 35, 0.8));
-  color: #ffffff;
-  box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
-}
-
-#skippers > .skipper:not(:last-child)::after {
-  content: " \00b7 ";
-  font-weight: 800;
-}
-
-#banner {
-  background: black;
-  background-color: var(--dim-color);
-  border-bottom: 1px solid var(--primary-color);
-}
-
-#banner::after {
-  box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35);
-  pointer-events: none;
-}
-
-#banner.dim img {
-  opacity: 0.8;
-}
-
-#header,
-#secondary-nav,
-#skippers,
-#footer,
-.sidebar {
-  font-size: 0.85em;
-}
-
-.sidebar,
-#content,
-#header,
-#secondary-nav,
-#skippers,
-#footer {
-  background-color: rgba(0, 0, 0, 0.6);
-  border: 1px dotted var(--primary-color);
-  border-radius: 3px;
-  transition: background-color 0.2s;
-}
-
-.sidebar:focus-within,
-#content:focus-within,
-#header:focus-within,
-#secondary-nav:focus-within,
-#skippers:focus-within,
-#footer:focus-within {
-  background-color: rgba(0, 0, 0, 0.85);
-  border-style: solid;
-}
-
-.sidebar > h1,
-.sidebar > h2,
-.sidebar > h3,
-.sidebar > p {
-  text-align: center;
-}
-
-.sidebar h1 {
-  font-size: 1.25em;
-}
-
-.sidebar h2 {
-  font-size: 1.1em;
-  margin: 0;
-}
-
-.sidebar h3 {
-  font-size: 1.1em;
-  font-style: oblique;
-  font-variant: small-caps;
-  margin-top: 0.3em;
-  margin-bottom: 0em;
-}
-
-.sidebar > p {
-  margin: 0.5em 0;
-  padding: 0 5px;
-}
-
-.sidebar hr {
-  color: #555;
-  margin: 10px 5px;
-}
-
-.sidebar > ol,
-.sidebar > ul {
-  padding-left: 30px;
-  padding-right: 15px;
-}
-
-.sidebar > dl {
-  padding-right: 15px;
-  padding-left: 0;
-}
-
-.sidebar > dl dt {
-  padding-left: 10px;
-  margin-top: 0.5em;
-}
-
-.sidebar > dl dt.current {
-  font-weight: 800;
-}
-
-.sidebar > dl dd {
-  margin-left: 0;
-}
-
-.sidebar > dl dd ul {
-  padding-left: 30px;
-  margin-left: 0;
-}
-
-.sidebar > dl .side {
-  padding-left: 10px;
-}
-
-.sidebar li.current {
-  font-weight: 800;
-}
-
-.sidebar li {
-  overflow-wrap: break-word;
-}
-
-.sidebar > details.current summary {
-  font-weight: 800;
-}
-
-.sidebar > details summary {
-  margin-top: 0.5em;
-  padding-left: 5px;
-  user-select: none;
-}
-
-.sidebar > details summary .group-name {
-  color: var(--primary-color);
-}
-
-.sidebar > details summary:hover {
-  cursor: pointer;
-  text-decoration: underline;
-  text-decoration-color: var(--primary-color);
-}
-
-.sidebar > details ul,
-.sidebar > details ol {
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-.sidebar > details:last-child {
-  margin-bottom: 10px;
-}
-
-.sidebar > details[open] {
-  margin-bottom: 1em;
-}
-
-.sidebar article {
-  text-align: left;
-  margin: 5px 5px 15px 5px;
-}
-
-.sidebar article:last-child {
-  margin-bottom: 5px;
-}
-
-.sidebar article h2,
-.news-index h2 {
-  border-bottom: 1px dotted;
-}
-
-.sidebar article h2 time,
-.news-index time {
-  float: right;
-  font-weight: normal;
-}
-
-#content {
-  overflow-wrap: anywhere;
-}
-
-footer {
-  text-align: center;
-  font-style: oblique;
-}
-
-.footer-localization-links > span:not(:last-child)::after {
-  content: " \00b7 ";
-  font-weight: 800;
-}
-
-/* Design & Appearance - Content elements */
-
-a {
-  color: var(--primary-color);
-  text-decoration: none;
-}
-
-a:hover {
-  text-decoration: underline;
-}
-
-.nav-main-links > span {
-  white-space: nowrap;
-}
-
-.nav-main-links > span > a.current {
-  font-weight: 800;
-}
-
-.nav-links-index > span:not(:first-child):not(.no-divider)::before,
-.nav-links-groups > span:not(:first-child):not(.no-divider)::before {
-  content: "\0020\00b7\0020";
-  font-weight: 800;
-}
-
-.nav-links-hierarchy > span:not(:first-child):not(.no-divider)::before {
-  content: "\0020/\0020";
-}
-
-#header .chronology .heading,
-#header .chronology .buttons {
-  white-space: nowrap;
-}
-
-#secondary-nav {
-  text-align: center;
-}
-
-.nowrap {
-  white-space: nowrap;
-}
-
-.icons {
-  font-style: normal;
-  white-space: nowrap;
-}
-
-.icon {
-  display: inline-block;
-  width: 24px;
-  height: 1em;
-  position: relative;
-}
-
-.icon > svg {
-  width: 24px;
-  height: 24px;
-  top: -0.25em;
-  position: absolute;
-  fill: var(--primary-color);
-}
-
-.rerelease,
-.other-group-accent {
-  opacity: 0.7;
-  font-style: oblique;
-}
-
-.other-group-accent {
-  white-space: nowrap;
-}
-
-.content-columns {
-  columns: 2;
-}
-
-.content-columns .column {
-  break-inside: avoid;
-}
-
-.content-columns .column h2 {
-  margin-top: 0;
-  font-size: 1em;
-}
-
-#cover-art-container {
-  font-size: 0.8em;
-  box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.25);
-}
-
-#cover-art img {
-  display: block;
-  width: 100%;
-  height: 100%;
-}
-
-#cover-art-container p {
-  margin-top: 5px;
-}
-
-.js-hide,
-.js-show-once-data,
-.js-hide-once-data {
-  display: none;
-}
-
-a.box:focus {
-  outline: 3px double var(--primary-color);
-}
-
-a.box:focus:not(:focus-visible) {
-  outline: none;
-}
-
-a.box img {
-  display: block;
-  width: 100%;
-  height: 100%;
-}
-
-h1 {
-  font-size: 1.5em;
-}
-
-#content li {
-  margin-bottom: 4px;
-}
-
-#content li i {
-  white-space: nowrap;
-}
-
-#content.top-index h1,
-#content.flash-index h1 {
-  text-align: center;
-  font-size: 2em;
-}
-
-#content.flash-index h2 {
-  text-align: center;
-  font-size: 2.5em;
-  font-variant: small-caps;
-  font-style: oblique;
-  margin-bottom: 0;
-  text-align: center;
-  width: 100%;
-}
-
-#content.top-index h2 {
-  text-align: center;
-  font-size: 2em;
-  font-weight: normal;
-  margin-bottom: 0.25em;
-}
-
-.quick-info {
-  text-align: center;
-}
-
-ul.quick-info {
-  list-style: none;
-  padding-left: 0;
-}
-
-ul.quick-info li {
-  display: inline-block;
-}
-
-ul.quick-info li:not(:last-child)::after {
-  content: " \00b7 ";
-  font-weight: 800;
-}
-
-#intro-menu {
-  margin: 24px 0;
-  padding: 10px;
-  background-color: #222222;
-  text-align: center;
-  border: 1px dotted var(--primary-color);
-  border-radius: 2px;
-}
-
-#intro-menu p {
-  margin: 12px 0;
-}
-
-#intro-menu a {
-  margin: 0 6px;
-}
-
-li .by {
-  display: inline-block;
-  font-style: oblique;
-}
-
-li .by a {
-  display: inline-block;
-}
-
-p code {
-  font-size: 1em;
-  font-family: "courier new";
-  font-weight: 800;
-}
-
-blockquote {
-  margin-left: 40px;
-  max-width: 600px;
-  margin-right: 0;
-}
-
-.long-content {
-  margin-left: 12%;
-  margin-right: 12%;
-}
-
-p img {
-  max-width: 100%;
-  height: auto;
-}
-
-dl dt {
-  padding-left: 40px;
-  max-width: 600px;
-}
-
-dl dt {
-  margin-bottom: 2px;
-}
-
-dl dd {
-  margin-bottom: 1em;
-}
-
-dl ul,
-dl ol {
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-.album-group-list dt {
-  font-style: oblique;
-  padding-left: 0;
-}
-
-.album-group-list dd {
-  margin-left: 0;
-}
-
-.group-chronology-link {
-  font-style: oblique;
-}
-
-hr.split::before {
-  content: "(split)";
-  color: #808080;
-}
-
-hr.split {
-  position: relative;
-  overflow: hidden;
-  border: none;
-}
-
-hr.split::after {
-  display: inline-block;
-  content: "";
-  border: 1px inset #808080;
-  width: 100%;
-  position: absolute;
-  top: 50%;
-  margin-top: -2px;
-  margin-left: 10px;
-}
-
-li > ul {
-  margin-top: 5px;
-}
-
-/* Images */
-
-.image-container {
-  border: 2px solid var(--primary-color);
-  box-sizing: border-box;
-  position: relative;
-  padding: 5px;
-  text-align: left;
-  background-color: var(--dim-color);
-  color: white;
-  display: inline-block;
-  width: 100%;
-  height: 100%;
-}
-
-.image-inner-area {
-  overflow: hidden;
-  width: 100%;
-  height: 100%;
-  position: relative;
-}
-
-.image-text-area {
-  position: absolute;
-  top: 0;
-  left: 0;
-  bottom: 0;
-  right: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  text-align: center;
-  padding: 5px 15px;
-  background: rgba(0, 0, 0, 0.65);
-  box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset;
-  line-height: 1.35em;
-  color: var(--primary-color);
-  font-style: oblique;
-  text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75);
-}
-
-img {
-  object-fit: cover;
-  /* these unfortunately dont take effect while loading, so...
-    text-align: center;
-    line-height: 2em;
-    text-shadow: 0 0 5px black;
-    font-style: oblique;
-    */
-}
-
-.reveal {
-  position: relative;
-  width: 100%;
-  height: 100%;
-}
-
-.reveal img {
-  filter: blur(20px);
-  opacity: 0.4;
-}
-
-.reveal-text {
-  color: white;
-  position: absolute;
-  top: 15px;
-  left: 10px;
-  right: 10px;
-  text-align: center;
-  font-weight: bold;
-}
-
-.reveal-interaction {
-  opacity: 0.8;
-}
-
-.reveal.revealed img {
-  filter: none;
-  opacity: 1;
-}
-
-.reveal.revealed .reveal-text {
-  display: none;
-}
-
-/* Grid listings */
-
-.grid-listing {
-  display: flex;
-  flex-wrap: wrap;
-  justify-content: center;
-  align-items: flex-start;
-}
-
-.grid-item {
-  display: inline-block;
-  margin: 15px;
-  text-align: center;
-  background-color: #111111;
-  border: 1px dotted var(--primary-color);
-  border-radius: 2px;
-  padding: 5px;
-}
-
-.grid-item img {
-  width: 100%;
-  height: 100%;
-  margin-top: auto;
-  margin-bottom: auto;
-}
-
-.grid-item span {
-  overflow-wrap: break-word;
-  hyphens: auto;
-}
-
-.grid-item:hover {
-  text-decoration: none;
-}
-
-.grid-actions .grid-item:hover {
-  text-decoration: underline;
-}
-
-.grid-item > span {
-  display: block;
-}
-
-.grid-item > span:not(:first-child) {
-  margin-top: 2px;
-}
-
-.grid-item > span:first-of-type {
-  margin-top: 6px;
-}
-
-.grid-item:hover > span:first-of-type {
-  text-decoration: underline;
-}
-
-.grid-listing > .grid-item {
-  flex: 1 1 26%;
-}
-
-.grid-actions {
-  display: flex;
-  flex-direction: column;
-  margin: 15px;
-  align-self: center;
-}
-
-.grid-actions > .grid-item {
-  flex-basis: unset !important;
-  margin: 5px;
-  --primary-color: inherit !important;
-  --dim-color: inherit !important;
-}
-
-.grid-item {
-  flex-basis: 240px;
-  min-width: 200px;
-  max-width: 240px;
-}
-
-.grid-item:not(.large-grid-item) {
-  flex-basis: 180px;
-  min-width: 120px;
-  max-width: 180px;
-  font-size: 0.9em;
-}
-
-/* Squares */
-
-.square {
-  position: relative;
-  width: 100%;
-}
-
-.square::after {
-  content: "";
-  display: block;
-  padding-bottom: 100%;
-}
-
-.square-content {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-}
-
-.vertical-square {
-  position: relative;
-  height: 100%;
-}
-
-.vertical-square::after {
-  content: "";
-  display: block;
-  padding-right: 100%;
-}
-
-/* Info card */
-
-#info-card-container {
-  position: absolute;
-
-  left: 0;
-  right: 10px;
-
-  pointer-events: none; /* Padding area shouldn't 8e interactive. */
-  display: none;
-}
-
-#info-card-container.show,
-#info-card-container.hide {
-  display: flex;
-}
-
-#info-card-container > * {
-  flex-basis: 400px;
-}
-
-#info-card-container.show {
-  animation: 0.2s linear forwards info-card-show;
-  transition: top 0.1s, left 0.1s;
-}
-
-#info-card-container.hide {
-  animation: 0.2s linear forwards info-card-hide;
-}
-
-@keyframes info-card-show {
-  0% {
-    opacity: 0;
-    margin-top: -5px;
-  }
-
-  100% {
-    opacity: 1;
-    margin-top: 0;
-  }
-}
-
-@keyframes info-card-hide {
-  0% {
-    opacity: 1;
-    margin-top: 0;
-  }
-
-  100% {
-    opacity: 0;
-    margin-top: 5px;
-    display: none !important;
-  }
-}
-
-.info-card-decor {
-  padding-left: 3ch;
-  border-top: 1px solid white;
-}
-
-.info-card {
-  background-color: black;
-  color: white;
-
-  border: 1px dotted var(--primary-color);
-  border-radius: 3px;
-  box-shadow: 0 5px 5px black;
-
-  padding: 5px;
-  font-size: 0.9em;
-
-  pointer-events: none;
-}
-
-.info-card::after {
-  content: "";
-  display: block;
-  clear: both;
-}
-
-#info-card-container.show .info-card {
-  animation: 0.01s linear 0.2s forwards info-card-become-interactive;
-}
-
-@keyframes info-card-become-interactive {
-  to {
-    pointer-events: auto;
-  }
-}
-
-.info-card-art-container {
-  float: right;
-
-  width: 40%;
-  margin: 5px;
-  font-size: 0.8em;
-
-  /* Dynamically shown. */
-  display: none;
-}
-
-.info-card-art-container .image-container {
-  padding: 2px;
-}
-
-.info-card-art {
-  display: block;
-  width: 100%;
-  height: 100%;
-}
-
-.info-card-name {
-  font-size: 1em;
-  border-bottom: 1px dotted;
-  margin: 0;
-}
-
-.info-card p {
-  margin-top: 0.25em;
-  margin-bottom: 0.25em;
-}
-
-.info-card p:last-child {
-  margin-bottom: 0;
-}
-
-/* Sticky heading */
-
-#content:not(.no-sticky-heading) > h1:first-of-type,
-.sidebar:not(.no-sticky-heading) h1:first-of-type {
-  position: sticky;
-  top: 0;
-}
-
-#content .content-sticky-heading-container h1,
-#content:not(.no-sticky-heading) > h1:first-of-type,
-.sidebar:not(.no-sticky-heading) h1:first-of-type {
-  margin: calc(-1 * var(--content-padding));
-  margin-bottom: calc(0.5 * var(--content-padding));
-  padding:
-    calc(1.25 * var(--content-padding))
-    20px
-    calc(0.75 * var(--content-padding))
-    20px;
-
-  background: var(--bg-black-color);
-  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
-
-  -webkit-backdrop-filter: blur(6px);
-          backdrop-filter: blur(6px);
-}
-
-#content .content-sticky-heading-container {
-  position: sticky;
-  top: 0;
-
-  /* Safari doesn't always play nicely with position: sticky,
-   * this seems to fix images sometimes displaying above the
-   * position: absolute subheading (h2) child
-   *
-   * See also: https://stackoverflow.com/questions/50224855/
-   */
-  transform: translate3d(0, 0, 0);
-  z-index: 1;
-}
-
-#content .content-sticky-heading-container h1 {
-  margin-bottom: 0;
-}
-
-#content .content-sticky-heading-container h2 {
-  position: absolute;
-  margin: 0 calc(-1 * var(--content-padding));
-  width: 100%;
-  padding: 10px 40px 5px 20px;
-
-  font-size: 0.9em;
-  font-weight: normal;
-  font-style: oblique;
-  color: #eee;
-
-  background: var(--bg-black-color);
-  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
-
-  -webkit-backdrop-filter: blur(3px);
-          backdrop-filter: blur(3px);
-
-  transition: margin-top 0.35s, opacity 0.25s;
-}
-
-#content .content-sticky-heading-container h2:not(.visible) {
-  margin-top: -20px;
-  opacity: 0;
-}
-
-#content .content-sticky-heading-container h2.visible {
-  margin-top: 0;
-  opacity: 1;
-}
-
-#content:not(.no-sticky-heading) > h1:first-of-type {
-  z-index: 1;
-  box-shadow:
-    inset 0 10px 10px -5px var(--shadow-color),
-    0 4px 4px rgba(0, 0, 0, 0.8);
-}
-
-#content .content-sticky-heading-container h1 {
-  box-shadow:
-    inset 0 10px 10px -5px var(--shadow-color),
-    0 4px 4px rgba(0, 0, 0, 0.8);
-}
-
-#content .content-sticky-heading-container h2.visible {
-  box-shadow:
-    inset 0 10px 10px -5px var(--shadow-color),
-    0 4px 4px rgba(0, 0, 0, 0.8);
-}
-
-#content:not(.no-sticky-heading) .long-content h1:first-of-type {
-  margin-left: -40%;
-  margin-right: -40%;
-  padding-left: 40%;
-}
-
-#cover-art-container {
-  z-index: 2;
-  position: relative;
-}
-
-.sidebar:not(.no-sticky-heading) h1:first-of-type {
-  box-shadow:
-    inset 0 8px 8px -6px var(--shadow-color),
-    0 4px 4px rgba(0, 0, 0, 0.8);
-}
-
-#content, .sidebar {
-  contain: paint;
-}
-
-/* Sticky sidebar */
-
-.sidebar-column.sidebar.sticky-column,
-.sidebar-column.sidebar.sticky-last,
-.sidebar-multiple.sticky-last > .sidebar:last-child,
-.sidebar-multiple.sticky-column {
-  position: sticky;
-  top: 10px;
-}
-
-.sidebar-multiple.sticky-last {
-  align-self: stretch;
-}
-
-.sidebar-multiple.sticky-column {
-  align-self: flex-start;
-}
-
-/* important easter egg mode */
-
-html[data-language-code="preview-en"][data-url-key="localized.home"] #content
-  h1::after {
-  font-family: cursive;
-  display: block;
-  content: "(Preview Build)";
-}
diff --git a/src/static/site6.css b/src/static/site6.css
new file mode 100644
index 0000000..7372195
--- /dev/null
+++ b/src/static/site6.css
@@ -0,0 +1,2401 @@
+/* A frontend file! Wow.
+ * This file is just loaded statically 8y <link>s in the HTML files, so there's
+ * no need to re-run upd8.js when tweaking values here. Handy!
+ */
+
+/* Squares */
+
+/* This styling is kind of awkwardly placed at the very top. Sorry!
+ * We need to rework what order sets of styles get applied in to be
+ * much more explicit (so that overriding isn't a headache), and
+ * hopefully that can be done through @imports, but it'll take some
+ * reworking and cleaning up.
+ */
+
+.square {
+  position: relative;
+  width: 100%;
+}
+
+.square::after {
+  content: "";
+  display: block;
+  padding-bottom: 100%;
+}
+
+.square-content {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+}
+
+/* Layout - Common */
+
+body {
+  margin: 10px;
+  overflow-y: scroll;
+}
+
+body::before {
+  content: "";
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: -1;
+
+  /* NB: these are 100 LVW, "largest view width", etc.
+   * Stabilizes background on viewports with modal dimensions,
+   * e.g. expanding/shrinking tab bar or collapsible find bar.
+   * 100% dimensions are kept above for browser compatibility.
+   */
+  width: 100lvw;
+  height: 100lvh;
+}
+
+#page-container {
+  max-width: 1100px;
+  margin: 10px auto 50px;
+  padding: 15px 0;
+}
+
+#page-container > * {
+  margin-left: 15px;
+  margin-right: 15px;
+}
+
+#skippers:focus-within {
+  position: static;
+  width: unset;
+  height: unset;
+}
+
+#banner {
+  margin: 10px 0;
+  width: 100%;
+  position: relative;
+}
+
+#banner::after {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+#banner img {
+  display: block;
+  width: 100%;
+  height: auto;
+}
+
+#skippers {
+  position: absolute;
+  left: -10000px;
+  top: auto;
+  width: 1px;
+  height: 1px;
+}
+
+.layout-columns {
+  display: flex;
+  align-items: stretch;
+}
+
+#header,
+#secondary-nav,
+#skippers,
+#footer {
+  padding: 5px;
+}
+
+#header,
+#secondary-nav,
+#skippers {
+  margin-bottom: 10px;
+}
+
+#footer {
+  margin-top: 10px;
+}
+
+#header {
+  display: grid;
+}
+
+#header.nav-has-main-links.nav-has-content {
+  grid-template-columns: 2.5fr 3fr;
+  grid-template-rows: min-content 1fr;
+  grid-template-areas:
+    "main-links content"
+    "bottom-row content";
+}
+
+#header.nav-has-main-links:not(.nav-has-content) {
+  grid-template-columns: 1fr;
+  grid-template-areas:
+    "main-links"
+    "bottom-row";
+}
+
+.nav-main-links {
+  grid-area: main-links;
+  margin-right: 20px;
+}
+
+.nav-content {
+  grid-area: content;
+}
+
+.nav-bottom-row {
+  grid-area: bottom-row;
+  align-self: start;
+}
+
+.sidebar-column {
+  flex: 1 1 20%;
+  min-width: 150px;
+  max-width: 250px;
+  flex-basis: 250px;
+  align-self: flex-start;
+}
+
+.sidebar-column.wide {
+  max-width: 350px;
+  flex-basis: 300px;
+  flex-shrink: 0;
+  flex-grow: 1;
+}
+
+.sidebar-multiple {
+  display: flex;
+  flex-direction: column;
+}
+
+.sidebar-multiple .sidebar:not(:first-child) {
+  margin-top: 15px;
+}
+
+.sidebar {
+  --content-padding: 5px;
+  padding: var(--content-padding);
+}
+
+#sidebar-left {
+  margin-right: 10px;
+}
+
+#sidebar-right {
+  margin-left: 10px;
+}
+
+#content {
+  position: relative;
+  --content-padding: 20px;
+  box-sizing: border-box;
+  padding: var(--content-padding);
+  flex-grow: 1;
+  flex-shrink: 3;
+}
+
+.footer-content {
+  margin: 5px 12%;
+}
+
+.footer-content > :first-child {
+  margin-top: 0;
+}
+
+.footer-content > :last-child {
+  margin-bottom: 0;
+}
+
+.footer-localization-links {
+  margin: 5px 12%;
+}
+
+/* Design & Appearance - Layout elements */
+
+body {
+  background: black;
+}
+
+body::before {
+  background-image: url("../media/bg.jpg");
+  background-position: center;
+  background-size: cover;
+  opacity: 0.5;
+}
+
+#page-container {
+  background-color: var(--bg-color, rgba(35, 35, 35, 0.8));
+  color: #ffffff;
+  box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
+}
+
+#skippers > * {
+  display: inline-block;
+}
+
+#skippers > .skipper-list:not(:last-child)::after {
+  display: inline-block;
+  content: "\00a0";
+  margin-left: 2px;
+  margin-right: -2px;
+  border-left: 1px dotted;
+}
+
+#skippers .skipper-list > .skipper:not(:last-child)::after {
+  content: " \00b7 ";
+  font-weight: 800;
+}
+
+#banner {
+  background: black;
+  background-color: var(--dim-color);
+  border-bottom: 1px solid var(--primary-color);
+}
+
+#banner::after {
+  box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35);
+  pointer-events: none;
+}
+
+#banner.dim img {
+  opacity: 0.8;
+}
+
+#header,
+#secondary-nav,
+#skippers,
+#footer,
+.sidebar {
+  font-size: 0.85em;
+}
+
+.sidebar,
+#content,
+#header,
+#secondary-nav,
+#skippers,
+#footer {
+  background-color: rgba(0, 0, 0, 0.6);
+  border: 1px dotted var(--primary-color);
+  border-radius: 3px;
+  transition: background-color 0.2s;
+}
+
+/*
+.sidebar:focus-within,
+#content:focus-within,
+#header:focus-within,
+#secondary-nav:focus-within,
+#skippers:focus-within,
+#footer:focus-within {
+  background-color: rgba(0, 0, 0, 0.85);
+  border-style: solid;
+}
+*/
+
+.sidebar > h1,
+.sidebar > h2,
+.sidebar > h3,
+.sidebar > p {
+  text-align: center;
+  padding-left: 4px;
+  padding-right: 4px;
+}
+
+.sidebar h1 {
+  font-size: 1.25em;
+}
+
+.sidebar h2 {
+  font-size: 1.1em;
+  margin: 0;
+}
+
+.sidebar h3 {
+  font-size: 1.1em;
+  font-style: oblique;
+  font-variant: small-caps;
+  margin-top: 0.3em;
+  margin-bottom: 0em;
+}
+
+.sidebar > p {
+  margin: 0.5em 0;
+  padding: 0 5px;
+}
+
+.sidebar hr {
+  color: #555;
+  margin: 10px 5px;
+}
+
+.sidebar > ol,
+.sidebar > ul {
+  padding-left: 30px;
+  padding-right: 15px;
+}
+
+.sidebar > dl {
+  padding-right: 15px;
+  padding-left: 0;
+}
+
+.sidebar > dl dt {
+  padding-left: 10px;
+  margin-top: 0.5em;
+}
+
+.sidebar > dl dt.current {
+  font-weight: 800;
+}
+
+.sidebar > dl dd {
+  margin-left: 0;
+}
+
+.sidebar > dl dd ul {
+  padding-left: 30px;
+  margin-left: 0;
+}
+
+.sidebar > dl .side {
+  padding-left: 10px;
+}
+
+.sidebar li.current {
+  font-weight: 800;
+}
+
+.sidebar li {
+  overflow-wrap: break-word;
+}
+
+.sidebar > details.current summary {
+  font-weight: 800;
+}
+
+.sidebar > details summary {
+  margin-top: 0.5em;
+  padding-left: 5px;
+}
+
+summary > span:hover {
+  cursor: pointer;
+  text-decoration: underline;
+  text-decoration-color: var(--primary-color);
+}
+
+summary .group-name {
+  color: var(--primary-color);
+}
+
+.sidebar > details ul,
+.sidebar > details ol {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.sidebar > details:last-child {
+  margin-bottom: 10px;
+}
+
+.sidebar > details[open] {
+  margin-bottom: 1em;
+}
+
+.sidebar article {
+  text-align: left;
+  margin: 5px 5px 15px 5px;
+}
+
+.sidebar article:last-child {
+  margin-bottom: 5px;
+}
+
+.sidebar article h2,
+.news-index h2 {
+  border-bottom: 1px dotted;
+}
+
+.sidebar article h2 time,
+.news-index time {
+  float: right;
+  font-weight: normal;
+}
+
+#content {
+  overflow-wrap: anywhere;
+}
+
+footer {
+  text-align: center;
+  font-style: oblique;
+}
+
+.footer-localization-links > span:not(:last-child)::after {
+  content: " \00b7 ";
+  font-weight: 800;
+}
+
+/* Design & Appearance - Content elements */
+
+a {
+  color: var(--primary-color);
+  text-decoration: none;
+}
+
+a:hover {
+  text-decoration: underline;
+  text-decoration-style: solid !important;
+}
+
+a.current {
+  font-weight: 800;
+}
+
+a:not([href]) {
+  cursor: default;
+}
+
+a:not([href]):hover {
+  text-decoration: none;
+}
+
+.external-link:not(.from-content) {
+  white-space: nowrap;
+}
+
+.external-link.indicate-external::after {
+  content: '\00a0➚';
+}
+
+.external-link.indicate-external:hover::after {
+  color: white;
+}
+
+.nav-main-links .nav-link.current > span.nav-link-content > a {
+  font-weight: 800;
+}
+
+.nav-main-links .nav-link-accent {
+  display: inline-block;
+}
+
+.nav-links-index .nav-link.has-divider::before,
+.nav-links-groups .nav-link.has-divider::before {
+  content: "\0020\00b7\0020";
+  font-weight: 800;
+}
+
+.nav-links-hierarchical .nav-link.has-divider::before {
+  content: "\0020/\0020";
+}
+
+#header .chronology .heading,
+#header .chronology .buttons {
+  white-space: nowrap;
+}
+
+#secondary-nav {
+  text-align: center;
+}
+
+.nowrap {
+  white-space: nowrap;
+}
+
+.blockwrap, .chunkwrap {
+  display: inline-block;
+}
+
+.text-with-tooltip {
+  position: relative;
+}
+
+.text-with-tooltip .text-with-tooltip-interaction-cue {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+.text-with-tooltip > .hoverable:hover .text-with-tooltip-interaction-cue,
+.text-with-tooltip > .hoverable.has-visible-tooltip .text-with-tooltip-interaction-cue {
+  text-decoration-style: wavy !important;
+}
+
+.text-with-tooltip.datetimestamp .text-with-tooltip-interaction-cue,
+.text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue {
+  cursor: default;
+}
+
+.text-with-tooltip.missing-duration > .hoverable {
+  opacity: 0.5;
+}
+
+.text-with-tooltip.missing-duration > .hoverable:hover,
+.text-with-tooltip.missing-duration > .hoverable.has-visible-tooltip {
+  opacity: 1;
+}
+
+.text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue {
+  text-decoration: none !important;
+}
+
+.tooltip {
+  position: absolute;
+  z-index: 3;
+  left: -10px;
+  top: calc(1em + 1px);
+  display: none;
+}
+
+li:not(:first-child:last-child) .tooltip,
+.offset-tooltips > :not(:first-child:last-child) .tooltip {
+  left: 14px;
+}
+
+.tooltip-content {
+  display: block;
+
+  background: var(--bg-black-color);
+  border: 1px dotted var(--primary-color);
+  border-radius: 6px;
+
+  -webkit-backdrop-filter:
+    brightness(1.5) saturate(1.4) blur(4px);
+
+          backdrop-filter:
+    brightness(1.5) saturate(1.4) blur(4px);
+
+  box-shadow:
+    0 3px 4px 4px #000000aa,
+    0 -2px 4px -2px var(--primary-color) inset;
+}
+
+.icons-tooltip {
+  padding: 3px 6px 6px 6px;
+  left: -34px;
+}
+
+.datetimestamp-tooltip,
+.missing-duration-tooltip {
+  padding: 3px 4px 2px 2px;
+  left: -10px;
+}
+
+.thing-name-tooltip {
+  padding: 3px 4px 2px 2px;
+  left: -6px !important;
+
+  /* Terrifying?
+   * https://stackoverflow.com/a/64424759/4633828
+   */
+  margin-right: -120px;
+}
+
+.icons-tooltip .tooltip-content {
+  padding: 6px 2px 2px 2px;
+
+  -webkit-user-select: none;
+          user-select: none;
+
+  cursor: default;
+
+  display: grid;
+
+  grid-template-columns:
+    [icon-start] auto [icon-end domain-start] auto [domain-end];
+}
+
+.icons-tooltip .icon {
+  grid-column-start: icon-start;
+  grid-column-end: icon-end;
+}
+
+.icons-tooltip .icon-platform {
+  display: none;
+
+  grid-column-start: domain-start;
+  grid-column-end: domain-end;
+
+  --icon-platform-opacity: 0.8;
+  padding-right: 4px;
+  opacity: 0.8;
+}
+
+.icons-tooltip.show-info .icon-platform {
+  display: inline;
+  animation: icon-platform 0.2s forwards linear;
+}
+
+@keyframes icon-platform {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: var(--icon-platform-opacity);
+  }
+}
+
+.icons-tooltip .icon:hover + .icon-platform {
+  --icon-platform-opacity: 1;
+  text-decoration: underline;
+  text-decoration-color: #ffffffaa;
+}
+
+.datetimestamp-tooltip .tooltip-content,
+.missing-duration-tooltip .tooltip-content {
+  padding: 5px 6px;
+  white-space: nowrap;
+  font-size: 0.9em;
+}
+
+.thing-name-tooltip .tooltip-content {
+  padding: 3px 4.5px;
+}
+
+.icons {
+  font-style: normal;
+  white-space: nowrap;
+}
+
+.icons a:hover {
+  filter: brightness(1.4);
+}
+
+.icons a {
+  padding: 0 3px;
+}
+
+.icon {
+  display: inline-block;
+  width: 24px;
+  height: 1em;
+  position: relative;
+}
+
+.icon > svg {
+  width: 24px;
+  height: 24px;
+  top: -0.25em;
+  position: absolute;
+  fill: var(--primary-color);
+}
+
+.icon.has-text {
+  display: block;
+  width: unset;
+  height: 1.4em;
+}
+
+.icon.has-text > svg {
+  width: 18px;
+  height: 18px;
+  top: -0.1em;
+}
+
+.icon.has-text > .icon-text {
+  margin-left: 24px;
+  padding-right: 8px;
+}
+
+.rerelease,
+.other-group-accent {
+  opacity: 0.7;
+  font-style: oblique;
+}
+
+.other-group-accent {
+  white-space: nowrap;
+}
+
+.other-group-accent a {
+  color: var(--page-primary-color);
+}
+
+.content-columns {
+  columns: 2;
+}
+
+.content-columns .column {
+  break-inside: avoid;
+}
+
+.content-columns .column h2 {
+  margin-top: 0;
+  font-size: 1em;
+}
+
+p .current {
+  font-weight: 800;
+}
+
+#cover-art-container {
+  font-size: 0.8em;
+  border: 2px solid var(--primary-color);
+  box-shadow:
+    0 2px 14px -6px var(--primary-color),
+    0 0 12px 12px #00000080;
+
+  border-radius: 0 0 4px 4px;
+  background: var(--bg-black-color);
+
+  -webkit-backdrop-filter: blur(3px);
+          backdrop-filter: blur(3px);
+}
+
+#cover-art-container:has(.image-details),
+#cover-art-container.has-image-details {
+  border-radius: 0 0 6px 6px;
+}
+
+#cover-art-container:not(:has(.image-details)),
+#cover-art-container:not(.has-image-details) {
+  /* Hacky: `overflow: hidden` hides tag tooltips, so it can't be applied
+   * if we've got tags/details visible. But it's okay, because we only
+   * need to apply it if it *doesn't* - that's when the rounded border
+   * of #cover-art-container needs to cut off its child image-container
+   * (which has a background that otherwise causes sharp corners).
+   */
+  overflow: hidden;
+}
+
+#cover-art-container .image-container {
+  /* Border is handled on the cover-art-container. */
+  border: none;
+  border-radius: 0;
+}
+
+#cover-art-container .image-details {
+  border-top-color: var(--deep-color);
+}
+
+#cover-art-container .image {
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+
+.image-details {
+  display: block;
+
+  padding: 6px 9px 4px 9px;
+  margin-top: 0;
+  margin-bottom: 0;
+  border-top: 1px dashed var(--dim-color);
+}
+
+ul.image-details li {
+  display: inline-block;
+  margin: 0;
+}
+
+#cover-art-container ul li:not(:last-child)::after {
+  content: " \00b7 ";
+}
+
+.commentary-entry-heading {
+  margin-left: 15px;
+  padding-left: 5px;
+  max-width: 625px;
+  padding-bottom: 0.2em;
+  border-bottom: 1px dotted var(--primary-color);
+}
+
+.commentary-entry-accent {
+  font-style: oblique;
+}
+
+.commentary-art {
+  float: right;
+  width: 30%;
+  max-width: 250px;
+  margin: 15px 0 10px 20px;
+
+  /* This !important is unfortunate, but it's necessary
+   * even if the rule itself is placed lower, because this
+   * is a relatively low-priority selector compared to
+   * others that alter image shadows.
+   */
+  box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important;
+}
+
+.js-hide,
+.js-show-once-data,
+.js-hide-once-data {
+  display: none;
+}
+
+.content-image-container {
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+
+.content-image-container.align-center {
+  text-align: center;
+  margin-top: 1.5em;
+  margin-bottom: 1.5em;
+}
+
+.content-image {
+  display: inline-block !important;
+}
+
+.image-link {
+  display: block;
+  overflow: hidden;
+}
+
+.image-link:focus {
+  outline: 3px double white;
+}
+
+.image-link:focus:not(:focus-visible) {
+  outline: none;
+}
+
+.image-link .image {
+  display: block;
+  max-width: 100%;
+  height: auto;
+}
+
+.square .image-link {
+  width: 100%;
+  height: 100%;
+}
+
+.square .image {
+  width: 100%;
+  height: 100%;
+}
+
+h1 {
+  font-size: 1.5em;
+}
+
+#content li {
+  margin-bottom: 4px;
+}
+
+#content li i {
+  white-space: nowrap;
+}
+
+#content.top-index h1,
+#content.flash-index h1 {
+  text-align: center;
+  font-size: 2em;
+}
+
+html[data-url-key="localized.home"] #content h1 {
+  text-align: center;
+  font-size: 2.5em;
+}
+
+#content.flash-index h2 {
+  text-align: center;
+  font-size: 2.5em;
+  font-variant: small-caps;
+  font-style: oblique;
+  margin-bottom: 0;
+  text-align: center;
+  width: 100%;
+}
+
+#content.top-index h2 {
+  text-align: center;
+  font-size: 2em;
+  font-weight: normal;
+  margin-bottom: 0.25em;
+}
+
+.quick-info {
+  text-align: center;
+}
+
+ul.quick-info {
+  list-style: none;
+  padding-left: 0;
+}
+
+ul.quick-info li {
+  display: inline-block;
+}
+
+ul.quick-info li:not(:last-child)::after {
+  content: " \00b7 ";
+  font-weight: 800;
+}
+
+.carousel-container + .quick-info {
+  margin-top: 25px;
+}
+
+#intro-menu {
+  margin: 24px 0;
+  padding: 10px;
+  background-color: #222222;
+  text-align: center;
+  border: 1px dotted var(--primary-color);
+  border-radius: 2px;
+}
+
+#intro-menu p {
+  margin: 12px 0;
+}
+
+#intro-menu a {
+  margin: 0 6px;
+}
+
+li .by {
+  font-style: oblique;
+  max-width: 600px;
+}
+
+li .by a {
+  display: inline-block;
+}
+
+p code {
+  font-size: 0.95em;
+  font-family: "courier new", monospace;
+  font-weight: 800;
+  line-height: 1.1;
+}
+
+#content blockquote {
+  margin-left: 40px;
+  max-width: 600px;
+  margin-right: 0;
+}
+
+#content blockquote blockquote {
+  margin-left: 10px;
+  padding-left: 10px;
+  margin-right: 20px;
+  border-left: dotted 1px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+#content blockquote blockquote > :first-child {
+  margin-top: 0;
+}
+
+#content blockquote blockquote > :last-child {
+  margin-bottom: 0;
+}
+
+main.long-content {
+  --long-content-padding-ratio: 0.10;
+}
+
+main.long-content .main-content-container,
+main.long-content > h1 {
+  padding-left: calc(var(--long-content-padding-ratio) * 100%);
+  padding-right: calc(var(--long-content-padding-ratio) * 100%);
+}
+
+dl dt {
+  padding-left: 40px;
+  max-width: 600px;
+}
+
+dl dt {
+  margin-bottom: 2px;
+}
+
+dl dd {
+  margin-bottom: 1em;
+}
+
+dl ul,
+dl ol {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+ul > li.has-details {
+  list-style-type: none;
+  margin-left: -17px;
+}
+
+.album-group-list dt {
+  font-style: oblique;
+  padding-left: 0;
+}
+
+.album-group-list dd {
+  margin-left: 0;
+}
+
+.group-chronology-link {
+  font-style: oblique;
+}
+
+#content hr {
+  border: 1px inset #808080;
+  width: 100%;
+}
+
+#content hr.split::before {
+  content: "(split)";
+  color: #808080;
+}
+
+#content hr.split {
+  position: relative;
+  overflow: hidden;
+  border: none;
+}
+
+#content hr.split::after {
+  display: inline-block;
+  content: "";
+  border: 1px inset #808080;
+  width: 100%;
+  position: absolute;
+  top: 50%;
+  margin-top: -2px;
+  margin-left: 10px;
+}
+
+li > ul {
+  margin-top: 5px;
+}
+
+.additional-files-list {
+  padding-left: 0;
+}
+
+.additional-files-list > li {
+  list-style-type: none;
+}
+
+.additional-files-list summary {
+  /* Sorry, Safari!
+   * https://bugs.webkit.org/show_bug.cgi?id=157323
+   */
+  list-style-position: outside;
+  margin-left: 40px;
+}
+
+.additional-files-list details ul {
+  margin-left: 40px;
+  margin-top: 2px;
+  margin-bottom: 10px;
+}
+
+.additional-files-list .entry-description {
+  list-style-type: none;
+  max-width: 540px;
+
+  /* This should be margin-bottom, but cascading rules
+   * cause some awkwardness - `#content li` takes precedence.
+   */
+  padding-bottom: 3px;
+}
+
+.group-contributions-table {
+  display: inline-block;
+}
+
+.group-contributions-table .group-contributions-row {
+  display: flex;
+  justify-content: space-between;
+}
+
+.group-contributions-table .group-contributions-metrics {
+  margin-left: 1.5ch;
+  white-space: nowrap;
+}
+
+.group-contributions-sorted-by-count:not(.visible),
+.group-contributions-sorted-by-duration:not(.visible) {
+  display: none;
+}
+
+.group-contributions-sort-button {
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+html[data-url-key="localized.albumCommentary"] li.no-commentary {
+  opacity: 0.7;
+}
+
+html[data-url-key="localized.albumCommentary"] .content-heading-main-title {
+  margin-right: 0.25em;
+}
+
+html[data-url-key="localized.albumCommentary"] .content-heading-accent {
+  font-weight: normal;
+  font-style: oblique;
+  font-size: 0.9rem;
+  display: inline-block;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="random"] #data-loading-line,
+html[data-url-key="localized.listing"][data-url-value0="random"] #data-loaded-line,
+html[data-url-key="localized.listing"][data-url-value0="random"] #data-error-line {
+  display: none;
+}
+
+html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not([href]) {
+  opacity: 0.7;
+}
+
+html[data-url-key="localized.newsEntry"] .read-another-links {
+  font-style: oblique;
+  font-size: 0.9em;
+}
+
+/* Additional names (heading and box) */
+
+h1 a[href="#additional-names-box"] {
+  color: inherit;
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+h1 a[href="#additional-names-box"]:hover {
+  text-decoration-style: solid;
+}
+
+#additional-names-box {
+  --custom-scroll-offset: calc(0.5em - 2px);
+
+  margin: 1em 0 1em -10px;
+  padding: 15px 20px 10px 20px;
+  width: max-content;
+  max-width: min(60vw, 600px);
+
+  border: 1px dotted var(--primary-color);
+  border-radius: 6px;
+
+  background:
+    linear-gradient(var(--bg-color), var(--bg-color)),
+    linear-gradient(#000000bb, #000000bb),
+    var(--primary-color);
+
+  box-shadow: 0 -2px 6px -1px var(--dim-color) inset;
+
+  display: none;
+}
+
+#additional-names-box > :first-child { margin-top: 0; }
+#additional-names-box > :last-child { margin-bottom: 0; }
+
+#additional-names-box p {
+  padding-left: 10px;
+  padding-right: 10px;
+  margin-bottom: 0;
+  font-style: oblique;
+}
+
+#additional-names-box ul {
+  padding-left: 10px;
+  margin-top: 0.5em;
+}
+
+#additional-names-box li .additional-name {
+  margin-right: 0.25em;
+}
+
+#additional-names-box li .additional-name .content-image {
+  margin-bottom: 0.25em;
+  margin-top: 0.5em;
+}
+
+#additional-names-box li .accent {
+  opacity: 0.8;
+}
+
+#additional-names-box li .additional-name > img {
+  vertical-align: text-bottom;
+}
+
+/* Images */
+
+.image-container {
+  display: block;
+  box-sizing: border-box;
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+
+  background-color: var(--dim-color);
+  border: 2px solid var(--primary-color);
+  border-radius: 0;
+  box-shadow: 0 2px 4px -2px var(--bg-black-color) inset;
+
+  text-align: left;
+  color: white;
+}
+
+.image-text-area {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  padding: 5px 15px;
+
+  background: rgba(0, 0, 0, 0.65);
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset;
+
+  line-height: 1.35em;
+  color: var(--primary-color);
+  font-style: oblique;
+  text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75);
+}
+
+.image-outer-area {
+  width: 100%;
+  height: 100%;
+  padding: 5px;
+  box-sizing: border-box;
+}
+
+.image-link {
+  border-bottom: 1px solid #ffffff03;
+  border-radius: 2.5px 2.5px 3px 3px;
+  box-shadow:
+    0 1px 8px -3px var(--bg-black-color);
+}
+
+.image-inner-area {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+.image-link .image-inner-area {
+  /* Jankily fix a rendering issue with border-radius on Safari.
+   * The `-webkit-` prefix is only to keep this from applying on
+   * other browsers (well, Firefox), where it doesn't *break*
+   * anything, but also isn't necessary.
+   */
+  -webkit-transform: translateZ(0);
+}
+
+img {
+  object-fit: cover;
+}
+
+.image-inner-area::after {
+  content: "";
+  display: block;
+  position: absolute;
+  pointer-events: none;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border-bottom-left-radius: 0.5px;
+  opacity: 0.035;
+  box-shadow:
+    6px -6px 2px -4px white inset;
+}
+
+img.pixelate, .pixelate img {
+  image-rendering: crisp-edges;
+}
+
+.reveal-text-container {
+  position: absolute;
+  top: 15px;
+  left: 10px;
+  right: 10px;
+  bottom: 10px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.grid-item .reveal-text {
+  font-size: 0.9em;
+}
+
+.reveal-text {
+  color: white;
+  text-align: center;
+  font-weight: bold;
+  padding-bottom: 0.5em;
+  font-size: 0.8rem;
+}
+
+.reveal-symbol {
+  display: inline-block;
+  width: 1em;
+  height: 1em;
+  margin-bottom: 0.1em;
+
+  font-size: 1.6em;
+  opacity: 0.8;
+  background-image: url("warning.svg");
+}
+
+.reveal-interaction {
+  opacity: 0.8;
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+.reveal .image {
+  opacity: 0.7;
+  filter: blur(20px) brightness(0.7);
+}
+
+.reveal .image.reveal-thumbnail {
+  position: absolute;
+  top: 0;
+  left: 0;
+  image-rendering: pixelated;
+}
+
+.reveal.has-reveal-thumbnail:not(.revealed) .image:not(.reveal-thumbnail) {
+  /* Keep the main image as part of the box model.
+   * It's what actually defines the dimensions of the
+   * image-container, so those dimensions never shift
+   * once the image is actually revealed.
+   */
+  visibility: hidden;
+}
+
+.reveal.revealed.has-reveal-thumbnail .image.reveal-thumbnail {
+  display: none !important;
+}
+
+.reveal.revealed .image {
+  filter: none;
+  opacity: 1;
+}
+
+.reveal.revealed .reveal-text-container {
+  display: none;
+}
+
+.reveal:not(.revealed) .image-outer-area > * {
+  --reveal-border-radius: 6px;
+  position: relative;
+  overflow: hidden;
+  border-radius: var(--reveal-border-radius);
+}
+
+.reveal:not(.revealed) .image-outer-area > *::after {
+  content: "";
+  position: absolute;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  border: 1px dotted var(--primary-color);
+  border-radius: var(--reveal-border-radius);
+  pointer-events: none;
+
+  /* By an awkward DOM intersection, this element might be
+   * .image-inner-area::after, which is already styled with
+   * a slight visual effect. Guarantee that the properties
+   * set to that end are overwritten, and fully co-opt it
+   * to serve as the interaction cue instead.
+   */
+  box-shadow: none;
+  opacity: 1;
+}
+
+.reveal:not(.revealed) .image-inner-area {
+  background: var(--deep-color);
+}
+
+.reveal:not(.revealed) .image-outer-area > *:hover::after {
+  border-style: solid;
+  box-shadow: 0 0 0 1.5px #00000099 inset;
+}
+
+.reveal:not(.revealed) .image-outer-area > *:hover .image {
+  filter: blur(20px) brightness(0.6);
+  opacity: 0.6;
+}
+
+.reveal:not(.revealed) .image-outer-area > *:hover .reveal-interaction {
+  text-decoration-style: solid;
+}
+
+.image-container.has-link:not(.no-image-preview) {
+  background: var(--deep-color);
+  box-shadow: none;
+  border-radius: 0 0 4px 4px;
+}
+
+.sidebar .image-container {
+  max-width: 350px;
+}
+
+/* Grid listings */
+
+.grid-listing {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: center;
+  align-items: flex-start;
+  padding: 5px 15px;
+}
+
+.grid-item {
+  font-size: 0.9em;
+}
+
+.grid-item {
+  display: inline-block;
+  text-align: center;
+  background-color: #111111;
+  border: 1px dotted var(--primary-color);
+  border-radius: 2px;
+  padding: 5px;
+  margin: 10px;
+}
+
+.grid-item .image-container {
+  width: 100%;
+}
+
+.grid-item .image-inner-area {
+  border-radius: 0;
+  box-shadow: none;
+}
+
+.grid-item .image-inner-area::after {
+  box-shadow: none;
+}
+
+.grid-item .image {
+  width: 100%;
+  height: 100% !important;
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
+.grid-item:hover {
+  text-decoration: none;
+}
+
+.grid-actions .grid-item:hover {
+  text-decoration: underline;
+}
+
+.grid-item > span {
+  display: block;
+  overflow-wrap: break-word;
+  hyphens: auto;
+}
+
+.grid-item > span:not(:first-child) {
+  margin-top: 2px;
+}
+
+.grid-item > span:first-of-type {
+  margin-top: 6px;
+}
+
+.grid-item > span:not(:first-of-type) {
+  font-size: 0.9em;
+  opacity: 0.8;
+}
+
+.grid-item:hover > span:first-of-type {
+  text-decoration: underline;
+}
+
+.grid-listing > .grid-item {
+  flex: 1 25%;
+  max-width: 200px;
+}
+
+.grid-actions {
+  display: flex;
+  flex-direction: row;
+  margin: 15px;
+  align-self: center;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+.grid-actions > .grid-item {
+  flex-basis: unset !important;
+  margin: 5px;
+  width: 120px;
+  --primary-color: inherit !important;
+  --dim-color: inherit !important;
+}
+
+/* Carousel */
+
+.carousel-container {
+  --carousel-tile-min-width: 120px;
+  --carousel-row-count: 3;
+  --carousel-column-count: 6;
+
+  position: relative;
+  overflow: hidden;
+  margin: 20px 0 5px 0;
+  padding: 8px 0;
+}
+
+.carousel-container::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: -20;
+  background-color: var(--dim-color);
+  filter: brightness(0.6);
+}
+
+.carousel-container::after {
+  content: "";
+  pointer-events: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  border: 1px solid var(--primary-color);
+  border-radius: 4px;
+  z-index: 40;
+  box-shadow:
+    inset 20px 2px 40px var(--shadow-color),
+    inset -20px -2px 40px var(--shadow-color);
+}
+
+.carousel-container:hover .carousel-grid {
+  animation-play-state: running;
+}
+
+html[data-url-key="localized.home"] .carousel-container {
+  --carousel-tile-size: 140px;
+}
+
+.carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; }
+.carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; }
+.carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; }
+.carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; }
+.carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; }
+.carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; }
+
+.carousel-grid:nth-child(2),
+.carousel-grid:nth-child(3) {
+  position: absolute;
+  top: 8px;
+  left: 0;
+  right: 0;
+}
+
+.carousel-grid:nth-child(2) {
+  animation-name: carousel-marquee2;
+}
+
+.carousel-grid:nth-child(3) {
+  animation-name: carousel-marquee3;
+}
+
+@keyframes carousel-marquee1 {
+  0% {
+    transform: translateX(-100%) translateX(70px);
+  }
+
+  100% {
+    transform: translateX(-200%) translateX(70px);
+  }
+}
+
+@keyframes carousel-marquee2 {
+  0% {
+    transform: translateX(0%) translateX(70px);
+  }
+
+  100% {
+    transform: translateX(-100%) translateX(70px);
+  }
+}
+
+@keyframes carousel-marquee3 {
+  0% {
+    transform: translateX(100%) translateX(70px);
+  }
+
+  100% {
+    transform: translateX(0%) translateX(70px);
+  }
+}
+
+.carousel-grid {
+  /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */
+  --carousel-gap-count: calc(var(--carousel-column-count) - 1);
+  --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px);
+  --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count));
+
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr));
+  grid-template-rows: repeat(var(--carousel-row-count), auto);
+  grid-auto-flow: dense;
+  grid-auto-rows: 0;
+  overflow: hidden;
+  margin: auto;
+
+  transform: translateX(0);
+  animation: carousel-marquee1 40s linear infinite;
+  animation-play-state: paused;
+  z-index: 5;
+}
+
+.carousel-item {
+  display: inline-block;
+  margin: 0;
+  flex: 1 1 150px;
+  padding: 3px;
+  border-radius: 10px;
+  filter: brightness(0.8);
+}
+
+.carousel-item .image-container {
+  border: none;
+  border-radius: 5px;
+}
+
+.carousel-item .image-outer-area {
+  padding: 0;
+}
+
+.carousel-item .image-inner-area::after {
+  box-shadow: none;
+}
+
+.carousel-item .image {
+  width: 100%;
+  height: 100%;
+  margin-top: auto;
+  margin-bottom: auto;
+}
+
+.carousel-item:hover {
+  filter: brightness(1);
+  background: var(--dim-color);
+}
+
+/* Info card */
+
+#info-card-container {
+  position: absolute;
+
+  left: 0;
+  right: 10px;
+
+  pointer-events: none; /* Padding area shouldn't 8e interactive. */
+  display: none;
+}
+
+#info-card-container.show,
+#info-card-container.hide {
+  display: flex;
+}
+
+#info-card-container > * {
+  flex-basis: 400px;
+}
+
+#info-card-container.show {
+  animation: 0.2s linear forwards info-card-show;
+  transition: top 0.1s, left 0.1s;
+}
+
+#info-card-container.hide {
+  animation: 0.2s linear forwards info-card-hide;
+}
+
+@keyframes info-card-show {
+  0% {
+    opacity: 0;
+    margin-top: -5px;
+  }
+
+  100% {
+    opacity: 1;
+    margin-top: 0;
+  }
+}
+
+@keyframes info-card-hide {
+  0% {
+    opacity: 1;
+    margin-top: 0;
+  }
+
+  100% {
+    opacity: 0;
+    margin-top: 5px;
+    display: none !important;
+  }
+}
+
+.info-card-decor {
+  padding-left: 3ch;
+  border-top: 1px solid white;
+}
+
+.info-card {
+  background-color: black;
+  color: white;
+
+  border: 1px dotted var(--primary-color);
+  border-radius: 3px;
+  box-shadow: 0 5px 5px black;
+
+  padding: 5px;
+  font-size: 0.9em;
+
+  pointer-events: none;
+}
+
+.info-card::after {
+  content: "";
+  display: block;
+  clear: both;
+}
+
+#info-card-container.show .info-card {
+  animation: 0.01s linear 0.2s forwards info-card-become-interactive;
+}
+
+@keyframes info-card-become-interactive {
+  to {
+    pointer-events: auto;
+  }
+}
+
+.info-card-art-container {
+  float: right;
+
+  width: 40%;
+  margin: 5px;
+  font-size: 0.8em;
+
+  /* Dynamically shown. */
+  display: none;
+}
+
+.info-card-art-container .image-container {
+  padding: 2px;
+}
+
+.info-card-art {
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+
+.info-card-name {
+  font-size: 1em;
+  border-bottom: 1px dotted;
+  margin: 0;
+}
+
+.info-card p {
+  margin-top: 0.25em;
+  margin-bottom: 0.25em;
+}
+
+.info-card p:last-child {
+  margin-bottom: 0;
+}
+
+/* Custom hash links */
+
+.content-heading {
+  border-bottom: 3px double transparent;
+  margin-bottom: -3px;
+}
+
+.content-heading.highlight-hash-link {
+  animation: highlight-hash-link 4s;
+  animation-delay: 125ms;
+}
+
+h3.content-heading {
+  clear: both;
+}
+
+/* This animation's name is referenced in JavaScript */
+@keyframes highlight-hash-link {
+  0% {
+    border-bottom-color: transparent;
+  }
+
+  10% {
+    border-bottom-color: white;
+  }
+
+  25% {
+    border-bottom-color: white;
+  }
+
+  100% {
+    border-bottom-color: transparent;
+  }
+}
+
+/* Sticky heading */
+
+[id] {
+  --custom-scroll-offset: 0px;
+}
+
+#content [id] {
+  /* Adjust scroll margin. */
+  scroll-margin-top: calc(
+      74px /* Sticky heading */
+    + 33px /* Sticky subheading */
+    - 1em  /* One line of text (align bottom) */
+    - 12px /* Padding for hanging letters & focus ring */
+    + var(--custom-scroll-offset) /* Customizable offset */
+  );
+}
+
+.content-sticky-heading-container {
+  position: sticky;
+  top: 0;
+
+  margin: calc(-1 * var(--content-padding));
+  margin-bottom: calc(0.5 * var(--content-padding));
+
+  transform: translateY(-5px);
+}
+
+main.long-content .content-sticky-heading-container {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+main.long-content .content-sticky-heading-container .content-sticky-heading-row,
+main.long-content .content-sticky-heading-container .content-sticky-subheading-row {
+  padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding));
+  padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding));
+}
+
+.content-sticky-heading-row {
+  box-sizing: border-box;
+  padding:
+    calc(1.25 * var(--content-padding) + 5px)
+    20px
+    calc(0.75 * var(--content-padding))
+    20px;
+
+  width: 100%;
+  margin: 0;
+
+  background: var(--bg-black-color);
+  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
+
+  -webkit-backdrop-filter: blur(6px);
+          backdrop-filter: blur(6px);
+}
+
+.content-sticky-heading-container.has-cover .content-sticky-heading-row,
+.content-sticky-heading-container.has-cover .content-sticky-subheading-row {
+  display: grid;
+  grid-template-areas:
+    "title cover";
+  grid-template-columns: 1fr min(40%, 400px);
+}
+
+.content-sticky-heading-row h1 {
+  margin: 0;
+  padding-right: 20px;
+}
+
+.content-sticky-heading-cover-container {
+  position: relative;
+  height: 0;
+  margin: -15px 0px -5px -5px;
+}
+
+.content-sticky-heading-cover-needs-reveal {
+  display: none;
+}
+
+.content-sticky-heading-cover {
+  position: absolute;
+  top: 0;
+  width: 80px;
+  right: 10px;
+  box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25);
+  transition: transform 0.35s, opacity 0.25s;
+}
+
+.content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover {
+  opacity: 0;
+  transform: translateY(15px);
+  transition: transform 0.35s, opacity 0.30s;
+}
+
+.content-sticky-heading-cover .image-container {
+  border-width: 1px;
+  border-radius: 1.25px;
+  box-shadow: none;
+}
+
+.content-sticky-heading-container .image-outer-area {
+  padding: 3px;
+}
+
+.content-sticky-heading-container .image-inner-area {
+  border-radius: 1.75px;
+  overflow: hidden;
+}
+
+.content-sticky-heading-cover .image {
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+
+.content-sticky-subheading-row {
+  position: absolute;
+  width: 100%;
+  box-sizing: border-box;
+  padding: 10px 20px 5px 20px;
+  margin-top: 0;
+  z-index: -1;
+
+  background: var(--bg-black-color);
+  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
+  box-shadow:
+    0 2px 2px -1px #00000060,
+    0 4px 12px -4px #00000090;
+
+  -webkit-backdrop-filter: blur(4px);
+          backdrop-filter: blur(4px);
+
+  transition: margin-top 0.35s, opacity 0.25s;
+}
+
+.content-sticky-subheading-row h2 {
+  margin: 0;
+
+  font-size: 0.9em !important;
+  font-weight: normal;
+  font-style: oblique;
+  color: #eee;
+}
+
+.content-sticky-subheading-row:not(.visible) {
+  margin-top: -20px;
+  opacity: 0;
+}
+
+.content-sticky-subheading {
+  padding-right: 20px;
+}
+
+.content-sticky-heading-container h2.visible {
+  margin-top: 0;
+  opacity: 1;
+}
+
+.content-sticky-heading-row {
+  box-shadow:
+    inset 0 10px 10px -5px var(--shadow-color),
+    0 4px 8px -4px #000000b0;
+}
+
+#content, .sidebar {
+  contain: paint;
+}
+
+/* Sticky sidebar */
+
+.sidebar-column.sidebar.sticky-column,
+.sidebar-column.sidebar.sticky-last,
+.sidebar-multiple.sticky-last > .sidebar:last-child,
+.sidebar-multiple.sticky-column {
+  position: sticky;
+  top: 10px;
+}
+
+.sidebar-multiple.sticky-last {
+  align-self: stretch;
+}
+
+.sidebar-multiple.sticky-column {
+  align-self: flex-start;
+}
+
+.sidebar-column.sidebar.sticky-column {
+  max-height: calc(100vh - 20px);
+  align-self: start;
+  padding-bottom: 0;
+  box-sizing: border-box;
+  flex-basis: 275px;
+  padding-top: 0;
+  overflow-y: scroll;
+  scrollbar-width: thin;
+  scrollbar-color: var(--dark-color);
+}
+
+.sidebar-column.sidebar.sticky-column::-webkit-scrollbar {
+  background: var(--dark-color);
+  width: 12px;
+}
+
+.sidebar-column.sidebar.sticky-column::-webkit-scrollbar-thumb {
+  transition: background 0.2s;
+  background: rgba(255, 255, 255, 0.2);
+  border: 3px solid transparent;
+  border-radius: 10px;
+  background-clip: content-box;
+}
+
+.sidebar-column.sidebar.sticky-column > h1 {
+  position: sticky;
+  top: 0;
+  z-index: 2;
+
+  margin: 0 calc(-1 * var(--content-padding));
+  margin-bottom: 10px;
+
+  border-bottom: 1px dotted rgba(220, 220, 220, 0.4);
+  padding: 10px 5px;
+
+  background: var(--bg-black-color);
+  -webkit-backdrop-filter: blur(4px);
+  backdrop-filter: blur(4px);
+
+  box-shadow:
+    0 2px 3px -1px #0006,
+    0 4px 8px -2px #0009;
+}
+
+/* Image overlay */
+
+#image-overlay-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+
+  background: rgba(0, 0, 0, 0.8);
+  color: white;
+  padding: 20px 40px;
+  box-sizing: border-box;
+
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.4s;
+
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+#image-overlay-container::before {
+  content: '';
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+
+  background: var(--deep-color);
+  opacity: 0.20;
+}
+
+#image-overlay-container.visible {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+#image-overlay-content-container {
+  border-radius: 0 0 8px 8px;
+  border: 2px solid var(--primary-color);
+  background: var(--deep-ghost-color);
+  overflow: hidden;
+
+  box-shadow:
+    0 0 90px 30px #00000060,
+    0 0 20px 10px #00000040,
+    0 0 10px 3px #00000080;
+
+  -webkit-backdrop-filter: blur(3px);
+          backdrop-filter: blur(3px);
+}
+
+#image-overlay-image-container {
+  display: block;
+  position: relative;
+  overflow: hidden;
+  width: 80vmin;
+  height: 80vmin;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+#image-overlay-image,
+#image-overlay-image-thumb {
+  display: inline-block;
+  object-fit: contain;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.65);
+}
+
+#image-overlay-image {
+  position: absolute;
+  top: 3px;
+  left: 3px;
+  width: calc(100% - 6px);
+  height: calc(100% - 4px);
+}
+
+#image-overlay-image-thumb {
+  filter: blur(16px);
+  transform: scale(1.5);
+}
+
+#image-overlay-container.loaded #image-overlay-image-thumb {
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.25s;
+}
+
+#image-overlay-image-container::after {
+  content: "";
+  display: block;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  height: 4px;
+  width: var(--download-progress);
+  background: var(--primary-color);
+  box-shadow: 0 -3px 12px 4px var(--primary-color);
+  transition: 0.25s;
+}
+
+#image-overlay-container.loaded #image-overlay-image-container::after {
+  width: 100%;
+  background: white;
+  opacity: 0;
+}
+
+#image-overlay-container.errored #image-overlay-image-container::after {
+  width: 100%;
+  background: red;
+}
+
+#image-overlay-container:not(.visible) #image-overlay-image-container::after {
+  width: 0 !important;
+}
+
+#image-overlay-action-container {
+  padding: 7px 4px 7px 4px;
+  border-radius: 0 0 5px 5px;
+  background: var(--bg-black-color);
+  color: white;
+  font-style: oblique;
+  text-align: center;
+  box-shadow:
+    0 3px 8px -5px var(--primary-color) inset;
+}
+
+#image-overlay-container #image-overlay-action-content-without-size:not(.visible),
+#image-overlay-container #image-overlay-action-content-with-size:not(.visible),
+#image-overlay-container #image-overlay-file-size-warning:not(.visible),
+#image-overlay-container #image-overlay-file-size-kilobytes:not(.visible),
+#image-overlay-container #image-overlay-file-size-megabytes:not(.visible) {
+  display: none;
+}
+
+#image-overlay-file-size-warning {
+  opacity: 0.8;
+  font-size: 0.9em;
+}
+
+/* important easter egg mode */
+
+html[data-language-code="preview-en"][data-url-key="localized.home"] #content
+  h1::after {
+  font-family: cursive;
+  display: block;
+  content: "(Preview Build)";
+  font-size: 0.8em;
+}
+
+/* Layout - Wide (most computers) */
+
+@media (min-width: 900px) {
+  #page-container:not(.has-zero-sidebars) #secondary-nav {
+    display: none;
+  }
+}
+
+/* Layout - Medium (tablets, some landscape mobiles)
+ *
+ * Note: Rules defined here are exclusive to "medium" width, i.e. they don't
+ * additionally apply to "thin". Use the later section which applies to both
+ * if so desired.
+ */
+
+@media (min-width: 600px) and (max-width: 899.98px) {
+  /* Medium layout is mainly defined (to the user) by hiding the sidebar, so
+   * don't apply the similar layout change of widening the long-content area
+   * if this page doesn't have a sidebar to hide in the first place.
+   */
+  #page-container:not(.has-zero-sidebars) main.long-content {
+    --long-content-padding-ratio: 0.06;
+  }
+}
+
+/* Layout - Wide or Medium */
+
+@media (min-width: 600px) {
+  .content-sticky-heading-container {
+    /* Safari doesn't always play nicely with position: sticky,
+     * this seems to fix images sometimes displaying above the
+     * position: absolute subheading (h2) child
+     *
+     * See also: https://stackoverflow.com/questions/50224855/
+     */
+    transform: translate3d(0, 0, 0);
+    z-index: 1;
+  }
+
+  /* Cover art floats to the right. It's positioned in HTML beneath the
+   * heading, so pull it up a little to "float" on top.
+   */
+  #cover-art-container {
+    float: right;
+    width: 40%;
+    max-width: 400px;
+    margin: -60px 0 10px 20px;
+
+    position: relative;
+    z-index: 2;
+  }
+
+  html[data-url-key="localized.home"] #page-container.has-one-sidebar .grid-listing > .grid-item:not(:nth-child(n+7)) {
+    flex-basis: 23%;
+    margin: 15px;
+  }
+
+  html[data-url-key="localized.home"] #page-container.has-one-sidebar .grid-listing > .grid-item:nth-child(n+7) {
+    flex-basis: 18%;
+    margin: 10px;
+  }
+}
+
+/* Layout - Medium or Thin */
+
+@media (max-width: 899.98px) {
+  .sidebar-column:not(.no-hide) {
+    display: none;
+  }
+
+  #secondary-nav {
+    display: block;
+  }
+
+  .layout-columns.vertical-when-thin {
+    flex-direction: column;
+  }
+
+  .layout-columns.vertical-when-thin > *:not(:last-child) {
+    margin-bottom: 10px;
+  }
+
+  .sidebar-column.no-hide {
+    max-width: unset !important;
+    flex-basis: unset !important;
+    margin-right: 0 !important;
+    margin-left: 0 !important;
+    width: 100%;
+  }
+
+  .sidebar .news-entry:not(.first-news-entry) {
+    display: none;
+  }
+
+  .grid-listing > .grid-item {
+    flex-basis: 40%;
+  }
+}
+
+/* Layout - Thin (phones) */
+
+@media (max-width: 600px) {
+  .content-columns {
+    columns: 1;
+  }
+
+  main.long-content {
+    --long-content-padding-ratio: 0.02;
+  }
+
+  #cover-art-container {
+    margin: 25px 0 5px 0;
+    width: 100%;
+    max-width: unset;
+  }
+
+  #additional-names-box {
+    width: unset;
+    max-width: unset;
+  }
+
+  .nav-has-content .nav-main-links .nav-link-accent {
+    display: block;
+  }
+
+  /* Show sticky heading above cover art */
+
+  .content-sticky-heading-container {
+    z-index: 2;
+  }
+
+  .content-sticky-heading-row h1 {
+    padding-right: 10px;
+  }
+
+  /* Let sticky heading text span past lower-index cover art */
+
+  .content-sticky-heading-container.has-cover .content-sticky-heading-row,
+  .content-sticky-heading-container.has-cover .content-sticky-subheading-row {
+    grid-template-columns: 1fr 90px;
+  }
+
+  /* Disable grid features, just line header children up vertically */
+
+  #header {
+    display: block;
+  }
+
+  #header > div:not(:first-child) {
+    margin-top: 0.5em;
+  }
+}
diff --git a/src/static/warning.svg b/src/static/warning.svg
new file mode 100644
index 0000000..92e5577
--- /dev/null
+++ b/src/static/warning.svg
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="64mm"
+   height="64mm"
+   viewBox="0 0 64 64"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
+   sodipodi:docname="warning.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:showpageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#505050"
+     inkscape:document-units="mm"
+     showgrid="false"
+     inkscape:zoom="2.2162213"
+     inkscape:cx="164.24353"
+     inkscape:cy="147.77405"
+     inkscape:window-width="1309"
+     inkscape:window-height="865"
+     inkscape:window-x="172"
+     inkscape:window-y="117"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2">
+    <inkscape:path-effect
+       effect="bool_op"
+       operand-path="#path1050"
+       id="path-effect1430"
+       is_visible="true"
+       lpeversion="1"
+       operation="diff"
+       swap-operands="false"
+       filltype-this="positive"
+       filter=""
+       filltype-operand="nonzero" />
+    <filter
+       id="selectable_hidder_filter"
+       width="1"
+       height="1"
+       x="0"
+       y="0"
+       style="color-interpolation-filters:sRGB;"
+       inkscape:label="LPE boolean visibility">
+      <feComposite
+         id="boolops_hidder_primitive"
+         result="composite1"
+         operator="arithmetic"
+         in2="SourceGraphic"
+         in="BackgroundImage" />
+    </filter>
+    <inkscape:path-effect
+       effect="bool_op"
+       operand-path=""
+       id="path-effect1410"
+       is_visible="true"
+       lpeversion="1"
+       operation="diff"
+       swap-operands="false"
+       filltype-this="from-curve"
+       filter=""
+       filltype-operand="from-curve" />
+    <linearGradient
+       id="linearGradient1106"
+       inkscape:swatch="solid">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop1104" />
+    </linearGradient>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       id="path1458"
+       style="color:#000000;fill:#ffffff;stroke-width:11;stroke-linejoin:round;-inkscape-stroke:none;paint-order:fill markers stroke"
+       d="M 32.000114 8.4041382 A 5.50055 5.50055 0 0 0 27.320296 11.013798 L 4.9805745 47.209005 A 5.50055 5.50055 0 0 0 9.6619425 55.59764 L 54.337769 55.59764 A 5.50055 5.50055 0 0 0 59.019653 47.209005 L 36.679932 11.013798 A 5.50055 5.50055 0 0 0 32.000114 8.4041382 z M 29.123287 18.81849 L 34.876941 18.81849 A 1.069827 1.069827 0 0 1 35.945093 19.935734 L 35.052641 39.585697 A 1.069827 1.069827 0 0 1 33.808789 40.593905 C 33.210998 40.494454 32.606121 40.443789 32.000114 40.443526 C 31.395269 40.445711 30.791425 40.496549 30.195056 40.597522 A 1.069827 1.069827 0 0 1 28.94707 39.591899 L 28.054618 19.935734 A 1.069827 1.069827 0 0 1 29.123287 18.81849 z M 32.000114 42.910042 A 3.5582614 3.5582614 0 0 1 35.558036 46.468481 A 3.5582614 3.5582614 0 0 1 32.000114 50.026404 A 3.5582614 3.5582614 0 0 1 28.441675 46.468481 A 3.5582614 3.5582614 0 0 1 32.000114 42.910042 z " />
+  </g>
+</svg>