From d604ca637d388e0a9723bcb638df9d9bdacafb5e Mon Sep 17 00:00:00 2001
From: prescientmoon <git@moonythm.dev>
Date: Thu, 7 Nov 2024 08:50:05 +0100
Subject: [PATCH] 404 page, sitemap, and some linters

---
 .gitignore                         |    2 +
 Cargo.lock                         |   79 ++
 Cargo.toml                         |    2 +
 content/404.dj                     |   10 +
 content/echoes/arcaea.dj           |    6 +-
 content/echoes/games/index.dj      |   66 +-
 content/echoes/index.dj            |    6 +
 flake.nix                          |   11 +
 justfile                           |   41 +
 public/styles.css                  |   59 +-
 src/generate.rs                    |   83 +-
 src/html.rs                        |  188 +--
 src/main.rs                        |    8 +-
 src/metadata.rs                    |   60 +-
 src/templates/aside.html           |    2 +-
 src/templates/character-aside.html |    4 +-
 src/templates/long-aside.html      |    4 +-
 src/templates/page.html            |    2 +-
 src/templates/post-summary.html    |    2 +-
 src/templates/post.html            |    4 +-
 tooling/htmltest.yml               |   10 +
 tooling/htmlvalidate.json          |    9 +
 tooling/package-lock.json          | 1857 ++++++++++++++++++++++++++++
 tooling/package.json               |    7 +
 tooling/stylelintrc.json           |    8 +
 25 files changed, 2358 insertions(+), 172 deletions(-)
 create mode 100644 content/404.dj
 create mode 100644 justfile
 create mode 100644 tooling/htmltest.yml
 create mode 100644 tooling/htmlvalidate.json
 create mode 100644 tooling/package-lock.json
 create mode 100644 tooling/package.json
 create mode 100644 tooling/stylelintrc.json

diff --git a/.gitignore b/.gitignore
index 84379db..13352ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ target
 dist
 oldicons
 result
+node_modules
+tmp
diff --git a/Cargo.lock b/Cargo.lock
index 76f0d83..73ff5fa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,6 +38,21 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.16.0"
@@ -80,12 +95,51 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.15.0"
@@ -169,11 +223,13 @@ name = "moonythm"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "base64",
  "chrono",
  "jotdown",
  "once_cell",
  "pulldown-latex",
  "serde",
+ "sha2",
  "toml",
  "tree-sitter",
  "tree-sitter-highlight",
