diff --git a/package-lock.json b/package-lock.json
index 0f83775..4e24956 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,11 +12,14 @@
         "@databases/pg": "^5.4.1",
         "@fastify/cookie": "^9.0.4",
         "@fastify/static": "^6.10.2",
-        "@firebox/components": "^0.1.5",
+        "@firebox/components": "^0.1.7",
         "@firebox/tsutil": "^0.1.2",
         "@sinclair/typebox": "^0.31.5",
+        "@types/katex": "^0.16.7",
         "dotenv": "^16.3.1",
-        "fastify": "^4.22.0"
+        "fastify": "^4.22.0",
+        "katex": "^0.16.10",
+        "tsx": "^4.7.2"
       },
       "devDependencies": {
         "@databases/pg-migrations": "^5.0.2",
@@ -558,14 +561,28 @@
       "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
       "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
     },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
-      "integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -575,13 +592,12 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
-      "integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -591,13 +607,12 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
-      "integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -607,13 +622,12 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
-      "integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -623,13 +637,12 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
-      "integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -639,13 +652,12 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
-      "integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -655,13 +667,12 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
-      "integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -671,13 +682,12 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
-      "integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -687,13 +697,12 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
-      "integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -703,13 +712,12 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
-      "integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -719,13 +727,12 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
-      "integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
       "cpu": [
         "loong64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -735,13 +742,12 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
-      "integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
       "cpu": [
         "mips64el"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -751,13 +757,12 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
-      "integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -767,13 +772,12 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
-      "integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -783,13 +787,12 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
-      "integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
       "cpu": [
         "s390x"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -799,13 +802,12 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
-      "integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -815,13 +817,12 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
-      "integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "netbsd"
@@ -831,13 +832,12 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
-      "integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "openbsd"
@@ -847,13 +847,12 @@
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
-      "integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "sunos"
@@ -863,13 +862,12 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
-      "integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -879,13 +877,12 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
-      "integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -895,13 +892,12 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
-      "integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1057,9 +1053,9 @@
       }
     },
     "node_modules/@firebox/components": {
-      "version": "0.1.5",
-      "resolved": "https://nodepack.playbox.link/@firebox/components/-/components-0.1.5.tgz",
-      "integrity": "sha512-oDGEvUD+1TTpLATUGAhkXNr/ygNaFgCEVrBe0zUP+U5Dz77rMU1KxnfkYscKBx4ZpGThxLNaOmLn5BN7ZsJUaQ==",
+      "version": "0.1.7",
+      "resolved": "https://nodepack.playbox.link/@firebox/components/-/components-0.1.7.tgz",
+      "integrity": "sha512-DR6G7JCKPwfZK5LFpUndghCsflukY7TahDV/BuaODjfyuobqdQSKC8BT/AtGNWbX8H5aA/m+5Ck3cFh4Aujfhw==",
       "dependencies": {
         "@emotion/css": "^11.11.2"
       },
@@ -1172,6 +1168,11 @@
       "resolved": "https://registry.npmjs.org/@types/cuid/-/cuid-1.3.1.tgz",
       "integrity": "sha512-LwQOxZtpN3aEGElEicpHx1I6exi+mLBecAdLMWNRjGaYByD2CqGjSH1oVEQGeNSqgYBhLC1pIJQMDgcpxk0t8Q=="
     },
+    "node_modules/@types/katex": {
+      "version": "0.16.7",
+      "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
+      "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="
+    },
     "node_modules/@types/node": {
       "version": "20.5.6",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz",
@@ -1866,10 +1867,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
-      "integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
+      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
@@ -1878,28 +1878,29 @@
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.19.2",
-        "@esbuild/android-arm64": "0.19.2",
-        "@esbuild/android-x64": "0.19.2",
-        "@esbuild/darwin-arm64": "0.19.2",
-        "@esbuild/darwin-x64": "0.19.2",
-        "@esbuild/freebsd-arm64": "0.19.2",
-        "@esbuild/freebsd-x64": "0.19.2",
-        "@esbuild/linux-arm": "0.19.2",
-        "@esbuild/linux-arm64": "0.19.2",
-        "@esbuild/linux-ia32": "0.19.2",
-        "@esbuild/linux-loong64": "0.19.2",
-        "@esbuild/linux-mips64el": "0.19.2",
-        "@esbuild/linux-ppc64": "0.19.2",
-        "@esbuild/linux-riscv64": "0.19.2",
-        "@esbuild/linux-s390x": "0.19.2",
-        "@esbuild/linux-x64": "0.19.2",
-        "@esbuild/netbsd-x64": "0.19.2",
-        "@esbuild/openbsd-x64": "0.19.2",
-        "@esbuild/sunos-x64": "0.19.2",
-        "@esbuild/win32-arm64": "0.19.2",
-        "@esbuild/win32-ia32": "0.19.2",
-        "@esbuild/win32-x64": "0.19.2"
+        "@esbuild/aix-ppc64": "0.19.12",
+        "@esbuild/android-arm": "0.19.12",
+        "@esbuild/android-arm64": "0.19.12",
+        "@esbuild/android-x64": "0.19.12",
+        "@esbuild/darwin-arm64": "0.19.12",
+        "@esbuild/darwin-x64": "0.19.12",
+        "@esbuild/freebsd-arm64": "0.19.12",
+        "@esbuild/freebsd-x64": "0.19.12",
+        "@esbuild/linux-arm": "0.19.12",
+        "@esbuild/linux-arm64": "0.19.12",
+        "@esbuild/linux-ia32": "0.19.12",
+        "@esbuild/linux-loong64": "0.19.12",
+        "@esbuild/linux-mips64el": "0.19.12",
+        "@esbuild/linux-ppc64": "0.19.12",
+        "@esbuild/linux-riscv64": "0.19.12",
+        "@esbuild/linux-s390x": "0.19.12",
+        "@esbuild/linux-x64": "0.19.12",
+        "@esbuild/netbsd-x64": "0.19.12",
+        "@esbuild/openbsd-x64": "0.19.12",
+        "@esbuild/sunos-x64": "0.19.12",
+        "@esbuild/win32-arm64": "0.19.12",
+        "@esbuild/win32-ia32": "0.19.12",
+        "@esbuild/win32-x64": "0.19.12"
       }
     },
     "node_modules/escape-html": {
@@ -2114,7 +2115,6 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "os": [
@@ -2158,6 +2158,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/get-tsconfig": {
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+      "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
     "node_modules/glob": {
       "version": "7.1.6",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -2778,6 +2789,29 @@
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
     },
+    "node_modules/katex": {
+      "version": "0.16.10",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
+      "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
+      "funding": [
+        "https://opencollective.com/katex",
+        "https://github.com/sponsors/katex"
+      ],
+      "dependencies": {
+        "commander": "^8.3.0"
+      },
+      "bin": {
+        "katex": "cli.js"
+      }
+    },
+    "node_modules/katex/node_modules/commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/light-my-request": {
       "version": "5.10.0",
       "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.10.0.tgz",
@@ -3711,6 +3745,14 @@
         "node": ">=4"
       }
     },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
     "node_modules/restore-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -4132,6 +4174,24 @@
       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
       "dev": true
     },
