« get me outta code hell

break up utility file, get build for sure working - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static
diff options
context:
space:
mode:
author(quasar) nebula <towerofnix@gmail.com>2021-05-06 14:56:18 -0300
committer(quasar) nebula <towerofnix@gmail.com>2021-05-06 14:56:18 -0300
commitead9bdc9fc1e9cc62a26e59f6880a13aa864f931 (patch)
treed459b47dbb17ad99615ca595bbe1e92d651eab15 /src/static
parent2260541dc69c19e7444348ac3243f96e4321b781 (diff)
break up utility file, get build for sure working
still Much Work Yet Ahead but this is good progress!! also the site is
in a working state afaict and thats a kinda nice milestone lmbo
Diffstat (limited to 'src/static')
-rw-r--r--src/static/client.js415
-rw-r--r--src/static/icons.svg11
-rw-r--r--src/static/lazy-loading.js51
-rw-r--r--src/static/site-basic.css19
-rw-r--r--src/static/site.css872
5 files changed, 1368 insertions, 0 deletions
diff --git a/src/static/client.js b/src/static/client.js
new file mode 100644
index 0000000..c12ff35
--- /dev/null
+++ b/src/static/client.js
@@ -0,0 +1,415 @@
+// This is the JS file that gets loaded on the client! It's only really used for
+// the random track feature right now - the idea is we only use it for stuff
+// that cannot 8e done at static-site compile time, 8y its fundamentally
+// ephemeral nature.
+//
+// Upd8: As of 04/02/2021, it's now used for info cards too! Nice.
+
+import {
+    getColors
+} from '../util/colors.js';
+
+let albumData, artistData, flashData;
+let officialAlbumData, fandomAlbumData, artistNames;
+
+let ready = false;
+
+// Localiz8tion nonsense ----------------------------------
+
+const language = document.documentElement.getAttribute('lang');
+
+let list;
+if (
+    typeof Intl === 'object' &&
+    typeof Intl.ListFormat === 'function'
+) {
+    const getFormat = type => {
+        const formatter = new Intl.ListFormat(language, {type});
+        return formatter.format.bind(formatter);
+    };
+
+    list = {
+        conjunction: getFormat('conjunction'),
+        disjunction: getFormat('disjunction'),
+        unit: getFormat('unit')
+    };
+} else {
+    // Not a gr8 mock we've got going here, 8ut it's *mostly* language-free.
+    // We use the same mock for every list 'cuz we don't have any of the
+    // necessary CLDR info to appropri8tely distinguish 8etween them.
+    const arbitraryMock = array => array.join(', ');
+
+    list = {
+        conjunction: arbitraryMock,
+        disjunction: arbitraryMock,
+        unit: arbitraryMock
+    };
+}
+
+// Miscellaneous helpers ----------------------------------
+
+function rebase(href, rebaseKey = 'rebaseLocalized') {
+    const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/';
+    if (relative) {
+        return relative + href;
+    } else {
+        return href;
+    }
+}
+
+function pick(array) {
+    return array[Math.floor(Math.random() * array.length)];
+}
+
+function cssProp(el, key) {
+    return getComputedStyle(el).getPropertyValue(key).trim();
+}
+
+function getRefDirectory(ref) {
+    return ref.split(':')[1];
+}
+
+function getAlbum(el) {
+    const directory = cssProp(el, '--album-directory');
+    return albumData.find(album => album.directory === directory);
+}
+
+function getFlash(el) {
+    const directory = cssProp(el, '--flash-directory');
+    return flashData.find(flash => flash.directory === directory);
+}
+
+// TODO: These should pro8a8ly access some shared urlSpec path. We'd need to
+// separ8te the tooling around that into common-shared code too.
+const getLinkHref = (type, directory) => rebase(`${type}/${directory}`);
+const openAlbum = d => rebase(`album/${d}`);
+const openTrack = d => rebase(`track/${d}`);
+const openArtist = d => rebase(`artist/${d}`);
+const openFlash = d => rebase(`flash/${d}`);
+
+function getTrackListAndIndex() {
+    const album = getAlbum(document.body);
+    const directory = cssProp(document.body, '--track-directory');
+    if (!directory && !album) return {};
+    if (!directory) return {list: album.tracks};
+    const trackIndex = album.tracks.findIndex(track => track.directory === directory);
+    return {list: album.tracks, index: trackIndex};
+}
+
+function openRandomTrack() {
+    const { list } = getTrackListAndIndex();
+    if (!list) return;
+    return openTrack(pick(list));
+}
+
+function getFlashListAndIndex() {
+    const list = flashData.filter(flash => !flash.act8r8k)
+    const flash = getFlash(document.body);
+    if (!flash) return {list};
+    const flashIndex = list.indexOf(flash);
+    return {list, index: flashIndex};
+}
+
+// TODO: This should also use urlSpec.
+function fetchData(type, directory) {
+    return fetch(rebase(`${type}/${directory}/data.json`, 'rebaseData'))
+        .then(res => res.json());
+}
+
+// JS-based links -----------------------------------------
+
+for (const a of document.body.querySelectorAll('[data-random]')) {
+    a.addEventListener('click', evt => {
+        if (!ready) {
+            evt.preventDefault();
+            return;
+        }
+
+        setTimeout(() => {
+            a.href = rebase('js-disabled');
+        });
+        switch (a.dataset.random) {
+            case 'album': return a.href = openAlbum(pick(albumData).directory);
+            case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData).directory);
+            case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData).directory);
+            case 'track': return a.href = openTrack(getRefDirectory(pick(albumData.map(a => a.tracks).reduce((a, b) => a.concat(b), []))));
+            case 'track-in-album': return a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks)));
+            case 'track-in-fandom': return a.href = openTrack(getRefDirectory(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))));
+            case 'track-in-official': return a.href = openTrack(getRefDirectory(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))));
+            case 'artist': return a.href = openArtist(pick(artistData).directory);
+            case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistData.filter(artist => C.getArtistNumContributions(artist) > 1)).directory);
+        }
+    });
+}
+
+const next = document.getElementById('next-button');
+const previous = document.getElementById('previous-button');
+const random = document.getElementById('random-button');
+
+const prependTitle = (el, prepend) => {
+    const existing = el.getAttribute('title');
+    if (existing) {
+        el.setAttribute('title', prepend + ' ' + existing);
+    } else {
+        el.setAttribute('title', prepend);
+    }
+};
+
+if (next) prependTitle(next, '(Shift+N)');
+if (previous) prependTitle(previous, '(Shift+P)');
+if (random) prependTitle(random, '(Shift+R)');
+
+document.addEventListener('keypress', event => {
+    if (event.shiftKey) {
+        if (event.charCode === 'N'.charCodeAt(0)) {
+            if (next) next.click();
+        } else if (event.charCode === 'P'.charCodeAt(0)) {
+            if (previous) previous.click();
+        } else if (event.charCode === 'R'.charCodeAt(0)) {
+            if (random && ready) random.click();
+        }
+    }
+});
+
+for (const reveal of document.querySelectorAll('.reveal')) {
+    reveal.addEventListener('click', event => {
+        if (!reveal.classList.contains('revealed')) {
+            reveal.classList.add('revealed');
+            event.preventDefault();
+            event.stopPropagation();
+        }
+    });
+}
+
+const elements1 = document.getElementsByClassName('js-hide-once-data');
+const elements2 = document.getElementsByClassName('js-show-once-data');
+
+for (const element of elements1) element.style.display = 'block';
+
+fetch(rebase('data.json', 'rebaseShared')).then(data => data.json()).then(data => {
+    albumData = data.albumData;
+    artistData = data.artistData;
+    flashData = data.flashData;
+
+    officialAlbumData = albumData.filter(album => album.groups.includes('group:official'));
+    fandomAlbumData = albumData.filter(album => !album.groups.includes('group:official'));
+    artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name);
+
+    for (const element of elements1) element.style.display = 'none';
+    for (const element of elements2) element.style.display = 'block';
+
+    ready = true;
+});
+
+// Data & info card ---------------------------------------
+
+const NORMAL_HOVER_INFO_DELAY = 750;
+const FAST_HOVER_INFO_DELAY = 250;
+const END_FAST_HOVER_DELAY = 500;
+const HIDE_HOVER_DELAY = 250;
+
+let fastHover = false;
+let endFastHoverTimeout = null;
+
+function colorLink(a, color) {
+    if (color) {
+        const { primary, dim } = getColors(color);
+        a.style.setProperty('--primary-color', primary);
+        a.style.setProperty('--dim-color', dim);
+    }
+}
+
+function link(a, type, {name, directory, color}) {
+    colorLink(a, color);
+    a.innerText = name
+    a.href = getLinkHref(type, directory);
+}
+
+function joinElements(type, elements) {
+    // We can't use the Intl APIs with elements, 8ecuase it only oper8tes on
+    // strings. So instead, we'll pass the element's outer HTML's (which means
+    // the entire HTML of that element).
+    //
+    // That does mean this function returns a string, so always 8e sure to
+    // set innerHTML when using it (not appendChild).
+
+    return list[type](elements.map(el => el.outerHTML));
+}
+
+const infoCard = (() => {
+    const container = document.getElementById('info-card-container');
+
+    let cancelShow = false;
+    let hideTimeout = null;
+    let showing = false;
+
+    container.addEventListener('mouseenter', cancelHide);
+    container.addEventListener('mouseleave', readyHide);
+
+    function show(type, target) {
+        cancelShow = false;
+
+        fetchData(type, target.dataset[type]).then(data => {
+            // Manual DOM 'cuz we're laaaazy.
+
+            if (cancelShow) {
+                return;
+            }
+
+            showing = true;
+
+            const rect = target.getBoundingClientRect();
+
+            container.style.setProperty('--primary-color', data.color);
+
+            container.style.top = window.scrollY + rect.bottom + 'px';
+            container.style.left = window.scrollX + rect.left + 'px';
+
+            // Use a short timeout to let a currently hidden (or not yet shown)
+            // info card teleport to the position set a8ove. (If it's currently
+            // shown, it'll transition to that position.)
+            setTimeout(() => {
+                container.classList.remove('hide');
+                container.classList.add('show');
+            }, 50);
+
+            // 8asic details.
+
+            const nameLink = container.querySelector('.info-card-name a');
+            link(nameLink, 'track', data);
+
+            const albumLink = container.querySelector('.info-card-album a');
+            link(albumLink, 'album', data.album);
+
+            const artistSpan = container.querySelector('.info-card-artists span');
+            artistSpan.innerHTML = joinElements('conjunction', data.artists.map(({ artist }) => {
+                const a = document.createElement('a');
+                a.href = getLinkHref('artist', artist.directory);
+                a.innerText = artist.name;
+                return a;
+            }));
+
+            const coverArtistParagraph = container.querySelector('.info-card-cover-artists');
+            const coverArtistSpan = coverArtistParagraph.querySelector('span');
+            if (data.coverArtists.length) {
+                coverArtistParagraph.style.display = 'block';
+                coverArtistSpan.innerHTML = joinElements('conjunction', data.coverArtists.map(({ artist }) => {
+                    const a = document.createElement('a');
+                    a.href = getLinkHref('artist', artist.directory);
+                    a.innerText = artist.name;
+                    return a;
+                }));
+            } else {
+                coverArtistParagraph.style.display = 'none';
+            }
+
+            // Cover art.
+
+            const [ containerNoReveal, containerReveal ] = [
+                container.querySelector('.info-card-art-container.no-reveal'),
+                container.querySelector('.info-card-art-container.reveal')
+            ];
+
+            const [ containerShow, containerHide ] = (data.cover.warnings.length
+                ? [containerReveal, containerNoReveal]
+                : [containerNoReveal, containerReveal]);
+
+            containerHide.style.display = 'none';
+            containerShow.style.display = 'block';
+
+            const img = containerShow.querySelector('.info-card-art');
+            img.src = rebase(data.cover.paths.small, 'rebaseMedia');
+
+            const imgLink = containerShow.querySelector('a');
+            colorLink(imgLink, data.color);
+            imgLink.href = rebase(data.cover.paths.original, 'rebaseMedia');
+
+            if (containerShow === containerReveal) {
+                const cw = containerShow.querySelector('.info-card-art-warnings');
+                cw.innerText = list.unit(data.cover.warnings);
+
+                const reveal = containerShow.querySelector('.reveal');
+                reveal.classList.remove('revealed');
+            }
+        });
+    }
+
+    function hide() {
+        container.classList.remove('show');
+        container.classList.add('hide');
+        cancelShow = true;
+        showing = false;
+    }
+
+    function readyHide() {
+        if (!hideTimeout && showing) {
+            hideTimeout = setTimeout(hide, HIDE_HOVER_DELAY);
+        }
+    }
+
+    function cancelHide() {
+        if (hideTimeout) {
+            clearTimeout(hideTimeout);
+            hideTimeout = null;
+        }
+    }
+
+    return {
+        show,
+        hide,
+        readyHide,
+        cancelHide
+    };
+})();
+
+function makeInfoCardLinkHandlers(type) {
+    let hoverTimeout = null;
+
+    return {
+        mouseenter(evt) {
+            hoverTimeout = setTimeout(() => {
+                fastHover = true;
+                infoCard.show(type, evt.target);
+            }, fastHover ? FAST_HOVER_INFO_DELAY : NORMAL_HOVER_INFO_DELAY);
+
+            clearTimeout(endFastHoverTimeout);
+            endFastHoverTimeout = null;
+
+            infoCard.cancelHide();
+        },
+
+        mouseleave(evt) {
+            clearTimeout(hoverTimeout);
+
+            if (fastHover && !endFastHoverTimeout) {
+                endFastHoverTimeout = setTimeout(() => {
+                    endFastHoverTimeout = null;
+                    fastHover = false;
+                }, END_FAST_HOVER_DELAY);
+            }
+
+            infoCard.readyHide();
+        }
+    };
+}
+
+const infoCardLinkHandlers = {
+    track: makeInfoCardLinkHandlers('track')
+};
+
+function addInfoCardLinkHandlers(type) {
+    for (const a of document.querySelectorAll(`a[data-${type}]`)) {
+        for (const [ eventName, handler ] of Object.entries(infoCardLinkHandlers[type])) {
+            a.addEventListener(eventName, handler);
+        }
+    }
+}
+
+// Info cards are disa8led for now since they aren't quite ready for release,
+// 8ut you can try 'em out 8y setting this localStorage flag!
+//
+//     localStorage.tryInfoCards = true;
+//
+if (localStorage.tryInfoCards) {
+    addInfoCardLinkHandlers('track');
+}
diff --git a/src/static/icons.svg b/src/static/icons.svg
new file mode 100644
index 0000000..1e4351b
--- /dev/null
+++ b/src/static/icons.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" display="none" width="0" height="0">
+	<symbol id="icon-globe" viewBox="0 0 40 40"><path d="M20,3C10.6,3,3,10.6,3,20s7.6,17,17,17s17-7.6,17-17S29.4,3,20,3z M34.8,18.9h-6.2c-0.1-2.2-0.3-4.2-0.8-6.1 c1.5-0.4,3-0.9,4.2-1.5c0.6,0.9,1.2,1.8,1.6,2.9C34.3,15.7,34.7,17.3,34.8,18.9z M25.7,26.7c-1.5-0.3-3.1-0.4-4.6-0.5v-5.1h5.4 c-0.1,1.8-0.3,3.5-0.6,5.1C25.8,26.3,25.8,26.5,25.7,26.7z M14.2,26.2c-0.3-1.6-0.6-3.3-0.6-5.1h5.4v5.1c-1.6,0-3.2,0.2-4.6,0.5 C14.2,26.5,14.2,26.3,14.2,26.2z M14.3,13.3c1.5,0.3,3.1,0.4,4.6,0.5v5.1h-5.4c0.1-1.8,0.3-3.5,0.6-5.1 C14.2,13.7,14.2,13.5,14.3,13.3z M21.1,5.4C21.4,5.6,21.7,5.7,22,6c0.8,0.7,1.6,1.7,2.2,3c0.4,0.7,0.7,1.5,0.9,2.3 c-1.3,0.2-2.7,0.4-4,0.4V5.4z M18,6c0.3-0.3,0.6-0.4,0.9-0.6v6.2c-1.4,0-2.8-0.2-4-0.4c0.3-0.8,0.6-1.6,0.9-2.3 C16.5,7.7,17.2,6.7,18,6z M18.9,28.4v6.2c-0.3-0.1-0.6-0.3-0.9-0.6c-0.8-0.7-1.6-1.7-2.2-3c-0.4-0.7-0.7-1.5-0.9-2.3 C16.2,28.6,17.5,28.4,18.9,28.4z M22,34c-0.3,0.3-0.6,0.4-0.9,0.6v-6.2c1.4,0,2.8,0.2,4,0.4c-0.3,0.8-0.6,1.6-0.9,2.3 C23.5,32.3,22.8,33.3,22,34z M21.1,18.9v-5.1c1.6,0,3.2-0.2,4.6-0.5c0,0.2,0.1,0.4,0.1,0.5c0.3,1.6,0.6,3.3,0.6,5.1H21.1z M30.5,9.5 c0,0,0.1,0.1,0.1,0.1c-1,0.4-2.2,0.8-3.4,1.1c-0.6-1.9-1.4-3.5-2.4-4.8c0.3,0.1,0.6,0.2,0.9,0.3C27.5,7.1,29.1,8.1,30.5,9.5z M14.2,6.3c0.3-0.1,0.6-0.2,0.9-0.3c-0.9,1.3-1.7,2.9-2.4,4.8c-1.2-0.3-2.3-0.7-3.4-1.1c0,0,0.1-0.1,0.1-0.1 C10.9,8.1,12.5,7.1,14.2,6.3z M7.9,11.4c1.3,0.6,2.7,1.1,4.2,1.5c-0.4,1.9-0.7,3.9-0.8,6.1H5.2c0.1-1.6,0.5-3.2,1.1-4.7 C6.8,13.2,7.3,12.3,7.9,11.4z M5.2,21.1h6.2c0.1,2.2,0.3,4.2,0.8,6.1c-1.5,0.4-3,0.9-4.2,1.5c-0.6-0.9-1.2-1.8-1.6-2.9 C5.7,24.3,5.3,22.7,5.2,21.1z M9.5,30.5c0,0-0.1-0.1-0.1-0.1c1-0.4,2.2-0.8,3.4-1.1c0.6,1.9,1.4,3.5,2.4,4.8 c-0.3-0.1-0.6-0.2-0.9-0.3C12.5,32.9,10.9,31.9,9.5,30.5z M25.8,33.7c-0.3,0.1-0.6,0.2-0.9,0.3c0.9-1.3,1.7-2.9,2.4-4.8 c1.2,0.3,2.3,0.7,3.4,1.1c0,0-0.1,0.1-0.1,0.1C29.1,31.9,27.5,32.9,25.8,33.7z M32.1,28.6c-1.3-0.6-2.7-1.1-4.2-1.5 c0.4-1.9,0.7-3.9,0.8-6.1h6.2c-0.1,1.6-0.5,3.2-1.1,4.7C33.2,26.8,32.7,27.7,32.1,28.6z"/></symbol>
+	<symbol id="icon-bandcamp" viewBox="0 0 40 40"><path d="M7.1,13.3c5.6,0,11.1,0,16.7,0c0,1.5,0,3.1,0,4.6c0.7-0.7,1.5-1.5,3.2-1.3c2.6,0.3,3.8,3,3.6,5.6c-0.1,1.1-0.5,2.4-1.3,3.1 c-0.9,0.9-2.9,1.4-4.6,0.5c-0.4-0.2-0.7-0.6-1-1.1c0,0.4,0,0.8,0,1.3c-0.6,0-1.3,0-1.9,0c0-4.2,0-8.3,0-12.5 c-2.3,3.9-4.6,8.4-6.9,12.5c-4.9,0-9.8,0-14.7,0C2.5,21.7,4.8,17.5,7.1,13.3L7.1,13.3z M24.3,19c-1.4,1.9-0.4,6.7,2.8,5.5 c2.4-0.9,2-6.6-1.2-6.3C25.2,18.3,24.7,18.5,24.3,19L24.3,19z"/> <path d="M39.7,19.9c-0.6,0-1.3,0-2,0c0-1.6-1.9-2-3.1-1.5c-2.3,1.1-1.8,7.1,1.6,6.2c0.8-0.2,1.2-0.9,1.4-2c0.6,0,1.3,0,2,0 c-0.1,2.4-2.1,3.9-4.4,3.7c-2.1-0.1-3.8-1.8-4-4.2c-0.2-2.9,1.3-5.9,5-5.5C38.3,16.8,39.6,17.9,39.7,19.9z"/></symbol>
+	<symbol id="icon-deviantart" viewBox="0 0 40 40"><path d="M30,9.2L24,20.9l0.5,0.6H30v8.3H19.9L19,30.5l-2.8,5.5l-0.6,0.6h-6v-6.1l6.1-11.7l-0.5-0.6H9.5V9.8h10.2l0.9-0.6l2.8-5.5 L24,3.2h6C30,3.2,30,9.2,30,9.2z"/></symbol>
+	<symbol id="icon-soundcloud" viewBox="0 0 40 40"><path d="M13.8,27.4l0.3-4.2L13.8,14c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1C13.1,13.8,13,13.9,13,14 l-0.2,9.2l0.2,4.2c0,0.1,0.1,0.2,0.1,0.3c0.1,0.1,0.2,0.1,0.3,0.1C13.7,27.8,13.8,27.7,13.8,27.4z M18.8,26.9l0.2-3.7l-0.2-10.3 c0-0.2-0.1-0.3-0.2-0.4c-0.1-0.1-0.2-0.1-0.3-0.1s-0.2,0-0.3,0.1c-0.1,0.1-0.2,0.2-0.2,0.4l0,0.1l-0.2,10.1c0,0,0.1,1.4,0.2,4.1v0 c0,0.1,0,0.2,0.1,0.3c0.1,0.1,0.2,0.2,0.4,0.2c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.2-0.2,0.2-0.4L18.8,26.9z M1.2,20.9l0.3,2.2 l-0.3,2.2c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.1-0.1-0.2-0.2l-0.3-2.2l0.3-2.2c0-0.1,0.1-0.2,0.2-0.2S1.2,20.8,1.2,20.9z M2.7,19.5 l0.4,3.6l-0.4,3.6c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L2,23.2l0.4-3.6c0-0.1,0.1-0.2,0.2-0.2C2.6,19.4,2.7,19.4,2.7,19.5z M4.2,18.9l0.4,4.3l-0.4,4.2c0,0.1-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2l-0.4-4.2l0.4-4.3c0-0.1,0.1-0.2,0.2-0.2 C4.2,18.7,4.2,18.7,4.2,18.9z M5.8,18.8l0.4,4.4l-0.4,4.3c0,0.2-0.1,0.2-0.2,0.2c-0.1,0-0.2-0.1-0.2-0.2L5,23.2l0.4-4.4 c0-0.2,0.1-0.2,0.2-0.2C5.7,18.5,5.8,18.6,5.8,18.8z M7.4,19.1l0.4,4.1l-0.4,4.3c0,0.2-0.1,0.3-0.3,0.3c-0.1,0-0.1,0-0.2-0.1 c-0.1-0.1-0.1-0.1-0.1-0.2l-0.3-4.3l0.3-4.1c0-0.1,0-0.1,0.1-0.2C7,18.8,7,18.8,7.1,18.8C7.3,18.8,7.4,18.9,7.4,19.1L7.4,19.1z M9,16.5l0.4,6.7L9,27.5c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.1,0.1-0.2,0.1c-0.2,0-0.3-0.1-0.3-0.3l-0.3-4.3l0.3-6.7 c0-0.2,0.1-0.3,0.3-0.3c0.1,0,0.1,0,0.2,0.1S9,16.4,9,16.5z M10.5,15l0.3,8.2l-0.3,4.3c0,0.1,0,0.2-0.1,0.2 c-0.1,0.1-0.1,0.1-0.2,0.1c-0.2,0-0.3-0.1-0.3-0.3l-0.3-4.3L9.9,15c0-0.2,0.1-0.3,0.3-0.3c0.1,0,0.2,0,0.2,0.1 C10.5,14.8,10.5,14.9,10.5,15z M12.2,14.3l0.3,8.9l-0.3,4.2c0,0.2-0.1,0.4-0.4,0.4c-0.2,0-0.3-0.1-0.4-0.4l-0.3-4.2l0.3-8.9 c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0.1C12.1,14.1,12.2,14.2,12.2,14.3z M18.8,27.3L18.8,27.3L18.8,27.3z M15.4,14.2l0.3,8.9l-0.3,4.2c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.1-0.3,0.1c-0.1,0-0.2,0-0.3-0.1s-0.1-0.2-0.1-0.3l-0.2-4.2 l0.2-8.9c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1C15.4,14,15.4,14.1,15.4,14.2L15.4,14.2z M17.1,14.6 l0.2,8.6l-0.2,4.1c0,0.1,0,0.2-0.1,0.3c-0.1,0.1-0.2,0.1-0.3,0.1c-0.1,0-0.2,0-0.3-0.1c-0.1-0.1-0.1-0.2-0.2-0.3L16,23.2l0.2-8.6 c0-0.1,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1C17.1,14.3,17.1,14.4,17.1,14.6z M20.7,23.2l-0.2,4 c0,0.2-0.1,0.3-0.2,0.4c-0.1,0.1-0.2,0.2-0.4,0.2c-0.1,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.2-0.2-0.4l-0.1-2l-0.1-2L19.4,12V12 c0-0.2,0.1-0.3,0.2-0.4c0.1-0.1,0.2-0.1,0.3-0.1c0.1,0,0.2,0,0.3,0.1c0.2,0.1,0.2,0.2,0.3,0.5L20.7,23.2z M39.4,22.9 c0,1.4-0.5,2.5-1.4,3.5c-0.9,1-2,1.4-3.4,1.4H21.4c-0.1,0-0.3-0.1-0.4-0.2c-0.1-0.1-0.2-0.2-0.2-0.4V11.5c0-0.3,0.2-0.5,0.5-0.6 c1-0.4,2-0.6,3-0.6c2.2,0,4.1,0.8,5.7,2.3c1.6,1.5,2.5,3.4,2.7,5.7c0.6-0.3,1.2-0.4,1.8-0.4c1.3,0,2.4,0.5,3.4,1.5 C38.9,20.3,39.4,21.5,39.4,22.9L39.4,22.9z"/></symbol>
+	<symbol id="icon-tumblr" viewBox="0 0 40 40"><path d="M26.8,29.7l1.6,4.6c-0.3,0.5-1,0.9-2.2,1.3s-2.3,0.6-3.4,0.6c-1.4,0-2.6-0.1-3.7-0.5s-2.1-0.8-2.8-1.4 c-0.7-0.6-1.3-1.3-1.9-2.1c-0.5-0.8-0.9-1.6-1.1-2.3c-0.2-0.8-0.3-1.5-0.3-2.3V16.9H9.7v-4.2c0.9-0.3,1.8-0.8,2.5-1.4 s1.3-1.1,1.8-1.8s0.8-1.3,1.1-2c0.3-0.7,0.5-1.4,0.7-1.9S16,4.6,16.1,4c0-0.1,0-0.1,0.1-0.2s0.1-0.1,0.1-0.1h4.8V12h6.5v4.9h-6.5V27 c0,0.4,0,0.8,0.1,1.1c0.1,0.3,0.2,0.7,0.4,1s0.5,0.6,1,0.8c0.4,0.2,1,0.3,1.6,0.3C25.2,30.2,26.1,30,26.8,29.7L26.8,29.7z"/></symbol>
+	<symbol id="icon-twitter" viewBox="0 0 40 40"><path d="M36.3,10.2c-1,1.3-2.1,2.5-3.4,3.5c0,0.2,0,0.4,0,1c0,1.7-0.2,3.6-0.9,5.3c-0.6,1.7-1.2,3.5-2.4,5.1 c-1.1,1.5-2.3,3.1-3.7,4.3c-1.4,1.2-3.3,2.3-5.3,3c-2.1,0.8-4.2,1.2-6.6,1.2c-3.6,0-7-1-10.2-3c0.4,0,1.1,0.1,1.5,0.1 c3.1,0,5.9-1,8.2-2.9c-1.4,0-2.7-0.4-3.8-1.3c-1.2-1-1.9-2-2.2-3.3c0.4,0.1,1,0.1,1.2,0.1c0.6,0,1.2-0.1,1.7-0.2 c-1.4-0.3-2.7-1.1-3.7-2.3s-1.4-2.6-1.4-4.2v-0.1c1,0.6,2,0.9,3,0.9c-1-0.6-1.5-1.3-2.2-2.4c-0.6-1-0.9-2.1-0.9-3.3s0.3-2.3,1-3.4 c1.5,2.1,3.6,3.6,6,4.9s4.9,2,7.6,2.1c-0.1-0.6-0.1-1.1-0.1-1.4c0-1.8,0.8-3.5,2-4.7c1.2-1.2,2.9-2,4.7-2c2,0,3.6,0.8,4.8,2.1 c1.4-0.3,2.9-0.9,4.2-1.5c-0.4,1.5-1.4,2.7-2.9,3.6C33.8,11.2,35.1,10.9,36.3,10.2L36.3,10.2z"/></symbol>
+	<symbol id="icon-youtube" viewBox="0 0 40 40"><path d="M24.3,27v4.2c0,0.9-0.3,1.3-0.8,1.3c-0.3,0-0.6-0.1-0.9-0.4v-6c0.3-0.3,0.6-0.4,0.9-0.4C24,25.6,24.3,26.1,24.3,27L24.3,27z M31.1,27v0.9h-1.8V27c0-0.9,0.3-1.4,0.9-1.4C30.8,25.6,31.1,26.1,31.1,27L31.1,27z M11.7,22.6h2.1v-1.9H7.6v1.9h2.1v11.4h2 L11.7,22.6L11.7,22.6z M17.5,34.1h1.8v-9.9h-1.8v7.6c-0.4,0.6-0.8,0.8-1.1,0.8c-0.2,0-0.4-0.1-0.4-0.4c0,0,0-0.3,0-0.7v-7.3h-1.8V32 c0,0.7,0.1,1.1,0.2,1.5c0.2,0.5,0.5,0.7,1.2,0.7c0.6,0,1.3-0.4,2-1.2L17.5,34.1L17.5,34.1z M26.1,31.1v-4c0-1-0.1-1.6-0.2-2 c-0.2-0.7-0.7-1.1-1.4-1.1c-0.7,0-1.3,0.4-1.9,1.1v-4.4h-1.8v13.3h1.8v-1c0.6,0.7,1.2,1.1,1.9,1.1c0.7,0,1.2-0.4,1.4-1.1 C26,32.7,26.1,32.1,26.1,31.1L26.1,31.1z M32.9,30.9v-0.3H31c0,0.7,0,1.1,0,1.2c-0.1,0.5-0.4,0.7-0.8,0.7c-0.6,0-0.9-0.5-0.9-1.4 v-1.7h3.6v-2.1c0-1.1-0.2-1.8-0.5-2.3c-0.5-0.7-1.2-1-2.1-1c-0.9,0-1.6,0.3-2.1,1c-0.4,0.5-0.6,1.3-0.6,2.3v3.5 c0,1.1,0.2,1.8,0.6,2.3c0.5,0.7,1.2,1,2.2,1c1,0,1.7-0.4,2.2-1.1c0.2-0.4,0.4-0.7,0.4-1.1C32.9,31.9,32.9,31.5,32.9,30.9L32.9,30.9z M20.7,12.5V8.3c0-0.9-0.3-1.4-0.9-1.4c-0.6,0-0.9,0.5-0.9,1.4v4.2c0,0.9,0.3,1.4,0.9,1.4C20.4,14,20.7,13.5,20.7,12.5z M35.1,27.6 c0,3.1-0.2,5.5-0.5,7c-0.2,0.8-0.6,1.5-1.2,2c-0.6,0.5-1.3,0.8-2,0.9c-2.5,0.3-6.2,0.4-11.1,0.4s-8.7-0.1-11.1-0.4 c-0.8-0.1-1.5-0.4-2.1-0.9c-0.6-0.5-1-1.2-1.2-2c-0.3-1.5-0.5-3.8-0.5-7c0-3.1,0.2-5.5,0.5-7c0.2-0.8,0.6-1.5,1.2-2 c0.6-0.5,1.3-0.9,2.1-0.9c2.5-0.3,6.2-0.4,11.1-0.4s8.7,0.1,11.1,0.4c0.8,0.1,1.5,0.4,2.1,0.9c0.6,0.5,1,1.2,1.2,2 C34.9,22.1,35.1,24.4,35.1,27.6z M15.1,2h2l-2.4,8v5.4h-2V10c-0.2-1-0.6-2.4-1.2-4.3c-0.5-1.4-0.9-2.6-1.3-3.8h2.1l1.4,5.3L15.1,2z M22.5,8.7v3.5c0,1.1-0.2,1.9-0.6,2.4c-0.5,0.7-1.2,1-2.1,1c-0.9,0-1.6-0.3-2.1-1c-0.4-0.5-0.6-1.3-0.6-2.4V8.7 c0-1.1,0.2-1.9,0.6-2.3c0.5-0.7,1.2-1,2.1-1c0.9,0,1.6,0.3,2.1,1C22.3,6.8,22.5,7.6,22.5,8.7z M29.2,5.4v10h-1.8v-1.1 c-0.7,0.8-1.4,1.2-2.1,1.2c-0.6,0-1-0.2-1.2-0.7C24,14.5,24,14,24,13.4V5.4h1.8v7.4c0,0.4,0,0.7,0,0.7c0,0.3,0.2,0.4,0.4,0.4 c0.4,0,0.7-0.3,1.1-0.9V5.4C27.4,5.4,29.2,5.4,29.2,5.4z"/></symbol>
+    <symbol id="icon-instagram" viewBox="0 0 40 40"><path d="M20,7c4.2,0,4.7,0,6.3,0.1c1.5,0.1,2.3,0.3,3,0.5C30,8,30.5,8.3,31.1,8.9c0.5,0.5,0.9,1.1,1.2,1.8c0.2,0.5,0.5,1.4,0.5,3 C33,15.3,33,15.8,33,20s0,4.7-0.1,6.3c-0.1,1.5-0.3,2.3-0.5,3c-0.3,0.7-0.6,1.2-1.2,1.8c-0.5,0.5-1.1,0.9-1.8,1.2 c-0.5,0.2-1.4,0.5-3,0.5C24.7,33,24.2,33,20,33s-4.7,0-6.3-0.1c-1.5-0.1-2.3-0.3-3-0.5C10,32,9.5,31.7,8.9,31.1 C8.4,30.6,8,30,7.7,29.3c-0.2-0.5-0.5-1.4-0.5-3C7,24.7,7,24.2,7,20s0-4.7,0.1-6.3c0.1-1.5,0.3-2.3,0.5-3C8,10,8.3,9.5,8.9,8.9 C9.4,8.4,10,8,10.7,7.7c0.5-0.2,1.4-0.5,3-0.5C15.3,7.1,15.8,7,20,7z M20,4.3c-4.3,0-4.8,0-6.5,0.1c-1.6,0-2.8,0.3-3.8,0.7 C8.7,5.5,7.8,6,6.9,6.9C6,7.8,5.5,8.7,5.1,9.7c-0.4,1-0.6,2.1-0.7,3.8c-0.1,1.7-0.1,2.2-0.1,6.5s0,4.8,0.1,6.5 c0,1.6,0.3,2.8,0.7,3.8c0.4,1,0.9,1.9,1.8,2.8c0.9,0.9,1.7,1.4,2.8,1.8c1,0.4,2.1,0.6,3.8,0.7c1.6,0.1,2.2,0.1,6.5,0.1 s4.8,0,6.5-0.1c1.6-0.1,2.9-0.3,3.8-0.7c1-0.4,1.9-0.9,2.8-1.8c0.9-0.9,1.4-1.7,1.8-2.8c0.4-1,0.6-2.1,0.7-3.8 c0.1-1.6,0.1-2.2,0.1-6.5s0-4.8-0.1-6.5c-0.1-1.6-0.3-2.9-0.7-3.8c-0.4-1-0.9-1.9-1.8-2.8c-0.9-0.9-1.7-1.4-2.8-1.8 c-1-0.4-2.1-0.6-3.8-0.7C24.8,4.3,24.3,4.3,20,4.3L20,4.3L20,4.3z"/><path d="M20,11.9c-4.5,0-8.1,3.7-8.1,8.1s3.7,8.1,8.1,8.1s8.1-3.7,8.1-8.1S24.5,11.9,20,11.9z M20,25.2c-2.9,0-5.2-2.3-5.2-5.2 s2.3-5.2,5.2-5.2s5.2,2.3,5.2,5.2S22.9,25.2,20,25.2z"/><path d="M30.6,11.6c0,1-0.8,1.9-1.9,1.9c-1,0-1.9-0.8-1.9-1.9s0.8-1.9,1.9-1.9C29.8,9.7,30.6,10.5,30.6,11.6z"/></symbol>
+    <symbol id="icon-mastodon" viewBox="-20 -20 237 255"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z"/></symbol>
+</svg>
diff --git a/src/static/lazy-loading.js b/src/static/lazy-loading.js
new file mode 100644
index 0000000..a403d7c
--- /dev/null
+++ b/src/static/lazy-loading.js
@@ -0,0 +1,51 @@
+// Lazy loading! Roll your own. Woot.
+// This file includes a 8unch of fall8acks and stuff like that, and is written
+// with fairly Olden JavaScript(TM), so as to work on pretty much any 8rowser
+// with JS ena8led. (If it's disa8led, there are gener8ted <noscript> tags to
+// work there.)
+
+var observer;
+
+function loadImage(image) {
+    image.src = image.dataset.original;
+}
+
+function lazyLoad(elements) {
+    for (var i = 0; i < elements.length; i++) {
+        var item = elements[i];
+        if (item.intersectionRatio > 0) {
+            observer.unobserve(item.target);
+            loadImage(item.target);
+        }
+    }
+}
+
+function lazyLoadMain() {
+    // This is a live HTMLCollection! We can't iter8te over it normally 'cuz
+    // we'd 8e mutating its value just 8y interacting with the DOM elements it
+    // contains. A while loop works just fine, even though you'd think reading
+    // over this code that this would 8e an infinitely hanging loop. It isn't!
+    var elements = document.getElementsByClassName('js-hide');
+    while (elements.length) {
+        elements[0].classList.remove('js-hide');
+    }
+
+    var lazyElements = document.getElementsByClassName('lazy');
+    if (window.IntersectionObserver) {
+        observer = new IntersectionObserver(lazyLoad, {
+            rootMargin: '200px',
+            threshold: 1.0
+        });
+        for (var i = 0; i < lazyElements.length; i++) {
+            observer.observe(lazyElements[i]);
+        }
+    } else {
+        for (var i = 0; i < lazyElements.length; i++) {
+            var element = lazyElements[i];
+            var original = element.getAttribute('data-original');
+            element.setAttribute('src', original);
+        }
+    }
+}
+
+document.addEventListener('DOMContentLoaded', lazyLoadMain);
diff --git a/src/static/site-basic.css b/src/static/site-basic.css
new file mode 100644
index 0000000..d26584a
--- /dev/null
+++ b/src/static/site-basic.css
@@ -0,0 +1,19 @@
+/**
+ * For redirects and stuff like that.
+ * Small file, not so much helped 8y this comment.
+ */
+
+html {
+    background-color: #222222;
+    color: white;
+}
+
+body {
+    padding: 15px;
+}
+
+main {
+    background-color: rgba(0, 0, 0, 0.6);
+    border: 1px dotted white;
+    padding: 20px;
+}
diff --git a/src/static/site.css b/src/static/site.css
new file mode 100644
index 0000000..ae41f88
--- /dev/null
+++ b/src/static/site.css
@@ -0,0 +1,872 @@
+/* A frontend file! Wow.
+ * This file is just loaded statically 8y <link>s in the HTML files, so there's
+ * no need to re-run upd8.js when tweaking values here. Handy!
+ */
+
+:root {
+    --primary-color: #0088ff;
+}
+
+body {
+    background: black;
+    margin: 10px;
+    overflow-y: scroll;
+}
+
+body::before {
+    content: "";
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: -1;
+
+    background-image: url("../media/bg.jpg");
+    background-position: center;
+    background-size: cover;
+    opacity: 0.5;
+}
+
+#page-container {
+    background-color: rgba(35, 35, 35, 0.80);
+    backdrop-filter: blur(4px);
+    color: #ffffff;
+
+    max-width: 1100px;
+    margin: 10px auto 50px;
+    padding: 15px 0;
+
+    box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
+}
+
+#page-container > * {
+    margin-left: 15px;
+    margin-right: 15px;
+}
+
+#banner {
+    margin: 10px 0;
+    width: 100%;
+    background: black;
+    background-color: var(--dim-color);
+    border-bottom: 1px solid var(--primary-color);
+    position: relative;
+}
+
+#banner::after {
+    content: "";
+    box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35);
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    pointer-events: none;
+}
+
+#banner.dim img {
+    opacity: 0.8;
+}
+
+#banner img {
+    display: block;
+    width: 100%;
+    height: auto;
+}
+
+a {
+    color: var(--primary-color);
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+#skippers {
+    position: absolute;
+    left: -10000px;
+    top: auto;
+    width: 1px;
+    height: 1px;
+}
+
+#skippers:focus-within {
+    position: static;
+    width: unset;
+    height: unset;
+}
+
+#skippers > .skipper:not(:last-child)::after {
+    content: " \00b7 ";
+    font-weight: 800;
+}
+
+.layout-columns {
+    display: flex;
+}
+
+#header, #skippers, #footer {
+    padding: 5px;
+    font-size: 0.85em;
+}
+
+#header, #skippers {
+    margin-bottom: 10px;
+}
+
+#footer {
+    margin-top: 10px;
+}
+
+#header {
+    display: flex;
+}
+
+#header > h2 {
+    font-size: 1em;
+    margin: 0 20px 0 0;
+    font-weight: normal;
+}
+
+#header > h2 a.current {
+    font-weight: 800;
+}
+
+#header > h2.dot-between-spans > span:not(:last-child)::after {
+    content: " \00b7 ";
+    font-weight: 800;
+}
+
+#header > h2 > span {
+    white-space: nowrap;
+}
+
+#header > div {
+    flex-grow: 1;
+}
+
+#header > div > *:not(:last-child)::after {
+    content: " \00b7 ";
+    font-weight: 800;
+}
+
+#header .chronology {
+    display: inline-block;
+}
+
+#header .chronology .heading,
+#header .chronology .buttons {
+    display: inline-block;
+}
+
+footer {
+    text-align: center;
+    font-style: oblique;
+}
+
+footer > :first-child {
+    margin-top: 0;
+}
+
+footer > :last-child {
+    margin-bottom: 0;
+}
+
+.nowrap {
+    white-space: nowrap;
+}
+
+.icons {
+    font-style: normal;
+    white-space: nowrap;
+}
+
+.icon {
+    display: inline-block;
+    width: 24px;
+    height: 1em;
+    position: relative;
+}
+
+.icon > svg {
+    width: 24px;
+    height: 24px;
+    top: -0.25em;
+    position: absolute;
+    fill: var(--primary-color);
+}
+
+.rerelease {
+    opacity: 0.7;
+    font-style: oblique;
+}
+
+.content-columns {
+    columns: 2;
+}
+
+.content-columns .column {
+    break-inside: avoid;
+}
+
+.content-columns .column h2 {
+    margin-top: 0;
+    font-size: 1em;
+}
+
+.sidebar, #content, #header, #skippers, #footer {
+    background-color: rgba(0, 0, 0, 0.6);
+    border: 1px dotted var(--primary-color);
+    border-radius: 3px;
+}
+
+.sidebar-column {
+    flex: 1 1 20%;
+    min-width: 150px;
+    max-width: 250px;
+    flex-basis: 250px;
+    height: 100%;
+}
+
+.sidebar-multiple {
+    display: flex;
+    flex-direction: column;
+}
+
+.sidebar-multiple .sidebar:not(:first-child) {
+    margin-top: 10px;
+}
+
+.sidebar {
+    padding: 5px;
+    font-size: 0.85em;
+}
+
+#sidebar-left {
+    margin-right: 10px;
+}
+
+#sidebar-right {
+    margin-left: 10px;
+}
+
+.sidebar.wide {
+    max-width: 350px;
+    flex-basis: 300px;
+    flex-shrink: 0;
+    flex-grow: 1;
+}
+
+#content {
+    padding: 20px;
+    flex-grow: 1;
+    flex-shrink: 3;
+}
+
+.sidebar > h1,
+.sidebar > h2,
+.sidebar > h3,
+.sidebar > p {
+    text-align: center;
+}
+
+.sidebar h1 {
+    font-size: 1.25em;
+}
+
+.sidebar h2 {
+    font-size: 1.1em;
+    margin: 0;
+}
+
+.sidebar h3 {
+    font-size: 1.1em;
+    font-style: oblique;
+    font-variant: small-caps;
+    margin-top: 0.3em;
+    margin-bottom: 0em;
+}
+
+.sidebar > p {
+    margin: 0.5em 0;
+    padding: 0 5px;
+}
+
+.sidebar hr {
+    color: #555;
+    margin: 10px 5px;
+}
+
+.sidebar > ol, .sidebar > ul {
+    padding-left: 30px;
+    padding-right: 15px;
+}
+
+.sidebar > dl {
+    padding-right: 15px;
+    padding-left: 0;
+}
+
+.sidebar > dl dt {
+    padding-left: 10px;
+    margin-top: 0.5em;
+}
+
+.sidebar > dl dt.current {
+    font-weight: 800;
+}
+
+.sidebar > dl dd {
+    margin-left: 0;
+}
+
+.sidebar > dl dd ul {
+    padding-left: 30px;
+    margin-left: 0;
+}
+
+.sidebar > dl .side {
+    padding-left: 10px;
+}
+
+.sidebar li.current {
+    font-weight: 800;
+}
+
+.sidebar li {
+    overflow-wrap: break-word;
+}
+
+.sidebar article {
+    text-align: left;
+    margin: 5px 5px 15px 5px;
+}
+
+.sidebar article:last-child {
+    margin-bottom: 5px;
+}
+
+.sidebar article h2,
+.news-index h2 {
+    border-bottom: 1px dotted;
+}
+
+.sidebar article h2 time,
+.news-index time {
+    float: right;
+    font-weight: normal;
+}
+
+#cover-art-container {
+    float: right;
+    width: 40%;
+    max-width: 400px;
+    margin: 0 0 10px 10px;
+    font-size: 0.8em;
+}
+
+#cover-art img {
+    display: block;
+    width: 100%;
+    height: 100%;
+}
+
+#cover-art-container p {
+    margin-top: 5px;
+}
+
+.image-container {
+    border: 2px solid var(--primary-color);
+    box-sizing: border-box;
+    position: relative;
+    padding: 5px;
+    text-align: left;
+    background-color: var(--dim-color);
+    color: white;
+    display: inline-block;
+    width: 100%;
+    height: 100%;
+}
+
+.image-inner-area {
+    overflow: hidden;
+    width: 100%;
+    height: 100%;
+}
+
+img {
+    object-fit: cover;
+    /* these unfortunately dont take effect while loading, so...
+    text-align: center;
+    line-height: 2em;
+    text-shadow: 0 0 5px black;
+    font-style: oblique;
+    */
+}
+
+.js-hide,
+.js-show-once-data,
+.js-hide-once-data {
+    display: none;
+}
+
+a.box:focus {
+    outline: 3px double var(--primary-color);
+}
+
+a.box:focus:not(:focus-visible) {
+    outline: none;
+}
+
+a.box img {
+    display: block;
+    width: 100%;
+    height: 100%;
+}
+
+h1 {
+    font-size: 1.5em;
+}
+
+#content li {
+    margin-bottom: 4px;
+}
+
+#content li i {
+    white-space: nowrap;
+}
+
+.grid-listing {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    align-items: center;
+}
+
+.grid-item {
+    display: inline-block;
+    margin: 15px;
+    text-align: center;
+    background-color: #111111;
+    border: 1px dotted var(--primary-color);
+    border-radius: 2px;
+    padding: 5px;
+}
+
+.grid-item img {
+    width: 100%;
+    height: 100%;
+    margin-top: auto;
+    margin-bottom: auto;
+}
+
+.grid-item span {
+    overflow-wrap: break-word;
+    hyphens: auto;
+}
+
+.grid-item:hover {
+    text-decoration: none;
+}
+
+.grid-actions .grid-item:hover {
+    text-decoration: underline;
+}
+
+.grid-item > span:first-of-type {
+    margin-top: 0.45em;
+    display: block;
+}
+
+.grid-item:hover > span:first-of-type {
+    text-decoration: underline;
+}
+
+.grid-listing > .grid-item {
+    flex: 1 1 26%;
+}
+
+.grid-actions {
+    display: flex;
+    flex-direction: column;
+    margin: 15px;
+}
+
+.grid-actions > .grid-item {
+    flex-basis: unset !important;
+    margin: 5px;
+    --primary-color: inherit !important;
+    --dim-color: inherit !important;
+}
+
+.grid-item {
+    flex-basis: 240px;
+    min-width: 200px;
+    max-width: 240px;
+}
+
+.grid-item:not(.large-grid-item) {
+    flex-basis: 180px;
+    min-width: 120px;
+    max-width: 180px;
+    font-size: 0.9em;
+}
+
+.square {
+    position: relative;
+    width: 100%;
+}
+
+.square::after {
+    content: "";
+    display: block;
+    padding-bottom: 100%;
+}
+
+.square-content {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+}
+
+.vertical-square {
+    position: relative;
+    height: 100%;
+}
+
+.vertical-square::after {
+    content: "";
+    display: block;
+    padding-right: 100%;
+}
+
+.reveal {
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+.reveal img {
+    filter: blur(20px);
+    opacity: 0.4;
+}
+
+.reveal-text {
+    color: white;
+    position: absolute;
+    top: 15px;
+    left: 10px;
+    right: 10px;
+    text-align: center;
+    font-weight: bold;
+}
+
+.reveal-interaction {
+    opacity: 0.8;
+}
+
+.reveal.revealed img {
+    filter: none;
+    opacity: 1;
+}
+
+.reveal.revealed .reveal-text {
+    display: none;
+}
+
+#content.top-index h1,
+#content.flash-index h1 {
+    text-align: center;
+    font-size: 2em;
+}
+
+#content.flash-index h2 {
+    text-align: center;
+    font-size: 2.5em;
+    font-variant: small-caps;
+    font-style: oblique;
+    margin-bottom: 0;
+    text-align: center;
+    width: 100%;
+}
+
+#content.top-index h2 {
+    text-align: center;
+    font-size: 2em;
+    font-weight: normal;
+    margin-bottom: 0.25em;
+}
+
+.quick-info {
+    text-align: center;
+}
+
+ul.quick-info {
+    list-style: none;
+    padding-left: 0;
+}
+
+ul.quick-info li {
+    display: inline-block;
+}
+
+ul.quick-info li:not(:last-child)::after {
+    content: " \00b7 ";
+    font-weight: 800;
+}
+
+#intro-menu {
+    margin: 24px 0;
+    padding: 10px;
+    background-color: #222222;
+    text-align: center;
+    border: 1px dotted var(--primary-color);
+    border-radius: 2px;
+}
+
+#intro-menu p {
+    margin: 12px 0;
+}
+
+#intro-menu a {
+    margin: 0 6px;
+}
+
+li .by {
+    white-space: nowrap;
+    font-style: oblique;
+}
+
+p code {
+    font-size: 1em;
+    font-family: 'courier new';
+    font-weight: 800;
+}
+
+blockquote {
+    max-width: 600px;
+    margin-right: 0;
+}
+
+.long-content {
+    margin-left: 12%;
+    margin-right: 12%;
+}
+
+p img {
+    max-width: 100%;
+    height: auto;
+}
+
+dl dt {
+    padding-left: 2em;
+}
+
+dl dt {
+    margin-bottom: 2px;
+}
+
+dl dd {
+    margin-bottom: 1em;
+}
+
+dl ul, dl ol {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+.album-group-list dt {
+    font-style: oblique;
+    padding-left: 0;
+}
+
+.album-group-list dd {
+    margin-left: 0;
+}
+
+.group-chronology-link {
+    font-style: oblique;
+}
+
+hr.split::before {
+    content: "(split)";
+    color: #808080;
+}
+
+hr.split {
+    position: relative;
+    overflow: hidden;
+    border: none;
+}
+
+hr.split::after {
+    display: inline-block;
+    content: "";
+    border: 1px inset #808080;
+    width: 100%;
+    position: absolute;
+    top: 50%;
+    margin-top: -2px;
+    margin-left: 10px;
+}
+
+li > ul {
+    margin-top: 5px;
+}
+
+#info-card-container {
+    position: absolute;
+
+    left: 0;
+    right: 10px;
+
+    pointer-events: none; /* Padding area shouldn't 8e interactive. */
+    display: none;
+}
+
+#info-card-container.show,
+#info-card-container.hide {
+    display: flex;
+}
+
+#info-card-container > * {
+    flex-basis: 400px;
+}
+
+#info-card-container.show {
+    animation: 0.2s linear forwards info-card-show;
+    transition: top 0.1s, left 0.1s;
+}
+
+#info-card-container.hide {
+    animation: 0.2s linear forwards info-card-hide;
+}
+
+@keyframes info-card-show {
+    0% {
+        opacity: 0;
+        margin-top: -5px;
+    }
+
+    100% {
+        opacity: 1;
+        margin-top: 0;
+    }
+}
+
+@keyframes info-card-hide {
+    0% {
+        opacity: 1;
+        margin-top: 0;
+    }
+
+    100% {
+        opacity: 0;
+        margin-top: 5px;
+        display: none !important;
+    }
+}
+
+.info-card-decor {
+    padding-left: 3ch;
+    border-top: 1px solid white;
+}
+
+.info-card {
+    background-color: black;
+    color: white;
+
+    border: 1px dotted var(--primary-color);
+    border-radius: 3px;
+    box-shadow: 0 5px 5px black;
+
+    padding: 5px;
+    font-size: 0.9em;
+
+    pointer-events: none;
+}
+
+.info-card::after {
+    content: "";
+    display: block;
+    clear: both;
+}
+
+#info-card-container.show .info-card {
+    animation: 0.01s linear 0.2s forwards info-card-become-interactive;
+}
+
+@keyframes info-card-become-interactive {
+    to {
+        pointer-events: auto;
+    }
+}
+
+.info-card-art-container {
+    float: right;
+
+    width: 40%;
+    margin: 5px;
+    font-size: 0.8em;
+
+    /* Dynamically shown. */
+    display: none;
+}
+
+.info-card-art-container .image-container {
+    padding: 2px;
+}
+
+.info-card-art {
+    display: block;
+    width: 100%;
+    height: 100%;
+}
+
+.info-card-name {
+    font-size: 1em;
+    border-bottom: 1px dotted;
+    margin: 0;
+}
+
+.info-card p {
+    margin-top: 0.25em;
+    margin-bottom: 0.25em;
+}
+
+.info-card p:last-child {
+    margin-bottom: 0;
+}
+
+@media (max-width: 900px) {
+    .sidebar-column:not(.no-hide) {
+        display: none;
+    }
+
+    .layout-columns.vertical-when-thin {
+        flex-direction: column;
+    }
+
+    .layout-columns.vertical-when-thin > *:not(:last-child) {
+        margin-bottom: 10px;
+    }
+
+    .sidebar-column.no-hide {
+        max-width: unset !important;
+        flex-basis: unset !important;
+        margin-right: 0 !important;
+        margin-left: 0 !important;
+    }
+
+    .sidebar .news-entry:not(.first-news-entry) {
+        display: none;
+    }
+}
+
+@media (max-width: 600px) {
+    .content-columns {
+        columns: 1;
+    }
+}