@@ -280,6 +336,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -399,12 +466,24 @@ dependencies = [
  "tree-sitter-language",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
 
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.95"
diff --git a/Cargo.toml b/Cargo.toml
index 1d4dc81..213d48e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,3 +17,5 @@ chrono = { version = "0.4.38", features = ["serde"] }
 tree-sitter = "0.24.3"
 tree-sitter-rust = "0.23.0"
 tree-sitter-highlight = "0.24.3"
+sha2 = "0.10.8"
+base64 = "0.22.1"
diff --git a/content/404.dj b/content/404.dj
new file mode 100644
index 0000000..d555f7c
--- /dev/null
+++ b/content/404.dj
@@ -0,0 +1,10 @@
+{ role=config }
+``` =toml
+sitemap_exclude = true
+```
+
+# Ooooops
+
+This page does not seem to exist. If this page was here before, please [contact me][Page containing my contact info].
+
+[Page containing my contact info]: /
diff --git a/content/echoes/arcaea.dj b/content/echoes/arcaea.dj
index 8c1811e..739ebbf 100644
--- a/content/echoes/arcaea.dj
+++ b/content/echoes/arcaea.dj
@@ -23,7 +23,7 @@ A discussion on pretty much every aspect of the mobile rhythm game "Arcaea".
 {% [[[ %}
 The game judges the way the player hits (or fails to do so) every note by quantizing the inputs into [a few judgements](https://arcaea.fandom.com/wiki/Scoring): a PURE judgement is awarded within a 50ms timing window, the more lenient FAR judgement has twice as much (100ms), with all the other notes being marked as LOST. Moreover, an optional tighter (25ms) timing window awards the MAX PURE (usually referred to as "shiny pure" by the community) judgement.
 
-{ title="Lagrange's extras: Ghost tapping" character="lagrange" }
+{ title="Lagrange's extras: Ghost tapping" character="lagrange" id="ghost-tapping" }
 ::: char-aside
 The game only awards judgements for notes in the chart. That is, tapping while no notes are nearby will not influence the score in any way! This turns out to be quite useful, leading to what is referred to as "ghost tapping", which can be a useful way to keep the rhythm or have fun with the chart. An example of this in action can be seen in [this clip][A clip of @gba553 PMing ℵ~0~] of [@gba553](https://www.youtube.com/@Gba553) timing the final pair of tricky notes in [ℵ~0~][The ℵ~0~ chart].
 
@@ -37,7 +37,7 @@ The score is then calculated using a 2:1 PURE to FAR ratio (that is, every PURE
 
 A play with no LOST judgements is called a "full recall" (also known as a "full combo" in other games). Furthermore, a play with no FAR judgements is called a "pure memory" (also known as an "all perfect" in other games, and commonly abbreviated as a "PM"). Finally, a play where every note was hit perfectly (that is, one where everything is a MAX PURE) is called a "max pure memory" (commonly abbreviated as "MPM" by the community).
 
-{title="Lagrange's extras: The scoring formula" character="lagrange"}
+{title="Lagrange's extras: The scoring formula" character="lagrange" id="scoring-formula" }
 ::: char-aside
 Let's write the score formula in a nice, closed form!
 
@@ -48,7 +48,7 @@ $$`m + \left\lfloor (2(m + p) + f) \frac{10'000'000}{2(m + p + f + l)} \right\rf
 
 While full recalls and max-PMs are certainly celebrated by players and the larger community alike, PMs are usually the sweet spot between accurate play and fun which many Arcaea players strive for. This is usually the case because the game hardly rewards (if acknowledge at all) plays that are better than a PM (this will become obvious when we discuss the game's rating system).
 
-{title="Lagrange's extras: ζ-scoring" character="lagrange"}
+{title="Lagrange's extras: ζ-scoring" character="lagrange" id="ex-scoring"}
 ::: long-aside
 UWAAA, but what if we wanted MAX PURE notes to have a more major contribution to the score? For one, we could start by giving them their own place in the scoring ratio. What would a good ratio look like? A naive approach idea would be to keep the same rate of growth and go with a ratio of 4\:2\:1 for MAX PURE to PURE to FAR. Sadly, issues arise because this can lead to PMs possibly producing terrible scores — it's too big of a departure from the original formula. It turns out the aforementioned [Sound Voltex][] has already figured out a solution with their optional "EX-scoring" system, which uses a ratio of 5\:4\:2, thus awarding 1.25x the normal points for a MAX PURE.
 
diff --git a/content/echoes/games/index.dj b/content/echoes/games/index.dj
index 88ea19a..04e57af 100644
--- a/content/echoes/games/index.dj
+++ b/content/echoes/games/index.dj
@@ -68,10 +68,10 @@ tiers:
       </div>
     </a>
     <div class="tier-elements">
-      <a href="#arcaea"><img src="icons/arcaea.png" /></a>
-      <a href="#noita"><img src="icons/noita.png" /></a>
-      <a href="#rainworld"><img src="icons/rainworld.png" /></a>
-      <a href="#factorio"><img src="icons/factorio.png" /></a>
+      <a href="#arcaea"><img alt="ooo" src="icons/arcaea.png" /></a>
+      <a href="#noita"><img alt="ooo"  src="icons/noita.png" /></a>
+      <a href="#rainworld"><img alt="ooo"  src="icons/rainworld.png" /></a>
+      <a href="#factorio"><img alt="ooo"  src="icons/factorio.png" /></a>
     </div>
   </div>
   <div class="tier">
@@ -81,9 +81,9 @@ tiers:
       </div>
     </a>
     <div class="tier-elements">
-      <a href="#tunic"><img src="icons/tunic.ico" /></a>
-      <a href="#ultrakill"><img src="icons/ultrakill.png" /></a>
-      <a href="#talos"><img src="icons/thetalosprinciple.jpg" /></a>
+      <a href="#tunic"><img alt="ooo"  src="icons/tunic.ico" /></a>
+      <a href="#ultrakill"><img alt="ooo"  src="icons/ultrakill.png" /></a>
+      <a href="#talos"><img alt="ooo"  src="icons/thetalosprinciple.jpg" /></a>
     </div>
   </div>
   <div class="tier">
@@ -93,13 +93,13 @@ tiers:
       </div>
     </a>
     <div class="tier-elements">
-      <a href="#celeste"><img src="icons/celeste.png" /></a>
-      <a href="#downpour"><img src="icons/downpour.jpg" /></a>
-      <a href="#hollow-knight"><img src="icons/hollow-knight.png" /></a>
-      <a href="#slay-the-spire"><img src="icons/slaythespire.png" /></a>
-      <a href="#hades"><img src="icons/hades.ico" /></a>
-      <a href="#portal2"><img src="icons/portal2.png" /></a>
-      <a href="#baba"><img src="icons/babaisyou.png" /></a>
+      <a href="#celeste"><img alt="ooo"  src="icons/celeste.png" /></a>
+      <a href="#downpour"><img alt="ooo"  src="icons/downpour.jpg" /></a>
+      <a href="#hollow-knight"><img alt="ooo"  src="icons/hollow-knight.png" /></a>
+      <a href="#slay-the-spire"><img alt="ooo"  src="icons/slaythespire.png" /></a>
+      <a href="#hades"><img alt="ooo"  src="icons/hades.ico" /></a>
+      <a href="#portal2"><img alt="ooo"  src="icons/portal2.png" /></a>
+      <a href="#baba"><img alt="ooo"  src="icons/babaisyou.png" /></a>
     </div>
   </div>
   <div class="tier">
@@ -163,15 +163,27 @@ The developers have a (sometimes weekly) [blog](https://www.factorio.com/blog/)
 I highly recommend not to have your first experience with this game be a co-op session with someone who's played before. Even if the other person is trying their best to let you figure stuff out, they _will_ slip out details or tehniques, if for no other reason than practices feeling _wrong_ to someone who's seen "the light". That's not even mentioning the subset of players whose preferred playstyles involves placing down other people's blueprints. While there's nothing inherently wrong with that, it can take a lot away from one's initial sense of discovery with the game. I'm saying this because I initially bounced away from the game after trying to play with such a friend. In the end, it's up to you.
 
 { #arcaea }
-## Arcaea
+### Arcaea
 
 I love rhythm games, and in my humble opinion, [Arcaea](https://arcaea.lowiro.com/) is one of the best ones out there. I'm working on a huge article covering this game extensively, so for now this placeholder paragraph will have to do.
 
+{ #noita }
+### Noita
+
+{ #rainworld }
+### Rain world
+
 { #masterpiece }
 ## Masterpiece
 
 This tier contains games that are truly special, although haven't implanted themselves in my head for as long (nor as deeply) as the games in the previous tier.
 
+{ #talos }
+### The Talos Principle
+
+{ #tunic }
+### Tunic
+
 { #ultrakill }
 ### Ultrakill
 
@@ -180,3 +192,27 @@ Ultrakill is a game all about doing cool shit. There's so many instances where y
 After each level, the game rates you on three axes: kills, time, and style. Improving until you can chain together stylish combos in order to P-rank (i.e. perfect) a hard level is super fun. Ultrakill has some of my favourite boss fights in all of gaming, all to the tune of a soundtrack that goes incredibly hard.
 
 The game has some technical issues (it often forgets the resolution you configure in the settings after alt-tabbing around, but this seems to be an issue a lot of Unity games have, so I'll add it to the ever-growing list of justicications for me hating engines). The community seems to be a bit predisposed to spoiling people on gameplay elements, so if you want to go in blind, I recommend staying away from any kind of content related to the game.^
+
+{ #amazing }
+## Amazing
+
+{ #hollow-knight }
+### Hollow Knight
+
+{ #celeste }
+### Celeste
+
+{ #downpour }
+### Downpour
+
+{ #slay-the-spire }
+### Slay the Spire
+
+{ #hades }
+### Hades
+
+{ #portal2 }
+### Portal 2
+
+{ #baba }
+### Baba is you
diff --git a/content/echoes/index.dj b/content/echoes/index.dj
index 96ebdf5..e103e33 100644
--- a/content/echoes/index.dj
+++ b/content/echoes/index.dj
@@ -1,3 +1,9 @@
+{ role=config }
+``` =toml
+sitemap_changefreq = "weekly"
+sitemap_priority = 0.7
+```
+
 # Echoes
 
 > _"Remnants of the One who once dwelled within the Silver of the Sky. They now wander endlessly through the mists of the heavens, at times drawn to lost vessels who seek to hear them. Thence, thou with this knowledge are yet to attain the full knowledge of the Plan, but worry not — the World shall linger much longer still."_
diff --git a/flake.nix b/flake.nix
index f91b47c..88e3f91 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,13 +11,24 @@
         {
           devShells.default = pkgs.mkShell rec {
             nativeBuildInputs = with pkgs; [
+              # Rust tooling
               pkgs.cargo
               pkgs.rustc
               pkgs.clippy
               pkgs.rust-analyzer
               pkgs.rustfmt
+
+              # Dev tooling
               pkgs.imagemagick
               pkgs.http-server
+
+              # Build / test pipeline
+              pkgs.nodejs
+              pkgs.just
+              pkgs.terser
+              pkgs.libxml2
+              pkgs.validator-nu
+              pkgs.htmltest
             ];
 
             buildInputs = with pkgs; [ ];
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..a2b5abb
--- /dev/null
+++ b/justfile
@@ -0,0 +1,41 @@
+minify-sitemap:
+  xmllint --noblanks dist/sitemap.xml --output dist/sitemap.xml
+
+# {{{ Linting
+lint: lint-vnu lint-css lint-htmltest lint-htmlvalidate
+
+lint-htmltest:
+  htmltest -c tooling/htmltest.yml dist
+
+lint-htmlvalidate:
+  #!/usr/bin/env bash
+  shopt -s globstar
+  shopt -s extglob
+  npx --prefix tooling \
+    html-validate -c tooling/htmlvalidate.json dist/**/*.html
+
+lint-vnu:
+  #!/usr/bin/env bash
+  shopt -s globstar
+  shopt -s extglob
+
+  output=$(
+    vnu --also-check-svg --no-langdetect \
+      --stdout --exit-zero-always \
+      dist/**/*.{html,svg} 2>&1 \
+      | grep -v "Trailing slash on void elements"
+  )
+
+  if [ -n "$output" ]; then
+    echo "$output"
+    exit 1
+  else
+    echo "VNU checks passed succesfully"
+    exit 0
+  fi
+
+lint-css:
+  npx --prefix tooling stylelint dist/**/*.css \
+    --config ./tooling/stylelintrc.json \
+    --rd --rdd # All disables must come with an explanation and must be necessary
+# }}}
diff --git a/public/styles.css b/public/styles.css
index 013083e..f654577 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -5,22 +5,7 @@ body {
   padding: 1em;
 }
 
-.article-content {
-  max-width: 40em;
-  margin: auto;
-}
-
-.article-list {
-  max-width: 36em;
-  margin: auto;
-  list-style-type: none;
-
-  ul {
-    list-style-type: disc;
-  }
-}
-
-/* {{{ General element tweaks*/
+/* {{{ General element tweaks */
 blockquote {
   padding-left: 1.25rem;
   border-left: 3px solid;
@@ -49,7 +34,23 @@ h6 {
 math[display="block"] {
   margin: 1.5em 0;
 }
-/* }}}*/
+/* }}} */
+/* {{{ General article styling */
+.article-content {
+  max-width: 40em;
+  margin: auto;
+}
+
+.article-list {
+  max-width: 36em;
+  margin: auto;
+  list-style-type: none;
+
+  ul {
+    list-style-type: disc;
+  }
+}
+/* }}} */
 /* {{{ Asides */
 /* TODO: remove aside-header altogether */
 .aside-header {
@@ -86,13 +87,13 @@ math[display="block"] {
   }
 }
 
-/* {{{ Override marker*/
+/* {{{ Override marker */
 .aside > summary {
   &::marker {
     display: none;
   }
 
-  &:before {
+  &::before {
     content: "▶";
     font-size: 0.75rem;
     padding: 0 0.75rem 0 0.25rem;
@@ -100,11 +101,11 @@ math[display="block"] {
   }
 }
 
-.aside[open] > summary:before {
+.aside[open] > summary::before {
   content: "▼";
 }
-/* }}}*/
-/* }}}*/
+/* }}} */
+/* }}} */
 /* {{{ Tier lists */
 .tier-list {
   background: #444;
@@ -161,7 +162,7 @@ math[display="block"] {
     }
   }
 
-  /* {{{ Tier colors*/
+  /* {{{ Tier colors */
   .tier:nth-child(1) > .tier-heading {
     background: #b39ddb;
   }
@@ -202,9 +203,9 @@ math[display="block"] {
       #e2baff
     );
   }
-  /* }}}*/
+  /* }}} */
 }
-/* }}}*/
+/* }}} */
 
 /* {{{ Light theme */
 @media (prefers-color-scheme: light) {
@@ -217,11 +218,7 @@ math[display="block"] {
     padding: 1rem;
   }
 
-  /* {{{ Syntax highlighting*/
-  span.variable {
-    color: #4c4f69;
-  }
-
+  /* {{{ Syntax highlighting */
   span.attribute,
   span.constant {
     color: #fe640b;
@@ -307,6 +304,6 @@ math[display="block"] {
   span.variable-parameter {
     color: #e64553;
   }
-  /* }}}*/
+  /* }}} */
 }
 /* }}} */
diff --git a/src/generate.rs b/src/generate.rs
index 4079b8f..c3ba588 100644
--- a/src/generate.rs
+++ b/src/generate.rs
@@ -1,8 +1,8 @@
+use std::fmt::Write;
 use std::path::{Path, PathBuf};
 use std::process::Command;
 use std::str::FromStr;
 
-use crate::html::render_html;
 use crate::metadata::PageMetadata;
 use crate::template;
 use anyhow::{bail, Context};
@@ -13,13 +13,26 @@ pub fn copy_recursively(from: &Path, to: &Path) -> anyhow::Result<()> {
 	Ok(())
 }
 
-#[derive(Debug, Default)]
 pub struct Pages<'a> {
 	pages: Vec<PageMetadata<'a>>,
 	assets: Vec<PathBuf>,
+
+	content_root: PathBuf,
+	out_root: PathBuf,
+	base_url: &'a str,
 }
 
 impl<'a> Pages<'a> {
+	pub fn new(content_root: PathBuf, out_root: PathBuf) -> Self {
+		Self {
+			pages: Vec::new(),
+			assets: Vec::new(),
+			content_root,
+			out_root,
+			base_url: "http://localhost:3000",
+		}
+	}
+
 	// {{{ Collect simple pages
 	pub fn add_page(&mut self, path: &Path) -> anyhow::Result<()> {
 		let content_path = PathBuf::from_str("content")?.join(path);
@@ -63,9 +76,9 @@ impl<'a> Pages<'a> {
 	}
 	// }}}
 	// {{{ Generate
-	pub fn generate(&self, out_root: PathBuf) -> anyhow::Result<()> {
+	pub fn generate(&self) -> anyhow::Result<()> {
 		for page in &self.pages {
-			let out_dir = out_root.join(page.route.to_path());
+			let out_dir = self.out_root.join(page.route.to_path());
 			std::fs::create_dir_all(&out_dir)
 				.with_context(|| format!("Failed to generate {out_dir:?} directory"))?;
 
@@ -75,8 +88,13 @@ impl<'a> Pages<'a> {
 			page_renderer.feed(&mut out, |label, out| {
 				match label {
 					"content" => {
-						let events = jotdown::Parser::new(page.source);
-						render_html(page, &self.pages, events, out)?;
+						let mut w = crate::html::Writer::new(page, &self.pages, self.base_url);
+
+						for event in jotdown::Parser::new(page.source) {
+							w.render_event(&event, out)?;
+						}
+
+						w.render_epilogue(out)?;
 					}
 					_ => bail!("Unknown label {label} in page template"),
 				}
@@ -88,15 +106,62 @@ impl<'a> Pages<'a> {
 				.with_context(|| format!("Failed to write {out_dir:?} post"))?;
 		}
 
-		let content_root = PathBuf::from_str("content")?;
 		for path in &self.assets {
-			std::fs::create_dir_all(out_root.join(path).parent().unwrap())
+			std::fs::create_dir_all(self.out_root.join(path).parent().unwrap())
 				.with_context(|| format!("Failed to create parent dir for asset at {path:?}"))?;
 
-			std::fs::copy(content_root.join(path), out_root.join(path))
+			std::fs::copy(self.content_root.join(path), self.out_root.join(path))
 				.with_context(|| format!("Failed to copy asset at {path:?}"))?;
 		}
 
+		self.generate_sitemap()?;
+
+		Ok(())
+	}
+	// }}}
+	// {{{ Generate sitemap
+	fn generate_sitemap(&self) -> anyhow::Result<()> {
+		let mut out = String::new();
+
+		write!(
+			out,
+			r#"
+        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+			  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+      "#
+		)?;
+
+		for page in &self.pages {
+			if page.config.sitemap_exclude {
+				continue;
+			}
+
+			write!(
+				out,
+				"<url>
+          <loc>https://moonythm.dev/{}</loc>
+          <lastmod>{}</lastmod>
+        ",
+				page.route.to_path().to_str().unwrap(),
+				page.last_modified.to_rfc3339()
+			)?;
+
+			if let Some(priority) = page.config.sitemap_priority {
+				write!(out, "<priority>{priority}</priority>")?;
+			}
+
+			if let Some(changefreq) = &page.config.sitemap_changefreq {
+				write!(out, "<changefreq>{changefreq}</changefreq>")?;
+			}
+
+			write!(out, "</url>")?;
+		}
+
+		write!(out, "</urlset>")?;
+
+		std::fs::write(self.out_root.join("sitemap.xml"), out.trim())
+			.with_context(|| "Failed to write sitemap")?;
+
 		Ok(())
 	}
 	// }}}
diff --git a/src/html.rs b/src/html.rs
index 140885d..837bee8 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -27,28 +27,13 @@ use crate::metadata::PageRoute;
 use crate::template;
 use crate::template::TemplateRenderer;
 
-// {{{ Renderer
-/// Render djot content as HTML.
-pub fn render_html<'s>(
-	metadata: &'s PageMetadata,
-	pages: &'s [PageMetadata],
-	mut events: impl Iterator<Item = Event<'s>>,
-	out: &mut impl std::fmt::Write,
-) -> anyhow::Result<()> {
-	let mut w = Writer::new(Some(metadata), pages);
-	events.try_for_each(|e| w.render_event(&e, out))?;
-	w.render_epilogue(out)?;
-
-	Ok(())
-}
-// }}}
-
 pub struct Writer<'s> {
 	list_tightness: Vec<bool>,
 	states: Vec<State<'s>>,
 	footnotes: Footnotes<'s>,
-	metadata: Option<&'s PageMetadata<'s>>,
+	metadata: &'s PageMetadata<'s>,
 	pages: &'s [PageMetadata<'s>],
+	base_url: &'s str,
 }
 
 #[derive(Debug, Clone)]
@@ -66,13 +51,14 @@ enum State<'s> {
 }
 
 impl<'s> Writer<'s> {
-	pub fn new(metadata: Option<&'s PageMetadata>, pages: &'s [PageMetadata]) -> Self {
+	pub fn new(metadata: &'s PageMetadata, pages: &'s [PageMetadata], base_url: &'s str) -> Self {
 		Self {
 			list_tightness: Vec::new(),
 			states: Vec::new(),
 			footnotes: Footnotes::default(),
 			metadata,
 			pages,
+			base_url,
 		}
 	}
 
@@ -133,22 +119,21 @@ impl<'s> Writer<'s> {
 			Event::Start(c, attrs) => {
 				match &c {
 					// {{{ Section
-					Container::Section { id } => match self.metadata {
-						Some(meta) if meta.title.id == *id => {
-							if matches!(meta.route, PageRoute::Post(_)) {
+					Container::Section { id } => {
+						if self.metadata.title.id == *id {
+							if matches!(self.metadata.route, PageRoute::Post(_)) {
 								let mut renderer = template!("templates/post.html", out)?;
 
 								assert_eq!(renderer.current(), Some("attrs"));
-								write!(out, "{}", Attr("aria-labeledby", id))?;
+								write!(out, "{}", Attr("aria-labelledby", id))?;
 								renderer.next(out)?;
 
 								self.states.push(State::Article(renderer));
 							}
+						} else {
+							write!(out, "<section {}>", Attr("aria-labelledby", id))?;
 						}
-						_ => {
-							write!(out, "<section {}>", Attr("aria-labeledby", id))?;
-						}
-					},
+					}
 					// }}}
 					// {{{ Aside
 					Container::Div {
@@ -164,6 +149,13 @@ impl<'s> Writer<'s> {
 
 						while let Some(label) = renderer.current() {
 							match label {
+								"id" => {
+									let id = attrs.get_value("id").ok_or_else(|| {
+										anyhow!("Cannot find `id` attribute on `aside` element")
+									})?;
+
+									write_attribute(out, &id)?;
+								}
 								"character" => {
 									let character =
 										attrs.get_value("character").ok_or_else(|| {
@@ -304,7 +296,7 @@ impl<'s> Writer<'s> {
 											}
 										}
 										_ => {
-											if !fill_metadata_label(out, label, post)? {
+											if !Self::write_metadata_label(out, label, post)? {
 												bail!("Unknown label {label} in `post-summary` template");
 											};
 										}
@@ -343,7 +335,6 @@ impl<'s> Writer<'s> {
 					}
 					Container::Math { display } => {
 						self.states.push(State::Math(*display));
-						out.write_str("<math>")?;
 					}
 					Container::Image(_, _) => {
 						self.states.push(State::TextOnly);
@@ -417,12 +408,11 @@ impl<'s> Writer<'s> {
 						// Sanity check
 						assert!(matches!(self.states.last(), Some(State::Math(_))));
 						self.states.pop();
-						out.write_str(r#"</math>"#)?;
 					}
 					// }}}
 					// {{{ Section
-					Container::Section { id, .. } => match self.metadata {
-						Some(meta) if meta.title.id == *id => {
+					Container::Section { id, .. } => {
+						if self.metadata.title.id == *id {
 							if matches!(self.states.last(), Some(State::Article(_))) {
 								let Some(State::Article(renderer)) = self.states.pop() else {
 									unreachable!()
@@ -430,9 +420,10 @@ impl<'s> Writer<'s> {
 
 								renderer.finish(out)?;
 							}
+						} else {
+							out.write_str("</section>")?
 						}
-						_ => out.write_str("</section>")?,
-					},
+					}
 					// }}}
 					// {{{ Aside
 					Container::Div {
@@ -452,11 +443,10 @@ impl<'s> Writer<'s> {
 						// {{{ Article title
 						if let Some(State::Article(renderer)) = self.states.last_mut() {
 							if renderer.current() == Some("title") {
-								// SAFETY: we can never enter into the `article` state without having
-								// some metadata on hand.
-								let meta = self.metadata.unwrap();
 								while let Some(label) = renderer.next(out)? {
-									if !fill_metadata_label(out, label, meta)? {
+									if !Self::write_common_label(out, label, self.base_url)?
+										&& !Self::write_metadata_label(out, label, self.metadata)?
+									{
 										break;
 									}
 								}
@@ -658,7 +648,7 @@ impl<'s> Writer<'s> {
 			Event::Hardbreak => out.write_str("<br>")?,
 			Event::Softbreak => out.write_char('\n')?,
 			// }}}
-			Event::ThematicBreak(_) => out.write_str("<hr>")?,
+			Event::ThematicBreak(_) => out.write_str("<hr />")?,
 			Event::Escape | Event::Blankline | Event::Attributes(_) => {}
 		}
 
@@ -666,10 +656,10 @@ impl<'s> Writer<'s> {
 	}
 
 	// {{{ Render epilogue
-	fn render_epilogue(&mut self, out: &mut impl std::fmt::Write) -> anyhow::Result<()> {
+	pub fn render_epilogue(&mut self, out: &mut impl std::fmt::Write) -> anyhow::Result<()> {
 		if self.footnotes.reference_encountered() {
 			// TODO: rewrite this using a template
-			out.write_str("<section role=\"doc-endnotes\"><hr><ol>")?;
+			out.write_str("<section role=\"doc-endnotes\"><hr/><ol>")?;
 
 			while let Some((number, events)) = self.footnotes.next() {
 				write!(out, r#"<li id="fn{number}">"#)?;
@@ -694,6 +684,72 @@ impl<'s> Writer<'s> {
 		Ok(())
 	}
 	// }}}
+	// {{{ Fill in metadata labels
+	fn write_common_label(
+		out: &mut impl std::fmt::Write,
+		label: &str,
+		base_url: &str,
+	) -> anyhow::Result<bool> {
+		if label == "base_url" {
+			write!(out, "{}", base_url)?;
+		} else {
+			return Ok(false);
+		}
+
+		Ok(true)
+	}
+
+	fn write_metadata_label(
+		out: &mut impl std::fmt::Write,
+		label: &str,
+		meta: &PageMetadata,
+	) -> anyhow::Result<bool> {
+		if label == "posted_on" {
+			if let Some(d) = meta.config.created_at {
+				write!(out, "Posted on ")?;
+				write_datetime(out, &d)?;
+			} else {
+				write!(out, "Being conjured by ")?;
+			}
+		} else if label == "updated_on" {
+			write_datetime(out, &meta.last_modified)?;
+		} else if label == "word_count" {
+			let wc = meta.word_count;
+			if wc < 400 {
+				write!(out, "{}", wc)?;
+			} else if wc < 1000 {
+				write!(out, "{}", wc / 10 * 10)?;
+			} else if wc < 2000 {
+				write!(out, "{}", wc / 100 * 100)?;
+			} else {
+				write!(out, "{} thousand", wc / 1000)?;
+			}
+		} else if label == "reading_duration" {
+			let minutes = meta.word_count / 200;
+			if minutes == 0 {
+				let seconds = meta.word_count * 60 / 200;
+				write!(out, "very short {seconds} second")?;
+			} else if minutes < 10 {
+				write!(out, "short {minutes} minute")?;
+			} else if minutes < 20 {
+				write!(out, "somewhat short {minutes} minute")?;
+			} else if minutes < 30 {
+				write!(out, "somewhat long {minutes}")?;
+			} else if minutes < 60 {
+				write!(out, "long {minutes}")?;
+			} else {
+				let hours = minutes / 60;
+				let minutes = minutes % 60;
+				write!(out, "very long {hours} hour and {minutes} minute")?;
+			}
+		} else {
+			return Ok(false);
+		}
+
+		Ok(true)
+	}
+
+	// }}}
 }
 
 // {{{ HTMl escaper
@@ -812,55 +868,3 @@ impl<'s> Iterator for Footnotes<'s> {
 	}
 }
 // }}}
-// {{{ Fill in metadata labels
-fn fill_metadata_label(
-	out: &mut impl std::fmt::Write,
-	label: &str,
-	meta: &PageMetadata<'_>,
-) -> anyhow::Result<bool> {
-	if label == "posted_on" {
-		if let Some(d) = meta.config.created_at {
-			write!(out, "Posted on ")?;
-			write_datetime(out, &d)?;
-		} else {
-			write!(out, "Being conjured by ")?;
-		}
-	} else if label == "updated_on" {
-		write_datetime(out, &meta.last_modified)?;
-	} else if label == "word_count" {
-		let wc = meta.word_count;
-		if wc < 400 {
-			write!(out, "{}", wc)?;
-		} else if wc < 1000 {
-			write!(out, "{}", wc / 10 * 10)?;
-		} else if wc < 2000 {
-			write!(out, "{}", wc / 100 * 100)?;
-		} else {
-			write!(out, "{} thousand", wc / 1000)?;
-		}
-	} else if label == "reading_duration" {
-		let minutes = meta.word_count / 200;
-		if minutes == 0 {
-			let seconds = meta.word_count * 60 / 200;
-			write!(out, "very short {seconds} second")?;
-		} else if minutes < 10 {
-			write!(out, "short {minutes} minute")?;
-		} else if minutes < 20 {
-			write!(out, "somewhat short {minutes} minute")?;
-		} else if minutes < 30 {
-			write!(out, "somewhat long {minutes}")?;
-		} else if minutes < 60 {
-			write!(out, "long {minutes}")?;
-		} else {
-			let hours = minutes / 60;
-			let minutes = minutes % 60;
-			write!(out, "very long {hours} hour and {minutes} minute")?;
-		}
-	} else {
-		return Ok(false);
-	}
-
-	Ok(true)
-}
-
-// }}}
diff --git a/src/main.rs b/src/main.rs
index 4903629..b57c45d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,10 +18,14 @@ fn main() -> anyhow::Result<()> {
 
 	std::fs::create_dir(&dist_path).with_context(|| "Cannot create `dist` directory")?;
 
-	let mut page = generate::Pages::default();
+	let mut page = generate::Pages::new(
+		PathBuf::from_str("content").unwrap(),
+		PathBuf::from_str("dist").unwrap(),
+	);
+
 	page.add_dir(&PathBuf::from_str("")?)
 		.with_context(|| "Failed to collect directories")?;
-	page.generate(PathBuf::from_str("dist")?)
+	page.generate()
 		.with_context(|| "Failed to generate markup")?;
 
 	for p in std::fs::read_dir(public_path)? {
diff --git a/src/metadata.rs b/src/metadata.rs
index 576a283..48edd5d 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -13,30 +13,65 @@ use serde::Deserialize;
 #[derive(Deserialize, Debug, Default)]
 pub struct PageConfig {
 	pub created_at: Option<DateTime<FixedOffset>>,
+	pub sitemap_priority: Option<f32>,
+	pub sitemap_changefreq: Option<String>,
+
+	#[serde(default)]
+	pub sitemap_exclude: bool,
 }
 
 impl PageConfig {
-	/// Merge another config into the current one. Might error out on duplicate values.
-	fn merge(&mut self, other: PageConfig) -> anyhow::Result<()> {
-		match &self.created_at {
-			None => self.created_at = other.created_at,
-			Some(first_created) => {
-				if let Some(second_created) = other.created_at {
-					if second_created != *first_created {
-						bail!("Conflicting values for `created_at` page attribute: {first_created} and {second_created}");
+	// {{{ Merge a single property. Errors out on duplicate values.
+	fn merge_prop<A: PartialEq + std::fmt::Debug>(
+		label: &str,
+		first: Option<A>,
+		second: Option<A>,
+	) -> anyhow::Result<Option<A>> {
+		match first {
+			None => Ok(second),
+			Some(first) => {
+				if let Some(second) = second {
+					if second != first {
+						bail!(
+							"Conflicting values for `{label}` page attribute: {first:?} and {second:?}"
+						);
 					}
 				}
+
+				Ok(Some(first))
 			}
-		};
+		}
+	}
+	// }}}
+	// {{{ Config merging
+	/// Merge another config into the current one. Might error out on duplicate values.
+	fn merge(&mut self, other: PageConfig) -> anyhow::Result<()> {
+		self.created_at = Self::merge_prop("created_at", self.created_at, other.created_at)?;
+
+		self.sitemap_priority = Self::merge_prop(
+			"sitemap_priority",
+			self.sitemap_priority,
+			other.sitemap_priority,
+		)?;
+
+		self.sitemap_changefreq = Self::merge_prop(
+			"sitemap_changefreq",
+			self.sitemap_changefreq.take(),
+			other.sitemap_changefreq,
+		)?;
+
+		self.sitemap_exclude |= other.sitemap_exclude;
 
 		Ok(())
 	}
+	// }}}
 }
 // }}}
 // {{{ Routing
 #[derive(Debug)]
 pub enum PageRoute {
 	Home,
+	NotFound,
 	Posts,
 	Post(String),
 }
@@ -50,9 +85,11 @@ impl PageRoute {
 
 		let result = if first == OsStr::new("index.dj") {
 			Self::Home
+		} else if first == OsStr::new("404.dj") {
+			Self::NotFound
 		} else if first == OsStr::new("echoes") {
 			let Some(Component::Normal(second)) = path.components().nth(2) else {
-				bail!("Cannot convert path '{:?}' to page route", path);
+				bail!("Cannot convert path '{:?}' to echo route", path);
 			};
 			let mut slice = second.to_str().unwrap();
 			if slice.ends_with(".dj") {
@@ -75,7 +112,8 @@ impl PageRoute {
 	#[inline]
 	pub fn to_path(&self) -> PathBuf {
 		match self {
-			Self::Home => PathBuf::from_str(".").unwrap(),
+			Self::Home => PathBuf::from_str("").unwrap(),
+			Self::NotFound => PathBuf::from_str("404").unwrap(),
 			Self::Posts => PathBuf::from_str("echoes").unwrap(),
 			Self::Post(id) => PathBuf::from_str(&format!("echoes/{id}")).unwrap(),
 		}
diff --git a/src/templates/aside.html b/src/templates/aside.html
index e1ef5da..8267422 100644
--- a/src/templates/aside.html
+++ b/src/templates/aside.html
@@ -1 +1 @@
-<aside class="aside">{{content}}</aside>
+<div class="aside">{{content}}</div>
diff --git a/src/templates/character-aside.html b/src/templates/character-aside.html
index 65bf2b7..511f7a5 100644
--- a/src/templates/character-aside.html
+++ b/src/templates/character-aside.html
@@ -1,10 +1,10 @@
-<aside class="aside">
+<aside class="aside" aria-labelledby="{{id}}">
   <div class="aside-header">
     <img
       alt="{{character}}"
       src="/assets/icons/characters/{{character}}.webp"
     />
-    <h3>{{title}}</h3>
+    <h3 id="{{id}}">{{title}}</h3>
   </div>
 
   {{content}}
diff --git a/src/templates/long-aside.html b/src/templates/long-aside.html
index 23d7df7..8f0bb40 100644
--- a/src/templates/long-aside.html
+++ b/src/templates/long-aside.html
@@ -1,11 +1,11 @@
-<aside>
+<aside aria-labelledby="{{id}}">
   <details class="aside aside-long">
     <summary class="aside-header">
       <img
         alt="{{character}}"
         src="/assets/icons/characters/{{character}}.webp"
       />
-      <h3>{{title}}</h3>
+      <h3 id="{{id}}">{{title}}</h3>
     </summary>
     {{content}}
   </details>
diff --git a/src/templates/page.html b/src/templates/page.html
index 433c545..c55dfc0 100644
--- a/src/templates/page.html
+++ b/src/templates/page.html
@@ -29,7 +29,7 @@
     <header>
       <nav>
         <a href="/"><code>~</code></a> /
-        <a href="/echoes"><code>echoes</code></a>
+        <a href="/echoes/"><code>echoes</code></a>
       </nav>
     </header>
     <main>{{content}}</main>
diff --git a/src/templates/post-summary.html b/src/templates/post-summary.html
index b40fc11..4a2fb02 100644
--- a/src/templates/post-summary.html
+++ b/src/templates/post-summary.html
@@ -1,7 +1,7 @@
 <li>
   <article>
     <h2>
-      <a href="/echoes/{{id}}" rel="bookmark">{{title}}</a>
+      <a href="/echoes/{{id}}/" rel="bookmark">{{title}}</a>
     </h2>
 
     <ul>
diff --git a/src/templates/post.html b/src/templates/post.html
index 54cf6c0..2817a7b 100644
--- a/src/templates/post.html
+++ b/src/templates/post.html
@@ -5,7 +5,7 @@
     <ul>
       <li>
         {{posted_on}} by <a href="about:blank">prescientmoon</a> on their
-        <a href="https://moonythm.dev">website</a>
+        <a href="{{base_url}}">website</a>
       </li>
       <li>
         Last updated on {{updated_on}}. <a href="about:blank">Source</a> |
@@ -17,5 +17,5 @@
     <hr />
   </header>
 
-  <section class="article-content">{{content}}</section>
+  <div class="article-content">{{content}}</div>
 </article>
diff --git a/tooling/htmltest.yml b/tooling/htmltest.yml
new file mode 100644
index 0000000..4048dea
--- /dev/null
+++ b/tooling/htmltest.yml
@@ -0,0 +1,10 @@
+CacheExpires: "168h" # one week
+CheckFavicon: true
+EnforceHTML5: true
+EnforceHTTPS: true
+ExternalTimeout: 20
+HTTPConcurrencyLimit: 128
+IgnoreAltEmpty: true
+IgnoreHTTPS:
+  - "http://localhost:"
+IgnoreURLs:
diff --git a/tooling/htmlvalidate.json b/tooling/htmlvalidate.json
new file mode 100644
index 0000000..aecff00
--- /dev/null
+++ b/tooling/htmlvalidate.json
@@ -0,0 +1,9 @@
+{
+  "extends": [
+    "html-validate:recommended",
+    "html-validate:standard",
+    "html-validate:a11y",
+    "html-validate:document",
+    "html-validate:prettier"
+  ]
+}
diff --git a/tooling/package-lock.json b/tooling/package-lock.json
new file mode 100644
index 0000000..82da090
--- /dev/null
+++ b/tooling/package-lock.json
@@ -0,0 +1,1857 @@
+{
+  "name": "tooling",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "html-validate": "^8.24.2",
+        "stylelint": "^16.10.0",
+        "stylelint-config-standard": "^36.0.1"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@csstools/css-parser-algorithms": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
+      "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@csstools/css-tokenizer": "^3.0.3"
+      }
+    },
+    "node_modules/@csstools/css-tokenizer": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
+      "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@csstools/media-query-list-parser": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz",
+      "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^3.0.1",
+        "@csstools/css-tokenizer": "^3.0.1"
+      }
+    },
+    "node_modules/@csstools/selector-specificity": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-4.0.0.tgz",
+      "integrity": "sha512-189nelqtPd8++phaHNwYovKZI0FOzH1vQEE3QhHHkNIGrg5fSs9CbYP3RvfEH5geztnIA9Jwq91wyOIwAW5JIQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "postcss-selector-parser": "^6.1.0"
+      }
+    },
+    "node_modules/@dual-bundle/import-meta-resolve": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
+      "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/@html-validate/stylish": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-4.2.0.tgz",
+      "integrity": "sha512-Nl8HCv0hGRSLQ+n1OD4Hk3a+Urwk9HH0vQkAzzCarT4KlA7bRl+6xEiS5PZVwOmjtC7XiH/oNe3as9Fxcr2A1w==",
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@sidvind/better-ajv-errors": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@sidvind/better-ajv-errors/-/better-ajv-errors-3.0.1.tgz",
+      "integrity": "sha512-++1mEYIeozfnwWI9P1ECvOPoacy+CgDASrmGvXPMCcqgx0YUzB01vZ78uHdQ443V6sTY+e9MzHqmN9DOls02aw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "kleur": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 16.14"
+      },
+      "peerDependencies": {
+        "ajv": "^6.12.3 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
+    "node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
+      "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/brace-expansion/node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/colord": {
+      "version": "2.9.3",
+      "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+      "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+      "license": "MIT"
+    },
+    "node_modules/cosmiconfig": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+      "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+      "license": "MIT",
+      "dependencies": {
+        "env-paths": "^2.2.1",
+        "import-fresh": "^3.3.0",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/d-fischer"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.9.5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cross-spawn/node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-functions-list": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz",
+      "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12 || >=16"
+      }
+    },
+    "node_modules/css-tree": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.1.tgz",
+      "integrity": "sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "mdn-data": "2.12.1",
+        "source-map-js": "^1.0.1"
+      },
+      "engines": {
+        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deepmerge": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-uri": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
+      "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fastest-levenshtein": {
+      "version": "1.0.16",
+      "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+      "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.9.1"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz",
+      "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==",
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz",
+      "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==",
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.3.1",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+      "license": "ISC"
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+      "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/global-modules": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+      "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+      "license": "MIT",
+      "dependencies": {
+        "global-prefix": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/global-prefix": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+      "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+      "license": "MIT",
+      "dependencies": {
+        "ini": "^1.3.5",
+        "kind-of": "^6.0.2",
+        "which": "^1.3.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "license": "MIT",
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globjoin": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+      "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/html-tags": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
+      "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/html-validate": {
+      "version": "8.24.2",
+      "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.24.2.tgz",
+      "integrity": "sha512-PpZLlH3gpOSBLRBID4QovOyZ97Ka5dCIhHMPjUxQlMGqk5pc+mgG5enCXbC8ngdjG0jpkTvM8AMhnPv1EiU9Pw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/html-validate"
+        }
+      ],
+      "license": "MIT",
+      "workspaces": [
+        "docs",
+        "tests/vitest"
+      ],
+      "dependencies": {
+        "@html-validate/stylish": "^4.1.0",
+        "@sidvind/better-ajv-errors": "3.0.1",
+        "ajv": "^8.0.0",
+        "deepmerge": "4.3.1",
+        "glob": "^10.0.0",
+        "ignore": "5.3.2",
+        "kleur": "^4.1.0",
+        "minimist": "^1.2.0",
+        "prompts": "^2.0.0",
+        "semver": "^7.0.0"
+      },
+      "bin": {
+        "html-validate": "bin/html-validate.js"
+      },
+      "engines": {
+        "node": ">= 16.14"
+      },
+      "peerDependencies": {
+        "jest": "^27.1 || ^28.1.3 || ^29.0.3",
+        "jest-diff": "^27.1 || ^28.1.3 || ^29.0.3",
+        "jest-snapshot": "^27.1 || ^28.1.3 || ^29.0.3",
+        "vitest": "^0.34.0 || ^1.0.0 || ^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "jest": {
+          "optional": true
+        },
+        "jest-diff": {
+          "optional": true
+        },
+        "jest-snapshot": {
+          "optional": true
+        },
+        "vitest": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/import-fresh/node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "license": "ISC"
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "license": "MIT"
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "license": "MIT"
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/known-css-properties": {
+      "version": "0.34.0",
+      "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz",
+      "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==",
+      "license": "MIT"
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.truncate": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+      "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/mathml-tag-names": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
+      "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/mdn-data": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.1.tgz",
+      "integrity": "sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==",
+      "license": "CC0-1.0"
+    },
+    "node_modules/meow": {
+      "version": "13.2.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+      "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.47",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+      "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-resolve-nested-selector": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz",
+      "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==",
+      "license": "MIT"
+    },
+    "node_modules/postcss-safe-parser": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+      "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.31"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "license": "MIT"
+    },
+    "node_modules/prompts": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+      "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^3.0.3",
+        "sisteransi": "^1.0.5"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/prompts/node_modules/kleur": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+      "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/sisteransi": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+      "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+      "license": "MIT"
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/slice-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/stylelint": {
+      "version": "16.10.0",
+      "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.10.0.tgz",
+      "integrity": "sha512-z/8X2rZ52dt2c0stVwI9QL2AFJhLhbPkyfpDFcizs200V/g7v+UYY6SNcB9hKOLcDDX/yGLDsY/pX08sLkz9xQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/stylelint"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/stylelint"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/css-parser-algorithms": "^3.0.1",
+        "@csstools/css-tokenizer": "^3.0.1",
+        "@csstools/media-query-list-parser": "^3.0.1",
+        "@csstools/selector-specificity": "^4.0.0",
+        "@dual-bundle/import-meta-resolve": "^4.1.0",
+        "balanced-match": "^2.0.0",
+        "colord": "^2.9.3",
+        "cosmiconfig": "^9.0.0",
+        "css-functions-list": "^3.2.3",
+        "css-tree": "^3.0.0",
+        "debug": "^4.3.7",
+        "fast-glob": "^3.3.2",
+        "fastest-levenshtein": "^1.0.16",
+        "file-entry-cache": "^9.1.0",
+        "global-modules": "^2.0.0",
+        "globby": "^11.1.0",
+        "globjoin": "^0.1.4",
+        "html-tags": "^3.3.1",
+        "ignore": "^6.0.2",
+        "imurmurhash": "^0.1.4",
+        "is-plain-object": "^5.0.0",
+        "known-css-properties": "^0.34.0",
+        "mathml-tag-names": "^2.1.3",
+        "meow": "^13.2.0",
+        "micromatch": "^4.0.8",
+        "normalize-path": "^3.0.0",
+        "picocolors": "^1.0.1",
+        "postcss": "^8.4.47",
+        "postcss-resolve-nested-selector": "^0.1.6",
+        "postcss-safe-parser": "^7.0.1",
+        "postcss-selector-parser": "^6.1.2",
+        "postcss-value-parser": "^4.2.0",
+        "resolve-from": "^5.0.0",
+        "string-width": "^4.2.3",
+        "supports-hyperlinks": "^3.1.0",
+        "svg-tags": "^1.0.0",
+        "table": "^6.8.2",
+        "write-file-atomic": "^5.0.1"
+      },
+      "bin": {
+        "stylelint": "bin/stylelint.mjs"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      }
+    },
+    "node_modules/stylelint-config-recommended": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz",
+      "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/stylelint"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/stylelint"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "stylelint": "^16.1.0"
+      }
+    },
+    "node_modules/stylelint-config-standard": {
+      "version": "36.0.1",
+      "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz",
+      "integrity": "sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/stylelint"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/stylelint"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "stylelint-config-recommended": "^14.0.1"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "stylelint": "^16.1.0"
+      }
+    },
+    "node_modules/stylelint/node_modules/ignore": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz",
+      "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-hyperlinks": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz",
+      "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0",
+        "supports-color": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/svg-tags": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+      "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA=="
+    },
+    "node_modules/table": {
+      "version": "6.8.2",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz",
+      "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "ajv": "^8.0.1",
+        "lodash.truncate": "^4.4.2",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/write-file-atomic": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+      "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+      "license": "ISC",
+      "dependencies": {
+        "imurmurhash": "^0.1.4",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    }
+  }
+}
diff --git a/tooling/package.json b/tooling/package.json
new file mode 100644
index 0000000..1bd346b
--- /dev/null
+++ b/tooling/package.json
@@ -0,0 +1,7 @@
+{
+  "dependencies": {
+    "html-validate": "^8.24.2",
+    "stylelint": "^16.10.0",
+    "stylelint-config-standard": "^36.0.1"
+  }
+}
diff --git a/tooling/stylelintrc.json b/tooling/stylelintrc.json
new file mode 100644
index 0000000..dd7a12d
--- /dev/null
+++ b/tooling/stylelintrc.json
@@ -0,0 +1,8 @@
+{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "comment-empty-line-before": null,
+    "no-descending-specificity": null,
+    "declaration-empty-line-before": null
+  }
+}