+    "node_modules/tsx": {
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.2.tgz",
+      "integrity": "sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==",
+      "dependencies": {
+        "esbuild": "~0.19.10",
+        "get-tsconfig": "^4.7.2"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
     "node_modules/type-fest": {
       "version": "0.21.3",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
@@ -4780,158 +4840,142 @@
       "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
       "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
     },
+    "@esbuild/aix-ppc64": {
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+      "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+      "optional": true
+    },
     "@esbuild/android-arm": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
-      "integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+      "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
       "optional": true
     },
     "@esbuild/android-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
-      "integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+      "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
       "optional": true
     },
     "@esbuild/android-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
-      "integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+      "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
       "optional": true
     },
     "@esbuild/darwin-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
-      "integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+      "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
       "optional": true
     },
     "@esbuild/darwin-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
-      "integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+      "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
       "optional": true
     },
     "@esbuild/freebsd-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
-      "integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+      "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
       "optional": true
     },
     "@esbuild/freebsd-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
-      "integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+      "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
       "optional": true
     },
     "@esbuild/linux-arm": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
-      "integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+      "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
       "optional": true
     },
     "@esbuild/linux-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
-      "integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+      "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
       "optional": true
     },
     "@esbuild/linux-ia32": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
-      "integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+      "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
       "optional": true
     },
     "@esbuild/linux-loong64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
