From a2fbf908aa044e9cbe34deac988ea28aa5c5fb83 Mon Sep 17 00:00:00 2001
From: Bad Manners <me@badmanners.xyz>
Date: Sat, 7 Sep 2024 18:02:53 -0300
Subject: [PATCH] Final touches to the facelift

---
 package-lock.json                             | 98 ++++++++++++++-----
 package.json                                  |  1 +
 scripts/export-story.ts                       | 14 ++-
 src/components/MastodonComments.astro         |  6 +-
 src/content/config.ts                         |  1 +
 src/i18n/index.ts                             |  2 +-
 src/layouts/GalleryLayout.astro               | 76 +++++++++-----
 src/layouts/PublishedContentLayout.astro      | 20 ++--
 src/pages/404.astro                           |  3 +-
 src/pages/games.astro                         |  3 +-
 src/pages/index.astro                         |  7 +-
 src/pages/search.astro                        |  3 +-
 src/pages/stories/[page].astro                |  3 +-
 .../stories/the-lost-of-the-marshes.astro     |  5 +-
 src/pages/tags.astro                          |  3 +-
 src/styles/base.css                           | 17 ++++
 src/utils/format_copyrighted_characters.ts    |  2 +-
 src/utils/parse_partial_html_tag.ts           |  6 +-
 tailwind.config.mjs                           | 11 ++-
 19 files changed, 194 insertions(+), 87 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index a0b46b9..1a78914 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
         "astro-htaccess": "^0.1.2",
         "astro-pagefind": "^1.6.0",
         "clsx": "^2.1.1",
+        "fluid-tailwind": "^1.0.3",
         "github-slugger": "^2.0.0",
         "marked": "^14.0.0",
         "pagefind": "^1.1.0",
@@ -1601,65 +1602,71 @@
       }
     },
     "node_modules/@pagefind/darwin-arm64": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.1.0.tgz",
-      "integrity": "sha512-SLsXNLtSilGZjvqis8sX42fBWsWAVkcDh1oerxwqbac84HbiwxpxOC2jm8hRwcR0Z55HPZPWO77XeRix/8GwTg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.1.1.tgz",
+      "integrity": "sha512-tZ9tysUmQpFs2EqWG2+E1gc+opDAhSyZSsgKmFzhnWfkK02YHZhvL5XJXEZDqYy3s1FAKhwjTg8XDxneuBlDZQ==",
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
     "node_modules/@pagefind/darwin-x64": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.1.0.tgz",
-      "integrity": "sha512-QjQSE/L5oS1C8N8GdljGaWtjCBMgMtfrPAoiCmINTu9Y9dp0ggAyXvF8K7Qg3VyIMYJ6v8vg2PN7Z3b+AaAqUA==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.1.1.tgz",
+      "integrity": "sha512-ChohLQ39dLwaxQv0jIQB/SavP3TM5K5ENfDTqIdzLkmfs3+JlzSDyQKcJFjTHYcCzQOZVeieeGq8PdqvLJxJxQ==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "darwin"
       ]
     },
     "node_modules/@pagefind/default-ui": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.1.0.tgz",
-      "integrity": "sha512-+XiAJAK++C64nQcD7s3Prdmd5S92lT05fwjOxm0L1jj80jbL+tmvcqkkFnPpoqhnicIPgcAX/Y5W0HRZnBt35w=="
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.1.1.tgz",
+      "integrity": "sha512-ZM0zDatWDnac/VGHhQCiM7UgA4ca8jpjA+VfuTJyHJBaxGqZMQnm4WoTz9E0KFcue1Bh9kxpu7uWFZfwpZZk0A==",
+      "license": "MIT"
     },
     "node_modules/@pagefind/linux-arm64": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.1.0.tgz",
-      "integrity": "sha512-8zjYCa2BtNEL7KnXtysPtBELCyv5DSQ4yHeK/nsEq6w4ToAMTBl0K06khqxdSGgjMSwwrxvLzq3so0LC5Q14dA==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.1.1.tgz",
+      "integrity": "sha512-H5P6wDoCoAbdsWp0Zx0DxnLUrwTGWGLu/VI1rcN2CyFdY2EGSvPQsbGBMrseKRNuIrJDFtxHHHyjZ7UbzaM9EA==",
       "cpu": [
         "arm64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@pagefind/linux-x64": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.1.0.tgz",
-      "integrity": "sha512-4lsg6VB7A6PWTwaP8oSmXV4O9H0IHX7AlwTDcfyT+YJo/sPXOVjqycD5cdBgqNLfUk8B9bkWcTDCRmJbHrKeCw==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.1.1.tgz",
+      "integrity": "sha512-yJs7tTYbL2MI3HT+ngs9E1BfUbY9M4/YzA0yEM5xBo4Xl8Yu8Qg2xZTOQ1/F6gwvMrjCUFo8EoACs6LRDhtMrQ==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "linux"
       ]
     },
     "node_modules/@pagefind/windows-x64": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.1.0.tgz",
