« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static/js/client.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/static/js/client.js')
-rw-r--r--src/static/js/client.js4956
1 files changed, 0 insertions, 4956 deletions
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);
-    }
-  }
-}