-      "integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+      "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
       "optional": true
     },
     "@esbuild/linux-mips64el": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
-      "integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+      "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
       "optional": true
     },
     "@esbuild/linux-ppc64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
-      "integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+      "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
       "optional": true
     },
     "@esbuild/linux-riscv64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
-      "integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+      "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
       "optional": true
     },
     "@esbuild/linux-s390x": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
-      "integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+      "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
       "optional": true
     },
     "@esbuild/linux-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
-      "integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+      "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
       "optional": true
     },
     "@esbuild/netbsd-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
-      "integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
       "optional": true
     },
     "@esbuild/openbsd-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
-      "integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+      "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
       "optional": true
     },
     "@esbuild/sunos-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
-      "integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+      "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
       "optional": true
     },
     "@esbuild/win32-arm64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
-      "integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+      "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
       "optional": true
     },
     "@esbuild/win32-ia32": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
-      "integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+      "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
       "optional": true
     },
     "@esbuild/win32-x64": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
-      "integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+      "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
       "optional": true
     },
     "@fastify/accept-negotiator": {
@@ -5054,9 +5098,9 @@
       }
     },
     "@firebox/components": {
-      "version": "0.1.5",
-      "resolved": "https://nodepack.playbox.link/@firebox/components/-/components-0.1.5.tgz",
-      "integrity": "sha512-oDGEvUD+1TTpLATUGAhkXNr/ygNaFgCEVrBe0zUP+U5Dz77rMU1KxnfkYscKBx4ZpGThxLNaOmLn5BN7ZsJUaQ==",
+      "version": "0.1.7",
+      "resolved": "https://nodepack.playbox.link/@firebox/components/-/components-0.1.7.tgz",
+      "integrity": "sha512-DR6G7JCKPwfZK5LFpUndghCsflukY7TahDV/BuaODjfyuobqdQSKC8BT/AtGNWbX8H5aA/m+5Ck3cFh4Aujfhw==",
       "requires": {
         "@emotion/css": "^11.11.2"
       }
@@ -5150,6 +5194,11 @@
       "resolved": "https://registry.npmjs.org/@types/cuid/-/cuid-1.3.1.tgz",
       "integrity": "sha512-LwQOxZtpN3aEGElEicpHx1I6exi+mLBecAdLMWNRjGaYByD2CqGjSH1oVEQGeNSqgYBhLC1pIJQMDgcpxk0t8Q=="
     },
+    "@types/katex": {
+      "version": "0.16.7",
+      "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz",
+      "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="
+    },
     "@types/node": {
       "version": "20.5.6",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz",
@@ -5665,33 +5714,33 @@
       }
     },
     "esbuild": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