-      "integrity": "sha512-OboCM76BcMKT9IoSfZuFhiqMRgTde8x4qDDvKulFmycgiJrlL5WnIqBHJLQxZq+o2KyZpoHF97iwsGAm8c32sQ==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.1.1.tgz",
+      "integrity": "sha512-b7/qPqgIl+lMzkQ8fJt51SfguB396xbIIR+VZ3YrL2tLuyifDJ1wL5mEm+ddmHxJ2Fki340paPcDan9en5OmAw==",
       "cpu": [
         "x64"
       ],
+      "license": "MIT",
       "optional": true,
       "os": [
         "win32"
@@ -3370,6 +3377,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/filter-obj": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
+      "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/find-up": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -3413,6 +3432,20 @@
         "node": ">=8"
       }
     },
+    "node_modules/fluid-tailwind": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/fluid-tailwind/-/fluid-tailwind-1.0.3.tgz",
+      "integrity": "sha512-JhSklHiXUcKnb/PIru1TbyorLD+k0rCIgeAbFwzu1QyQHYDKmKKsKzN7xPisSeGGr7FtYY9S63t+tzddUlxxrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "filter-obj": "^5.1.0",
+        "map-obj": "^5.0.2",
+        "picocolors": "^1.0.0"
+      },
+      "peerDependencies": {
+        "tailwindcss": "^3.2.0"
+      }
+    },
     "node_modules/foreground-child": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@@ -4266,6 +4299,18 @@
       "optional": true,
       "peer": true
     },
+    "node_modules/map-obj": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.2.tgz",
+      "integrity": "sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/markdown-table": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz",
@@ -5361,18 +5406,19 @@
       }
     },
     "node_modules/pagefind": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.1.0.tgz",
-      "integrity": "sha512-1nmj0/vfYcMxNEQj0YDRp6bTVv9hI7HLdPhK/vBBYlrnwjATndQvHyicj5Y7pUHrpCFZpFnLVQXIF829tpFmaw==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.1.1.tgz",
+      "integrity": "sha512-U2YR0dQN5B2fbIXrLtt/UXNS0yWSSYfePaad1KcBPTi0p+zRtsVjwmoPaMQgTks5DnHNbmDxyJUL5TGaLljK3A==",
+      "license": "MIT",
       "bin": {
         "pagefind": "lib/runner/bin.cjs"
       },
       "optionalDependencies": {
-        "@pagefind/darwin-arm64": "1.1.0",
-        "@pagefind/darwin-x64": "1.1.0",
-        "@pagefind/linux-arm64": "1.1.0",
-        "@pagefind/linux-x64": "1.1.0",
-        "@pagefind/windows-x64": "1.1.0"
+        "@pagefind/darwin-arm64": "1.1.1",
+        "@pagefind/darwin-x64": "1.1.1",
+        "@pagefind/linux-arm64": "1.1.1",
+        "@pagefind/linux-x64": "1.1.1",
+        "@pagefind/windows-x64": "1.1.1"
       }
     },
     "node_modules/parse-latin": {
diff --git a/package.json b/package.json
index 1b441c4..493010b 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "astro-htaccess": "^0.1.2",
     "astro-pagefind": "^1.6.0",
     "clsx": "^2.1.1",
+    "fluid-tailwind": "^1.0.3",
     "github-slugger": "^2.0.0",
     "marked": "^14.0.0",
     "pagefind": "^1.1.0",
diff --git a/scripts/export-story.ts b/scripts/export-story.ts
index c92174d..f98ba21 100644
--- a/scripts/export-story.ts
+++ b/scripts/export-story.ts
@@ -168,10 +168,16 @@ async function exportStory(slug: string, options: { outputDir: string }) {
       );
       const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
       const rtfStyles = getRTFStyles(rtfText);
-      await writeFile(
-        join(outputDir, `${slug}.rtf`),
-        rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
-      );
+      if (!rtfStyles["Preformatted Text"]) {
+        console.warn(`Missing RTF style "Preformatted Text"! Skipping RTF file generation.`);
+      } else if (!rtfStyles["Normal"]) {
+        console.warn(`Missing RTF style "Normal"! Skipping RTF file generation.`);
+      } else {
+        await writeFile(
+          join(outputDir, `${slug}.rtf`),
+          rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
+        );
+      }
     })(),
   ]);
   console.log("Success!");
