« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/client.js166
-rw-r--r--static/icons.svg10
-rw-r--r--static/lazy-fallback.js19
-rw-r--r--static/lazy-loading.js25
-rw-r--r--static/lazy-show.js16
-rw-r--r--static/site.css494
6 files changed, 730 insertions, 0 deletions
diff --git a/static/client.js b/static/client.js
new file mode 100644
index 00000000..5d706b24
--- /dev/null
+++ b/static/client.js
@@ -0,0 +1,166 @@
+// 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.
+
+'use strict';
+
+const officialAlbumData = albumData.filter(album => !album.isFanon);
+const fandomAlbumData = albumData.filter(album => album.isFanon);
+const artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name);
+const allTracks = C.getAllTracks(albumData);
+
+function rebase(href) {
+    const relative = document.documentElement.dataset.rebase;
+    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 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);
+}
+
+function openAlbum(album) {
+    location.href = rebase(`${C.ALBUM_DIRECTORY}/${album.directory}/index.html`);
+}
+
+function openTrack(track) {
+    location.href = rebase(`${C.TRACK_DIRECTORY}/${track.directory}/index.html`);
+}
+
+function openArtist(artist) {
+    location.href = rebase(`${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist)}/index.html`);
+}
+
+function openFlash(flash) {
+    location.href = rebase(`${C.FLASH_DIRECTORY}/${flash.directory}/index.html`);
+}
+
+/* i implemented these functions but we dont actually use them anywhere lol
+function isFlashPage() {
+    return !!cssProp(document.body, '--flash-directory');
+}
+
+function isTrackOrAlbumPage() {
+    return !!cssProp(document.body, '--album-directory');
+}
+
+function isTrackPage() {
+    return !!cssProp(document.body, '--track-directory');
+}
+*/
+
+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 openNextTrack() {
+    const { list, index } = getTrackListAndIndex();
+    if (!list) return;
+    if (index === list.length) return;
+    openTrack(list[index + 1]);
+}
+
+function openPreviousTrack() {
+    const { list, index } = getTrackListAndIndex();
+    if (!list) return;
+    if (index === 0) return;
+    openTrack(list[index - 1]);
+}
+
+function openRandomTrack() {
+    const { list } = getTrackListAndIndex();
+    if (!list) 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};
+}
+
+function openNextFlash() {
+    const { list, index } = getFlashListAndIndex();
+    if (index === list.length) return;
+    openFlash(list[index + 1]);
+}
+
+function openPreviousFlash() {
+    const { list, index } = getFlashListAndIndex();
+    if (index === 0) return;
+    openFlash(list[index - 1]);
+}
+
+for (const a of document.body.querySelectorAll('[data-random]')) {
+    a.addEventListener('click', evt => {
+        try {
+            switch (a.dataset.random) {
+                case 'album': return openAlbum(pick(albumData));
+                case 'album-in-fandom': return openAlbum(pick(fandomAlbumData));
+                case 'album-in-official': openAlbum(pick(officialAlbumData));
+                case 'track': return openTrack(pick(allTracks));
+                case 'track-in-album': return openTrack(pick(getAlbum(a).tracks));
+                case 'track-in-fandom': return openTrack(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), [])));
+                case 'track-in-official': return openTrack(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), [])));
+                case 'artist': return openArtist(pick(artistNames));
+                case 'artist-more-than-one-contrib': return openArtist(pick(artistNames.filter(name => C.getArtistNumContributions(name, {albumData, allTracks, flashData}) > 1)));
+            }
+        } finally {
+            evt.preventDefault();
+        }
+    });
+}
+
+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) random.click();
+        }
+    }
+});
diff --git a/static/icons.svg b/static/icons.svg
new file mode 100644
index 00000000..83933b36
--- /dev/null
+++ b/static/icons.svg
@@ -0,0 +1,10 @@
+<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>
+</svg>
diff --git a/static/lazy-fallback.js b/static/lazy-fallback.js
new file mode 100644
index 00000000..a66b922c
--- /dev/null
+++ b/static/lazy-fallback.js
@@ -0,0 +1,19 @@
+// Fall8ack code for lazy loading. 8asically, this runs if the stuff in
+// lazy-loading.js doesn't; while that file's written with the same kinda
+// modern syntax/APIs used all over the site, displaying the images is a pretty
+// damn important thing to do, so we have this goodol' Olde JavaScripte fix for
+// 8rowsers which have JS ena8led (and so won't display gener8ted <noscript>
+// tags) 8ut don't support what we use for lazy loading.
+
+if (!window.lazyLoadingExecuted) {
+    lazyLoadingFallback();
+}
+
+function lazyLoadingFallback() {
+    var lazyElements = document.getElementsByClassName('lazy');
+    for (var i = 0; i < lazyElements.length; i++) {
+        var element = lazyElements[i];
+        var original = element.getAttribute('data-original');
+        element.setAttribute('src', original);
+    }
+}
diff --git a/static/lazy-loading.js b/static/lazy-loading.js
new file mode 100644
index 00000000..22f95eb0
--- /dev/null
+++ b/static/lazy-loading.js
@@ -0,0 +1,25 @@
+// Lazy loading! Roll your own. Woot.
+
+function loadImage(image) {
+    image.src = image.dataset.original;
+}
+
+function lazyLoad(elements) {
+    for (const item of elements) {
+        if (item.intersectionRatio > 0) {
+            observer.unobserve(item.target);
+            loadImage(item.target);
+        }
+    }
+}
+
+const observer = new IntersectionObserver(lazyLoad, {
+    rootMargin: '200px',
+    threshold: 1.0
+});
+
+for (const image of document.querySelectorAll('img.lazy')) {
+    observer.observe(image);
+}
+
+window.lazyLoadingExecuted = true;
diff --git a/static/lazy-show.js b/static/lazy-show.js
new file mode 100644
index 00000000..c6a1bf25
--- /dev/null
+++ b/static/lazy-show.js
@@ -0,0 +1,16 @@
+// Kinda like lazy-fallback.js in that this should work on any 8rowser, period.
+// Shows the lazy loading images iff JS is enabled (so that you don't have a
+// duplicate image if JS is disabled).
+
+lazyLoadingShowHiddenImages();
+
+function lazyLoadingShowHiddenImages() {
+    // 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');
+    }
+}
diff --git a/static/site.css b/static/site.css
new file mode 100644
index 00000000..346c28a3
--- /dev/null
+++ b/static/site.css
@@ -0,0 +1,494 @@
+/* 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!
+ */
+
+/* Pages can specify their own theme colors (used for links, and may8e other
+ * things) by changing this CSS varia8le through their 8ody's style attri8ute.
+ * And yes, CSS varia8les are supported in 8asically every major 8rowser.
+ * No, I don't care a8out Internet Explorer.
+ */
+:root {
+    --fg-color: #0088ff;
+    --bg-color: #222222;
+    --theme: 0; /* 0: dark (below light), 1: light (below dark) */
+}
+
+body {
+    background-color: var(--bg-color);
+
+    --bg-shade: calc(255 * var(--theme));
+    --fg-shade: calc(255 * (1 - var(--theme)));
+
+    color: rgb(var(--fg-shade), var(--fg-shade), var(--fg-shade));
+
+    padding: 15px;
+}
+
+a {
+    color: var(--fg-color);
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+.columns {
+    display: flex;
+}
+
+#header {
+    margin-bottom: 10px;
+    padding: 5px;
+    font-size: 0.85em;
+    display: flex;
+}
+
+#header > h2 {
+    font-size: 1em;
+    margin: 0 20px 0 0;
+    font-weight: normal;
+}
+
+#header > h2 a.current,
+#header > h2.highlight-last-link > a:last-of-type {
+    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;
+    white-space: nowrap;
+}
+
+.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(--fg-color);
+}
+
+@media (max-width: 780px) {
+    #sidebar:not(.no-hide) {
+        display: none;
+    }
+
+    .columns.vertical-when-thin {
+        flex-direction: column;
+    }
+
+    .columns.vertical-when-thin > *:not(:last-child) {
+        margin-bottom: 10px;
+    }
+
+    #sidebar.no-hide {
+        max-width: unset !important;
+        flex-basis: unset !important;
+        margin-right: 0;
+    }
+}
+
+#sidebar, #content, #header {
+    background-color: rgba(var(--bg-shade), var(--bg-shade), var(--bg-shade), 0.6);
+    border: 1px dotted var(--fg-color);
+    border-radius: 3px;
+}
+
+#sidebar {
+    flex: 1 1 20%;
+    min-width: 200px;
+    max-width: 250px;
+    flex-basis: 250px;
+    float: left;
+    padding: 5px;
+    margin-right: 10px;
+    font-size: 0.85em;
+    height: 100%;
+}
+
+#sidebar.wide {
+    max-width: 350px;
+    flex-basis: 300px;
+    flex-shrink: 0;
+    flex-grow: 1;
+}
+
+#content {
+    padding: 20px;
+    flex-grow: 1;
+}
+
+#sidebar h1 {
+    font-size: 1.25em;
+    text-align: center;
+}
+
+#sidebar h2 {
+    font-size: 1.1em;
+    text-align: center;
+    margin: 0;
+}
+
+#sidebar h3 {
+    font-size: 1.1em;
+    text-align: center;
+    font-style: oblique;
+    font-variant: small-caps;
+    margin-top: 0.3em;
+    margin-bottom: 0em;
+}
+
+#sidebar p {
+    text-align: center;
+    margin: 0.5em 0;
+}
+
+#sidebar p:last-child {
+    margin-bottom: 0;
+}
+
+#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;
+}
+
+#cover-art {
+    float: right;
+    width: 40%;
+    max-width: 400px;
+    margin: 0 0 10px 10px;
+    background-color: #181818;
+}
+
+#cover-art img {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+}
+
+img {
+    border: 2px solid var(--fg-color);
+    box-sizing: border-box;
+    position: relative;
+    padding: 5px;
+    text-align: left;
+    background-color: var(--dim-color);
+    color: white;
+    display: inline-block;
+    object-fit: contain;
+    /* 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 {
+    display: none;
+}
+
+a.box:focus {
+    outline: 3px double var(--fg-color);
+}
+
+a.box:focus:not(:focus-visible) {
+    outline: none;
+}
+
+a.box img {
+    display: block;
+}
+
+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(--fg-color);
+    border-radius: 2px;
+    padding: 5px;
+}
+
+.grid-item img {
+    width: 100%;
+    height: 100%;
+}
+
+.grid-item span {
+    margin-top: 0.45em;
+    display: block;
+}
+
+.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;
+}
+
+.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%;
+}
+
+#content.top-index h1,
+#content.flash-index h1 {
+    text-align: center;
+    font-size: 2em;
+}
+
+#content.flash-index h2 {
+    text-align: center;
+    font-size: 3em;
+    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;
+}
+
+#intro-menu {
+    margin: 24px 0;
+    padding: 10px;
+    background-color: #222222;
+    text-align: center;
+    border: 1px dotted var(--fg-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;
+}
+
+.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;
+}
+
+.new {
+    animation: new 1s infinite;
+}
+
+@keyframes new {
+    0% {
+        color: #bbdd00;
+    }
+
+    50% {
+        color: #eeff22;
+    }
+
+    100% {
+        color: #bbdd00;
+    }
+}
+
+/* fake :P */
+.loading::after {
+    content: '.';
+    animation: loading 6s infinite;
+}
+
+@keyframes loading {
+    0 {
+        content: '.';
+    }
+
+    33% {
+        content: '..';
+    }
+
+    66% {
+        content: '...';
+    }
+
+    100% {
+        content: '.';
+    }
+}