-      "integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
-      "dev": true,
+      "version": "0.19.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
+      "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
       "requires": {
-        "@esbuild/android-arm": "0.19.2",
-        "@esbuild/android-arm64": "0.19.2",
-        "@esbuild/android-x64": "0.19.2",
-        "@esbuild/darwin-arm64": "0.19.2",
-        "@esbuild/darwin-x64": "0.19.2",
-        "@esbuild/freebsd-arm64": "0.19.2",
-        "@esbuild/freebsd-x64": "0.19.2",
-        "@esbuild/linux-arm": "0.19.2",
-        "@esbuild/linux-arm64": "0.19.2",
-        "@esbuild/linux-ia32": "0.19.2",
-        "@esbuild/linux-loong64": "0.19.2",
-        "@esbuild/linux-mips64el": "0.19.2",
-        "@esbuild/linux-ppc64": "0.19.2",
-        "@esbuild/linux-riscv64": "0.19.2",
-        "@esbuild/linux-s390x": "0.19.2",
-        "@esbuild/linux-x64": "0.19.2",
-        "@esbuild/netbsd-x64": "0.19.2",
-        "@esbuild/openbsd-x64": "0.19.2",
-        "@esbuild/sunos-x64": "0.19.2",
-        "@esbuild/win32-arm64": "0.19.2",
-        "@esbuild/win32-ia32": "0.19.2",
-        "@esbuild/win32-x64": "0.19.2"
+        "@esbuild/aix-ppc64": "0.19.12",
+        "@esbuild/android-arm": "0.19.12",
+        "@esbuild/android-arm64": "0.19.12",
+        "@esbuild/android-x64": "0.19.12",
+        "@esbuild/darwin-arm64": "0.19.12",
+        "@esbuild/darwin-x64": "0.19.12",
+        "@esbuild/freebsd-arm64": "0.19.12",
+        "@esbuild/freebsd-x64": "0.19.12",
+        "@esbuild/linux-arm": "0.19.12",
+        "@esbuild/linux-arm64": "0.19.12",
+        "@esbuild/linux-ia32": "0.19.12",
+        "@esbuild/linux-loong64": "0.19.12",
+        "@esbuild/linux-mips64el": "0.19.12",
+        "@esbuild/linux-ppc64": "0.19.12",
+        "@esbuild/linux-riscv64": "0.19.12",
+        "@esbuild/linux-s390x": "0.19.12",
+        "@esbuild/linux-x64": "0.19.12",
+        "@esbuild/netbsd-x64": "0.19.12",
+        "@esbuild/openbsd-x64": "0.19.12",
+        "@esbuild/sunos-x64": "0.19.12",
+        "@esbuild/win32-arm64": "0.19.12",
+        "@esbuild/win32-ia32": "0.19.12",
+        "@esbuild/win32-x64": "0.19.12"
       }
     },
     "escape-html": {
@@ -5872,7 +5921,6 @@
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
       "optional": true
     },
     "function-bind": {
@@ -5903,6 +5951,14 @@
         "has-symbols": "^1.0.3"
       }
     },
+    "get-tsconfig": {
+      "version": "4.7.3",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+      "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
+      "requires": {
+        "resolve-pkg-maps": "^1.0.0"
+      }
+    },
     "glob": {
       "version": "7.1.6",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -6337,6 +6393,21 @@
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
     },
+    "katex": {
+      "version": "0.16.10",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
+      "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
+      "requires": {
+        "commander": "^8.3.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "8.3.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+          "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
+        }
+      }
+    },
     "light-my-request": {
       "version": "5.10.0",
       "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.10.0.tgz",
@@ -7013,6 +7084,11 @@
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
     },
+    "resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
+    },
     "restore-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@@ -7318,6 +7394,16 @@
       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
       "dev": true
     },
