From d529b043c6a4d71334ea3726500ecbf681cdf985 Mon Sep 17 00:00:00 2001
From: Bad Manners <me@badmanners.xyz>
Date: Sat, 10 Aug 2024 01:17:33 -0300
Subject: [PATCH] Improved readability and accessibility for CSS-less mode

---
 README.md                                  |  4 +-
 package-lock.json                          |  4 +-
 package.json                               |  2 +-
 src/components/AgeRestrictedModal.astro    | 10 +--
 src/components/CopyrightedCharacters.astro | 11 +--
 src/components/DarkModeScript.astro        |  2 +
 src/components/MastodonComments.astro      | 13 +++-
 src/components/UserComponent.astro         |  7 +-
 src/content/config.ts                      |  4 +-
 src/i18n/index.ts                          | 23 ++++++
 src/layouts/GalleryLayout.astro            | 18 +++--
 src/layouts/GameLayout.astro               | 59 +++++++++-----
 src/layouts/StoryLayout.astro              | 91 ++++++++++++++--------
 src/pages/index.astro                      | 12 +--
 14 files changed, 166 insertions(+), 94 deletions(-)

diff --git a/README.md b/README.md
index 0878ec1..d63f5a0 100644
--- a/README.md
+++ b/README.md
@@ -40,8 +40,8 @@ npm run export-story -- -o ~/Documents/TO_UPLOAD slug-for-story-to-export
 npm run build
 ```
 
-Then, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`:
+Then, after configuring the `gallerybm` host (or the name of your choosing) in `~/.ssh/config`, you can use a command like:
 
 ```bash
-rsync --delete -acP dist/ gallerybm:/home/public
+rsync --delete-after -acP dist/ gallerybm:/home/public
 ```
