« get me outta code hell

client: client modules - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static/js
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-06-19 19:50:55 -0300
committer(quasar) nebula <qznebula@protonmail.com>2024-06-20 10:01:05 -0300
commitdf5f6048e6a177b5ce3920d10a1249129c669a1f (patch)
treef94c984a6f21069c02e33a8b9184580916606041 /src/static/js
parente254ad385d33739d3e6b4a04d79325e2bca840b8 (diff)
client: client modules
Diffstat (limited to 'src/static/js')
-rw-r--r--src/static/js/client-util.js122
-rw-r--r--src/static/js/client.js4956
-rw-r--r--src/static/js/client/additional-names-box.js83
-rw-r--r--src/static/js/client/album-commentary-sidebar.js212
-rw-r--r--src/static/js/client/artist-external-link-tooltip.js196
-rw-r--r--src/static/js/client/css-compatibility-assistant.js22
-rw-r--r--src/static/js/client/datetimestamp-tooltip.js36
-rw-r--r--src/static/js/client/hash-link.js146
-rw-r--r--src/static/js/client/hoverable-tooltip.js1083
-rw-r--r--src/static/js/client/index.js226
-rw-r--r--src/static/js/client/live-mouse-position.js21
-rw-r--r--src/static/js/client/quick-description.js62
-rw-r--r--src/static/js/client/scripted-link.js275
-rw-r--r--src/static/js/client/sidebar-search.js903
-rw-r--r--src/static/js/client/sticky-heading.js257
-rw-r--r--src/static/js/client/summary-nested-link.js48
-rw-r--r--src/static/js/client/text-with-tooltip.js34
-rw-r--r--src/static/js/client/wiki-search.js239
-rw-r--r--src/static/js/group-contributions-table.js35
-rw-r--r--src/static/js/image-overlay.js256
-rw-r--r--src/static/js/info-card.js181
-rw-r--r--src/static/js/localization-nonsense.js30
-rw-r--r--src/static/js/rectangles.js513
23 files changed, 4980 insertions, 4956 deletions
diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js
new file mode 100644
index 00000000..f06b707a
--- /dev/null
+++ b/src/static/js/client-util.js
@@ -0,0 +1,122 @@
+/* eslint-env browser */
+
+export function rebase(href, rebaseKey = 'rebaseLocalized') {
+  const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/';
+  if (relative) {
+    return relative + href;
+  } else {
+    return href;
+  }
+}
+
+export 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);
+    }
+  }
+}
+
+export function templateContent(el) {
+  if (el === null) {
+    return null;
+  }
+
+  if (el?.nodeName !== 'TEMPLATE') {
+    throw new Error(`Expected a <template> element`);
+  }
+
+  return el.content.cloneNode(true);
+}
+
+// Curry-style, so multiple points can more conveniently be tested at once.
+export function pointIsOverAnyOf(elements) {
+  return (clientX, clientY) => {
+    const element = document.elementFromPoint(clientX, clientY);
+    return elements.some(el => el.contains(element));
+  };
+}
+
+export 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}`);
+*/
+
+export const openAlbum = d => rebase(`album/${d}`);
+export const openArtTag = d => rebase(`tag/${d}`);
+export const openArtist = d => rebase(`artist/${d}`);
+export const openFlash = d => rebase(`flash/${d}`);
+export const openGroup = d => rebase(`group/${d}`);
+export const openTrack = d => rebase(`track/${d}`);
+
+// TODO: This should also use urlSpec.
+
+/*
+export function fetchData(type, directory) {
+  return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData')).then(
+    (res) => res.json()
+  );
+}
+*/
+
+// TODO: This should probably be imported from another file.
+export function dispatchInternalEvent(event, eventName, ...args) {
+  const info = event[Symbol.for('hsmusic.clientInfo')];
+
+  if (!info) {
+    throw new Error(`Expected event to be stored on clientInfo`);
+  }
+
+  const infoName = info.id;
+
+  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.error(`Uncaught error in listener for ${infoName}.${eventName}`);
+      console.error(error);
+      results.push(undefined);
+    }
+  }
+
+  return results;
+}
diff --git a/src/static/js/client.js b/src/static/js/client.js
deleted file mode 100644
index 21c3911a..00000000
--- a/src/static/js/client.js
+++ /dev/null
@@ -1,4956 +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.
-
-import {getColors} from '../shared-util/colors.js';
-
-import {
-  accumulateSum,
-  atOffset,
-  empty,
-  filterMultipleArrays,
-  promiseWithResolvers,
-  stitchArrays,
-  withEntries,
-} from '../shared-util/sugar.js';
-
-import {fetchWithProgress} from './xhr-util.js';
-
-const clientInfo = window.hsmusicClientInfo = Object.create(null);
-
-const clientSteps = {
-  getPageReferences: [],
-  addInternalListeners: [],
-  mutatePageContent: [],
-  initializeState: [],
-  addPageListeners: [],
-};
-
-function initInfo(infoKey, description) {
-  const object = {...description};
-
-  for (const obj of [
-    object,
-    object.state,
-    object.settings,
-    object.event,
-  ]) {
-    if (!obj) continue;
-    Object.preventExtensions(obj);
-  }
-
-  if (object.session) {
-    const sessionSpecs = object.session;
-
-    object.session = {};
-
-    for (const [key, spec] of Object.entries(sessionSpecs)) {
-      const hasSpec =
-        typeof spec === 'object' && spec !== null;
-
-      const defaultValue =
-        (hasSpec
-          ? spec.default ?? null
-          : spec);
-
-      let formatRead = value => value;
-      let formatWrite = value => value;
-      if (hasSpec && spec.type) {
-        switch (spec.type) {
-          case 'number':
-            formatRead = parseFloat;
-            formatWrite = String;
-            break;
-
-          case 'boolean':
-            formatRead = Boolean;
-            formatWrite = String;
-            break;
-
-          case 'string':
-            formatRead = String;
-            formatWrite = String;
-            break;
-
-          case 'json':
-            formatRead = JSON.parse;
-            formatWrite = JSON.stringify;
-            break;
-
-          default:
-            throw new Error(`Unknown type for session storage spec "${spec.type}"`);
-        }
-      }
-
-      let getMaxLength =
-        (!hasSpec
-          ? () => Infinity
-       : typeof spec.maxLength === 'function'
-          ? (object.settings
-              ? () => spec.maxLength(object.settings)
-              : () => spec.maxLength())
-          : () => spec.maxLength);
-
-      const storageKey = `hsmusic.${infoKey}.${key}`;
-
-      let fallbackValue = defaultValue;
-
-      Object.defineProperty(object.session, key, {
-        get: () => {
-          let value;
-          try {
-            value = sessionStorage.getItem(storageKey) ?? defaultValue;
-          } catch (error) {
-            if (error instanceof DOMException) {
-              value = fallbackValue;
-            } else {
-              throw error;
-            }
-          }
-
-          if (value === null) {
-            return null;
-          }
-
-          return formatRead(value);
-        },
-
-        set: (value) => {
-          if (value !== null && value !== '') {
-            value = formatWrite(value);
-          }
-
-          if (value === null) {
-            value = '';
-          }
-
-          const maxLength = getMaxLength();
-          if (value.length > maxLength) {
-            console.warn(
-              `Requested to set session storage ${storageKey} ` +
-              `beyond maximum length ${maxLength}, ` +
-              `ignoring this value.`);
-            console.trace();
-            return;
-          }
-
-          let operation;
-          if (value === '') {
-            fallbackValue = null;
-            operation = () => {
-              sessionStorage.removeItem(storageKey);
-            };
-          } else {
-            fallbackValue = value;
-            operation = () => {
-              sessionStorage.setItem(storageKey, value);
-            };
-          }
-
-          try {
-            operation();
-          } catch (error) {
-            if (!(error instanceof DOMException)) {
-              throw error;
-            }
-          }
-        },
-      });
-    }
-
-    Object.preventExtensions(object.session);
-  }
-
-  clientInfo[infoKey] = 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);
-    }
-  }
-}
-
-function templateContent(el) {
-  if (el === null) {
-    return null;
-  }
-
-  if (el?.nodeName !== 'TEMPLATE') {
-    throw new Error(`Expected a <template> element`);
-  }
-
-  return el.content.cloneNode(true);
-}
-
-// 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 openArtTag = d => rebase(`tag/${d}`);
-const openArtist = d => rebase(`artist/${d}`);
-const openFlash = d => rebase(`flash/${d}`);
-const openGroup = d => rebase(`group/${d}`);
-const openTrack = d => rebase(`track/${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.error(`Uncaught error in listener for ${infoName}.${eventName}`);
-      console.error(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');
-        }
-      }
-    });
-}
-
-// Links nested in summaries ------------------------------
-
-const summaryNestedLinksInfo = initInfo('summaryNestedLinksInfo', {
-  summaries: null,
-  links: null,
-});
-
-function getSummaryNestedLinksReferences() {
-  const info = summaryNestedLinksInfo;
-
-  info.summaries =
-    Array.from(document.getElementsByTagName('summary'));
-
-  info.links =
-    info.summaries
-      .map(summary =>
-        Array.from(summary.getElementsByTagName('a')));
-
-  filterMultipleArrays(
-    info.summaries,
-    info.links,
-    (_summary, links) => !empty(links));
-}
-
-function addSummaryNestedLinksPageListeners() {
-  const info = summaryNestedLinksInfo;
-
-  for (const {summary, links} of stitchArrays({
-    summary: info.summaries,
-    links: info.links,
-  })) {
-    for (const link of links) {
-      link.addEventListener('mouseover', () => {
-        link.classList.add('nested-hover');
-        summary.classList.add('has-nested-hover');
-      });
-
-      link.addEventListener('mouseout', () => {
-        link.classList.remove('nested-hover');
-        summary.classList.remove('has-nested-hover');
-      });
-    }
-  }
-}
-
-clientSteps.getPageReferences.push(getSummaryNestedLinksReferences);
-clientSteps.getPageReferences.push(addSummaryNestedLinksPageListeners);
-
-// 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(),
-
-    // This is a two-item array that tracks the direction we've already
-    // dynamically placed the current tooltip. If we *reposition* the tooltip
-    // (because its dimensions changed), we'll try to follow this anchor first.
-    dynamicTooltipAnchorDirection: null,
-  },
-
-  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;
-
-  state.dynamicTooltipAnchorDirection = 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);
-
-  // After a tooltip is shown, if we *didn't* specify an anchor,
-  // assume it was shown in its default position - generally presented
-  // as down and to the right. Successive repositioning will base on this.
-  state.dynamicTooltipAnchorDirection ??= ['down', 'right'];
-
-  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 repositionCurrentTooltip() {
-  const {state} = hoverableTooltipInfo;
-  const {currentlyActiveHoverable} = state;
-
-  if (!currentlyActiveHoverable) {
-    throw new Error(`No hoverable active to reposition tooltip from`);
-  }
-
-  positionTooltipFromHoverableWithBrains(currentlyActiveHoverable);
-}
-
-function positionTooltipFromHoverableWithBrains(hoverable) {
-  const {state} = hoverableTooltipInfo;
-  const {tooltip} = state.registeredHoverables.get(hoverable);
-
-  const anchorDirection = state.dynamicTooltipAnchorDirection;
-
-  // 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;
-  }
-
-  const tryDirection = (dir1, dir2, i) => {
-    selectedRect = opportunities[dir1][dir2][i];
-    return !!selectedRect;
-  };
-
-  let selectedRect = null;
-  selectRect: {
-    if (anchorDirection) {
-      for (let i = 0; i < numBaselineRects; i++) {
-        if (tryDirection(...anchorDirection, i)) {
-          break selectRect;
-        }
-      }
-    }
-
-    for (let i = 0; i < numBaselineRects; i++) {
-      for (const [dir1, dir2] of [
-        ['right', 'down'],
-        ['left', 'down'],
-        ['right', 'up'],
-        ['left', 'up'],
-        ['down', 'right'],
-        ['down', 'left'],
-        ['up', 'right'],
-        ['up', 'left'],
-      ]) {
-        if (tryDirection(dir1, dir2, i)) {
-          state.dynamicTooltipAnchorDirection = [dir1, dir2];
-          break selectRect;
-        }
-      }
-    }
-
-    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;
-
-  const upTopDown =
-    WikiRect.beneath(
-      hoverableRect.top + neededVerticalOverlap - tooltipRect.height);
-
-  const downBottomUp =
-    WikiRect.above(
-      hoverableRect.bottom - neededVerticalOverlap + tooltipRect.height);
-
-  // Please don't ask us to make this but horizontal?
-  const prepareVerticalOrientationRects = (regionRects) => {
-    const orientations = {};
-
-    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 rightRightLeft =
-    WikiRect.leftOf(
-      hoverableRect.left - neededHorizontalOverlap + tooltipRect.width);
-
-  const leftLeftRight =
-    WikiRect.rightOf(
-      hoverableRect.left + neededHorizontalOverlap - tooltipRect.width);
-
-  // Oops.
-  const prepareHorizontalOrientationRects = (regionRects) => {
-    const orientations = {};
-
-    const orientVertically = (rect, i) => {
-      if (!rect) return null;
-
-      const regionRect = regionRects[i];
-
-      if (regionRect.height > 0) {
-        return rect;
-      } else {
-        return WikiRect.fromRect({
-          x: rect.x,
-          y: regionRect.bottom - tooltipRect.height,
-          width: rect.width,
-          height: rect.height,
-        });
-      }
-    };
-
-    orientations.left =
-      regionRects
-        .map(rect => rect?.intersectionWith(leftLeftRight))
-        .map(orientVertically)
-        .map(keepIfFits);
-
-    orientations.right =
-      regionRects
-        .map(rect => rect?.intersectionWith(rightRightLeft))
-        .map(rect =>
-          (rect
-            ? rect.intersectionWith(WikiRect.fromRect({
-                x: rect.right - tooltipRect.width,
-                y: rect.y,
-                width: rect.width,
-                height: tooltipRect.height,
-              }))
-            : null))
-        .map(orientVertically)
-        .map(keepIfFits);
-
-    // No analogous center because we don't actually use
-    // center alignment...
-
-    return orientations;
-  };
-
-  const orientationRects = {
-    left: prepareVerticalOrientationRects(regionRects.left),
-    right: prepareVerticalOrientationRects(regionRects.right),
-    down: prepareHorizontalOrientationRects(regionRects.bottom),
-    up: prepareHorizontalOrientationRects(regionRects.top),
-  };
-
-  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 =
-      templateContent(
-        closestHeading.querySelector('.content-heading-sticky-title')) ??
-      closestHeading.querySelector('.content-heading-main-title') ??
-      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');
-    }
-  }
-}
-
-async 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);
-
-  const showProgress = amount => {
-    cssProp(container, '--download-progress', `${amount * 100}%`);
-  };
-
-  showProgress(0.00);
-
-  const response =
-    await fetchWithProgress(mainSrc, progress => {
-      if (progress === -1) {
-        // TODO: Indeterminate response progress cue
-        showProgress(0.00);
-      } else {
-        showProgress(0.20 + 0.80 * progress);
-      }
-    });
-
-  if (!response.status.toString().startsWith('2')) {
-    handleMainImageErrored();
-    return;
-  }
-
-  const blob = await response.blob();
-  const blobSrc = URL.createObjectURL(blob);
-
-  mainImage.src = blobSrc;
-  showProgress(1.00);
-
-  function handleMainImageLoaded() {
-    container.classList.add('loaded');
-    removeEventListeners();
-  }
-
-  function handleMainImageErrored() {
-    container.classList.add('errored');
-    removeEventListeners();
-  }
-
-  function removeEventListeners() {
-    mainImage.removeEventListener('load', handleMainImageLoaded);
-    mainImage.removeEventListener('error', handleMainImageErrored);
-  }
-}
-
-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();
-
-// "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('contribution-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');
-  }
-
-  repositionCurrentTooltip();
-}
-
-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);
-
-// Quick description --------------------------------------
-
-const quickDescriptionInfo = initInfo('quickDescriptionInfo', {
-  quickDescriptionContainers: null,
-
-  quickDescriptionsAreExpandable: null,
-
-  expandDescriptionLinks: null,
-  collapseDescriptionLinks: null,
-});
-
-function getQuickDescriptionReferences() {
-  const info = quickDescriptionInfo;
-
-  info.quickDescriptionContainers =
-    Array.from(document.querySelectorAll('#content .quick-description'));
-
-  info.quickDescriptionsAreExpandable =
-    info.quickDescriptionContainers
-      .map(container =>
-        container.querySelector('.quick-description-actions.when-expanded'));
-
-  info.expandDescriptionLinks =
-    info.quickDescriptionContainers
-      .map(container =>
-        container.querySelector('.quick-description-actions .expand-link'));
-
-  info.collapseDescriptionLinks =
-    info.quickDescriptionContainers
-      .map(container =>
-        container.querySelector('.quick-description-actions .collapse-link'));
-}
-
-function addQuickDescriptionListeners() {
-  const info = quickDescriptionInfo;
-
-  for (const {
-    isExpandable,
-    container,
-    expandLink,
-    collapseLink,
-  } of stitchArrays({
-    isExpandable: info.quickDescriptionsAreExpandable,
-    container: info.quickDescriptionContainers,
-    expandLink: info.expandDescriptionLinks,
-    collapseLink: info.collapseDescriptionLinks,
-  })) {
-    if (!isExpandable) continue;
-
-    expandLink.addEventListener('click', event => {
-      event.preventDefault();
-      container.classList.add('expanded');
-      container.classList.remove('collapsed');
-    });
-
-    collapseLink.addEventListener('click', event => {
-      event.preventDefault();
-      container.classList.add('collapsed');
-      container.classList.remove('expanded');
-    });
-  }
-}
-
-clientSteps.getPageReferences.push(getQuickDescriptionReferences);
-clientSteps.addPageListeners.push(addQuickDescriptionListeners);
-
-// Internal search functionality --------------------------
-
-const wikiSearchInfo = initInfo('wikiSearchInfo', {
-  state: {
-    worker: null,
-
-    workerReadyPromise: null,
-    workerReadyPromiseResolvers: null,
-
-    workerActionCounter: 0,
-    workerActionPromiseResolverMap: new Map(),
-
-    downloads: Object.create(null),
-  },
-
-  event: {
-    whenWorkerAlive: [],
-    whenWorkerReady: [],
-    whenWorkerFailsToInitialize: [],
-    whenWorkerHasRuntimeError: [],
-
-    whenDownloadBegins: [],
-    whenDownloadsBegin: [],
-    whenDownloadProgresses: [],
-    whenDownloadEnds: [],
-  },
-});
-
-async function initializeSearchWorker() {
-  const {state} = wikiSearchInfo;
-
-  if (state.worker) {
-    return await state.workerReadyPromise;
-  }
-
-  state.worker =
-    new Worker(
-      import.meta.resolve('./search-worker.js'),
-      {type: 'module'});
-
-  state.worker.onmessage = handleSearchWorkerMessage;
-
-  const {promise, resolve, reject} = promiseWithResolvers();
-
-  state.workerReadyPromiseResolvers = {resolve, reject};
-
-  return await (state.workerReadyPromise = promise);
-}
-
-function handleSearchWorkerMessage(message) {
-  switch (message.data.kind) {
-    case 'status':
-      handleSearchWorkerStatusMessage(message);
-      break;
-
-    case 'result':
-      handleSearchWorkerResultMessage(message);
-      break;
-
-    case 'download-begun':
-      handleSearchWorkerDownloadBegunMessage(message);
-      break;
-
-    case 'download-progress':
-      handleSearchWorkerDownloadProgressMessage(message);
-      break;
-
-    case 'download-complete':
-      handleSearchWorkerDownloadCompleteMessage(message);
-      break;
-
-    default:
-      console.warn(`Unknown message kind "${message.data.kind}" <- from search worker`);
-      break;
-  }
-}
-
-function handleSearchWorkerStatusMessage(message) {
-  const {state, event} = wikiSearchInfo;
-
-  switch (message.data.status) {
-    case 'alive':
-      console.debug(`Search worker is alive, but not yet ready.`);
-      dispatchInternalEvent(event, 'whenWorkerAlive');
-      break;
-
-    case 'ready':
-      console.debug(`Search worker has loaded corpuses and is ready.`);
-      state.workerReadyPromiseResolvers.resolve(state.worker);
-      dispatchInternalEvent(event, 'whenWorkerReady');
-      break;
-
-    case 'setup-error':
-      console.debug(`Search worker failed to initialize.`);
-      state.workerReadyPromiseResolvers.reject(new Error('Received "setup-error" status from worker'));
-      dispatchInternalEvent(event, 'whenWorkerFailsToInitialize');
-      break;
-
-    case 'runtime-error':
-      console.debug(`Search worker had an uncaught runtime error.`);
-      dispatchInternalEvent(event, 'whenWorkerHasRuntimeError');
-      break;
-
-    default:
-      console.warn(`Unknown status "${message.data.status}" <- from search worker`);
-      break;
-  }
-}
-
-function handleSearchWorkerResultMessage(message) {
-  const {state} = wikiSearchInfo;
-  const {id} = message.data;
-
-  if (!id) {
-    console.warn(`Result without id <- from search worker:`, message.data);
-    return;
-  }
-
-  if (!state.workerActionPromiseResolverMap.has(id)) {
-    console.warn(`Runaway result id <- from search worker:`, message.data);
-    return;
-  }
-
-  const {resolve, reject} =
-    state.workerActionPromiseResolverMap.get(id);
-
-  switch (message.data.status) {
-    case 'resolve':
-      resolve(message.data.value);
-      break;
-
-    case 'reject':
-      reject(message.data.value);
-      break;
-
-    default:
-      console.warn(`Unknown result status "${message.data.status}" <- from search worker`);
-      return;
-  }
-
-  state.workerActionPromiseResolverMap.delete(id);
-}
-
-function handleSearchWorkerDownloadBegunMessage(message) {
-  const {event} = wikiSearchInfo;
-  const {context: contextKey, keys} = message.data;
-
-  const context = getSearchWorkerDownloadContext(contextKey, true);
-
-  for (const key of keys) {
-    context[key] = 0.00;
-
-    dispatchInternalEvent(event, 'whenDownloadBegins', {
-      context: contextKey,
-      key,
-    });
-  }
-
-  dispatchInternalEvent(event, 'whenDownloadsBegin', {
-    context: contextKey,
-    keys,
-  });
-}
-
-function handleSearchWorkerDownloadProgressMessage(message) {
-  const {event} = wikiSearchInfo;
-  const {context: contextKey, key, progress} = message.data;
-
-  const context = getSearchWorkerDownloadContext(contextKey);
-
-  context[key] = progress;
-
-  dispatchInternalEvent(event, 'whenDownloadProgresses', {
-    context: contextKey,
-    key,
-    progress,
-  });
-}
-
-function handleSearchWorkerDownloadCompleteMessage(message) {
-  const {event} = wikiSearchInfo;
-  const {context: contextKey, key} = message.data;
-
-  const context = getSearchWorkerDownloadContext(contextKey);
-
-  context[key] = 1.00;
-
-  dispatchInternalEvent(event, 'whenDownloadEnds', {
-    context: contextKey,
-    key,
-  });
-}
-
-function getSearchWorkerDownloadContext(context, initialize = false) {
-  const {state} = wikiSearchInfo;
-
-  if (context in state.downloads) {
-    return state.downloads[context];
-  }
-
-  if (!initialize) {
-    return null;
-  }
-
-  return state.downloads[context] = Object.create(null);
-}
-
-async function postSearchWorkerAction(action, options) {
-  const {state} = wikiSearchInfo;
-
-  const worker = await initializeSearchWorker();
-  const id = ++state.workerActionCounter;
-
-  const {promise, resolve, reject} = promiseWithResolvers();
-
-  state.workerActionPromiseResolverMap.set(id, {resolve, reject});
-
-  worker.postMessage({
-    kind: 'action',
-    action: action,
-    id,
-    options,
-  });
-
-  return await promise;
-}
-
-async function searchAll(query, options = {}) {
-  return await postSearchWorkerAction('search', {
-    query,
-    options,
-  });
-}
-
-// Sidebar search box -------------------------------------
-
-const sidebarSearchInfo = initInfo('sidebarSearchInfo', {
-  pageContainer: null,
-
-  searchSidebarColumn: null,
-  searchBox: null,
-  searchLabel: null,
-  searchInput: null,
-
-  progressRule: null,
-  progressContainer: null,
-  progressLabel: null,
-  progressBar: null,
-
-  failedRule: null,
-  failedContainer: null,
-
-  resultsRule: null,
-  resultsContainer: null,
-  results: null,
-
-  endSearchRule: null,
-  endSearchLine: null,
-  endSearchLink: null,
-
-  preparingString: null,
-  loadingDataString: null,
-  searchingString: null,
-  failedString: null,
-
-  noResultsString: null,
-  currentResultString: null,
-  endSearchString: null,
-
-  albumResultKindString: null,
-  artistResultKindString: null,
-  groupResultKindString: null,
-  tagResultKindString: null,
-
-  state: {
-    sidebarColumnShownForSearch: null,
-
-    tidiedSidebar: null,
-    collapsedDetailsForTidiness: null,
-
-    workerStatus: null,
-    searchStage: null,
-
-    stoppedTypingTimeout: null,
-    stoppedScrollingTimeout: null,
-
-    indexDownloadStatuses: Object.create(null),
-  },
-
-  session: {
-    activeQuery: {
-      type: 'string',
-    },
-
-    activeQueryResults: {
-      type: 'json',
-      maxLength: settings => settings.maxActiveResultsStorage,
-    },
-
-    repeatQueryOnReload: {
-      type: 'boolean',
-      default: false,
-    },
-
-    resultsScrollOffset: {
-      type: 'number',
-    },
-  },
-
-  settings: {
-    stoppedTypingDelay: 800,
-    stoppedScrollingDelay: 200,
-
-    maxActiveResultsStorage: 100000,
-  },
-});
-
-function getSidebarSearchReferences() {
-  const info = sidebarSearchInfo;
-
-  info.pageContainer =
-    document.getElementById('page-container');
-
-  info.searchBox =
-    document.querySelector('.wiki-search-sidebar-box');
-
-  if (!info.searchBox) {
-    return;
-  }
-
-  info.searchLabel =
-    info.searchBox.querySelector('.wiki-search-label');
-
-  info.searchInput =
-    info.searchBox.querySelector('.wiki-search-input');
-
-  info.searchSidebarColumn =
-    info.searchBox.closest('.sidebar-column');
-
-  const findString = classPart =>
-    info.searchBox.querySelector(`.wiki-search-${classPart}-string`);
-
-  info.preparingString =
-    findString('preparing');
-
-  info.loadingDataString =
-    findString('loading-data');
-
-  info.searchingString =
-    findString('searching');
-
-  info.failedString =
-    findString('failed');
-
-  info.noResultsString =
-    findString('no-results');
-
-  info.currentResultString =
-    findString('current-result');
-
-  info.endSearchString =
-    findString('end-search');
-
-  info.albumResultKindString =
-    findString('album-result-kind');
-
-  info.artistResultKindString =
-    findString('artist-result-kind');
-
-  info.groupResultKindString =
-    findString('group-result-kind');
-
-  info.tagResultKindString =
-    findString('tag-result-kind');
-}
-
-function addSidebarSearchInternalListeners() {
-  const info = sidebarSearchInfo;
-
-  if (!info.searchBox) return;
-
-  wikiSearchInfo.event.whenWorkerAlive.push(
-    trackSidebarSearchWorkerAlive,
-    updateSidebarSearchStatus);
-
-  wikiSearchInfo.event.whenWorkerReady.push(
-    trackSidebarSearchWorkerReady,
-    updateSidebarSearchStatus);
-
-  wikiSearchInfo.event.whenWorkerFailsToInitialize.push(
-    trackSidebarSearchWorkerFailsToInitialize,
-    updateSidebarSearchStatus);
-
-  wikiSearchInfo.event.whenWorkerHasRuntimeError.push(
-    trackSidebarSearchWorkerHasRuntimeError,
-    updateSidebarSearchStatus);
-
-  wikiSearchInfo.event.whenDownloadsBegin.push(
-    trackSidebarSearchDownloadsBegin,
-    updateSidebarSearchStatus);
-
-  wikiSearchInfo.event.whenDownloadProgresses.push(
-    updateSidebarSearchStatus);
-
-  wikiSearchInfo.event.whenDownloadEnds.push(
-    trackSidebarSearchDownloadEnds,
-    updateSidebarSearchStatus);
-}
-
-function mutateSidebarSearchContent() {
-  const info = sidebarSearchInfo;
-
-  if (!info.searchBox) return;
-
-  // Progress section
-
-  info.progressRule =
-    document.createElement('hr');
-
-  info.progressContainer =
-    document.createElement('div');
-
-  info.progressContainer.classList.add('wiki-search-progress-container');
-
-  cssProp(info.progressRule, 'display', 'none');
-  cssProp(info.progressContainer, 'display', 'none');
-
-  info.progressLabel =
-    document.createElement('label');
-
-  info.progressLabel.classList.add('wiki-search-progress-label');
-  info.progressLabel.htmlFor = 'wiki-search-progress-bar';
-
-  info.progressBar =
-    document.createElement('progress');
-
-  info.progressBar.classList.add('wiki-search-progress-bar');
-  info.progressBar.id = 'wiki-search-progress-bar';
-
-  info.progressContainer.appendChild(info.progressLabel);
-  info.progressContainer.appendChild(info.progressBar);
-
-  info.searchBox.appendChild(info.progressRule);
-  info.searchBox.appendChild(info.progressContainer);
-
-  // Search failed section
-
-  info.failedRule =
-    document.createElement('hr');
-
-  info.failedContainer =
-    document.createElement('div');
-
-  info.failedContainer.classList.add('wiki-search-failed-container');
-
-  {
-    const p = document.createElement('p');
-    p.appendChild(templateContent(info.failedString));
-    info.failedContainer.appendChild(p);
-  }
-
-  cssProp(info.failedRule, 'display', 'none');
-  cssProp(info.failedContainer, 'display', 'none');
-
-  info.searchBox.appendChild(info.failedRule);
-  info.searchBox.appendChild(info.failedContainer);
-
-  // Results section
-
-  info.resultsRule =
-    document.createElement('hr');
-
-  info.resultsContainer =
-    document.createElement('div');
-
-  info.resultsContainer.classList.add('wiki-search-results-container');
-
-  cssProp(info.resultsRule, 'display', 'none');
-  cssProp(info.resultsContainer, 'display', 'none');
-
-  info.results =
-    document.createElement('div');
-
-  info.results.classList.add('wiki-search-results');
-
-  info.resultsContainer.appendChild(info.results);
-
-  info.searchBox.appendChild(info.resultsRule);
-  info.searchBox.appendChild(info.resultsContainer);
-
-  // End search section
-
-  info.endSearchRule =
-    document.createElement('hr');
-
-  info.endSearchLine =
-    document.createElement('p');
-
-  info.endSearchLink =
-    document.createElement('a');
-
-  {
-    const p = info.endSearchLine;
-    const a = info.endSearchLink;
-    p.classList.add('wiki-search-end-search-line');
-    a.setAttribute('href', '#');
-    a.appendChild(templateContent(info.endSearchString));
-    p.appendChild(a);
-  }
-
-  cssProp(info.endSearchRule, 'display', 'none');
-  cssProp(info.endSearchLine, 'display', 'none');
-
-  info.searchBox.appendChild(info.endSearchRule);
-  info.searchBox.appendChild(info.endSearchLine);
-}
-
-function addSidebarSearchListeners() {
-  const info = sidebarSearchInfo;
-
-  if (!info.searchInput) return;
-
-  info.searchInput.addEventListener('change', domEvent => {
-    if (info.searchInput.value) {
-      activateSidebarSearch(info.searchInput.value);
-    }
-  });
-
-  info.searchInput.addEventListener('input', domEvent => {
-    const {settings, state} = info;
-
-    if (!info.searchInput.value) {
-      clearSidebarSearch();
-      return;
-    }
-
-    if (state.stoppedTypingTimeout) {
-      clearTimeout(state.stoppedTypingTimeout);
-    }
-
-    state.stoppedTypingTimeout =
-      setTimeout(() => {
-        activateSidebarSearch(info.searchInput.value);
-      }, settings.stoppedTypingDelay);
-  });
-
-  info.endSearchLink.addEventListener('click', domEvent => {
-    domEvent.preventDefault();
-    clearSidebarSearch();
-    possiblyHideSearchSidebarColumn();
-    restoreSidebarSearchColumn();
-  });
-
-  info.resultsContainer.addEventListener('scroll', () => {
-    const {settings, state} = info;
-
-    if (state.stoppedScrollingTimeout) {
-      clearTimeout(state.stoppedScrollingTimeout);
-    }
-
-    state.stoppedScrollingTimeout =
-      setTimeout(() => {
-        saveSidebarSearchResultsScrollOffset();
-      }, settings.stoppedScrollingDelay);
-  });
-}
-
-function initializeSidebarSearchState() {
-  const info = sidebarSearchInfo;
-  const {session} = info;
-
-  if (!info.searchInput) return;
-
-  if (session.activeQuery) {
-    info.searchInput.value = session.activeQuery;
-    if (session.repeatQueryOnReload) {
-      activateSidebarSearch(session.activeQuery);
-    } else if (session.activeQueryResults) {
-      showSidebarSearchResults(session.activeQueryResults);
-    }
-  }
-}
-
-function trackSidebarSearchWorkerAlive() {
-  const {state} = sidebarSearchInfo;
-
-  state.workerStatus = 'alive';
-}
-
-function trackSidebarSearchWorkerReady() {
-  const {state} = sidebarSearchInfo;
-
-  state.workerStatus = 'ready';
-  state.searchStage = 'searching';
-}
-
-function trackSidebarSearchWorkerFailsToInitialize() {
-  const {state} = sidebarSearchInfo;
-
-  state.workerStatus = 'failed';
-  state.searchStage = 'failed';
-}
-
-function trackSidebarSearchWorkerHasRuntimeError() {
-  const {state} = sidebarSearchInfo;
-
-  state.workerStatus = 'failed';
-  state.searchStage = 'failed';
-}
-
-function trackSidebarSearchDownloadsBegin(event) {
-  const {state} = sidebarSearchInfo;
-
-  if (event.context === 'search-indexes') {
-    for (const key of event.keys) {
-      state.indexDownloadStatuses[key] = 'active';
-    }
-  }
-}
-
-function trackSidebarSearchDownloadEnds(event) {
-  const {state} = sidebarSearchInfo;
-
-  if (event.context === 'search-indexes') {
-    state.indexDownloadStatuses[event.key] = 'complete';
-
-    const statuses = Object.values(state.indexDownloadStatuses);
-    if (statuses.every(status => status === 'complete')) {
-      for (const key of Object.keys(state.indexDownloadStatuses)) {
-        delete state.indexDownloadStatuses[key];
-      }
-    }
-  }
-}
-
-async function activateSidebarSearch(query) {
-  const {session, settings, state} = sidebarSearchInfo;
-
-  if (state.stoppedTypingTimeout) {
-    clearTimeout(state.stoppedTypingTimeout);
-    state.stoppedTypingTimeout = null;
-  }
-
-  state.searchStage =
-    (state.workerStatus === 'ready'
-      ? 'searching'
-      : 'preparing');
-  updateSidebarSearchStatus();
-
-  let results;
-  try {
-    results = await searchAll(query, {enrich: true});
-  } catch (error) {
-    console.error(`There was an error performing a sidebar search:`);
-    console.error(error);
-    showSidebarSearchFailed();
-    return;
-  }
-
-  state.searchStage = 'complete';
-  updateSidebarSearchStatus();
-
-  session.activeQuery = query;
-  session.activeQueryResults = results;
-  session.resultsScrollOffset = 0;
-
-  showSidebarSearchResults(results);
-}
-
-function clearSidebarSearch() {
-  const info = sidebarSearchInfo;
-  const {session, state} = info;
-
-  if (state.stoppedTypingTimeout) {
-    clearTimeout(state.stoppedTypingTimeout);
-    state.stoppedTypingTimeout = null;
-  }
-
-  info.searchBox.classList.remove('showing-results');
-  info.searchSidebarColumn.classList.remove('search-showing-results');
-
-  info.searchInput.value = '';
-
-  state.searchStage = null;
-
-  session.activeQuery = null;
-  session.activeQueryResults = null;
-  session.resultsScrollOffset = null;
-
-  hideSidebarSearchResults();
-}
-
-function updateSidebarSearchStatus() {
-  const info = sidebarSearchInfo;
-  const {state} = info;
-
-  if (state.searchStage === 'failed') {
-    hideSidebarSearchResults();
-    showSidebarSearchFailed();
-
-    return;
-  }
-
-  const searchIndexDownloads =
-    getSearchWorkerDownloadContext('search-indexes');
-
-  const downloadProgressValues =
-    Object.values(searchIndexDownloads ?? {});
-
-  if (downloadProgressValues.some(v => v < 1.00)) {
-    const total = Object.keys(state.indexDownloadStatuses).length;
-    const sum = accumulateSum(downloadProgressValues);
-    showSidebarSearchProgress(
-      sum / total,
-      templateContent(info.loadingDataString));
-
-    return;
-  }
-
-  if (state.searchStage === 'preparing') {
-    showSidebarSearchProgress(
-      null,
-      templateContent(info.preparingString));
-
-    return;
-  }
-
-  if (state.searchStage === 'searching') {
-    showSidebarSearchProgress(
-      null,
-      templateContent(info.searchingString));
-
-    return;
-  }
-
-  hideSidebarSearchProgress();
-}
-
-function showSidebarSearchProgress(progress, label) {
-  const info = sidebarSearchInfo;
-
-  cssProp(info.progressRule, 'display', null);
-  cssProp(info.progressContainer, 'display', null);
-
-  if (progress === null) {
-    info.progressBar.removeAttribute('value');
-  } else {
-    info.progressBar.value = progress;
-  }
-
-  while (info.progressLabel.firstChild) {
-    info.progressLabel.firstChild.remove();
-  }
-
-  info.progressLabel.appendChild(label);
-}
-
-function hideSidebarSearchProgress() {
-  const info = sidebarSearchInfo;
-
-  cssProp(info.progressRule, 'display', 'none');
-  cssProp(info.progressContainer, 'display', 'none');
-}
-
-function showSidebarSearchFailed() {
-  const info = sidebarSearchInfo;
-  const {state} = info;
-
-  hideSidebarSearchProgress();
-  hideSidebarSearchResults();
-
-  cssProp(info.failedRule, 'display', null);
-  cssProp(info.failedContainer, 'display', null);
-
-  info.searchLabel.classList.add('disabled');
-  info.searchInput.disabled = true;
-
-  if (state.stoppedTypingTimeout) {
-    clearTimeout(state.stoppedTypingTimeout);
-    state.stoppedTypingTimeout = null;
-  }
-}
-
-function showSidebarSearchResults(results) {
-  const info = sidebarSearchInfo;
-
-  console.debug(`Showing search results:`, results);
-
-  showSearchSidebarColumn();
-
-  const flatResults =
-    Object.entries(results)
-      .filter(([index]) => index === 'generic')
-      .flatMap(([index, results]) => results
-        .flatMap(({doc, id}) => ({
-          index,
-          reference: id ?? null,
-          referenceType: (id ? id.split(':')[0] : null),
-          directory: (id ? id.split(':')[1] : null),
-          data: doc,
-        })));
-
-  info.searchBox.classList.add('showing-results');
-  info.searchSidebarColumn.classList.add('search-showing-results');
-
-  while (info.results.firstChild) {
-    info.results.firstChild.remove();
-  }
-
-  cssProp(info.resultsRule, 'display', 'block');
-  cssProp(info.resultsContainer, 'display', 'block');
-
-  if (empty(flatResults)) {
-    const p = document.createElement('p');
-    p.classList.add('wiki-search-no-results');
-    p.appendChild(templateContent(info.noResultsString));
-    info.results.appendChild(p);
-  }
-
-  for (const result of flatResults) {
-    const el = generateSidebarSearchResult(result);
-    if (!el) continue;
-
-    info.results.appendChild(el);
-  }
-
-  if (!empty(flatResults)) {
-    cssProp(info.endSearchRule, 'display', 'block');
-    cssProp(info.endSearchLine, 'display', 'block');
-
-    tidySidebarSearchColumn();
-  }
-
-  restoreSidebarSearchResultsScrollOffset();
-}
-
-function generateSidebarSearchResult(result) {
-  const info = sidebarSearchInfo;
-
-  const preparedSlots = {
-    color:
-      result.data.color ?? null,
-
-    name:
-      result.data.name ?? result.data.primaryName ?? null,
-
-    imageSource:
-      getSearchResultImageSource(result),
-  };
-
-  switch (result.referenceType) {
-    case 'album': {
-      preparedSlots.href =
-        openAlbum(result.directory);
-
-      preparedSlots.kindString =
-        info.albumResultKindString;
-
-      break;
-    }
-
-    case 'artist': {
-      preparedSlots.href =
-        openArtist(result.directory);
-
-      preparedSlots.kindString =
-        info.artistResultKindString;
-
-      break;
-    }
-
-    case 'group': {
-      preparedSlots.href =
-        openGroup(result.directory);
-
-      preparedSlots.kindString =
-        info.groupResultKindString;
-
-      break;
-    }
-
-    case 'flash': {
-      preparedSlots.href =
-        openFlash(result.directory);
-
-      break;
-    }
-
-    case 'tag': {
-      preparedSlots.href =
-        openArtTag(result.directory);
-
-      preparedSlots.kindString =
-        info.tagResultKindString;
-
-      break;
-    }
-
-    case 'track': {
-      preparedSlots.href =
-        openTrack(result.directory);
-
-      break;
-    }
-
-    default:
-      return null;
-  }
-
-  return generateSidebarSearchResultTemplate(preparedSlots);
-}
-
-function getSearchResultImageSource(result) {
-  const {artwork} = result.data;
-  if (!artwork) return null;
-
-  return (
-    rebase(
-      artwork.replace('<>', result.directory),
-      'rebaseThumb'));
-}
-
-function generateSidebarSearchResultTemplate(slots) {
-  const info = sidebarSearchInfo;
-
-  const link = document.createElement('a');
-  link.classList.add('wiki-search-result');
-
-  if (slots.href) {
-    link.setAttribute('href', slots.href);
-  }
-
-  if (slots.color) {
-    cssProp(link, '--primary-color', slots.color);
-
-    try {
-      const colors = getColors(slots.color, {chroma});
-      cssProp(link, '--light-ghost-color', colors.lightGhost);
-      cssProp(link, '--deep-color', colors.deep);
-    } catch (error) {
-      console.warn(error);
-    }
-  }
-
-  const imgContainer = document.createElement('span');
-  imgContainer.classList.add('wiki-search-result-image-container');
-
-  if (slots.imageSource) {
-    const img = document.createElement('img');
-    img.classList.add('wiki-search-result-image');
-    img.setAttribute('src', slots.imageSource);
-    imgContainer.appendChild(img);
-    if (slots.imageSource.endsWith('.mini.jpg')) {
-      img.classList.add('has-warning');
-    }
-  } else {
-    const placeholder = document.createElement('span');
-    placeholder.classList.add('wiki-search-result-image-placeholder');
-    imgContainer.appendChild(placeholder);
-  }
-
-  link.appendChild(imgContainer);
-
-  const text = document.createElement('span');
-  text.classList.add('wiki-search-result-text-area');
-
-  if (slots.name) {
-    const span = document.createElement('span');
-    span.classList.add('wiki-search-result-name');
-    span.appendChild(document.createTextNode(slots.name));
-    text.appendChild(span);
-  }
-
-  let accentSpan = null;
-
-  if (link.href) {
-    const here = location.href.replace(/\/$/, '');
-    const there = link.href.replace(/\/$/, '');
-    if (here === there) {
-      link.classList.add('current-result');
-      accentSpan = document.createElement('span');
-      accentSpan.classList.add('wiki-search-current-result-text');
-      accentSpan.appendChild(templateContent(info.currentResultString));
-    }
-  }
-
-  if (!accentSpan && slots.kindString) {
-    accentSpan = document.createElement('span');
-    accentSpan.classList.add('wiki-search-result-kind');
-    accentSpan.appendChild(templateContent(slots.kindString));
-  }
-
-  if (accentSpan) {
-    text.appendChild(document.createTextNode(' '));
-    text.appendChild(accentSpan);
-  }
-
-  link.appendChild(text);
-
-  link.addEventListener('click', () => {
-    saveSidebarSearchResultsScrollOffset();
-  });
-
-  return link;
-}
-
-function hideSidebarSearchResults() {
-  const info = sidebarSearchInfo;
-
-  cssProp(info.resultsRule, 'display', 'none');
-  cssProp(info.resultsContainer, 'display', 'none');
-
-  while (info.results.firstChild) {
-    info.results.firstChild.remove();
-  }
-
-  cssProp(info.endSearchRule, 'display', 'none');
-  cssProp(info.endSearchLine, 'display', 'none');
-}
-
-function saveSidebarSearchResultsScrollOffset() {
-  const info = sidebarSearchInfo;
-  const {session} = info;
-
-  session.resultsScrollOffset = info.resultsContainer.scrollTop;
-}
-
-function restoreSidebarSearchResultsScrollOffset() {
-  const info = sidebarSearchInfo;
-  const {session} = info;
-
-  if (session.resultsScrollOffset) {
-    info.resultsContainer.scrollTop = session.resultsScrollOffset;
-  }
-}
-
-function showSearchSidebarColumn() {
-  const info = sidebarSearchInfo;
-  const {state} = info;
-
-  if (!info.searchSidebarColumn) {
-    return;
-  }
-
-  if (!info.searchSidebarColumn.classList.contains('initially-hidden')) {
-    return;
-  }
-
-  info.searchSidebarColumn.classList.remove('initially-hidden');
-
-  if (info.searchSidebarColumn.id === 'sidebar-left') {
-    info.pageContainer.classList.add('showing-sidebar-left');
-  } else if (info.searchSidebarColumn.id === 'sidebar-right') {
-    info.pageContainer.classList.add('showing-sidebar-right');
-  }
-
-  state.sidebarColumnShownForSearch = true;
-}
-
-function possiblyHideSearchSidebarColumn() {
-  const info = sidebarSearchInfo;
-  const {state} = info;
-
-  if (!info.searchSidebarColumn) {
-    return;
-  }
-
-  if (!state.sidebarColumnShownForSearch) {
-    return;
-  }
-
-  info.searchSidebarColumn.classList.add('initially-hidden');
-
-  if (info.searchSidebarColumn.id === 'sidebar-left') {
-    info.pageContainer.classList.remove('showing-sidebar-left');
-  } else if (info.searchSidebarColumn.id === 'sidebar-right') {
-    info.pageContainer.classList.remove('showing-sidebar-right');
-  }
-
-  state.sidebarColumnShownForSearch = null;
-}
-
-// This should be called after results are shown, since it checks the
-// elements added to understand the current search state.
-function tidySidebarSearchColumn() {
-  const info = sidebarSearchInfo;
-  const {state} = info;
-
-  // Don't *re-tidy* the sidebar if we've already tidied it to display
-  // some results. This flag will get cleared if the search is dismissed
-  // altogether (and the pre-tidy state is restored).
-  if (state.tidiedSidebar) {
-    return;
-  }
-
-  const here = location.href.replace(/\/$/, '');
-  const currentPageIsResult =
-    Array.from(info.results.querySelectorAll('a'))
-      .some(link => {
-        const there = link.href.replace(/\/$/, '');
-        return here === there;
-      });
-
-  // Don't tidy the sidebar if you've navigated to some other page than
-  // what's in the current result list.
-  if (!currentPageIsResult) {
-    return;
-  }
-
-  state.tidiedSidebar = true;
-  state.collapsedDetailsForTidiness = [];
-
-  for (const box of info.searchSidebarColumn.querySelectorAll('.sidebar')) {
-    if (box === info.searchBox) {
-      continue;
-    }
-
-    for (const details of box.getElementsByTagName('details')) {
-      if (details.open) {
-        details.removeAttribute('open');
-        state.collapsedDetailsForTidiness.push(details);
-      }
-    }
-  }
-}
-
-function restoreSidebarSearchColumn() {
-  const {state} = sidebarSearchInfo;
-
-  if (!state.tidiedSidebar) {
-    return;
-  }
-
-  for (const details of state.collapsedDetailsForTidiness) {
-    details.setAttribute('open', '');
-  }
-
-  state.collapsedDetailsForTidiness = [];
-  state.tidiedSidebar = null;
-}
-
-clientSteps.getPageReferences.push(getSidebarSearchReferences);
-clientSteps.addInternalListeners.push(addSidebarSearchInternalListeners);
-clientSteps.mutatePageContent.push(mutateSidebarSearchContent);
-clientSteps.addPageListeners.push(addSidebarSearchListeners);
-clientSteps.initializeState.push(initializeSidebarSearchState);
-
-// 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.error(`During ${key}, failed to run ${step.name}`);
-      console.error(error);
-    }
-  }
-}
diff --git a/src/static/js/client/additional-names-box.js b/src/static/js/client/additional-names-box.js
new file mode 100644
index 00000000..558ef06f
--- /dev/null
+++ b/src/static/js/client/additional-names-box.js
@@ -0,0 +1,83 @@
+/* eslint-env browser */
+
+import {cssProp} from '../client-util.js';
+
+import {info as hashLinkInfo} from './hash-link.js';
+
+export const info = {
+  id: 'additionalNamesBoxInfo',
+
+  box: null,
+  links: null,
+  mainContentContainer: null,
+
+  state: {
+    visible: false,
+  },
+};
+
+export function getPageReferences() {
+  info.box =
+    document.getElementById('additional-names-box');
+
+  info.links =
+    document.querySelectorAll('a[href="#additional-names-box"]');
+
+  info.mainContentContainer =
+    document.querySelector('#content .main-content-container');
+}
+
+export function addInternalListeners() {
+  hashLinkInfo.event.beforeHashLinkScrolls.push(({target}) => {
+    if (target === info.box) {
+      return false;
+    }
+  });
+}
+
+export function addPageListeners() {
+  for (const link of info.links) {
+    link.addEventListener('click', domEvent => {
+      handleAdditionalNamesBoxLinkClicked(domEvent);
+    });
+  }
+}
+
+function handleAdditionalNamesBoxLinkClicked(domEvent) {
+  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();
+  }
+}
+
+export function toggleAdditionalNamesBox() {
+  const {state} = info;
+
+  state.visible = !state.visible;
+  info.box.style.display =
+    (state.visible
+      ? 'block'
+      : 'none');
+}
diff --git a/src/static/js/client/album-commentary-sidebar.js b/src/static/js/client/album-commentary-sidebar.js
new file mode 100644
index 00000000..c5eaf81b
--- /dev/null
+++ b/src/static/js/client/album-commentary-sidebar.js
@@ -0,0 +1,212 @@
+/* eslint-env browser */
+
+import {empty} from '../../shared-util/sugar.js';
+
+import {info as hashLinkInfo} from './hash-link.js';
+import {info as stickyHeadingInfo} from './sticky-heading.js';
+
+export const info = {
+  id: 'albumCommentarySidebarInfo',
+
+  sidebar: null,
+  sidebarHeading: null,
+
+  sidebarTrackLinks: null,
+  sidebarTrackDirectories: null,
+
+  sidebarTrackSections: null,
+  sidebarTrackSectionStartIndices: null,
+
+  state: {
+    currentTrackSection: null,
+    currentTrackLink: null,
+    justChangedTrackSection: false,
+  },
+};
+
+export function getPageReferences() {
+  if (document.documentElement.dataset.urlKey !== 'localized.albumCommentary') {
+    return;
+  }
+
+  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 {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 {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;
+  }
+}
+
+export function addInternalListeners() {
+  if (!info.sidebar) {
+    return;
+  }
+
+  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);
+  });
+}
diff --git a/src/static/js/client/artist-external-link-tooltip.js b/src/static/js/client/artist-external-link-tooltip.js
new file mode 100644
index 00000000..21ddfb91
--- /dev/null
+++ b/src/static/js/client/artist-external-link-tooltip.js
@@ -0,0 +1,196 @@
+/* eslint-env browser */
+
+import {accumulateSum, empty} from '../../shared-util/sugar.js';
+
+import {info as hoverableTooltipInfo, repositionCurrentTooltip}
+  from './hoverable-tooltip.js';
+
+// These don't need to have tooltip events specially added as
+// they're implemented with "text with tooltip" components.
+
+export const info = {
+  id: '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: [],
+  },
+};
+
+export function getPageReferences() {
+  info.tooltips =
+    Array.from(document.getElementsByClassName('contribution-tooltip'));
+
+  info.tooltipRows =
+    info.tooltips.map(tooltip =>
+      Array.from(tooltip.getElementsByClassName('icon')));
+}
+
+export function addInternalListeners() {
+  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;
+    }
+  });
+}
+
+export function addPageListeners() {
+  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 {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);
+    }
+  }
+
+  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 {state} = info;
+
+  state.showingTooltipInfo = true;
+
+  for (const tooltip of info.tooltips) {
+    tooltip.classList.add('show-info');
+  }
+
+  repositionCurrentTooltip();
+}
+
+function hideArtistExternalLinkTooltipInfo() {
+  const {state} = info;
+
+  state.showingTooltipInfo = false;
+
+  for (const tooltip of info.tooltips) {
+    tooltip.classList.remove('show-info');
+  }
+}
diff --git a/src/static/js/client/css-compatibility-assistant.js b/src/static/js/client/css-compatibility-assistant.js
new file mode 100644
index 00000000..6e7b15b5
--- /dev/null
+++ b/src/static/js/client/css-compatibility-assistant.js
@@ -0,0 +1,22 @@
+/* eslint-env browser */
+
+export const info = {
+  id: 'cssCompatibilityAssistantInfo',
+
+  coverArtContainer: null,
+  coverArtImageDetails: null,
+};
+
+export function getPageReferences() {
+  info.coverArtContainer =
+    document.getElementById('cover-art-container');
+
+  info.coverArtImageDetails =
+    info.coverArtContainer?.querySelector('.image-details');
+}
+
+export function mutatePageContent() {
+  if (info.coverArtImageDetails) {
+    info.coverArtContainer.classList.add('has-image-details');
+  }
+}
diff --git a/src/static/js/client/datetimestamp-tooltip.js b/src/static/js/client/datetimestamp-tooltip.js
new file mode 100644
index 00000000..46d1cd5b
--- /dev/null
+++ b/src/static/js/client/datetimestamp-tooltip.js
@@ -0,0 +1,36 @@
+/* eslint-env browser */
+
+// TODO: Maybe datetimestamps can just be incorporated into text-with-tooltip?
+
+import {stitchArrays} from '../../shared-util/sugar.js';
+
+import {registerTooltipElement, registerTooltipHoverableElement}
+  from './hoverable-tooltip.js';
+
+export const info = {
+  id: 'datetimestampTooltipInfo',
+
+  hoverables: null,
+  tooltips: null,
+};
+
+export function getPageReferences() {
+  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'));
+}
+
+export function addPageListeners() {
+  for (const {hoverable, tooltip} of stitchArrays({
+    hoverable: info.hoverables,
+    tooltip: info.tooltips,
+  })) {
+    registerTooltipElement(tooltip);
+    registerTooltipHoverableElement(hoverable, tooltip);
+  }
+}
diff --git a/src/static/js/client/hash-link.js b/src/static/js/client/hash-link.js
new file mode 100644
index 00000000..27035e29
--- /dev/null
+++ b/src/static/js/client/hash-link.js
@@ -0,0 +1,146 @@
+/* eslint-env browser */
+
+import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js';
+
+import {dispatchInternalEvent} from '../client-util.js';
+
+export const info = {
+  id: 'hashLinkInfo',
+
+  links: null,
+  hrefs: null,
+  targets: null,
+
+  state: {
+    highlightedTarget: null,
+    scrollingAfterClick: false,
+    concludeScrollingStateInterval: null,
+  },
+
+  event: {
+    beforeHashLinkScrolls: [],
+    whenHashLinkClicked: [],
+  },
+};
+
+export function getPageReferences() {
+  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} = info;
+
+  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);
+}
+
+export function addPageListeners() {
+  // 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 {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;
+    });
+  }
+}
diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js
new file mode 100644
index 00000000..484f2ab0
--- /dev/null
+++ b/src/static/js/client/hoverable-tooltip.js
@@ -0,0 +1,1083 @@
+/* eslint-env browser */
+
+import {empty, filterMultipleArrays} from '../../shared-util/sugar.js';
+
+import {WikiRect} from '../rectangles.js';
+
+import {
+  cssProp,
+  dispatchInternalEvent,
+  getVisuallyContainingElement,
+  pointIsOverAnyOf,
+} from '../client-util.js';
+
+import {info as stickyHeadingInfo} from './sticky-heading.js';
+
+export const info = {
+  id: '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(),
+
+    // This is a two-item array that tracks the direction we've already
+    // dynamically placed the current tooltip. If we *reposition* the tooltip
+    // (because its dimensions changed), we'll try to follow this anchor first.
+    dynamicTooltipAnchorDirection: null,
+  },
+
+  event: {
+    whenTooltipShows: [],
+    whenTooltipHides: [],
+  },
+};
+
+// Adds DOM event listeners, so must be called during addPageListeners step.
+export function registerTooltipElement(tooltip) {
+  const {state} = info;
+
+  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.
+export function registerTooltipHoverableElement(hoverable, tooltip) {
+  const {state} = info;
+
+  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} = info;
+
+  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} = info;
+
+  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} = info;
+
+  // 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} = info;
+  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} = info;
+
+  // 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} = info;
+
+  // 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} = info;
+
+  // 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} = info;
+  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} = info;
+
+  // 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();
+  }
+}
+
+export function currentlyShownTooltipHasFocus(focusElement = document.activeElement) {
+  const {state} = info;
+
+  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;
+}
+
+export function beginTransitioningTooltipHidden(tooltip) {
+  const {settings, state} = info;
+
+  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);
+}
+
+export function cancelTransitioningTooltipHidden(andShow = false) {
+  const {state} = info;
+
+  endTransitioningTooltipHidden();
+
+  if (andShow) {
+    showTooltipFromHoverable(state.previouslyActiveHoverable);
+  }
+}
+
+export function endTransitioningTooltipHidden() {
+  const {state} = info;
+  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;
+  }
+}
+
+export function hideCurrentlyShownTooltip(intendingToReplace = false) {
+  const {settings, state, event} = info;
+  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;
+
+  state.dynamicTooltipAnchorDirection = null;
+
+  // Set this for one tick of the event cycle.
+  state.tooltipWasJustHidden = true;
+  setTimeout(() => {
+    state.tooltipWasJustHidden = false;
+  });
+
+  dispatchInternalEvent(event, 'whenTooltipHides', {
+    tooltip,
+  });
+
+  return true;
+}
+
+export function showTooltipFromHoverable(hoverable) {
+  const {state, event} = info;
+  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;
+}
+
+export 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);
+  }
+}
+
+export function repositionCurrentTooltip() {
+  const {state} = info;
+  const {currentlyActiveHoverable} = state;
+
+  if (!currentlyActiveHoverable) {
+    throw new Error(`No hoverable active to reposition tooltip from`);
+  }
+
+  positionTooltipFromHoverableWithBrains(currentlyActiveHoverable);
+}
+
+export function positionTooltipFromHoverableWithBrains(hoverable) {
+  const {state} = info;
+  const {tooltip} = state.registeredHoverables.get(hoverable);
+
+  const anchorDirection = state.dynamicTooltipAnchorDirection;
+
+  // 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;
+  }
+
+  const tryDirection = (dir1, dir2, i) => {
+    selectedRect = opportunities[dir1][dir2][i];
+    return !!selectedRect;
+  };
+
+  let selectedRect = null;
+  selectRect: {
+    if (anchorDirection) {
+      for (let i = 0; i < numBaselineRects; i++) {
+        if (tryDirection(...anchorDirection, i)) {
+          break selectRect;
+        }
+      }
+    }
+
+    for (let i = 0; i < numBaselineRects; i++) {
+      for (const [dir1, dir2] of [
+        ['right', 'down'],
+        ['left', 'down'],
+        ['right', 'up'],
+        ['left', 'up'],
+        ['down', 'right'],
+        ['down', 'left'],
+        ['up', 'right'],
+        ['up', 'left'],
+      ]) {
+        if (tryDirection(dir1, dir2, i)) {
+          state.dynamicTooltipAnchorDirection = [dir1, dir2];
+          break selectRect;
+        }
+      }
+    }
+
+    selectedRect = baselineRect;
+  }
+
+  positionTooltip(tooltip, selectedRect.x, selectedRect.y);
+}
+
+export 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`,
+  });
+}
+
+export function resetDynamicTooltipPositioning(tooltip) {
+  cssProp(tooltip, {
+    left: null,
+    top: null,
+  });
+}
+
+export function getTooltipFromHoverablePlacementOpportunityAreas(hoverable) {
+  const {state} = info;
+  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;
+
+  const upTopDown =
+    WikiRect.beneath(
+      hoverableRect.top + neededVerticalOverlap - tooltipRect.height);
+
+  const downBottomUp =
+    WikiRect.above(
+      hoverableRect.bottom - neededVerticalOverlap + tooltipRect.height);
+
+  // Please don't ask us to make this but horizontal?
+  const prepareVerticalOrientationRects = (regionRects) => {
+    const orientations = {};
+
+    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 rightRightLeft =
+    WikiRect.leftOf(
+      hoverableRect.left - neededHorizontalOverlap + tooltipRect.width);
+
+  const leftLeftRight =
+    WikiRect.rightOf(
+      hoverableRect.left + neededHorizontalOverlap - tooltipRect.width);
+
+  // Oops.
+  const prepareHorizontalOrientationRects = (regionRects) => {
+    const orientations = {};
+
+    const orientVertically = (rect, i) => {
+      if (!rect) return null;
+
+      const regionRect = regionRects[i];
+
+      if (regionRect.height > 0) {
+        return rect;
+      } else {
+        return WikiRect.fromRect({
+          x: rect.x,
+          y: regionRect.bottom - tooltipRect.height,
+          width: rect.width,
+          height: rect.height,
+        });
+      }
+    };
+
+    orientations.left =
+      regionRects
+        .map(rect => rect?.intersectionWith(leftLeftRight))
+        .map(orientVertically)
+        .map(keepIfFits);
+
+    orientations.right =
+      regionRects
+        .map(rect => rect?.intersectionWith(rightRightLeft))
+        .map(rect =>
+          (rect
+            ? rect.intersectionWith(WikiRect.fromRect({
+                x: rect.right - tooltipRect.width,
+                y: rect.y,
+                width: rect.width,
+                height: tooltipRect.height,
+              }))
+            : null))
+        .map(orientVertically)
+        .map(keepIfFits);
+
+    // No analogous center because we don't actually use
+    // center alignment...
+
+    return orientations;
+  };
+
+  const orientationRects = {
+    left: prepareVerticalOrientationRects(regionRects.left),
+    right: prepareVerticalOrientationRects(regionRects.right),
+    down: prepareHorizontalOrientationRects(regionRects.bottom),
+    up: prepareHorizontalOrientationRects(regionRects.top),
+  };
+
+  return {
+    numBaselineRects: baselineRects.length,
+    idealBaseline: baselineRects[0],
+    ...orientationRects,
+  };
+}
+
+export 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;
+}
+
+export function addPageListeners() {
+  const {state} = info;
+
+  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();
+    }
+  });
+}
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
new file mode 100644
index 00000000..8870a4d1
--- /dev/null
+++ b/src/static/js/client/index.js
@@ -0,0 +1,226 @@
+/* eslint-env browser */
+
+import '../group-contributions-table.js';
+import '../image-overlay.js';
+
+import * as additionalNamesBoxModule from './additional-names-box.js';
+import * as albumCommentarySidebarModule from './album-commentary-sidebar.js';
+import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip.js';
+import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js';
+import * as datetimestampTooltipModule from './datetimestamp-tooltip.js';
+import * as hashLinkModule from './hash-link.js';
+import * as hoverableTooltipModule from './hoverable-tooltip.js';
+import * as liveMousePositionModule from './live-mouse-position.js';
+import * as quickDescriptionModule from './quick-description.js';
+import * as scriptedLinkModule from './scripted-link.js';
+import * as sidebarSearchModule from './sidebar-search.js';
+import * as stickyHeadingModule from './sticky-heading.js';
+import * as summaryNestedLinkModule from './summary-nested-link.js';
+import * as textWithTooltipModule from './text-with-tooltip.js';
+import * as wikiSearchModule from './wiki-search.js';
+
+export const modules = [
+  additionalNamesBoxModule,
+  albumCommentarySidebarModule,
+  artistExternalLinkTooltipModule,
+  cssCompatibilityAssistantModule,
+  datetimestampTooltipModule,
+  hashLinkModule,
+  hoverableTooltipModule,
+  liveMousePositionModule,
+  quickDescriptionModule,
+  scriptedLinkModule,
+  sidebarSearchModule,
+  stickyHeadingModule,
+  summaryNestedLinkModule,
+  textWithTooltipModule,
+  wikiSearchModule,
+];
+
+const clientInfo = window.hsmusicClientInfo = Object.create(null);
+
+const clientSteps = {
+  getPageReferences: [],
+  addInternalListeners: [],
+  mutatePageContent: [],
+  initializeState: [],
+  addPageListeners: [],
+};
+
+for (const module of modules) {
+  const {info} = module;
+
+  if (!info) {
+    throw new Error(`Module missing info`);
+  }
+
+  const {id: infoKey} = info;
+
+  if (!infoKey) {
+    throw new Error(`Module info missing id: ` + JSON.stringify(info));
+  }
+
+  clientInfo[infoKey] = info;
+
+  for (const obj of [
+    info,
+    info.state,
+    info.settings,
+    info.event,
+  ]) {
+    if (!obj) continue;
+
+    if (obj !== info) {
+      obj[Symbol.for('hsmusic.clientInfo')] = info;
+    }
+
+    Object.preventExtensions(obj);
+  }
+
+  if (info.session) {
+    const sessionSpecs = info.session;
+
+    info.session = {};
+
+    for (const [key, spec] of Object.entries(sessionSpecs)) {
+      const hasSpec =
+        typeof spec === 'object' && spec !== null;
+
+      const defaultValue =
+        (hasSpec
+          ? spec.default ?? null
+          : spec);
+
+      let formatRead = value => value;
+      let formatWrite = value => value;
+      if (hasSpec && spec.type) {
+        switch (spec.type) {
+          case 'number':
+            formatRead = parseFloat;
+            formatWrite = String;
+            break;
+
+          case 'boolean':
+            formatRead = Boolean;
+            formatWrite = String;
+            break;
+
+          case 'string':
+            formatRead = String;
+            formatWrite = String;
+            break;
+
+          case 'json':
+            formatRead = JSON.parse;
+            formatWrite = JSON.stringify;
+            break;
+
+          default:
+            throw new Error(`Unknown type for session storage spec "${spec.type}"`);
+        }
+      }
+
+      let getMaxLength =
+        (!hasSpec
+          ? () => Infinity
+       : typeof spec.maxLength === 'function'
+          ? (info.settings
+              ? () => spec.maxLength(info.settings)
+              : () => spec.maxLength())
+          : () => spec.maxLength);
+
+      const storageKey = `hsmusic.${infoKey}.${key}`;
+
+      let fallbackValue = defaultValue;
+
+      Object.defineProperty(info.session, key, {
+        get: () => {
+          let value;
+          try {
+            value = sessionStorage.getItem(storageKey) ?? defaultValue;
+          } catch (error) {
+            if (error instanceof DOMException) {
+              value = fallbackValue;
+            } else {
+              throw error;
+            }
+          }
+
+          if (value === null) {
+            return null;
+          }
+
+          return formatRead(value);
+        },
+
+        set: (value) => {
+          if (value !== null && value !== '') {
+            value = formatWrite(value);
+          }
+
+          if (value === null) {
+            value = '';
+          }
+
+          const maxLength = getMaxLength();
+          if (value.length > maxLength) {
+            console.warn(
+              `Requested to set session storage ${storageKey} ` +
+              `beyond maximum length ${maxLength}, ` +
+              `ignoring this value.`);
+            console.trace();
+            return;
+          }
+
+          let operation;
+          if (value === '') {
+            fallbackValue = null;
+            operation = () => {
+              sessionStorage.removeItem(storageKey);
+            };
+          } else {
+            fallbackValue = value;
+            operation = () => {
+              sessionStorage.setItem(storageKey, value);
+            };
+          }
+
+          try {
+            operation();
+          } catch (error) {
+            if (!(error instanceof DOMException)) {
+              throw error;
+            }
+          }
+        },
+      });
+    }
+
+    Object.preventExtensions(info.session);
+  }
+
+  for (const key of Object.keys(clientSteps)) {
+    if (Object.hasOwn(module, key)) {
+      const fn = module[key];
+
+      Object.defineProperty(fn, 'name', {
+        value: `${infoKey}/${fn.name}`,
+      });
+
+      clientSteps[key].push(fn);
+    }
+  }
+}
+
+for (const [key, steps] of Object.entries(clientSteps)) {
+  for (const step of steps) {
+    try {
+      step();
+    } catch (error) {
+      // TODO: Be smarter about not running later steps for the same module!
+      // Or maybe not, since an error is liable to cause explosions anyway.
+      console.error(`During ${key}, failed to run ${step.name}`);
+      console.error(error);
+    }
+  }
+}
diff --git a/src/static/js/client/live-mouse-position.js b/src/static/js/client/live-mouse-position.js
new file mode 100644
index 00000000..36a28429
--- /dev/null
+++ b/src/static/js/client/live-mouse-position.js
@@ -0,0 +1,21 @@
+/* eslint-env browser */
+
+export const info = {
+  id: 'liveMousePositionInfo',
+
+  state: {
+    clientX: null,
+    clientY: null,
+  },
+};
+
+export function addPageListeners() {
+  const {state} = info;
+
+  document.body.addEventListener('mousemove', domEvent => {
+    Object.assign(state, {
+      clientX: domEvent.clientX,
+      clientY: domEvent.clientY,
+    });
+  });
+}
diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js
new file mode 100644
index 00000000..cff82252
--- /dev/null
+++ b/src/static/js/client/quick-description.js
@@ -0,0 +1,62 @@
+/* eslint-env browser */
+
+import {stitchArrays} from '../../shared-util/sugar.js';
+
+export const info = {
+  id: 'quickDescriptionInfo',
+
+  quickDescriptionContainers: null,
+
+  quickDescriptionsAreExpandable: null,
+
+  expandDescriptionLinks: null,
+  collapseDescriptionLinks: null,
+};
+
+export function getPageReferences() {
+  info.quickDescriptionContainers =
+    Array.from(document.querySelectorAll('#content .quick-description'));
+
+  info.quickDescriptionsAreExpandable =
+    info.quickDescriptionContainers
+      .map(container =>
+        container.querySelector('.quick-description-actions.when-expanded'));
+
+  info.expandDescriptionLinks =
+    info.quickDescriptionContainers
+      .map(container =>
+        container.querySelector('.quick-description-actions .expand-link'));
+
+  info.collapseDescriptionLinks =
+    info.quickDescriptionContainers
+      .map(container =>
+        container.querySelector('.quick-description-actions .collapse-link'));
+}
+
+export function addPageListeners() {
+  for (const {
+    isExpandable,
+    container,
+    expandLink,
+    collapseLink,
+  } of stitchArrays({
+    isExpandable: info.quickDescriptionsAreExpandable,
+    container: info.quickDescriptionContainers,
+    expandLink: info.expandDescriptionLinks,
+    collapseLink: info.collapseDescriptionLinks,
+  })) {
+    if (!isExpandable) continue;
+
+    expandLink.addEventListener('click', event => {
+      event.preventDefault();
+      container.classList.add('expanded');
+      container.classList.remove('collapsed');
+    });
+
+    collapseLink.addEventListener('click', event => {
+      event.preventDefault();
+      container.classList.add('collapsed');
+      container.classList.remove('expanded');
+    });
+  }
+}
diff --git a/src/static/js/client/scripted-link.js b/src/static/js/client/scripted-link.js
new file mode 100644
index 00000000..5db86e13
--- /dev/null
+++ b/src/static/js/client/scripted-link.js
@@ -0,0 +1,275 @@
+/* eslint-env browser */
+
+import {pick, stitchArrays} from '../../shared-util/sugar.js';
+
+import {
+  cssProp,
+  rebase,
+  openAlbum,
+  openArtist,
+  openTrack,
+} from '../client-util.js';
+
+export const info = {
+  id: 'scriptedLinkInfo',
+
+  randomLinks: null,
+  revealLinks: null,
+  revealContainers: null,
+
+  nextNavLink: null,
+  previousNavLink: null,
+  randomNavLink: null,
+
+  state: {
+    albumDirectories: null,
+    albumTrackDirectories: null,
+    artistDirectories: null,
+    artistNumContributions: null,
+  },
+};
+
+export function getPageReferences() {
+  info.randomLinks =
+    document.querySelectorAll('[data-random]');
+
+  info.revealLinks =
+    document.querySelectorAll('.reveal .image-outer-area > *');
+
+  info.revealContainers =
+    Array.from(info.revealLinks)
+      .map(link => link.closest('.reveal'));
+
+  info.nextNavLink =
+    document.getElementById('next-button');
+
+  info.previousNavLink =
+    document.getElementById('previous-button');
+
+  info.randomNavLink =
+    document.getElementById('random-button');
+}
+
+export function addPageListeners() {
+  addRandomLinkListeners();
+  addNavigationKeyPressListeners();
+  addRevealLinkClickListeners();
+}
+
+function addRandomLinkListeners() {
+  for (const a of info.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} = info;
+
+  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));
+    }
+  }
+}
+
+export function mutatePageContent() {
+  mutateNavigationLinkContent();
+}
+
+function mutateNavigationLinkContent() {
+  const prependTitle = (el, prepend) =>
+    el?.setAttribute('title',
+      (el.hasAttribute('title')
+        ? prepend + ' ' + el.getAttribute('title')
+        : prepend));
+
+  prependTitle(info.nextNavLink, '(Shift+N)');
+  prependTitle(info.previousNavLink, '(Shift+P)');
+  prependTitle(info.randomNavLink, '(Shift+R)');
+}
+
+function addNavigationKeyPressListeners() {
+  document.addEventListener('keypress', (event) => {
+    if (event.shiftKey) {
+      if (event.charCode === 'N'.charCodeAt(0)) {
+        info.nextNavLink?.click();
+      } else if (event.charCode === 'P'.charCodeAt(0)) {
+        info.previousNavLink?.click();
+      } else if (event.charCode === 'R'.charCodeAt(0)) {
+        info.randomNavLink?.click();
+      }
+    }
+  });
+}
+
+function addRevealLinkClickListeners() {
+  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'));
+}
+
+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} = info;
+
+      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(() => {
+      for (const a of info.randomLinks) {
+        const href = determineRandomLinkHref(a);
+        if (!href) {
+          a.removeAttribute('href');
+        }
+      }
+    });
+}
diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js
new file mode 100644
index 00000000..fd571ac0
--- /dev/null
+++ b/src/static/js/client/sidebar-search.js
@@ -0,0 +1,903 @@
+/* eslint-env browser */
+
+import {getColors} from '../../shared-util/colors.js';
+import {accumulateSum, empty} from '../../shared-util/sugar.js';
+
+import {
+  cssProp,
+  openAlbum,
+  openArtist,
+  openArtTag,
+  openFlash,
+  openGroup,
+  openTrack,
+  rebase,
+  templateContent,
+} from '../client-util.js';
+
+import {
+  info as wikiSearchInfo,
+  getSearchWorkerDownloadContext,
+  searchAll,
+} from './wiki-search.js';
+
+export const info = {
+  id: 'sidebarSearchInfo',
+
+  pageContainer: null,
+
+  searchSidebarColumn: null,
+  searchBox: null,
+  searchLabel: null,
+  searchInput: null,
+
+  progressRule: null,
+  progressContainer: null,
+  progressLabel: null,
+  progressBar: null,
+
+  failedRule: null,
+  failedContainer: null,
+
+  resultsRule: null,
+  resultsContainer: null,
+  results: null,
+
+  endSearchRule: null,
+  endSearchLine: null,
+  endSearchLink: null,
+
+  preparingString: null,
+  loadingDataString: null,
+  searchingString: null,
+  failedString: null,
+
+  noResultsString: null,
+  currentResultString: null,
+  endSearchString: null,
+
+  albumResultKindString: null,
+  artistResultKindString: null,
+  groupResultKindString: null,
+  tagResultKindString: null,
+
+  state: {
+    sidebarColumnShownForSearch: null,
+
+    tidiedSidebar: null,
+    collapsedDetailsForTidiness: null,
+
+    workerStatus: null,
+    searchStage: null,
+
+    stoppedTypingTimeout: null,
+    stoppedScrollingTimeout: null,
+
+    indexDownloadStatuses: Object.create(null),
+  },
+
+  session: {
+    activeQuery: {
+      type: 'string',
+    },
+
+    activeQueryResults: {
+      type: 'json',
+      maxLength: settings => settings.maxActiveResultsStorage,
+    },
+
+    repeatQueryOnReload: {
+      type: 'boolean',
+      default: false,
+    },
+
+    resultsScrollOffset: {
+      type: 'number',
+    },
+  },
+
+  settings: {
+    stoppedTypingDelay: 800,
+    stoppedScrollingDelay: 200,
+
+    maxActiveResultsStorage: 100000,
+  },
+};
+
+export function getPageReferences() {
+  info.pageContainer =
+    document.getElementById('page-container');
+
+  info.searchBox =
+    document.querySelector('.wiki-search-sidebar-box');
+
+  if (!info.searchBox) {
+    return;
+  }
+
+  info.searchLabel =
+    info.searchBox.querySelector('.wiki-search-label');
+
+  info.searchInput =
+    info.searchBox.querySelector('.wiki-search-input');
+
+  info.searchSidebarColumn =
+    info.searchBox.closest('.sidebar-column');
+
+  const findString = classPart =>
+    info.searchBox.querySelector(`.wiki-search-${classPart}-string`);
+
+  info.preparingString =
+    findString('preparing');
+
+  info.loadingDataString =
+    findString('loading-data');
+
+  info.searchingString =
+    findString('searching');
+
+  info.failedString =
+    findString('failed');
+
+  info.noResultsString =
+    findString('no-results');
+
+  info.currentResultString =
+    findString('current-result');
+
+  info.endSearchString =
+    findString('end-search');
+
+  info.albumResultKindString =
+    findString('album-result-kind');
+
+  info.artistResultKindString =
+    findString('artist-result-kind');
+
+  info.groupResultKindString =
+    findString('group-result-kind');
+
+  info.tagResultKindString =
+    findString('tag-result-kind');
+}
+
+export function addInternalListeners() {
+  if (!info.searchBox) return;
+
+  wikiSearchInfo.event.whenWorkerAlive.push(
+    trackSidebarSearchWorkerAlive,
+    updateSidebarSearchStatus);
+
+  wikiSearchInfo.event.whenWorkerReady.push(
+    trackSidebarSearchWorkerReady,
+    updateSidebarSearchStatus);
+
+  wikiSearchInfo.event.whenWorkerFailsToInitialize.push(
+    trackSidebarSearchWorkerFailsToInitialize,
+    updateSidebarSearchStatus);
+
+  wikiSearchInfo.event.whenWorkerHasRuntimeError.push(
+    trackSidebarSearchWorkerHasRuntimeError,
+    updateSidebarSearchStatus);
+
+  wikiSearchInfo.event.whenDownloadsBegin.push(
+    trackSidebarSearchDownloadsBegin,
+    updateSidebarSearchStatus);
+
+  wikiSearchInfo.event.whenDownloadProgresses.push(
+    updateSidebarSearchStatus);
+
+  wikiSearchInfo.event.whenDownloadEnds.push(
+    trackSidebarSearchDownloadEnds,
+    updateSidebarSearchStatus);
+}
+
+export function mutatePageContent() {
+  if (!info.searchBox) return;
+
+  // Progress section
+
+  info.progressRule =
+    document.createElement('hr');
+
+  info.progressContainer =
+    document.createElement('div');
+
+  info.progressContainer.classList.add('wiki-search-progress-container');
+
+  cssProp(info.progressRule, 'display', 'none');
+  cssProp(info.progressContainer, 'display', 'none');
+
+  info.progressLabel =
+    document.createElement('label');
+
+  info.progressLabel.classList.add('wiki-search-progress-label');
+  info.progressLabel.htmlFor = 'wiki-search-progress-bar';
+
+  info.progressBar =
+    document.createElement('progress');
+
+  info.progressBar.classList.add('wiki-search-progress-bar');
+  info.progressBar.id = 'wiki-search-progress-bar';
+
+  info.progressContainer.appendChild(info.progressLabel);
+  info.progressContainer.appendChild(info.progressBar);
+
+  info.searchBox.appendChild(info.progressRule);
+  info.searchBox.appendChild(info.progressContainer);
+
+  // Search failed section
+
+  info.failedRule =
+    document.createElement('hr');
+
+  info.failedContainer =
+    document.createElement('div');
+
+  info.failedContainer.classList.add('wiki-search-failed-container');
+
+  {
+    const p = document.createElement('p');
+    p.appendChild(templateContent(info.failedString));
+    info.failedContainer.appendChild(p);
+  }
+
+  cssProp(info.failedRule, 'display', 'none');
+  cssProp(info.failedContainer, 'display', 'none');
+
+  info.searchBox.appendChild(info.failedRule);
+  info.searchBox.appendChild(info.failedContainer);
+
+  // Results section
+
+  info.resultsRule =
+    document.createElement('hr');
+
+  info.resultsContainer =
+    document.createElement('div');
+
+  info.resultsContainer.classList.add('wiki-search-results-container');
+
+  cssProp(info.resultsRule, 'display', 'none');
+  cssProp(info.resultsContainer, 'display', 'none');
+
+  info.results =
+    document.createElement('div');
+
+  info.results.classList.add('wiki-search-results');
+
+  info.resultsContainer.appendChild(info.results);
+
+  info.searchBox.appendChild(info.resultsRule);
+  info.searchBox.appendChild(info.resultsContainer);
+
+  // End search section
+
+  info.endSearchRule =
+    document.createElement('hr');
+
+  info.endSearchLine =
+    document.createElement('p');
+
+  info.endSearchLink =
+    document.createElement('a');
+
+  {
+    const p = info.endSearchLine;
+    const a = info.endSearchLink;
+    p.classList.add('wiki-search-end-search-line');
+    a.setAttribute('href', '#');
+    a.appendChild(templateContent(info.endSearchString));
+    p.appendChild(a);
+  }
+
+  cssProp(info.endSearchRule, 'display', 'none');
+  cssProp(info.endSearchLine, 'display', 'none');
+
+  info.searchBox.appendChild(info.endSearchRule);
+  info.searchBox.appendChild(info.endSearchLine);
+}
+
+export function addPageListeners() {
+  if (!info.searchInput) return;
+
+  info.searchInput.addEventListener('change', _domEvent => {
+    if (info.searchInput.value) {
+      activateSidebarSearch(info.searchInput.value);
+    }
+  });
+
+  info.searchInput.addEventListener('input', _domEvent => {
+    const {settings, state} = info;
+
+    if (!info.searchInput.value) {
+      clearSidebarSearch();
+      return;
+    }
+
+    if (state.stoppedTypingTimeout) {
+      clearTimeout(state.stoppedTypingTimeout);
+    }
+
+    state.stoppedTypingTimeout =
+      setTimeout(() => {
+        activateSidebarSearch(info.searchInput.value);
+      }, settings.stoppedTypingDelay);
+  });
+
+  info.endSearchLink.addEventListener('click', domEvent => {
+    domEvent.preventDefault();
+    clearSidebarSearch();
+    possiblyHideSearchSidebarColumn();
+    restoreSidebarSearchColumn();
+  });
+
+  info.resultsContainer.addEventListener('scroll', () => {
+    const {settings, state} = info;
+
+    if (state.stoppedScrollingTimeout) {
+      clearTimeout(state.stoppedScrollingTimeout);
+    }
+
+    state.stoppedScrollingTimeout =
+      setTimeout(() => {
+        saveSidebarSearchResultsScrollOffset();
+      }, settings.stoppedScrollingDelay);
+  });
+}
+
+export function initializeState() {
+  const {session} = info;
+
+  if (!info.searchInput) return;
+
+  if (session.activeQuery) {
+    info.searchInput.value = session.activeQuery;
+    if (session.repeatQueryOnReload) {
+      activateSidebarSearch(session.activeQuery);
+    } else if (session.activeQueryResults) {
+      showSidebarSearchResults(session.activeQueryResults);
+    }
+  }
+}
+
+function trackSidebarSearchWorkerAlive() {
+  const {state} = info;
+
+  state.workerStatus = 'alive';
+}
+
+function trackSidebarSearchWorkerReady() {
+  const {state} = info;
+
+  state.workerStatus = 'ready';
+  state.searchStage = 'searching';
+}
+
+function trackSidebarSearchWorkerFailsToInitialize() {
+  const {state} = info;
+
+  state.workerStatus = 'failed';
+  state.searchStage = 'failed';
+}
+
+function trackSidebarSearchWorkerHasRuntimeError() {
+  const {state} = info;
+
+  state.workerStatus = 'failed';
+  state.searchStage = 'failed';
+}
+
+function trackSidebarSearchDownloadsBegin(event) {
+  const {state} = info;
+
+  if (event.context === 'search-indexes') {
+    for (const key of event.keys) {
+      state.indexDownloadStatuses[key] = 'active';
+    }
+  }
+}
+
+function trackSidebarSearchDownloadEnds(event) {
+  const {state} = info;
+
+  if (event.context === 'search-indexes') {
+    state.indexDownloadStatuses[event.key] = 'complete';
+
+    const statuses = Object.values(state.indexDownloadStatuses);
+    if (statuses.every(status => status === 'complete')) {
+      for (const key of Object.keys(state.indexDownloadStatuses)) {
+        delete state.indexDownloadStatuses[key];
+      }
+    }
+  }
+}
+
+async function activateSidebarSearch(query) {
+  const {session, state} = info;
+
+  if (state.stoppedTypingTimeout) {
+    clearTimeout(state.stoppedTypingTimeout);
+    state.stoppedTypingTimeout = null;
+  }
+
+  state.searchStage =
+    (state.workerStatus === 'ready'
+      ? 'searching'
+      : 'preparing');
+  updateSidebarSearchStatus();
+
+  let results;
+  try {
+    results = await searchAll(query, {enrich: true});
+  } catch (error) {
+    console.error(`There was an error performing a sidebar search:`);
+    console.error(error);
+    showSidebarSearchFailed();
+    return;
+  }
+
+  state.searchStage = 'complete';
+  updateSidebarSearchStatus();
+
+  session.activeQuery = query;
+  session.activeQueryResults = results;
+  session.resultsScrollOffset = 0;
+
+  showSidebarSearchResults(results);
+}
+
+function clearSidebarSearch() {
+  const {session, state} = info;
+
+  if (state.stoppedTypingTimeout) {
+    clearTimeout(state.stoppedTypingTimeout);
+    state.stoppedTypingTimeout = null;
+  }
+
+  info.searchBox.classList.remove('showing-results');
+  info.searchSidebarColumn.classList.remove('search-showing-results');
+
+  info.searchInput.value = '';
+
+  state.searchStage = null;
+
+  session.activeQuery = null;
+  session.activeQueryResults = null;
+  session.resultsScrollOffset = null;
+
+  hideSidebarSearchResults();
+}
+
+function updateSidebarSearchStatus() {
+  const {state} = info;
+
+  if (state.searchStage === 'failed') {
+    hideSidebarSearchResults();
+    showSidebarSearchFailed();
+
+    return;
+  }
+
+  const searchIndexDownloads =
+    getSearchWorkerDownloadContext('search-indexes');
+
+  const downloadProgressValues =
+    Object.values(searchIndexDownloads ?? {});
+
+  if (downloadProgressValues.some(v => v < 1.00)) {
+    const total = Object.keys(state.indexDownloadStatuses).length;
+    const sum = accumulateSum(downloadProgressValues);
+    showSidebarSearchProgress(
+      sum / total,
+      templateContent(info.loadingDataString));
+
+    return;
+  }
+
+  if (state.searchStage === 'preparing') {
+    showSidebarSearchProgress(
+      null,
+      templateContent(info.preparingString));
+
+    return;
+  }
+
+  if (state.searchStage === 'searching') {
+    showSidebarSearchProgress(
+      null,
+      templateContent(info.searchingString));
+
+    return;
+  }
+
+  hideSidebarSearchProgress();
+}
+
+function showSidebarSearchProgress(progress, label) {
+  cssProp(info.progressRule, 'display', null);
+  cssProp(info.progressContainer, 'display', null);
+
+  if (progress === null) {
+    info.progressBar.removeAttribute('value');
+  } else {
+    info.progressBar.value = progress;
+  }
+
+  while (info.progressLabel.firstChild) {
+    info.progressLabel.firstChild.remove();
+  }
+
+  info.progressLabel.appendChild(label);
+}
+
+function hideSidebarSearchProgress() {
+  cssProp(info.progressRule, 'display', 'none');
+  cssProp(info.progressContainer, 'display', 'none');
+}
+
+function showSidebarSearchFailed() {
+  const {state} = info;
+
+  hideSidebarSearchProgress();
+  hideSidebarSearchResults();
+
+  cssProp(info.failedRule, 'display', null);
+  cssProp(info.failedContainer, 'display', null);
+
+  info.searchLabel.classList.add('disabled');
+  info.searchInput.disabled = true;
+
+  if (state.stoppedTypingTimeout) {
+    clearTimeout(state.stoppedTypingTimeout);
+    state.stoppedTypingTimeout = null;
+  }
+}
+
+function showSidebarSearchResults(results) {
+  console.debug(`Showing search results:`, results);
+
+  showSearchSidebarColumn();
+
+  const flatResults =
+    Object.entries(results)
+      .filter(([index]) => index === 'generic')
+      .flatMap(([index, results]) => results
+        .flatMap(({doc, id}) => ({
+          index,
+          reference: id ?? null,
+          referenceType: (id ? id.split(':')[0] : null),
+          directory: (id ? id.split(':')[1] : null),
+          data: doc,
+        })));
+
+  info.searchBox.classList.add('showing-results');
+  info.searchSidebarColumn.classList.add('search-showing-results');
+
+  while (info.results.firstChild) {
+    info.results.firstChild.remove();
+  }
+
+  cssProp(info.resultsRule, 'display', 'block');
+  cssProp(info.resultsContainer, 'display', 'block');
+
+  if (empty(flatResults)) {
+    const p = document.createElement('p');
+    p.classList.add('wiki-search-no-results');
+    p.appendChild(templateContent(info.noResultsString));
+    info.results.appendChild(p);
+  }
+
+  for (const result of flatResults) {
+    const el = generateSidebarSearchResult(result);
+    if (!el) continue;
+
+    info.results.appendChild(el);
+  }
+
+  if (!empty(flatResults)) {
+    cssProp(info.endSearchRule, 'display', 'block');
+    cssProp(info.endSearchLine, 'display', 'block');
+
+    tidySidebarSearchColumn();
+  }
+
+  restoreSidebarSearchResultsScrollOffset();
+}
+
+function generateSidebarSearchResult(result) {
+  const preparedSlots = {
+    color:
+      result.data.color ?? null,
+
+    name:
+      result.data.name ?? result.data.primaryName ?? null,
+
+    imageSource:
+      getSearchResultImageSource(result),
+  };
+
+  switch (result.referenceType) {
+    case 'album': {
+      preparedSlots.href =
+        openAlbum(result.directory);
+
+      preparedSlots.kindString =
+        info.albumResultKindString;
+
+      break;
+    }
+
+    case 'artist': {
+      preparedSlots.href =
+        openArtist(result.directory);
+
+      preparedSlots.kindString =
+        info.artistResultKindString;
+
+      break;
+    }
+
+    case 'group': {
+      preparedSlots.href =
+        openGroup(result.directory);
+
+      preparedSlots.kindString =
+        info.groupResultKindString;
+
+      break;
+    }
+
+    case 'flash': {
+      preparedSlots.href =
+        openFlash(result.directory);
+
+      break;
+    }
+
+    case 'tag': {
+      preparedSlots.href =
+        openArtTag(result.directory);
+
+      preparedSlots.kindString =
+        info.tagResultKindString;
+
+      break;
+    }
+
+    case 'track': {
+      preparedSlots.href =
+        openTrack(result.directory);
+
+      break;
+    }
+
+    default:
+      return null;
+  }
+
+  return generateSidebarSearchResultTemplate(preparedSlots);
+}
+
+function getSearchResultImageSource(result) {
+  const {artwork} = result.data;
+  if (!artwork) return null;
+
+  return (
+    rebase(
+      artwork.replace('<>', result.directory),
+      'rebaseThumb'));
+}
+
+function generateSidebarSearchResultTemplate(slots) {
+  const link = document.createElement('a');
+  link.classList.add('wiki-search-result');
+
+  if (slots.href) {
+    link.setAttribute('href', slots.href);
+  }
+
+  if (slots.color) {
+    cssProp(link, '--primary-color', slots.color);
+
+    try {
+      const colors =
+        getColors(slots.color, {
+          chroma: window.chroma,
+        });
+      cssProp(link, '--light-ghost-color', colors.lightGhost);
+      cssProp(link, '--deep-color', colors.deep);
+    } catch (error) {
+      console.warn(error);
+    }
+  }
+
+  const imgContainer = document.createElement('span');
+  imgContainer.classList.add('wiki-search-result-image-container');
+
+  if (slots.imageSource) {
+    const img = document.createElement('img');
+    img.classList.add('wiki-search-result-image');
+    img.setAttribute('src', slots.imageSource);
+    imgContainer.appendChild(img);
+    if (slots.imageSource.endsWith('.mini.jpg')) {
+      img.classList.add('has-warning');
+    }
+  } else {
+    const placeholder = document.createElement('span');
+    placeholder.classList.add('wiki-search-result-image-placeholder');
+    imgContainer.appendChild(placeholder);
+  }
+
+  link.appendChild(imgContainer);
+
+  const text = document.createElement('span');
+  text.classList.add('wiki-search-result-text-area');
+
+  if (slots.name) {
+    const span = document.createElement('span');
+    span.classList.add('wiki-search-result-name');
+    span.appendChild(document.createTextNode(slots.name));
+    text.appendChild(span);
+  }
+
+  let accentSpan = null;
+
+  if (link.href) {
+    const here = location.href.replace(/\/$/, '');
+    const there = link.href.replace(/\/$/, '');
+    if (here === there) {
+      link.classList.add('current-result');
+      accentSpan = document.createElement('span');
+      accentSpan.classList.add('wiki-search-current-result-text');
+      accentSpan.appendChild(templateContent(info.currentResultString));
+    }
+  }
+
+  if (!accentSpan && slots.kindString) {
+    accentSpan = document.createElement('span');
+    accentSpan.classList.add('wiki-search-result-kind');
+    accentSpan.appendChild(templateContent(slots.kindString));
+  }
+
+  if (accentSpan) {
+    text.appendChild(document.createTextNode(' '));
+    text.appendChild(accentSpan);
+  }
+
+  link.appendChild(text);
+
+  link.addEventListener('click', () => {
+    saveSidebarSearchResultsScrollOffset();
+  });
+
+  return link;
+}
+
+function hideSidebarSearchResults() {
+  cssProp(info.resultsRule, 'display', 'none');
+  cssProp(info.resultsContainer, 'display', 'none');
+
+  while (info.results.firstChild) {
+    info.results.firstChild.remove();
+  }
+
+  cssProp(info.endSearchRule, 'display', 'none');
+  cssProp(info.endSearchLine, 'display', 'none');
+}
+
+function saveSidebarSearchResultsScrollOffset() {
+  const {session} = info;
+
+  session.resultsScrollOffset = info.resultsContainer.scrollTop;
+}
+
+function restoreSidebarSearchResultsScrollOffset() {
+  const {session} = info;
+
+  if (session.resultsScrollOffset) {
+    info.resultsContainer.scrollTop = session.resultsScrollOffset;
+  }
+}
+
+function showSearchSidebarColumn() {
+  const {state} = info;
+
+  if (!info.searchSidebarColumn) {
+    return;
+  }
+
+  if (!info.searchSidebarColumn.classList.contains('initially-hidden')) {
+    return;
+  }
+
+  info.searchSidebarColumn.classList.remove('initially-hidden');
+
+  if (info.searchSidebarColumn.id === 'sidebar-left') {
+    info.pageContainer.classList.add('showing-sidebar-left');
+  } else if (info.searchSidebarColumn.id === 'sidebar-right') {
+    info.pageContainer.classList.add('showing-sidebar-right');
+  }
+
+  state.sidebarColumnShownForSearch = true;
+}
+
+function possiblyHideSearchSidebarColumn() {
+  const {state} = info;
+
+  if (!info.searchSidebarColumn) {
+    return;
+  }
+
+  if (!state.sidebarColumnShownForSearch) {
+    return;
+  }
+
+  info.searchSidebarColumn.classList.add('initially-hidden');
+
+  if (info.searchSidebarColumn.id === 'sidebar-left') {
+    info.pageContainer.classList.remove('showing-sidebar-left');
+  } else if (info.searchSidebarColumn.id === 'sidebar-right') {
+    info.pageContainer.classList.remove('showing-sidebar-right');
+  }
+
+  state.sidebarColumnShownForSearch = null;
+}
+
+// This should be called after results are shown, since it checks the
+// elements added to understand the current search state.
+function tidySidebarSearchColumn() {
+  const {state} = info;
+
+  // Don't *re-tidy* the sidebar if we've already tidied it to display
+  // some results. This flag will get cleared if the search is dismissed
+  // altogether (and the pre-tidy state is restored).
+  if (state.tidiedSidebar) {
+    return;
+  }
+
+  const here = location.href.replace(/\/$/, '');
+  const currentPageIsResult =
+    Array.from(info.results.querySelectorAll('a'))
+      .some(link => {
+        const there = link.href.replace(/\/$/, '');
+        return here === there;
+      });
+
+  // Don't tidy the sidebar if you've navigated to some other page than
+  // what's in the current result list.
+  if (!currentPageIsResult) {
+    return;
+  }
+
+  state.tidiedSidebar = true;
+  state.collapsedDetailsForTidiness = [];
+
+  for (const box of info.searchSidebarColumn.querySelectorAll('.sidebar')) {
+    if (box === info.searchBox) {
+      continue;
+    }
+
+    for (const details of box.getElementsByTagName('details')) {
+      if (details.open) {
+        details.removeAttribute('open');
+        state.collapsedDetailsForTidiness.push(details);
+      }
+    }
+  }
+}
+
+function restoreSidebarSearchColumn() {
+  const {state} = info;
+
+  if (!state.tidiedSidebar) {
+    return;
+  }
+
+  for (const details of state.collapsedDetailsForTidiness) {
+    details.setAttribute('open', '');
+  }
+
+  state.collapsedDetailsForTidiness = [];
+  state.tidiedSidebar = null;
+}
diff --git a/src/static/js/client/sticky-heading.js b/src/static/js/client/sticky-heading.js
new file mode 100644
index 00000000..ae63eab5
--- /dev/null
+++ b/src/static/js/client/sticky-heading.js
@@ -0,0 +1,257 @@
+/* eslint-env browser */
+
+import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js';
+import {dispatchInternalEvent, templateContent} from '../client-util.js';
+
+export const info = {
+  id: '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: [],
+  },
+};
+
+export function getPageReferences() {
+  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')));
+}
+
+export function mutatePageContent() {
+  removeTextPlaceholderStickyHeadingCovers();
+  addRevealClassToStickyHeadingCovers();
+}
+
+function removeTextPlaceholderStickyHeadingCovers() {
+  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 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 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 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 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 {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 =
+      templateContent(
+        closestHeading.querySelector('.content-heading-sticky-title')) ??
+      closestHeading.querySelector('.content-heading-main-title') ??
+      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,
+  });
+}
+
+export function updateStickyHeadings(index) {
+  updateStickyCoverVisibility(index);
+  updateStickySubheadingContent(index);
+}
+
+export function initializeState() {
+  for (let i = 0; i < info.stickyContainers.length; i++) {
+    updateStickyHeadings(i);
+  }
+}
+
+export function addPageListeners() {
+  addRevealListenersForStickyHeadingCovers();
+  addScrollListenerForStickyHeadings();
+}
+
+function addScrollListenerForStickyHeadings() {
+  document.addEventListener('scroll', () => {
+    for (let i = 0; i < info.stickyContainers.length; i++) {
+      updateStickyHeadings(i);
+    }
+  });
+}
diff --git a/src/static/js/client/summary-nested-link.js b/src/static/js/client/summary-nested-link.js
new file mode 100644
index 00000000..23857fa5
--- /dev/null
+++ b/src/static/js/client/summary-nested-link.js
@@ -0,0 +1,48 @@
+/* eslint-env browser */
+
+import {
+  empty,
+  filterMultipleArrays,
+  stitchArrays,
+} from '../../shared-util/sugar.js';
+
+export const info = {
+  id: 'summaryNestedLinkInfo',
+
+  summaries: null,
+  links: null,
+};
+
+export function getPageReferences() {
+  info.summaries =
+    Array.from(document.getElementsByTagName('summary'));
+
+  info.links =
+    info.summaries
+      .map(summary =>
+        Array.from(summary.getElementsByTagName('a')));
+
+  filterMultipleArrays(
+    info.summaries,
+    info.links,
+    (_summary, links) => !empty(links));
+}
+
+export function addPageListeners() {
+  for (const {summary, links} of stitchArrays({
+    summary: info.summaries,
+    links: info.links,
+  })) {
+    for (const link of links) {
+      link.addEventListener('mouseover', () => {
+        link.classList.add('nested-hover');
+        summary.classList.add('has-nested-hover');
+      });
+
+      link.addEventListener('mouseout', () => {
+        link.classList.remove('nested-hover');
+        summary.classList.remove('has-nested-hover');
+      });
+    }
+  }
+}
diff --git a/src/static/js/client/text-with-tooltip.js b/src/static/js/client/text-with-tooltip.js
new file mode 100644
index 00000000..dd207e04
--- /dev/null
+++ b/src/static/js/client/text-with-tooltip.js
@@ -0,0 +1,34 @@
+/* eslint-env browser */
+
+import {stitchArrays} from '../../shared-util/sugar.js';
+
+import {registerTooltipElement, registerTooltipHoverableElement}
+  from './hoverable-tooltip.js';
+
+export const info = {
+  id: 'textWithTooltipInfo',
+
+  hoverables: null,
+  tooltips: null,
+};
+
+export function getPageReferences() {
+  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]);
+}
+
+export function addPageListeners() {
+  for (const {hoverable, tooltip} of stitchArrays({
+    hoverable: info.hoverables,
+    tooltip: info.tooltips,
+  })) {
+    registerTooltipElement(tooltip);
+    registerTooltipHoverableElement(hoverable, tooltip);
+  }
+}
diff --git a/src/static/js/client/wiki-search.js b/src/static/js/client/wiki-search.js
new file mode 100644
index 00000000..2446c172
--- /dev/null
+++ b/src/static/js/client/wiki-search.js
@@ -0,0 +1,239 @@
+/* eslint-env browser */
+
+import {promiseWithResolvers} from '../../shared-util/sugar.js';
+
+import {dispatchInternalEvent} from '../client-util.js';
+
+export const info = {
+  id: 'wikiSearchInfo',
+
+  state: {
+    worker: null,
+
+    workerReadyPromise: null,
+    workerReadyPromiseResolvers: null,
+
+    workerActionCounter: 0,
+    workerActionPromiseResolverMap: new Map(),
+
+    downloads: Object.create(null),
+  },
+
+  event: {
+    whenWorkerAlive: [],
+    whenWorkerReady: [],
+    whenWorkerFailsToInitialize: [],
+    whenWorkerHasRuntimeError: [],
+
+    whenDownloadBegins: [],
+    whenDownloadsBegin: [],
+    whenDownloadProgresses: [],
+    whenDownloadEnds: [],
+  },
+};
+
+export async function initializeSearchWorker() {
+  const {state} = info;
+
+  if (state.worker) {
+    return await state.workerReadyPromise;
+  }
+
+  state.worker =
+    new Worker(
+      import.meta.resolve('../search-worker.js'),
+      {type: 'module'});
+
+  state.worker.onmessage = handleSearchWorkerMessage;
+
+  const {promise, resolve, reject} = promiseWithResolvers();
+
+  state.workerReadyPromiseResolvers = {resolve, reject};
+
+  return await (state.workerReadyPromise = promise);
+}
+
+function handleSearchWorkerMessage(message) {
+  switch (message.data.kind) {
+    case 'status':
+      handleSearchWorkerStatusMessage(message);
+      break;
+
+    case 'result':
+      handleSearchWorkerResultMessage(message);
+      break;
+
+    case 'download-begun':
+      handleSearchWorkerDownloadBegunMessage(message);
+      break;
+
+    case 'download-progress':
+      handleSearchWorkerDownloadProgressMessage(message);
+      break;
+
+    case 'download-complete':
+      handleSearchWorkerDownloadCompleteMessage(message);
+      break;
+
+    default:
+      console.warn(`Unknown message kind "${message.data.kind}" <- from search worker`);
+      break;
+  }
+}
+
+function handleSearchWorkerStatusMessage(message) {
+  const {state, event} = info;
+
+  switch (message.data.status) {
+    case 'alive':
+      console.debug(`Search worker is alive, but not yet ready.`);
+      dispatchInternalEvent(event, 'whenWorkerAlive');
+      break;
+
+    case 'ready':
+      console.debug(`Search worker has loaded corpuses and is ready.`);
+      state.workerReadyPromiseResolvers.resolve(state.worker);
+      dispatchInternalEvent(event, 'whenWorkerReady');
+      break;
+
+    case 'setup-error':
+      console.debug(`Search worker failed to initialize.`);
+      state.workerReadyPromiseResolvers.reject(new Error('Received "setup-error" status from worker'));
+      dispatchInternalEvent(event, 'whenWorkerFailsToInitialize');
+      break;
+
+    case 'runtime-error':
+      console.debug(`Search worker had an uncaught runtime error.`);
+      dispatchInternalEvent(event, 'whenWorkerHasRuntimeError');
+      break;
+
+    default:
+      console.warn(`Unknown status "${message.data.status}" <- from search worker`);
+      break;
+  }
+}
+
+function handleSearchWorkerResultMessage(message) {
+  const {state} = info;
+  const {id} = message.data;
+
+  if (!id) {
+    console.warn(`Result without id <- from search worker:`, message.data);
+    return;
+  }
+
+  if (!state.workerActionPromiseResolverMap.has(id)) {
+    console.warn(`Runaway result id <- from search worker:`, message.data);
+    return;
+  }
+
+  const {resolve, reject} =
+    state.workerActionPromiseResolverMap.get(id);
+
+  switch (message.data.status) {
+    case 'resolve':
+      resolve(message.data.value);
+      break;
+
+    case 'reject':
+      reject(message.data.value);
+      break;
+
+    default:
+      console.warn(`Unknown result status "${message.data.status}" <- from search worker`);
+      return;
+  }
+
+  state.workerActionPromiseResolverMap.delete(id);
+}
+
+function handleSearchWorkerDownloadBegunMessage(message) {
+  const {event} = info;
+  const {context: contextKey, keys} = message.data;
+
+  const context = getSearchWorkerDownloadContext(contextKey, true);
+
+  for (const key of keys) {
+    context[key] = 0.00;
+
+    dispatchInternalEvent(event, 'whenDownloadBegins', {
+      context: contextKey,
+      key,
+    });
+  }
+
+  dispatchInternalEvent(event, 'whenDownloadsBegin', {
+    context: contextKey,
+    keys,
+  });
+}
+
+function handleSearchWorkerDownloadProgressMessage(message) {
+  const {event} = info;
+  const {context: contextKey, key, progress} = message.data;
+
+  const context = getSearchWorkerDownloadContext(contextKey);
+
+  context[key] = progress;
+
+  dispatchInternalEvent(event, 'whenDownloadProgresses', {
+    context: contextKey,
+    key,
+    progress,
+  });
+}
+
+function handleSearchWorkerDownloadCompleteMessage(message) {
+  const {event} = info;
+  const {context: contextKey, key} = message.data;
+
+  const context = getSearchWorkerDownloadContext(contextKey);
+
+  context[key] = 1.00;
+
+  dispatchInternalEvent(event, 'whenDownloadEnds', {
+    context: contextKey,
+    key,
+  });
+}
+
+export function getSearchWorkerDownloadContext(context, initialize = false) {
+  const {state} = info;
+
+  if (context in state.downloads) {
+    return state.downloads[context];
+  }
+
+  if (!initialize) {
+    return null;
+  }
+
+  return state.downloads[context] = Object.create(null);
+}
+
+export async function postSearchWorkerAction(action, options) {
+  const {state} = info;
+
+  const worker = await initializeSearchWorker();
+  const id = ++state.workerActionCounter;
+
+  const {promise, resolve, reject} = promiseWithResolvers();
+
+  state.workerActionPromiseResolverMap.set(id, {resolve, reject});
+
+  worker.postMessage({
+    kind: 'action',
+    action: action,
+    id,
+    options,
+  });
+
+  return await promise;
+}
+
+export async function searchAll(query, options = {}) {
+  return await postSearchWorkerAction('search', {
+    query,
+    options,
+  });
+}
diff --git a/src/static/js/group-contributions-table.js b/src/static/js/group-contributions-table.js
new file mode 100644
index 00000000..72ad2327
--- /dev/null
+++ b/src/static/js/group-contributions-table.js
@@ -0,0 +1,35 @@
+/* eslint-env browser */
+
+// 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');
+  });
+}
diff --git a/src/static/js/image-overlay.js b/src/static/js/image-overlay.js
new file mode 100644
index 00000000..1eee60e5
--- /dev/null
+++ b/src/static/js/image-overlay.js
@@ -0,0 +1,256 @@
+/* eslint-env browser */
+
+import {cssProp} from './client-util.js';
+import {fetchWithProgress} from './xhr-util.js';
+
+// 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');
+    }
+  }
+}
+
+async 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);
+
+  const showProgress = amount => {
+    cssProp(container, '--download-progress', `${amount * 100}%`);
+  };
+
+  showProgress(0.00);
+
+  const response =
+    await fetchWithProgress(mainSrc, progress => {
+      if (progress === -1) {
+        // TODO: Indeterminate response progress cue
+        showProgress(0.00);
+      } else {
+        showProgress(0.20 + 0.80 * progress);
+      }
+    });
+
+  if (!response.status.toString().startsWith('2')) {
+    handleMainImageErrored();
+    return;
+  }
+
+  const blob = await response.blob();
+  const blobSrc = URL.createObjectURL(blob);
+
+  mainImage.src = blobSrc;
+  showProgress(1.00);
+
+  function handleMainImageLoaded() {
+    container.classList.add('loaded');
+    removeEventListeners();
+  }
+
+  function handleMainImageErrored() {
+    container.classList.add('errored');
+    removeEventListeners();
+  }
+
+  function removeEventListeners() {
+    mainImage.removeEventListener('load', handleMainImageLoaded);
+    mainImage.removeEventListener('error', handleMainImageErrored);
+  }
+}
+
+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();
diff --git a/src/static/js/info-card.js b/src/static/js/info-card.js
new file mode 100644
index 00000000..1d9f7c86
--- /dev/null
+++ b/src/static/js/info-card.js
@@ -0,0 +1,181 @@
+/* eslint-env browser */
+
+// Note: This is a super ancient chunk of code which isn't actually in use,
+// so it's just commented out here.
+
+/*
+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');
+}
+*/
+
diff --git a/src/static/js/localization-nonsense.js b/src/static/js/localization-nonsense.js
new file mode 100644
index 00000000..8b6d1ef0
--- /dev/null
+++ b/src/static/js/localization-nonsense.js
@@ -0,0 +1,30 @@
+// Another old, unused chunk of code.
+
+/*
+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,
+  };
+}
+*/
diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js
new file mode 100644
index 00000000..cdab2cb8
--- /dev/null
+++ b/src/static/js/rectangles.js
@@ -0,0 +1,513 @@
+/* eslint-env browser */
+
+import {info as liveMousePositionInfo} from './client/live-mouse-position.js';
+
+export 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,
+    });
+  }
+}