+    "tsx": {
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.2.tgz",
+      "integrity": "sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==",
+      "requires": {
+        "esbuild": "~0.19.10",
+        "fsevents": "~2.3.3",
+        "get-tsconfig": "^4.7.2"
+      }
+    },
     "type-fest": {
       "version": "0.21.3",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
diff --git a/package.json b/package.json
index 45442c6..01f211c 100644
--- a/package.json
+++ b/package.json
@@ -6,15 +6,14 @@
   "main": "index.js",
   "scripts": {
     "dev-docker": "docker compose --profile dev up -d",
-    "dev-server": "echo \"starting server\" && npm run dev-ts ./src/server/index.ts",
-    "dev-ts": "nodemon --require 'dotenv/config'",
-    "dev-watch-client": "ts-node ./scripts/watch.ts",
+    "dev-server": "echo \"starting server\" && npm run withenv ./src/server/index.ts",
+    "dev-watch-client": "npm run withenv ./scripts/watch.ts",
     "dev-migrate": "source ./.env && pg-migrations apply --directory ./src/database/migrations",
     "prod-migrate": "pg-migrations apply --directory ./src/database/migrations",
-    "prod-build-client": "ts-node ./scripts/build.ts",
+    "prod-build-client": "npm run withenv ./scripts/build.ts",
     "prod-docker": "docker compose --profile prod up -d",
-    "prod-start": "echo \"building frontend\" && npm run prod-build-client && echo \"running migrations\" && npm run prod-migrate && echo \"starting server\" && npm run prod-ts ./src/server/index.ts",
-    "prod-ts": "ts-node --require 'dotenv/config'",
+    "prod-start": "echo \"building frontend\" && npm run prod-build-client && echo \"${DATABASE_URL}\" && echo \"running migrations\" && npm run prod-migrate && echo \"starting server\" && npm run withenv ./src/server/index.ts",
+    "withenv": "tsx ./scripts/run-with-env.ts",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "repository": {
@@ -30,8 +29,11 @@
     "@firebox/components": "^0.1.7",
     "@firebox/tsutil": "^0.1.2",
     "@sinclair/typebox": "^0.31.5",
+    "@types/katex": "^0.16.7",
     "dotenv": "^16.3.1",
-    "fastify": "^4.22.0"
+    "fastify": "^4.22.0",
+    "katex": "^0.16.10",
+    "tsx": "^4.7.2"
   },
   "devDependencies": {
     "@databases/pg-migrations": "^5.0.2",
diff --git a/scripts/run-with-env.ts b/scripts/run-with-env.ts
new file mode 100644
index 0000000..3e5216a
--- /dev/null
+++ b/scripts/run-with-env.ts
@@ -0,0 +1,11 @@
+import dotenv from 'dotenv';
+import * as url from 'url';
+import * as path from 'path';
+const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
+const dotenvPath = path.join(__dirname, "..", ".env");
+dotenv.config({
+	path: dotenvPath,
+});
+if (process.argv[2]) {
+	import(path.join(process.cwd(), process.argv[2]));
+}
\ No newline at end of file
diff --git a/src/client/app.tsx b/src/client/app.tsx
index 7d294f3..85e6f82 100644
--- a/src/client/app.tsx
+++ b/src/client/app.tsx
@@ -1,33 +1,27 @@
 import { css } from "@emotion/css";
 import { Center, Cover, Stack } from "@firebox/components";
+import { MathuscriptPlayer } from "./player/Player";
+import { Katex } from "./player/MathText";
+
+const script = String.raw`
+
+bridget (happy) "Hi, friends!"
+
+board "Given $f: \mathbb{Q} \to \mathbb{R}$ and $x \in \mathbb{Q}$, there is a unique $y \in \mathbb{N}$ such that $f(x)=y$"
+
+axelle (happy) "Wow, did you know that $a^2+b^2=c^2$?"
+
+`
 
 const App = (props: { name: string }) => {
 	const {name} = props;
 	return (
-		<Stack>
-			<div className={css`background-color: floralwhite;`}>
-				<Cover gap pad>
-					<Center>
-						<Stack gap={-1}>
-							<h1>Hello, {name}!</h1>
-							<p>Welcome to a website with a certain design philosophy. Tell me how it's working out! I want to see this text wrap a few times. Hopefully this sentence will help.</p>
-						</Stack>
-					</Center>
-					<Cover.Footer>A page by Dylan Pizzo</Cover.Footer>
-				</Cover>
-			</div>
-			<div className={css`background-color: aliceblue;`}>
-				<Cover gap pad>
-					<Center>
-						<Stack gap={-1}>
-							<h1>Hello, {name}!</h1>
-							<p>Welcome to a website with a certain design philosophy. Tell me how it's working out! I want to see this text wrap a few times. Hopefully this sentence will help.</p>
-						</Stack>
-					</Center>
-					<Cover.Footer>A page by Dylan Pizzo</Cover.Footer>
-				</Cover>
-			</div>
-		</Stack>
+		<div className={css`
+			margin: auto;
+		`}>
+			<h1>MathU</h1>
+			<MathuscriptPlayer script={script} />
+		</div>
 	);
 };
 
diff --git a/src/client/player/MathText.tsx b/src/client/player/MathText.tsx
new file mode 100644
index 0000000..e34f6ef
--- /dev/null
+++ b/src/client/player/MathText.tsx
@@ -0,0 +1,39 @@
+import { css } from "@emotion/css";
+import katex from "katex";
+import { useLayoutEffect, useRef } from "react";
+
+export const Katex = (props: { tex: string }) => {
+	const {tex} = props;
+	const ref = useRef<HTMLDivElement>(null);
+
+	useLayoutEffect(() => {
+		const element = ref.current;
+		if (!element) {
+			return;
+		}
+		katex.render(tex, element);
+	}, [tex])
+	return (
+		<span ref={ref} className={css`
+			math {
+				display: none;
+			}
+		`}></span>
+	);
+};
+
+export const MathText = (props: { children: string }) => {
+	const str = props.children;
+	const segments = str.split("$").map((s, i) => (i % 2 === 0 ? {type: "text", value: s} : {type: "math", value: s}));
+	const components = segments.map((segment, i) => {
+		if (segment.type === "text") {
+			return segment.value.split("\\\\").flatMap((s, j) => [<br key={j}/>, s]).slice(1);
+		} else if (segment.type === "math") {
+			return <Katex key={i} tex={segment.value}/>;
+		}
+	})
+
+	return (
+		<>{components}</>
+	);
+};
diff --git a/src/client/player/Player.tsx b/src/client/player/Player.tsx
new file mode 100644
index 0000000..f9fdf6b
--- /dev/null
+++ b/src/client/player/Player.tsx
@@ -0,0 +1,107 @@
+import { css } from "@emotion/css";
+import { Mathuscript, parseMathuscript } from "./parse";
+import katex from "katex";
+import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
+import { MathText } from "./MathText";
+
+type VisualState = {
+	characters: {name: string, emotion: string, x: number}[],
+	board: {text: string},
+	dialog: {name: string | null, text: string},
+}
+
+const afterStep = (state: VisualState, step: Mathuscript[number]): VisualState => {
+	const newState = structuredClone(state);
+	const {characters, board, dialog} = newState;
+	if (step.name === "board") {
+		board.text = step.text;
+	} else {
+		let char = characters.find(c => c.name === step.name);
+		if (!char) {
+			char = {name: step.name, emotion: "default", x: 0.5};
+		}
+		char.emotion = step.emotion ?? char.emotion;
+		dialog.name = step.name;
+		dialog.text = step.text;
+	}
+	console.log(newState);
+	return newState;
+}
+
+export const MathuscriptPlayer = (props: { script: string }) => {
+	const {script} = props;
+	const parsedScript = useMemo(() => parseMathuscript(script), [script]);
+	const [index, setIndex] = useState(0);
+	const [visualState, setVisualState] = useState<VisualState>({characters: [], board: {text: ""}, dialog: {name: null, text: ""}});
+
+	const doStep = useCallback(() => {
+		const step = parsedScript[index];
+		if (step) {
+			console.log(step);
+			setIndex(i => i+1);
+			setVisualState(state => afterStep(state, step));
+		} else {
+			console.log("the end");
+		}
+	}, [index, parsedScript, setIndex, setVisualState]);
+
+	return (
+		<>
+			<div className={css`
+				width: 800px;
+				height: 480px;
+				border: 1px solid black;
+				font-size: 16px;
+				margin: auto;
+				position: relative;
+				background-image: url("/assets/background.png");
+				background-size: 118%;
+				background-position-x: center;
+				background-position-y: bottom;
+			`}>
+				<div className={css`
+					position: absolute;
+					width: 58%;
+					top: 7%;
+					left: 21%;
+					height: 64%;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					color: white;
+					/* background-color: rgba(255, 0, 0, 0.3); */
+					padding: 1em;
+				`}>
+					<div>
+						<MathText>{visualState.board.text}</MathText>
+					</div>
+				</div>
+				<div className={css`
+					padding: 1em;
+					display: flex;
+					position: absolute;
+					width: 100%;
+					height: 25%;
+					bottom: 0;
+				`}>
+					<div className={css`
+						flex-basis: 0;
+						flex-grow: 1;
+						background-color: hsla(220, 50%, 40%, 0.85);
+						border: 2px solid hsla(220, 30%, 60%, 0.85);
+						border-radius: 0.25em;
+						padding: 0.5em;
+						color: white;
+					`}>
+						{
+							visualState.dialog.name && <>
+								<strong>{visualState.dialog.name}.</strong> <MathText>{visualState.dialog.text}</MathText>
+							</>
+						}
+					</div>
+				</div>
+			</div>
+			<button onClick={doStep}>Step</button>
+		</>
+	);
+};
\ No newline at end of file
diff --git a/src/client/player/parse.ts b/src/client/player/parse.ts
new file mode 100644
index 0000000..552ef2f
--- /dev/null
+++ b/src/client/player/parse.ts
@@ -0,0 +1,14 @@
+
+const lineRegex = /^\s*(?<name>[a-z-]+)\s+(?:\((?<emotion>[a-z-]+)\))?\s*"(?<text>(?:[^"\\]|\\.)*)"\s*$/;
+
+export const parseMathuscript = (script: string) => {
+	return script.split("\n").filter(Boolean).map(line => {
+		const match = line.match(lineRegex);
+		if (!match) {
+			throw Error(`Bad Line: ${JSON.stringify(line)}`);
+		}
+		return match.groups as Mathuscript[number];
+	}).filter(Boolean);
+}
+
+export type Mathuscript = {name: string, emotion: string | undefined, text: string}[];
\ No newline at end of file
diff --git a/src/client/player/render.ts b/src/client/player/render.ts
new file mode 100644
index 0000000..97ccfb3
--- /dev/null
+++ b/src/client/player/render.ts
@@ -0,0 +1,3 @@
+// export const render = (str: string) => {
+// 	str.split("$").map((s, i) => (i % 2 === 0 ? s : katex.))
+// }
\ No newline at end of file
diff --git a/src/client/player/script.txt b/src/client/player/script.txt
new file mode 100644
index 0000000..8e13616
--- /dev/null
+++ b/src/client/player/script.txt
@@ -0,0 +1,8 @@
+
+
+bridget (happy) "Hi, friends!"
+
+board "Given $f: \mathbb{Q} \to \mathbb{R}$ and $x \in \mathbb{Q}$, there\\is a unique $y \in \mathbb{N}$ such that $f(x)=y$"
+
+
+
diff --git a/src/server/public/assets/background.png b/src/server/public/assets/background.png
new file mode 100644
index 0000000..31b8e85
Binary files /dev/null and b/src/server/public/assets/background.png differ
diff --git a/src/server/public/assets/bridget.png b/src/server/public/assets/bridget.png
new file mode 100644
index 0000000..b023254
Binary files /dev/null and b/src/server/public/assets/bridget.png differ
diff --git a/src/server/public/index.html b/src/server/public/index.html
index f96075f..07feaeb 100644
--- a/src/server/public/index.html
+++ b/src/server/public/index.html
@@ -2,6 +2,7 @@
 <html>
 	<head>
 		<script src="dist/index.js" type="module"></script>
+		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
 		<style>
 			:root {
 				--measure: 64ch;