diff --git a/package-lock.json b/package-lock.json
index d6b5889..1a3ee93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "gallery-badmanners-xyz",
-  "version": "1.6.2",
+  "version": "1.6.3",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "gallery-badmanners-xyz",
-      "version": "1.6.2",
+      "version": "1.6.3",
       "hasInstallScript": true,
       "dependencies": {
         "@astrojs/check": "^0.9.2",
diff --git a/package.json b/package.json
index 8dfb0fb..0a8020c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "gallery-badmanners-xyz",
   "type": "module",
-  "version": "1.6.2",
+  "version": "1.6.3",
   "scripts": {
     "postinstall": "astro sync",
     "dev": "astro dev",
diff --git a/src/components/AgeRestrictedModal.astro b/src/components/AgeRestrictedModal.astro
index 7edb7af..4db69fd 100644
--- a/src/components/AgeRestrictedModal.astro
+++ b/src/components/AgeRestrictedModal.astro
@@ -11,22 +11,22 @@
     aria-hidden="false"
   >
     <div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
-      <div class="h-14 w-14 text-bm-500 sm:h-16 sm:w-16 dark:text-bm-400">
-        <svg class="fill-current" viewBox="0 0 512 512">
+      <div class="text-bm-500 dark:text-bm-400">
+        <svg width="3rem" height="3rem" class="fill-current" viewBox="0 0 512 512">
           <path
             d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
           ></path>
         </svg>
       </div>
-      <div class="pb-3 pt-2 text-2xl font-light text-stone-700 sm:pb-4 sm:pt-2 sm:text-3xl dark:text-stone-50">
+      <div class="pb-3 pt-2 text-3xl font-light text-stone-700 sm:pb-4 sm:pt-2 dark:text-stone-50">
         Age verification
       </div>
       <div
-        class="mx-6 mb-4 max-w-xl border-b border-stone-300 pb-4 text-base text-stone-700 sm:text-xl dark:border-stone-300 dark:text-stone-50"
+        class="mx-6 mb-4 max-w-xl border-b border-stone-300 pb-4 text-xl text-stone-700 dark:border-stone-300 dark:text-stone-50"
       >
         You must be 18+ to access this page.
       </div>
-      <p class="px-8 text-base font-light leading-snug text-stone-700 sm:max-w-2xl sm:text-lg dark:text-stone-50">
+      <p class="px-8 text-lg font-light leading-snug text-stone-700 sm:max-w-2xl dark:text-stone-50">
         By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent this
         screen from appearing in the future.
       </p>
diff --git a/src/components/CopyrightedCharacters.astro b/src/components/CopyrightedCharacters.astro
index 94a5ff5..156ac5e 100644
--- a/src/components/CopyrightedCharacters.astro
+++ b/src/components/CopyrightedCharacters.astro
@@ -1,23 +1,24 @@
 ---
-import type { CollectionEntry } from "astro:content";
-import type { Lang } from "../content/config";
+import type { CopyrightedCharacters, Lang } from "../content/config";
 import { t } from "../i18n";
 import UserComponent from "./UserComponent.astro";
 import CopyrightedCharactersItem from "./CopyrightedCharactersItem.astro";
+import { formatCopyrightedCharacters } from "../utils/format_copyrighted_characters";
 
 type Props = {
-  copyrightedCharacters?: Array<[CollectionEntry<"users">, string[]]>;
+  copyrightedCharacters?: CopyrightedCharacters;
   lang: Lang;
 };
 
 const { copyrightedCharacters, lang } = Astro.props;
+const charactersPerUser = copyrightedCharacters ? await formatCopyrightedCharacters(copyrightedCharacters) : null;
 ---
 
 {
-  copyrightedCharacters ? (
+  charactersPerUser ? (
     <section id="copyrighted-characters" aria-label={t(lang, "characters/copyrighted_characters_aria_label")}>
       <ul>
-        {copyrightedCharacters.map(([owner, characterList]) => (
+        {charactersPerUser.map(([owner, characterList]) => (
           <CopyrightedCharactersItem
             stringFunction={
               characterList[0] === ""
diff --git a/src/components/DarkModeScript.astro b/src/components/DarkModeScript.astro
index 1bf3f22..d4d4d88 100644
--- a/src/components/DarkModeScript.astro
+++ b/src/components/DarkModeScript.astro
@@ -11,6 +11,8 @@
       colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
     }
     document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
+      button.classList.remove("hidden");
+      button.removeAttribute("aria-hidden");
       button.addEventListener("click", (e) => {
         e.preventDefault();
         if (colorScheme === "dark") {
diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro
index 19fb3c2..5221167 100644
--- a/src/components/MastodonComments.astro
+++ b/src/components/MastodonComments.astro
@@ -41,7 +41,14 @@ const { link, instance, user, postId } = Astro.props;
 </template>
 
 <template id="template-button-loading">
-  <svg class="-mt-1 mr-1 inline h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24" aria-hidden>
+  <svg
+    width="1.25rem"
+    height="1.25rem"
+    class="-mt-1 mr-1 inline animate-spin"
+    fill="none"
+    viewBox="0 0 24 24"
+    aria-hidden
+  >
     <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
     <path
       class="opacity-100"
@@ -71,7 +78,7 @@ const { link, instance, user, postId } = Astro.props;
     <div class="ml-1 flex flex-row pb-2 pt-1">
       <div class="flex" aria-label="Favorites">
         <span data-favorites></span>
-        <svg class="ml-2 w-5 fill-current" viewBox="0 0 576 512" aria-hidden>
+        <svg width="1.25rem" class="ml-2 fill-current" viewBox="0 0 576 512" aria-hidden>
           <path
             d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
           ></path>
@@ -79,7 +86,7 @@ const { link, instance, user, postId } = Astro.props;
       </div>
       <div class="ml-4 flex" aria-label="Reblogs">
         <span data-reblogs></span>
-        <svg class="ml-2 w-5 fill-current" viewBox="0 0 512 512" aria-hidden>
+        <svg width="1.25rem" class="ml-2 fill-current" viewBox="0 0 512 512" aria-hidden>
           <path
             d="M0 224c0 17.7 14.3 32 32 32s32-14.3 32-32c0-53 43-96 96-96H320v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9S320 19.1 320 32V64H160C71.6 64 0 135.6 0 224zm512 64c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 53-43 96-96 96H192V352c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V448H352c88.4 0 160-71.6 160-160z"
           ></path>
diff --git a/src/components/UserComponent.astro b/src/components/UserComponent.astro
index cacc42a..8582f6b 100644
--- a/src/components/UserComponent.astro
+++ b/src/components/UserComponent.astro
@@ -8,12 +8,9 @@ type Props = {
   user: CollectionEntry<"users">;
 };
 
-let { user, lang } = Astro.props;
+const { user, lang } = Astro.props;
 const username = getUsernameForLang(user, lang);
-let link: string | null = null;
-if (user.data.preferredLink) {
-  link = user.data.links[user.data.preferredLink]!.link;
-}
+const link = user.data.preferredLink ? user.data.links[user.data.preferredLink]!.link : null;
 ---
 
 {
diff --git a/src/content/config.ts b/src/content/config.ts
index 0a889cb..a912c2b 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -25,8 +25,8 @@ function parseRegex<R extends { [key: string]: string }>(regex: RegExp) {
 
 /** Record of website links for a user.
  *
- * For each entry, you can enter a URL for the value or - for any key apart
- * from `website` - a pre-parsed object containing the link and username.
+ * For each entry, you can enter a URL for the value, or a pre-parsed object
+ * containing a link and a username (except for `website`).
  */
 const websiteLinks = z.object({
   website: z.object({ link: z.string().url() }).or(
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index d248527..62f6951 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -102,6 +102,10 @@ const UI_STRINGS = {
   "published_content/related_games": {
     en: "Related games",
   },
+  "published_content/copyright_aria_label": {
+    en: "Copyright",
+    tok: "toki lawa",
+  },
   // Story page-specific strings
   "story/return_to_stories": {
     en: "Return to stories",
@@ -138,6 +142,22 @@ const UI_STRINGS = {
     en: "Click to reveal",
     tok: "Click to reveal summary in English",
   },
+  "story/previous_story": {
+    en: (title: string) => `Previous: ${title}`,
+  },
+  "story/previous_story_aria_label": {
+    en: "Previous story",
+  },
+  "story/next_story": {
+    en: (title: string) => `Next: ${title}`,
+  },
+  "story/next_story_aria_label": {
+    en: "Next story",
+  },
+  "story/information_aria_label": {
+    en: "Story information",
+    tok: "sona tan lipu ni",
+  },
   "story/authors": {
     en: (authorsList: string[]) => `by ${UI_STRINGS["util/join_names"].en(authorsList)}`,
     tok: (authorsList: string[]) =>
@@ -158,6 +178,9 @@ const UI_STRINGS = {
   "game/title_aria_label": {
     en: "Game title",
   },
+  "game/information_aria_label": {
+    en: "Game information",
+  },
   "game/platforms": {
     en: (platforms: GamePlatform[]) => {
       if (platforms.length == 0) {
diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro
index fc53985..4aa945a 100644
--- a/src/layouts/GalleryLayout.astro
+++ b/src/layouts/GalleryLayout.astro
@@ -3,6 +3,7 @@ import { getImage } from "astro:assets";
 import BaseLayout from "./BaseLayout.astro";
 import Navigation from "../components/Navigation.astro";
 import logoBM from "../assets/images/logo_bm.png";
+import { t } from "../i18n";
 
 type Props = {
   pageTitle?: string;
@@ -45,31 +46,34 @@ const copyrightYear = currentYear > FIRST_YEAR ? `${FIRST_YEAR}–${currentYear}
         <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank">Licenses</a>
       </div>
       <div class="mt-2 flex items-center gap-x-1 pb-10">
-        <a class="text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-label="Main website">
-          <svg viewBox="0 0 576 512" class="h-6 w-6 fill-current" aria-hidden="true">
+        <a class="text-link p-1" href="https://badmanners.xyz/" target="_blank" aria-labelled-by="label-main-website">
+          <svg width="1.5rem" height="1.5rem" viewBox="0 0 576 512" class="fill-current" aria-hidden>
             <path
               d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
             ></path>
           </svg>
+          <span id="label-main-website" class="hidden">Main website</span>
         </a>
-        <a class="text-link p-1" href="/feed.xml" target="_blank" aria-label="RSS feed">
-          <svg viewBox="0 0 448 512" class="h-6 w-6 fill-current" aria-hidden="true">
+        <a class="text-link p-1" href="/feed.xml" target="_blank" aria-labelledby="label-rss-feed">
+          <svg width="1.5rem" height="1.5rem" viewBox="0 0 448 512" class="fill-current" aria-hidden>
             <path
               d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
             ></path>
           </svg>
+          <span id="label-rss-feed" class="hidden">RSS feed</span>
         </a>
-        <button data-dark-mode class="text-link p-1" aria-label="Toggle dark mode">
-          <svg viewBox="0 0 512 512" class="hidden h-6 w-6 fill-current dark:block" aria-hidden="true">
+        <button data-dark-mode class="text-link hidden p-1" aria-labelledby="label-toggle-dark-mode" aria-hidden>
+          <svg width="1.5rem" height="1.5rem" viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden>
             <path
               d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
             ></path>
           </svg>
-          <svg viewBox="0 0 512 512" class="block h-6 w-6 fill-current dark:hidden" aria-hidden="true">
+          <svg width="1.5rem" height="1.5rem" viewBox="0 0 512 512" class="block fill-current dark:hidden" aria-hidden>
             <path
               d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
             ></path>
           </svg>
+          <span id="label-toggle-dark-mode" class="hidden">{t("en", "published_content/toggle_dark_mode")}</span>
         </button>
       </div>
     </div>
diff --git a/src/layouts/GameLayout.astro b/src/layouts/GameLayout.astro
index 933c2bc..7cd5330 100644
--- a/src/layouts/GameLayout.astro
+++ b/src/layouts/GameLayout.astro
@@ -10,14 +10,12 @@ import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
 import Prose from "../components/Prose.astro";
 import MastodonComments from "../components/MastodonComments.astro";
 import UserComponent from "../components/UserComponent.astro";
-import { formatCopyrightedCharacters } from "../utils/format_copyrighted_characters";
 
 type Props = CollectionEntry<"games">["data"];
 
 const { props } = Astro;
 const series = props.series && (await getEntry(props.series));
 const authorsList = await getEntries(props.authors);
-const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
 const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
 const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
 const categorizedTags = Object.fromEntries(
@@ -75,45 +73,62 @@ const thumbnail =
       <div
         class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
       >
-        <a
-          href={series ? series.data.url : "/games"}
-          class="text-link my-1 h-9 w-9 p-2"
-          aria-label={series
-            ? t(props.lang, "published_content/return_to_series", series.data.name)
-            : t(props.lang, "game/return_to_games")}
-        >
-          <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
+        <a href={series ? series.data.url : "/games"} class="text-link my-1 p-2" aria-labelled-by="label-return-to">
+          <svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
             <path
               d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
             ></path>
           </svg>
+          <span class="hidden" id="label-return-to">
+            {
+              series
+                ? t(props.lang, "published_content/return_to_series", series.data.name)
+                : t(props.lang, "game/return_to_games")
+            }
+          </span>
         </a>
         <a
           href="#description"
-          class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label={t(props.lang, "published_content/go_to_description")}
+          class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
+          aria-labelled-by="label-go-to-description"
         >
-          <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
+          <svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
             <path
               d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
             ></path>
           </svg>
+          <span class="hidden" id="label-go-to-description">{t(props.lang, "published_content/go_to_description")}</span
+          >
         </a>
         <button
           data-dark-mode
-          class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label={t(props.lang, "published_content/toggle_dark_mode")}
+          class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
+          aria-labelled-by="label-toggle-dark-mode"
+          aria-hidden
         >
-          <svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
+          <svg
+            width="1.25rem"
+            height="1.25rem"
+            viewBox="0 0 512 512"
+            class="hidden fill-current dark:block"
+            aria-hidden
+          >
             <path
               d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
             ></path>
           </svg>
-          <svg viewBox="0 0 512 512" class="block fill-current dark:hidden" aria-hidden="true">
+          <svg
+            width="1.25rem"
+            height="1.25rem"
+            viewBox="0 0 512 512"
+            class="block fill-current dark:hidden"
+            aria-hidden
+          >
             <path
               d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
             ></path>
           </svg>
+          <span class="hidden" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
         </button>
       </div>
     </div>
@@ -132,6 +147,7 @@ const thumbnail =
       <section
         id="game-information"
         class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
+        aria-label={t(props.lang, "game/information_aria_label")}
       >
         <Authors lang={props.lang}>
           {authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
@@ -206,12 +222,12 @@ const thumbnail =
         </h2>
         <Prose>
           <Markdown of={props.description} />
-          <CopyrightedCharacters copyrightedCharacters={copyrightedCharacters} lang={props.lang} />
+          <CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
         </Prose>
       </section>
       <div class="pr-3 text-right print:hidden">
         <a href="#top" class="text-link inline-flex items-center underline"
-          ><svg class="mr-1 h-6 w-6 fill-current" viewBox="0 0 384 512" aria-hidden="true"
+          ><svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 384 512" aria-hidden
             ><path
               d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
             ></path></svg
@@ -274,7 +290,10 @@ const thumbnail =
       }
       {props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
     </main>
-    <div class="pt-6 text-center text-xs text-black dark:text-white">
+    <div
+      class="pt-6 text-center text-xs text-black dark:text-white"
+      aria-label={t(props.lang, "published_content/copyright_aria_label")}
+    >
       <span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
       <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
         >{t(props.lang, "published_content/licenses")}</a
diff --git a/src/layouts/StoryLayout.astro b/src/layouts/StoryLayout.astro
index b1ea437..192b729 100644
--- a/src/layouts/StoryLayout.astro
+++ b/src/layouts/StoryLayout.astro
@@ -12,7 +12,6 @@ import UserComponent from "../components/UserComponent.astro";
 import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
 import Prose from "../components/Prose.astro";
 import MastodonComments from "../components/MastodonComments.astro";
-import { formatCopyrightedCharacters } from "../utils/format_copyrighted_characters";
 
 type Props = CollectionEntry<"stories">["data"];
 
@@ -23,7 +22,6 @@ const series = props.series && (await getEntry(props.series));
 const authorsList = await getEntries(props.authors);
 const commissionersList = props.commissioner && (await getEntries(props.commissioner));
 const requestersList = props.requester && (await getEntries(props.requester));
-const copyrightedCharacters = await formatCopyrightedCharacters(props.copyrightedCharacters);
 const relatedStories = (await getEntries(props.relatedStories)).filter((story) => !story.data.isDraft);
 const relatedGames = (await getEntries(props.relatedGames)).filter((game) => !game.data.isDraft);
 const categorizedTags = Object.fromEntries(
@@ -82,45 +80,62 @@ const wordCount = props.wordCount?.toString();
       <div
         class="pointer-events-auto sticky top-6 flex rounded-full bg-white px-1 py-1 shadow-md dark:bg-black print:hidden"
       >
-        <a
-          href={series ? series.data.url : "/stories/1"}
-          class="text-link my-1 h-9 w-9 p-2"
-          aria-label={series
-            ? t(props.lang, "published_content/return_to_series", series.data.name)
-            : t(props.lang, "story/return_to_stories")}
-        >
-          <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
+        <a href={series ? series.data.url : "/stories/1"} class="text-link my-1 p-2" aria-labelled-by="label-return-to">
+          <svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
             <path
               d="M48.5 224H40c-13.3 0-24-10.7-24-24V72c0-9.7 5.8-18.5 14.8-22.2s19.3-1.7 26.2 5.2L98.6 96.6c87.6-86.5 228.7-86.2 315.8 1c87.5 87.5 87.5 229.3 0 316.8s-229.3 87.5-316.8 0c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0c62.5 62.5 163.8 62.5 226.3 0s62.5-163.8 0-226.3c-62.2-62.2-162.7-62.5-225.3-1L185 183c6.9 6.9 8.9 17.2 5.2 26.2s-12.5 14.8-22.2 14.8H48.5z"
             ></path>
           </svg>
+          <span class="hidden" id="label-return-to"
+            >{
+              series
+                ? t(props.lang, "published_content/return_to_series", series.data.name)
+                : t(props.lang, "story/return_to_stories")
+            }</span
+          >
         </a>
         <a
           href="#description"
-          class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label={t(props.lang, "published_content/go_to_description")}
+          class="text-link my-1 border-l border-stone-300 p-2 dark:border-stone-700"
+          aria-labelled-by="label-go-to-description"
         >
-          <svg viewBox="0 0 512 512" class="fill-current" aria-hidden="true">
+          <svg width="1.25rem" height="1.25rem" viewBox="0 0 512 512" class="fill-current" aria-hidden>
             <path
               d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
             ></path>
           </svg>
+          <span class="hidden" id="label-go-to-description">{t(props.lang, "published_content/go_to_description")}</span
+          >
         </a>
         <button
           data-dark-mode
-          class="text-link my-1 h-9 w-9 border-l border-stone-300 p-2 dark:border-stone-700"
-          aria-label={t(props.lang, "published_content/toggle_dark_mode")}
+          class="text-link my-1 hidden border-l border-stone-300 p-2 dark:border-stone-700"
+          aria-labelled-by="label-toggle-dark-mode"
+          aria-hidden
         >
-          <svg viewBox="0 0 512 512" class="hidden fill-current dark:block" aria-hidden="true">
+          <svg
+            width="1.25rem"
+            height="1.25rem"
+            viewBox="0 0 512 512"
+            class="hidden fill-current dark:block"
+            aria-hidden
+          >
             <path
               d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z"
             ></path>
           </svg>
-          <svg viewBox="0 0 512 512" class="block fill-current dark:hidden" aria-hidden="true">
+          <svg
+            width="1.25rem"
+            height="1.25rem"
+            viewBox="0 0 512 512"
+            class="block fill-current dark:hidden"
+            aria-hidden
+          >
             <path
               d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z"
             ></path>
           </svg>
+          <span class="hidden" id="label-toggle-dark-mode">{t(props.lang, "published_content/toggle_dark_mode")}</span>
         </button>
       </div>
     </div>
@@ -133,31 +148,33 @@ const wordCount = props.wordCount?.toString();
         (prev && !prev.data.isDraft) || (next && !next.data.isDraft) ? (
           <div class="print:hidden">
             <div id="story-nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2">
-              {prev ? (
+              {prev && !prev.data.isDraft ? (
                 <a
                   href={`/stories/${prev.slug}`}
                   class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
+                  aria-label={t(props.lang, "story/previous_story_aria_label")}
                 >
-                  <svg class="mr-1 h-5 w-5 fill-current" viewBox="0 0 320 512">
+                  <svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 320 512" aria-hidden>
                     <path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
                   </svg>
-                  <span>Previous: {prev.data.shortTitle || prev.data.title}</span>
+                  <span>{t(props.lang, "story/previous_story", prev.data.shortTitle || prev.data.title)}</span>
                 </a>
               ) : (
-                <div class="h-full border-r border-stone-400 dark:border-stone-600" />
+                <div class="h-full border-r border-stone-400 dark:border-stone-600" aria-hidden />
               )}
-              {next ? (
+              {next && !next.data.isDraft ? (
                 <a
                   href={`/stories/${next.slug}`}
                   class="text-link flex items-center justify-center px-1 py-3 font-light underline"
+                  aria-label={t(props.lang, "story/next_story_aria_label")}
                 >
-                  <span>Next: {next.data.shortTitle || next.data.title}</span>
-                  <svg class="ml-1 h-5 w-5 fill-current" viewBox="0 0 320 512">
+                  <span>{t(props.lang, "story/next_story", next.data.shortTitle || next.data.title)}</span>
+                  <svg width="1.25rem" height="1.25rem" class="ml-1 fill-current" viewBox="0 0 320 512" aria-hidden>
                     <path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
                   </svg>
                 </a>
               ) : (
-                <div />
+                <div aria-hidden />
               )}
             </div>
             <hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
@@ -174,6 +191,7 @@ const wordCount = props.wordCount?.toString();
       <section
         id="story-information"
         class="mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
+        aria-label={t(props.lang, "story/information_aria_label")}
       >
         <Authors lang={props.lang}>
           {authorsList.map((author) => <UserComponent user={author} lang={props.lang} />)}
@@ -260,7 +278,7 @@ const wordCount = props.wordCount?.toString();
         </h2>
         <Prose>
           <Markdown of={props.description} />
-          <CopyrightedCharacters copyrightedCharacters={copyrightedCharacters} lang={props.lang} />
+          <CopyrightedCharacters copyrightedCharacters={props.copyrightedCharacters} lang={props.lang} />
         </Prose>
       </section>
       {
@@ -283,12 +301,12 @@ const wordCount = props.wordCount?.toString();
         ) : null
       }
       <div class="pr-3 text-right print:hidden">
-        <a href="#top" class="text-link inline-flex items-center underline"
-          ><svg class="mr-1 h-6 w-6 fill-current" viewBox="0 0 384 512" aria-hidden="true"
+        <a href="#top" class="text-link inline-flex items-center underline" aria-labelledby="label-to-top"
+          ><svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 384 512" aria-hidden
             ><path
               d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
             ></path></svg
-          ><span>{t(props.lang, "published_content/to_top")}</span></a
+          ><span id="label-to-top">{t(props.lang, "published_content/to_top")}</span></a
         >
       </div>
       {
@@ -300,11 +318,12 @@ const wordCount = props.wordCount?.toString();
                 <a
                   href={`/stories/${prev.slug}`}
                   class="text-link flex items-center justify-center border-r border-stone-400 px-1 py-3 font-light underline dark:border-stone-600"
+                  aria-label={t(props.lang, "story/previous_story_aria_label")}
                 >
-                  <svg class="mr-1 h-5 w-5 fill-current" viewBox="0 0 320 512">
+                  <svg width="1.25rem" height="1.25rem" class="mr-1 fill-current" viewBox="0 0 320 512" aria-hidden>
                     <path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z" />
                   </svg>
-                  <span>Previous: {prev.data.shortTitle || prev.data.title}</span>
+                  <span>{t(props.lang, "story/previous_story", prev.data.shortTitle || prev.data.title)}</span>
                 </a>
               ) : (
                 <div class="h-full border-r border-stone-400 dark:border-stone-600" />
@@ -313,9 +332,10 @@ const wordCount = props.wordCount?.toString();
                 <a
                   href={`/stories/${next.slug}`}
                   class="text-link flex items-center justify-center px-1 py-3 font-light underline"
+                  aria-label={t(props.lang, "story/next_story_aria_label")}
                 >
-                  <span>Next: {next.data.shortTitle || next.data.title}</span>
-                  <svg class="ml-1 h-5 w-5 fill-current" viewBox="0 0 320 512">
+                  <span>{t(props.lang, "story/next_story", next.data.shortTitle || next.data.title)}</span>
+                  <svg width="1.25rem" height="1.25rem" class="ml-1 fill-current" viewBox="0 0 320 512" aria-hidden>
                     <path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z" />
                   </svg>
                 </a>
@@ -383,7 +403,10 @@ const wordCount = props.wordCount?.toString();
       }
       {props.posts.mastodon ? <MastodonComments lang={props.lang} {...props.posts.mastodon} /> : null}
     </main>
-    <div class="pt-6 text-center text-xs text-black dark:text-white">
+    <div
+      class="pt-6 text-center text-xs text-black dark:text-white"
+      aria-label={t(props.lang, "published_content/copyright_aria_label")}
+    >
       <span>{t(props.lang, "published_content/copyright_year", (props.pubDate || new Date()).getFullYear())} | </span>
       <a class="hover:underline focus:underline" href="/licenses.txt" target="_blank"
         >{t(props.lang, "published_content/licenses")}</a
diff --git a/src/pages/index.astro b/src/pages/index.astro
index b70bd29..e8609e4 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -8,7 +8,7 @@ const MAX_ITEMS = 8;
 
 interface LatestItemsEntry {
   type: string;
-  thumbnail: CollectionEntry<"stories">["data"]["thumbnail"];
+  thumbnail?: ImageMetadata;
   href: string;
   title: string;
   altText: string;
@@ -57,13 +57,9 @@ const latestItems: LatestItemsEntry[] = [
 <GalleryLayout pageTitle="Gallery">
   <meta slot="head-description" property="og:description" content="Bad Manners || Welcome to my gallery!" />
   <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1>
-  <p class="my-4 block md:hidden">
-    Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu above to navigate
-    through my content.
-  </p>
-  <p class="my-4 hidden md:block" aria-hidden>
-    Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to the left to
-    navigate through my content.
+  <p class="my-4">
+    Welcome to my gallery! You can expect lots of safe vore/endosoma ahead. Use the navigation menu to navigate through
+    my content.
   </p>
   <ul class="list-disc pl-8">
     <li><a class="text-link underline" href="/stories/1">Read my stories!</a></li>