diff --git a/src/components/MastodonComments.astro b/src/components/MastodonComments.astro
index b81d7b2..01d610e 100644
--- a/src/components/MastodonComments.astro
+++ b/src/components/MastodonComments.astro
@@ -254,11 +254,11 @@ const { link, instance, user, postId } = Astro.props;
         if (comment.in_reply_to_id === post.postId || !(comment.in_reply_to_id in commentMap)) {
           commentMap[comment.id] = commentsList.length;
           commentsList.push(commentBox);
-        } else {
-          const commentsIndex = commentMap[comment.in_reply_to_id];
+        } else if (commentMap[comment.in_reply_to_id]) {
+          const commentsIndex = commentMap[comment.in_reply_to_id]!;
           commentMap[comment.id] = commentsIndex;
           const parentThreadDiv =
-            commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
+            commentsList[commentsIndex]!.querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
           parentThreadDiv.setAttribute("aria-hidden", "false");
           parentThreadDiv.appendChild(commentBox);
         }
diff --git a/src/content/config.ts b/src/content/config.ts
index ed8b506..a3f4d34 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -222,6 +222,7 @@ export type GamePlatform = z.infer<typeof platform>;
 export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
 export type PublishedContent = z.infer<typeof publishedContent>;
 export type PostWebsite = keyof PublishedContent["posts"];
+export type Posts = PublishedContent["posts"];
 
 // Content collections
 
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index ccc56cc..9456b57 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -13,7 +13,7 @@ const UI_STRINGS = {
     tok: (names: string[]) => names.join(" en "),
   },
   "util/capitalize": {
-    en: (text: string) => (text.length > 0 ? `${text[0].toUpperCase()}${text.slice(1)}` : ""),
+    en: (text: string) => (text.length > 0 ? `${text[0]!.toUpperCase()}${text.slice(1)}` : ""),
   },
   "util/enumerate": {
     en: (count: number, nounSingular: string, nounPlural?: string) => {
diff --git a/src/layouts/GalleryLayout.astro b/src/layouts/GalleryLayout.astro
index c82927f..6d90491 100644
--- a/src/layouts/GalleryLayout.astro
+++ b/src/layouts/GalleryLayout.astro
@@ -37,30 +37,30 @@ const isCurrentRoute = (path: string) =>
     <slot name="head" />
   </Fragment>
   <div
-    class="flex min-h-screen flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
+    class="flex flex-col bg-stone-200 text-stone-800 md:flex-row dark:bg-stone-800 dark:text-stone-200 print:bg-none"
   >
     <nav
-      class="h-card static mb-4 flex flex-col items-center bg-bm-300 pb-10 pt-10 text-center text-stone-900 shadow-xl md:fixed md:inset-y-0 md:left-0 md:mb-0 md:w-60 md:pt-20 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
+      class="h-card mb-4 flex shrink-0 flex-col items-center border-b-4 border-bm-400 bg-bm-300 pb-10 pt-10 text-center text-stone-900 shadow-xl md:left-0 md:mb-0 md:min-h-screen md:w-72 md:justify-self-stretch md:border-b-0 md:border-r-4 md:pt-20 dark:border-green-950 dark:bg-green-900 dark:text-stone-100 print:bg-none print:shadow-none"
     >
       <img
         loading="eager"
         src={logo.src}
         alt="A pixelated metal briefcase over a background with a green gradient."
-        class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-950 shadow-md"
+        class="u-logo my-4 w-full max-w-[192px] rounded-sm border-2 border-green-800 shadow-md dark:border-green-950"
         width={192}
       />
-      <span class="p-name mb-4 mt-2 text-2xl font-semibold">Bad Manners</span>
-      <ul class="flex flex-col gap-y-2 pr-8 text-left sm:pr-2">
+      <span class="p-name mb-6 mt-4 text-3xl font-semibold">Bad Manners</span>
+      <ul class="flex w-[80%] max-w-sm flex-col gap-y-2 px-4 pr-8 text-left sm:pr-2">
         <li>
           <a class="u-url text-link group" href="https://badmanners.xyz/" data-age-restricted rel="me">
-            <IconHome width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">Main website</span>
+            <IconHome width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">Main website</span>
           </a>
         </li>
         <li>
           <a class="u-url text-link group" href="/" aria-current={isCurrentRoute("/") ? "page" : undefined}>
-            <IconBriefcase width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">Gallery</span>
+            <IconBriefcase width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">Gallery</span>
           </a>
         </li>
         <li>
@@ -69,20 +69,20 @@ const isCurrentRoute = (path: string) =>
             href="/stories/1"
             aria-current={isCurrentRoute("/stories/1") ? "page" : undefined}
           >
-            <IconBook width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">Stories</span>
+            <IconBook width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">Stories</span>
           </a>
         </li>
         <li>
           <a class="u-url text-link group" href="/games" aria-current={isCurrentRoute("/games") ? "page" : undefined}>
-            <IconGamepad width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">Games</span>
+            <IconGamepad width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">Games</span>
           </a>
         </li>
         <li>
           <a class="u-url text-link group" href="/tags" aria-current={isCurrentRoute("/tags") ? "page" : undefined}>
-            <IconTags width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">Tags</span>
+            <IconTags width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">Tags</span>
           </a>
         </li>
         <li>
@@ -92,27 +92,35 @@ const isCurrentRoute = (path: string) =>
             rel="search"
             aria-current={isCurrentRoute("/search") ? "page" : undefined}
           >
-            <IconMagnifyingGlass width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">Search</span>
+            <IconMagnifyingGlass width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">Search</span>
           </a>
         </li>
         <li>
           <a class="u-url text-link group" href="/feed.xml">
-            <IconSquareRSS width="1.25rem" height="1.25rem" class="inline align-text-top" />
-            <span class="group-hover:underline group-focus:underline">RSS feed</span>
+            <IconSquareRSS width="1.25rem" height="1.25rem" class="order-1 inline align-text-top" />
+            <span class="order-3 group-hover:underline group-focus:underline">RSS feed</span>
           </a>
         </li>
         <li>
-          <button data-dark-mode style={{ display: "none" }} class="text-link group">
-            <IconSun width="1.25rem" height="1.25rem" class="hidden align-middle dark:inline" />
-            <IconMoon width="1.25rem" height="1.25rem" class="inline align-text-top dark:hidden" />
-            <span class="group-hover:underline group-focus:underline"
-              >{t("en", "published_content/toggle_dark_mode")}</span
+          <button
+            data-dark-mode
+            style={{ display: "none" }}
+            class="text-link group"
+            aria-label={t("en", "published_content/toggle_dark_mode")}
+          >
+            <IconSun width="1.25rem" height="1.25rem" class="order-1 hidden align-text-top dark:inline" />
+            <IconMoon width="1.25rem" height="1.25rem" class="order-1 inline align-text-top dark:hidden" />
+            <span class="order-3 hidden group-hover:underline group-focus:underline dark:block" aria-hidden="true"
+              >Light mode</span
+            >
+            <span class="order-3 block group-hover:underline group-focus:underline dark:hidden" aria-hidden="true"
+              >Dark mode</span
             >
           </button>
         </li>
       </ul>
-      <div class="pt-4 text-center text-xs text-black dark:text-white">
+      <div class="py-6 text-center text-xs text-black dark:text-white">
         <span
           >&copy; {
             currentYear === "2024" ? (
@@ -128,10 +136,26 @@ const isCurrentRoute = (path: string) =>
       </div>
     </nav>
     <main
-      class:list={[className, "ml-0 max-w-6xl px-2 pb-12 pt-4 md:ml-60 md:px-4 print:pb-0"]}
+      class:list={[className, "ml-0 max-w-6xl px-2 pb-28 pt-4 md:px-4 lg:px-8 print:pb-0"]}
       data-pagefind-body={enablePagefind ? "" : undefined}
     >
       <slot />
     </main>
   </div>
 </BaseLayout>
+<style>
+  nav ul li a,
+  nav ul li button {
+    @apply flex w-full items-baseline justify-between;
+  }
+
+  nav ul li a::after,
+  nav ul li button::after {
+    @apply order-2 flex-grow self-end bg-bottom;
+    background: radial-gradient(circle, currentcolor 1px, transparent 1px);
+    background-size: 1ex 4.5px;
+    background-repeat: space no-repeat;
+    content: "";
+    height: 1ex;
+  }
+</style>
diff --git a/src/layouts/PublishedContentLayout.astro b/src/layouts/PublishedContentLayout.astro
index 8ef3593..edca29a 100644
--- a/src/layouts/PublishedContentLayout.astro
+++ b/src/layouts/PublishedContentLayout.astro
@@ -9,7 +9,7 @@ import BaseLayout from "./BaseLayout.astro";
 import CopyrightedCharacters from "../components/CopyrightedCharacters.astro";
 import Prose from "../components/Prose.astro";
 import MastodonComments from "../components/MastodonComments.astro";
-import type { CopyrightedCharacters as CopyrightedCharactersType } from "../content/config";
+import type { CopyrightedCharacters as CopyrightedCharactersType, Posts } from "../content/config";
 import { qualifyLocalURLsInMarkdown } from "../utils/qualify_local_urls_in_markdown";
 import {
   IconSun,
@@ -44,14 +44,7 @@ type Props = {
   next?: RelatedContent;
   relatedStories?: CollectionEntry<"stories">[];
   relatedGames?: CollectionEntry<"games">[];
-  posts: {
-    mastodon?: {
-      link: string;
-      instance: string;
-      user: string;
-      postId: string;
-    };
-  };
+  posts: Posts;
 
   /* Layout attributes */
   publishedContentType: "story" | "game";
@@ -159,14 +152,14 @@ const thumbnail =
       </div>
     </div>
     <main
-      class="h-entry mx-auto max-w-5xl rounded-lg bg-stone-50 px-2 pb-10 pt-12 shadow-sm sm:px-6 md:px-20 lg:px-36 dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none"
+      class="h-entry mx-auto max-w-6xl rounded-lg bg-stone-50 px-2 pb-10 shadow-sm sm:px-6 md:px-32 lg:px-64 dark:bg-stone-900 print:max-w-full print:bg-none print:shadow-none"
       data-pagefind-body={props.isDraft ? undefined : ""}
       data-pagefind-meta={`type:${props.publishedContentType}`}
     >
       {
         props.prev || props.next ? (
           <div class="print:hidden">
-            <div id="nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2 text-lg font-light">
+            <div id="nav-top" class="my-4 grid grid-cols-2 justify-items-stretch gap-2 pt-4 text-lg font-light">
               {props.prev ? (
                 <a
                   href={props.prev.link}
@@ -196,7 +189,9 @@ const thumbnail =
             </div>
             <hr class="mx-auto mb-5 w-full max-w-2xl border-stone-400 dark:border-stone-600" />
           </div>
-        ) : null
+        ) : (
+          <div class="pt-5 sm:pt-7" aria-hidden="true" />
+        )
       }
       <h1
         id="section-title"
@@ -205,6 +200,7 @@ const thumbnail =
       >
         {props.title}
       </h1>
+      <hr class="mb-3 ml-[2px] mt-2 h-[4px] w-1/2 rounded-sm bg-stone-800 dark:bg-stone-100" />
       <section
         id="section-information"
         class="p-summary mt-1 space-y-2 px-2 font-serif font-light italic text-stone-600 dark:text-stone-200"
diff --git a/src/pages/404.astro b/src/pages/404.astro
index 12a22ab..a21a43f 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -4,6 +4,7 @@ import GalleryLayout from "../layouts/GalleryLayout.astro";
 
 <GalleryLayout pageTitle="404">
   <meta slot="head" property="og:description" content="Not found" />
-  <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">404 &ndash; Not Found</h1>
+  <h1 class="m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">404 &ndash; Not Found</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <p class="my-4">The requested link could not be found. Make sure that the URL is correct.</p>
 </GalleryLayout>
diff --git a/src/pages/games.astro b/src/pages/games.astro
index bb766a0..39c0529 100644
--- a/src/pages/games.astro
+++ b/src/pages/games.astro
@@ -19,7 +19,8 @@ const games = await Promise.all(
 
 <GalleryLayout pageTitle="Games" class="h-feed">
   <meta slot="head" property="og:description" content="Bad Manners || A game that I've gone and done." />
-  <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Games</h1>
+  <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Games</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <p class="p-summary my-4">A game that I've gone and done.</p>
   <ul class="my-6 flex flex-wrap items-start justify-center gap-4 text-center md:justify-normal">
     {
diff --git a/src/pages/index.astro b/src/pages/index.astro
index e054bb5..a22865d 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -75,11 +75,12 @@ const latestItems: LatestItemsEntry[] = await Promise.all(
 
 <GalleryLayout pageTitle="Gallery" class="h-feed">
   <meta slot="head" property="og:description" content="Bad Manners || Welcome to my gallery!" />
-  <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">My gallery</h1>
+  <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Welcome to my gallery!</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <div class="p-summary">
     <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.
+      Glad to see you here. 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>
diff --git a/src/pages/search.astro b/src/pages/search.astro
index fd3f581..4253d51 100644
--- a/src/pages/search.astro
+++ b/src/pages/search.astro
@@ -10,6 +10,7 @@ import GalleryLayout from "../layouts/GalleryLayout.astro";
 
 <GalleryLayout pageTitle="Search">
   <meta slot="head" property="og:description" content="Bad Manners || Search" />
-  <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Search</h1>
+  <h1 class="m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Search</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <SearchComponent id="search" className="pagefind-ui my-4" />
 </GalleryLayout>
diff --git a/src/pages/stories/[page].astro b/src/pages/stories/[page].astro
index b8910c3..618b49c 100644
--- a/src/pages/stories/[page].astro
+++ b/src/pages/stories/[page].astro
@@ -30,7 +30,8 @@ const totalPages = Math.ceil(page.total / page.size);
 
 <GalleryLayout pageTitle="Stories" class="h-feed">
   <meta slot="head" property="og:description" content={`Bad Manners || ${page.total} stories.`} />
-  <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1>
+  <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">Stories</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <div class="p-summary">
     <p class="my-4">The bulk of my content!</p>
     <p class="text-center font-light text-stone-950 dark:text-white">
diff --git a/src/pages/stories/the-lost-of-the-marshes.astro b/src/pages/stories/the-lost-of-the-marshes.astro
index 12cb29e..313b6f5 100644
--- a/src/pages/stories/the-lost-of-the-marshes.astro
+++ b/src/pages/stories/the-lost-of-the-marshes.astro
@@ -26,7 +26,8 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
     property="og:description"
     content="The Lost of the Marshes || The story of Quince, Nikili, and Suu."
   />
-  <h1 class="p-name m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
+  <h1 class="p-name m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">{series.data.name}</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <p class="p-summary my-4">
     This is the main hub for the story of Quince, Nikili, and Suu, as well as all bonus content.
   </p>
@@ -43,7 +44,7 @@ const mainChaptersWithSummaries = mainChapters.filter((story) => story.data.summ
     >
       <summary class="rounded-lg bg-stone-200 px-2 py-1 dark:bg-stone-800">
         Click to reveal spoilers up to {
-          mainChaptersWithSummaries[mainChaptersWithSummaries.length - 1].data.title.match(/Chapter \d+\b/)?.[0]
+          mainChaptersWithSummaries[mainChaptersWithSummaries.length - 1]!.data.title.match(/Chapter \d+\b/)?.[0]
         }
       </summary>
       <ul class="border-t border-stone-400 px-1 dark:border-stone-500">
diff --git a/src/pages/tags.astro b/src/pages/tags.astro
index 93eb9df..58a2c89 100644
--- a/src/pages/tags.astro
+++ b/src/pages/tags.astro
@@ -71,7 +71,8 @@ if (uncategorizedTagsSet.size > 0) {
 
 <GalleryLayout pageTitle="Tags">
   <meta property="og:description" slot="head" content="Bad Manners || Find all content with a specific tag." />
-  <h1 class="m-2 text-2xl font-semibold text-stone-800 dark:text-stone-100">All available tags</h1>
+  <h1 class="m-2 text-3xl font-semibold text-stone-800 dark:text-stone-100">All available tags</h1>
+  <hr class="mb-3 ml-[2px] mt-2 h-[4px] max-w-xs rounded-sm bg-stone-800 dark:bg-stone-100" />
   <p class="my-4">You can find all content with a specific tag by selecting it below from the appropriate category.</p>
   <section class="my-2" aria-labelledby="category-series">
     <h2 id="category-series" class="p-2 text-xl font-semibold text-stone-800 dark:text-stone-100">Series</h2>
diff --git a/src/styles/base.css b/src/styles/base.css
index da6dd59..1676347 100644
--- a/src/styles/base.css
+++ b/src/styles/base.css
@@ -2,6 +2,23 @@
 @tailwind components;
 @tailwind utilities;
 
+/* Tippy tooltips */
+.tippy-box[data-theme~="bm"] {
+  @apply bg-stone-800 font-sans text-sm text-stone-50 dark:bg-zinc-900 dark:text-zinc-100;
+}
+.tippy-box[data-theme~="bm"][data-placement^="top"] > .tippy-arrow::before {
+  @apply border-t-stone-800 dark:border-t-zinc-900;
+}
+.tippy-box[data-theme~="bm"][data-placement^="bottom"] > .tippy-arrow::before {
+  @apply border-b-stone-800 dark:border-b-zinc-900;
+}
+.tippy-box[data-theme~="bm"][data-placement^="left"] > .tippy-arrow::before {
+  @apply border-l-stone-800 dark:border-l-zinc-900;
+}
+.tippy-box[data-theme~="bm"][data-placement^="right"] > .tippy-arrow::before {
+  @apply border-r-stone-800 dark:border-r-zinc-900;
+}
+
 @layer components {
   .text-link,
   .pagefind-ui .pagefind-ui__result-link {
diff --git a/src/utils/format_copyrighted_characters.ts b/src/utils/format_copyrighted_characters.ts
index 31a9bbf..f352a04 100644
--- a/src/utils/format_copyrighted_characters.ts
+++ b/src/utils/format_copyrighted_characters.ts
@@ -6,7 +6,7 @@ export async function formatCopyrightedCharacters(copyrightedCharacters: Copyrig
     Object.values(
       Object.keys(copyrightedCharacters).reduce(
         (acc, character) => {
-          const user = copyrightedCharacters[character];
+          const user = copyrightedCharacters[character]!;
           if (user.id in acc) {
             acc[user.id][1].push(character);
           } else {
diff --git a/src/utils/parse_partial_html_tag.ts b/src/utils/parse_partial_html_tag.ts
index 7727190..04c9ac6 100644
--- a/src/utils/parse_partial_html_tag.ts
+++ b/src/utils/parse_partial_html_tag.ts
@@ -18,7 +18,7 @@ export function parsePartialHTMLTag(text: string): ParsedHTMLTag {
   const openTag = partialText.match(OPEN_TAG_START_REGEX);
   if (openTag) {
     const result: ParsedHTMLTag = {
-      tag: openTag[1],
+      tag: openTag[1]!,
       type: "open",
     };
     partialText = partialText.slice(openTag[0].length);
@@ -30,7 +30,7 @@ export function parsePartialHTMLTag(text: string): ParsedHTMLTag {
       if (!result.attributes) {
         result.attributes = {};
       }
-      result.attributes[attribute[1]] = attribute[2] ? JSON.parse(attribute[2]) : null;
+      result.attributes[attribute[1]!] = attribute[2] ? JSON.parse(attribute[2]) : null;
       partialText = partialText.slice(attribute[0].length);
     }
     if (partialText.match(END_OPEN_REGEX)) {
@@ -44,7 +44,7 @@ export function parsePartialHTMLTag(text: string): ParsedHTMLTag {
     const closeTag = partialText.match(CLOSE_TAG_REGEX);
     if (closeTag) {
       return {
-        tag: closeTag[1],
+        tag: closeTag[1]!,
         type: "close",
       };
     }
diff --git a/tailwind.config.mjs b/tailwind.config.mjs
index 41dc640..9724321 100644
--- a/tailwind.config.mjs
+++ b/tailwind.config.mjs
@@ -1,9 +1,18 @@
 import defaultTheme from "tailwindcss/defaultTheme";
+import typography from "@tailwindcss/typography";
 
 /** @type {import('tailwindcss').Config} */
 export default {
   darkMode: ["variant", "@media not print { .dark & }"],
-  content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
+  content: {
+    files: [
+      "./src/components/**/*.astro",
+      "./src/content/games/**/*.{md,mdx}",
+      "./src/content/stories/**/*.{md,mdx}",
+      "./src/layouts/*.astro",
+      "./src/pages/**/*.{astro,ts}",
+    ],
+  },
   theme: {
     extend: {
       fontFamily: {