Преглед на файлове

Copy/paste over from another project

Lee Morgan преди 4 седмици
родител
ревизия
73a95a495a

+ 16 - 0
AGENT.md

@@ -0,0 +1,16 @@
+# AGENT.md — chatrpg-app
+
+## Core Tech
+- Svelte 5 with runes (`$state`, `$derived`, `$effect`, etc.).
+- SvelteKit.
+- Plain CSS only. All styles must live inside `<style>` tags in the Svelte files. **No Tailwind**, no PostCSS plugins, no external CSS frameworks or libraries.
+
+## Component Placement
+- Components that are **specific to a single route** (e.g. the landing page) must live in the route's directory (alongside `+page.svelte` in `src/routes/`).
+- Only components that are truly universal and will be reused across multiple routes/pages should go in `src/lib/components/`.
+
+## General
+- Follow explicit instructions given in chat (they take precedence).
+- Keep changes minimal and focused.
+- Prefer relative imports (`./Component.svelte`) when importing route-specific components.
+- Prefer double quotes over single quotes when possible. Use the backtick (``) only when variables need to be inserted in text

+ 1 - 0
env_example.txt

@@ -0,0 +1 @@
+PORT

+ 12 - 0
main.js

@@ -0,0 +1,12 @@
+import express from "express";
+import "dotenv/config";
+import {handler} from "./build/handler.js";
+import {fileURLToPath} from "url";
+import path from "path";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const app = express();
+
+app.use(express.static(path.join(__dirname, "build/client")));
+app.use(handler);
+app.listen(process.env.PORT);

+ 857 - 0
package-lock.json

@@ -7,6 +7,10 @@
 		"": {
 			"name": "chatrpg-app",
 			"version": "0.0.1",
+			"dependencies": {
+				"dotenv": "^17.4.2",
+				"express": "^5.2.1"
+			},
 			"devDependencies": {
 				"@sveltejs/adapter-auto": "^7.0.1",
 				"@sveltejs/kit": "^2.63.0",
@@ -538,6 +542,19 @@
 			"dev": true,
 			"license": "MIT"
 		},
+		"node_modules/accepts": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+			"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+			"license": "MIT",
+			"dependencies": {
+				"mime-types": "^3.0.0",
+				"negotiator": "^1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/acorn": {
 			"version": "8.17.0",
 			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
@@ -571,6 +588,81 @@
 				"node": ">= 0.4"
 			}
 		},
+		"node_modules/body-parser": {
+			"version": "2.3.0",
+			"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+			"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+			"license": "MIT",
+			"dependencies": {
+				"bytes": "^3.1.2",
+				"content-type": "^2.0.0",
+				"debug": "^4.4.3",
+				"http-errors": "^2.0.1",
+				"iconv-lite": "^0.7.2",
+				"on-finished": "^2.4.1",
+				"qs": "^6.15.2",
+				"raw-body": "^3.0.2",
+				"type-is": "^2.1.0"
+			},
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/body-parser/node_modules/content-type": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+			"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/bytes": {
+			"version": "3.1.2",
+			"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+			"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
+		"node_modules/call-bind-apply-helpers": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+			"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+			"license": "MIT",
+			"dependencies": {
+				"es-errors": "^1.3.0",
+				"function-bind": "^1.1.2"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/call-bound": {
+			"version": "1.0.4",
+			"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+			"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+			"license": "MIT",
+			"dependencies": {
+				"call-bind-apply-helpers": "^1.0.2",
+				"get-intrinsic": "^1.3.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
 		"node_modules/clsx": {
 			"version": "2.1.1",
 			"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -581,6 +673,28 @@
 				"node": ">=6"
 			}
 		},
+		"node_modules/content-disposition": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+			"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/content-type": {
+			"version": "1.0.5",
+			"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+			"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/cookie": {
 			"version": "0.6.0",
 			"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@@ -591,6 +705,32 @@
 				"node": ">= 0.6"
 			}
 		},
+		"node_modules/cookie-signature": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+			"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=6.6.0"
+			}
+		},
+		"node_modules/debug": {
+			"version": "4.4.3",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+			"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+			"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",
@@ -601,6 +741,15 @@
 				"node": ">=0.10.0"
 			}
 		},
+		"node_modules/depd": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+			"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/detect-libc": {
 			"version": "2.1.2",
 			"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -618,6 +767,83 @@
 			"dev": true,
 			"license": "MIT"
 		},
+		"node_modules/dotenv": {
+			"version": "17.4.2",
+			"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+			"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+			"license": "BSD-2-Clause",
+			"engines": {
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://dotenvx.com"
+			}
+		},
+		"node_modules/dunder-proto": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+			"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+			"license": "MIT",
+			"dependencies": {
+				"call-bind-apply-helpers": "^1.0.1",
+				"es-errors": "^1.3.0",
+				"gopd": "^1.2.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/ee-first": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+			"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+			"license": "MIT"
+		},
+		"node_modules/encodeurl": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+			"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
+		"node_modules/es-define-property": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+			"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/es-errors": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+			"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/es-object-atoms": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+			"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+			"license": "MIT",
+			"dependencies": {
+				"es-errors": "^1.3.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/escape-html": {
+			"version": "1.0.3",
+			"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+			"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+			"license": "MIT"
+		},
 		"node_modules/esm-env": {
 			"version": "1.2.2",
 			"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
@@ -643,6 +869,67 @@
 				}
 			}
 		},
+		"node_modules/etag": {
+			"version": "1.8.1",
+			"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+			"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/express": {
+			"version": "5.2.1",
+			"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+			"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+			"license": "MIT",
+			"dependencies": {
+				"accepts": "^2.0.0",
+				"body-parser": "^2.2.1",
+				"content-disposition": "^1.0.0",
+				"content-type": "^1.0.5",
+				"cookie": "^0.7.1",
+				"cookie-signature": "^1.2.1",
+				"debug": "^4.4.0",
+				"depd": "^2.0.0",
+				"encodeurl": "^2.0.0",
+				"escape-html": "^1.0.3",
+				"etag": "^1.8.1",
+				"finalhandler": "^2.1.0",
+				"fresh": "^2.0.0",
+				"http-errors": "^2.0.0",
+				"merge-descriptors": "^2.0.0",
+				"mime-types": "^3.0.0",
+				"on-finished": "^2.4.1",
+				"once": "^1.4.0",
+				"parseurl": "^1.3.3",
+				"proxy-addr": "^2.0.7",
+				"qs": "^6.14.0",
+				"range-parser": "^1.2.1",
+				"router": "^2.2.0",
+				"send": "^1.1.0",
+				"serve-static": "^2.2.0",
+				"statuses": "^2.0.1",
+				"type-is": "^2.0.1",
+				"vary": "^1.1.2"
+			},
+			"engines": {
+				"node": ">= 18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/express/node_modules/cookie": {
+			"version": "0.7.2",
+			"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+			"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
 		"node_modules/fdir": {
 			"version": "6.5.0",
 			"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -661,6 +948,45 @@
 				}
 			}
 		},
+		"node_modules/finalhandler": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+			"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+			"license": "MIT",
+			"dependencies": {
+				"debug": "^4.4.0",
+				"encodeurl": "^2.0.0",
+				"escape-html": "^1.0.3",
+				"on-finished": "^2.4.1",
+				"parseurl": "^1.3.3",
+				"statuses": "^2.0.1"
+			},
+			"engines": {
+				"node": ">= 18.0.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/forwarded": {
+			"version": "0.2.0",
+			"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+			"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/fresh": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+			"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/fsevents": {
 			"version": "2.3.3",
 			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -676,6 +1002,145 @@
 				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
 			}
 		},
+		"node_modules/function-bind": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+			"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+			"license": "MIT",
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/get-intrinsic": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+			"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+			"license": "MIT",
+			"dependencies": {
+				"call-bind-apply-helpers": "^1.0.2",
+				"es-define-property": "^1.0.1",
+				"es-errors": "^1.3.0",
+				"es-object-atoms": "^1.1.1",
+				"function-bind": "^1.1.2",
+				"get-proto": "^1.0.1",
+				"gopd": "^1.2.0",
+				"has-symbols": "^1.1.0",
+				"hasown": "^2.0.2",
+				"math-intrinsics": "^1.1.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/get-proto": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+			"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+			"license": "MIT",
+			"dependencies": {
+				"dunder-proto": "^1.0.1",
+				"es-object-atoms": "^1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/gopd": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+			"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/has-symbols": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+			"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/hasown": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+			"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+			"license": "MIT",
+			"dependencies": {
+				"function-bind": "^1.1.2"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/http-errors": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+			"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+			"license": "MIT",
+			"dependencies": {
+				"depd": "~2.0.0",
+				"inherits": "~2.0.4",
+				"setprototypeof": "~1.2.0",
+				"statuses": "~2.0.2",
+				"toidentifier": "~1.0.1"
+			},
+			"engines": {
+				"node": ">= 0.8"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/iconv-lite": {
+			"version": "0.7.2",
+			"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+			"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+			"license": "MIT",
+			"dependencies": {
+				"safer-buffer": ">= 2.1.2 < 3.0.0"
+			},
+			"engines": {
+				"node": ">=0.10.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/inherits": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+			"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+			"license": "ISC"
+		},
+		"node_modules/ipaddr.js": {
+			"version": "1.9.1",
+			"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+			"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.10"
+			}
+		},
+		"node_modules/is-promise": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+			"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+			"license": "MIT"
+		},
 		"node_modules/is-reference": {
 			"version": "3.0.3",
 			"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
@@ -986,6 +1451,61 @@
 				"@jridgewell/sourcemap-codec": "^1.5.5"
 			}
 		},
+		"node_modules/math-intrinsics": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+			"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/media-typer": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+			"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
+		"node_modules/merge-descriptors": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+			"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
+			}
+		},
+		"node_modules/mime-db": {
+			"version": "1.54.0",
+			"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+			"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/mime-types": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+			"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+			"license": "MIT",
+			"dependencies": {
+				"mime-db": "^1.54.0"
+			},
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
 		"node_modules/mrmime": {
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -996,6 +1516,12 @@
 				"node": ">=10"
 			}
 		},
+		"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.15",
 			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
@@ -1015,6 +1541,27 @@
 				"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
 			}
 		},
+		"node_modules/negotiator": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+			"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/object-inspect": {
+			"version": "1.13.4",
+			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+			"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
 		"node_modules/obug": {
 			"version": "2.1.3",
 			"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
@@ -1029,6 +1576,46 @@
 				"node": ">=12.20.0"
 			}
 		},
+		"node_modules/on-finished": {
+			"version": "2.4.1",
+			"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+			"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+			"license": "MIT",
+			"dependencies": {
+				"ee-first": "1.1.1"
+			},
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
+		"node_modules/once": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+			"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+			"license": "ISC",
+			"dependencies": {
+				"wrappy": "1"
+			}
+		},
+		"node_modules/parseurl": {
+			"version": "1.3.3",
+			"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+			"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
+		"node_modules/path-to-regexp": {
+			"version": "8.4.2",
+			"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+			"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+			"license": "MIT",
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
 		"node_modules/picocolors": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1078,6 +1665,58 @@
 				"node": "^10 || ^12 || >=14"
 			}
 		},
+		"node_modules/proxy-addr": {
+			"version": "2.0.7",
+			"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+			"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+			"license": "MIT",
+			"dependencies": {
+				"forwarded": "0.2.0",
+				"ipaddr.js": "1.9.1"
+			},
+			"engines": {
+				"node": ">= 0.10"
+			}
+		},
+		"node_modules/qs": {
+			"version": "6.15.2",
+			"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+			"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+			"license": "BSD-3-Clause",
+			"dependencies": {
+				"side-channel": "^1.1.0"
+			},
+			"engines": {
+				"node": ">=0.6"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/range-parser": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+			"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.6"
+			}
+		},
+		"node_modules/raw-body": {
+			"version": "3.0.2",
+			"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+			"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+			"license": "MIT",
+			"dependencies": {
+				"bytes": "~3.1.2",
+				"http-errors": "~2.0.1",
+				"iconv-lite": "~0.7.0",
+				"unpipe": "~1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.10"
+			}
+		},
 		"node_modules/rolldown": {
 			"version": "1.0.3",
 			"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
@@ -1112,6 +1751,73 @@
 				"@rolldown/binding-win32-x64-msvc": "1.0.3"
 			}
 		},
+		"node_modules/router": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+			"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+			"license": "MIT",
+			"dependencies": {
+				"debug": "^4.4.0",
+				"depd": "^2.0.0",
+				"is-promise": "^4.0.0",
+				"parseurl": "^1.3.3",
+				"path-to-regexp": "^8.0.0"
+			},
+			"engines": {
+				"node": ">= 18"
+			}
+		},
+		"node_modules/safer-buffer": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+			"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+			"license": "MIT"
+		},
+		"node_modules/send": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+			"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+			"license": "MIT",
+			"dependencies": {
+				"debug": "^4.4.3",
+				"encodeurl": "^2.0.0",
+				"escape-html": "^1.0.3",
+				"etag": "^1.8.1",
+				"fresh": "^2.0.0",
+				"http-errors": "^2.0.1",
+				"mime-types": "^3.0.2",
+				"ms": "^2.1.3",
+				"on-finished": "^2.4.1",
+				"range-parser": "^1.2.1",
+				"statuses": "^2.0.2"
+			},
+			"engines": {
+				"node": ">= 18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/serve-static": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+			"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+			"license": "MIT",
+			"dependencies": {
+				"encodeurl": "^2.0.0",
+				"escape-html": "^1.0.3",
+				"parseurl": "^1.3.3",
+				"send": "^1.2.0"
+			},
+			"engines": {
+				"node": ">= 18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
 		"node_modules/set-cookie-parser": {
 			"version": "3.1.0",
 			"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
@@ -1119,6 +1825,84 @@
 			"dev": true,
 			"license": "MIT"
 		},
+		"node_modules/setprototypeof": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+			"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+			"license": "ISC"
+		},
+		"node_modules/side-channel": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+			"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+			"license": "MIT",
+			"dependencies": {
+				"es-errors": "^1.3.0",
+				"object-inspect": "^1.13.4",
+				"side-channel-list": "^1.0.1",
+				"side-channel-map": "^1.0.1",
+				"side-channel-weakmap": "^1.0.2"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/side-channel-list": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+			"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+			"license": "MIT",
+			"dependencies": {
+				"es-errors": "^1.3.0",
+				"object-inspect": "^1.13.4"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/side-channel-map": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+			"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+			"license": "MIT",
+			"dependencies": {
+				"call-bound": "^1.0.2",
+				"es-errors": "^1.3.0",
+				"get-intrinsic": "^1.2.5",
+				"object-inspect": "^1.13.3"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
+		"node_modules/side-channel-weakmap": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+			"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+			"license": "MIT",
+			"dependencies": {
+				"call-bound": "^1.0.2",
+				"es-errors": "^1.3.0",
+				"get-intrinsic": "^1.2.5",
+				"object-inspect": "^1.13.3",
+				"side-channel-map": "^1.0.1"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
 		"node_modules/sirv": {
 			"version": "3.0.2",
 			"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
@@ -1144,6 +1928,15 @@
 				"node": ">=0.10.0"
 			}
 		},
+		"node_modules/statuses": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+			"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/svelte": {
 			"version": "5.56.3",
 			"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz",
@@ -1189,6 +1982,15 @@
 				"url": "https://github.com/sponsors/SuperchupuDev"
 			}
 		},
+		"node_modules/toidentifier": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+			"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=0.6"
+			}
+		},
 		"node_modules/totalist": {
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -1207,6 +2009,55 @@
 			"license": "0BSD",
 			"optional": true
 		},
+		"node_modules/type-is": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+			"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+			"license": "MIT",
+			"dependencies": {
+				"content-type": "^2.0.0",
+				"media-typer": "^1.1.0",
+				"mime-types": "^3.0.0"
+			},
+			"engines": {
+				"node": ">= 18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/type-is/node_modules/content-type": {
+			"version": "2.0.0",
+			"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+			"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=18"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/express"
+			}
+		},
+		"node_modules/unpipe": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+			"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
+		"node_modules/vary": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+			"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 0.8"
+			}
+		},
 		"node_modules/vite": {
 			"version": "8.0.16",
 			"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
@@ -1305,6 +2156,12 @@
 				}
 			}
 		},
+		"node_modules/wrappy": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+			"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+			"license": "ISC"
+		},
 		"node_modules/zimmerframe": {
 			"version": "1.1.4",
 			"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",

+ 4 - 0
package.json

@@ -15,5 +15,9 @@
 		"@sveltejs/vite-plugin-svelte": "^7.1.2",
 		"svelte": "^5.56.1",
 		"vite": "^8.0.16"
+	},
+	"dependencies": {
+		"dotenv": "^17.4.2",
+		"express": "^5.2.1"
 	}
 }

+ 1 - 1
src/app.html

@@ -3,7 +3,7 @@
 	<head>
 		<meta charset="utf-8" />
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
-		<meta name="text-scale" content="scale" />
+		<meta name="description" content="ChatRPG — AI-powered text-based RPG. Create your own world, play through infinite stories with natural language. $1 per million tokens." />
 		%sveltekit.head%
 	</head>
 	<body data-sveltekit-preload-data="hover">

BIN
src/lib/assets/favicon.jpg


BIN
src/lib/assets/favicon.png


BIN
src/lib/assets/image01.gif


BIN
src/lib/assets/image01.jpg


BIN
src/lib/assets/image02.jpg


BIN
src/lib/assets/image03.jpg


+ 158 - 1
src/routes/+layout.svelte

@@ -1,11 +1,168 @@
 <script>
-	import favicon from '$lib/assets/favicon.svg';
+	import favicon from '$lib/assets/favicon.png';
 
 	let { children } = $props();
 </script>
 
 <svelte:head>
 	<link rel="icon" href={favicon} />
+	<title>ChatRPG — AI-Powered Text RPG Adventures</title>
 </svelte:head>
 
 {@render children()}
+
+<style>
+	:global(html) {
+		scroll-behavior: smooth;
+	}
+	:global(body) {
+		margin: 0;
+		font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+		background: #0b0b10;
+		color: #e5e5ea;
+		line-height: 1.6;
+	}
+	:global(*) {
+		box-sizing: border-box;
+	}
+	:global(h1, h2, h3) {
+		font-weight: 700;
+		line-height: 1.1;
+		margin: 0;
+	}
+
+	/* Base reusable styles shared across components */
+	:global(.container) {
+		max-width: 1080px;
+		margin: 0 auto;
+		padding: 0 24px;
+	}
+	:global(.section) {
+		padding: 100px 0;
+	}
+	:global(.section-alt) {
+		background: #121218;
+	}
+	:global(.section-header) {
+		text-align: center;
+		max-width: 620px;
+		margin: 0 auto 56px;
+	}
+	:global(.section-header .eyebrow) {
+		font-size: 13px;
+		letter-spacing: 2px;
+		font-weight: 600;
+		text-transform: uppercase;
+		color: #c084fc;
+		margin-bottom: 12px;
+	}
+	:global(.section-header h2) {
+		font-size: clamp(2rem, 5vw, 2.8rem);
+		margin-bottom: 16px;
+	}
+	:global(.section-header p) {
+		font-size: 1.1rem;
+		color: #a1a1aa;
+		margin: 0;
+	}
+
+	/* Buttons */
+	:global(.btn) {
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+		gap: 8px;
+		font-weight: 600;
+		font-size: 15px;
+		padding: 10px 22px;
+		border-radius: 10px;
+		border: 1px solid transparent;
+		cursor: pointer;
+		transition: all 0.15s ease;
+		text-decoration: none;
+		white-space: nowrap;
+	}
+	:global(.btn.large) {
+		padding: 14px 28px;
+		font-size: 16px;
+	}
+	:global(.btn.full) {
+		width: 100%;
+	}
+	:global(.btn-primary) {
+		background: #c084fc;
+		color: #0b0b10;
+		border-color: #c084fc;
+	}
+	:global(.btn-primary:hover) {
+		background: #d8b4fe;
+		transform: translateY(-1px);
+	}
+	:global(.btn-secondary) {
+		background: transparent;
+		color: #e5e5ea;
+		border-color: #3f3f4a;
+	}
+	:global(.btn-secondary:hover) {
+		border-color: #c084fc;
+		color: #c084fc;
+	}
+
+	:global(.accent) {
+		background: linear-gradient(90deg, #c084fc, #f0abfc);
+		-webkit-background-clip: text;
+		-webkit-text-fill-color: transparent;
+	}
+
+	/* Logo base (used in nav + footer) */
+	:global(.logo) {
+		display: flex;
+		align-items: center;
+		gap: 10px;
+		cursor: pointer;
+		font-weight: 700;
+		font-size: 21px;
+	}
+	:global(.logo-icon) {
+		width: 28px;
+		height: 28px;
+		color: #c084fc;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+	:global(.logo-text) {
+		letter-spacing: -0.5px;
+	}
+
+	/* Responsive base adjustments (shared) */
+	@media (max-width: 700px) {
+		:global(.section) {
+			padding: 70px 0;
+		}
+		:global(.pricing-card) {
+			padding: 34px 24px;
+		}
+		:global(.price) {
+			font-size: 3.4rem;
+		}
+		:global(.footer-inner) {
+			flex-direction: column;
+			align-items: flex-start;
+			text-align: left;
+		}
+		:global(.footer-links) {
+			flex-direction: column;
+			gap: 8px;
+		}
+		:global(.footer-right) {
+			text-align: left;
+			margin-top: 20px;
+		}
+	}
+	@media (max-width: 500px) {
+		:global(.container) {
+			padding: 0 18px;
+		}
+	}
+</style>

+ 35 - 2
src/routes/+page.svelte

@@ -1,2 +1,35 @@
-<h1>Welcome to SvelteKit</h1>
-<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
+<script>
+	import Navbar from './Navbar.svelte';
+	import Hero from './Hero.svelte';
+	import HowItWorks from './HowItWorks.svelte';
+	import Features from './Features.svelte';
+	import Pricing from './Pricing.svelte';
+	import Faq from './Faq.svelte';
+	import Footer from './Footer.svelte';
+
+	// Shared scroll helper (parent provides the actual scrolling logic)
+	function scrollToSection(id) {
+		const el = document.getElementById(id);
+		if (el) {
+			const offset = 70;
+			const top = el.getBoundingClientRect().top + window.scrollY - offset;
+			window.scrollTo({ top, behavior: 'smooth' });
+		}
+	}
+</script>
+
+<Navbar scrollTo={scrollToSection} />
+<Hero scrollTo={scrollToSection} />
+<HowItWorks />
+<Features />
+<Pricing />
+<Faq />
+<Footer />
+
+<style>
+	/* Page-specific or overrides can go here if needed.
+	   Most styles are now co-located in the individual components
+	   or in +layout.svelte for shared base classes.
+	*/
+</style>
+

+ 96 - 0
src/routes/Faq.svelte

@@ -0,0 +1,96 @@
+<script>
+	// Self-contained FAQ state and logic
+	let openFaq = $state(null);
+
+	const faqs = [
+		{
+			q: "How does the LLM actually run the game?",
+			a: "You describe your character, the setting, genre, tone, and any special rules in plain English. The LLM then generates the opening scene and continues the story based on everything said so far. It maintains consistency, tracks important details, and reacts to your every input naturally."
+		},
+		{
+			q: "Is there a limit to what I can do or say?",
+			a: "The only real limits are the creativity of the model and basic content guidelines. You can try anything — fight, seduce, build, explore, betray, invent magic systems on the fly. Outcomes depend on context, your skills, luck, and the world's rules as established."
+		},
+		{
+			q: "How much does it cost in practice?",
+			a: "At $1 per million tokens, a rich hour of play typically uses 15,000–40,000 tokens depending on length and model. That’s usually 3–8 cents per hour. Long epic campaigns cost only a few dollars total. You are charged only for actual usage — no subscriptions or hidden fees."
+		},
+		{
+			q: "Can I play the same character/world across multiple sessions?",
+			a: "Yes. Your adventures are saved with full history and context. Resume anytime and the LLM will remember every decision, character, location, and plot thread. You can also branch into alternate timelines if you want."
+		},
+		{
+			q: "When will ChatRPG be available?",
+			a: "We are currently in active development and polishing the core experience. A full launch is coming soon."
+		}
+	];
+
+	function toggleFaq(index) {
+		openFaq = openFaq === index ? null : index;
+	}
+</script>
+
+<section id="faq" class="section section-alt">
+	<div class="container">
+		<div class="section-header">
+			<div class="eyebrow">QUESTIONS</div>
+			<h2>Frequently Asked Questions</h2>
+		</div>
+
+		<div class="faq-list">
+			{#each faqs as faq, i}
+				<div class="faq-item" class:open={openFaq === i}>
+					<button class="faq-question" onclick={() => toggleFaq(i)}>
+						<span>{faq.q}</span>
+						<span class="faq-arrow">{openFaq === i ? '−' : '+'}</span>
+					</button>
+					{#if openFaq === i}
+						<div class="faq-answer">{faq.a}</div>
+					{/if}
+				</div>
+			{/each}
+		</div>
+	</div>
+</section>
+
+<style>
+	/* FAQ specific */
+	.faq-list {
+		max-width: 780px;
+		margin: 0 auto;
+	}
+	.faq-item {
+		border-bottom: 1px solid #2a2a35;
+	}
+	.faq-item:last-child {
+		border-bottom: none;
+	}
+	.faq-question {
+		width: 100%;
+		background: none;
+		border: none;
+		color: #e5e5ea;
+		font-size: 1.05rem;
+		text-align: left;
+		padding: 20px 0;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		cursor: pointer;
+		font-weight: 500;
+	}
+	.faq-arrow {
+		font-size: 1.4rem;
+		line-height: 1;
+		color: #777;
+		width: 24px;
+		text-align: center;
+	}
+	.faq-answer {
+		color: #a1a1aa;
+		padding-bottom: 22px;
+		padding-right: 30px;
+		font-size: 1rem;
+		line-height: 1.65;
+	}
+</style>

+ 100 - 0
src/routes/Features.svelte

@@ -0,0 +1,100 @@
+<section id="features" class="section">
+	<div class="container">
+		<div class="section-header">
+			<div class="eyebrow">BUILT FOR STORYTELLERS</div>
+			<h2>Everything you need for unforgettable adventures</h2>
+		</div>
+
+		<div class="features-grid">
+			<div class="feature">
+				<div class="feature-icon">✍️</div>
+				<h3>Completely Freeform</h3>
+				<p>No parser. No verb menus. Write in full sentences or fragments. The LLM understands nuance, sarcasm, and subtext.</p>
+			</div>
+			<div class="feature">
+				<div class="feature-icon">
+					<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+						<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
+						<path d="M21 5v14c0 1.66-4 3-9 3s-9-1.34-9-3V5"></path>
+						<path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3"></path>
+					</svg>
+				</div>
+				<h3>Perfect Memory</h3>
+				<p>Every detail, promise, secret, and grudge is remembered. The world evolves consistently across hours or weeks of play.</p>
+			</div>
+			<div class="feature">
+				<div class="feature-icon">🌍</div>
+				<h3>Any Genre. Any Tone.</h3>
+				<p>Grimdark fantasy, cozy mystery, cosmic horror, humorous heist, political intrigue, post-apocalyptic romance. You set the rules.</p>
+			</div>
+			<div class="feature">
+				<div class="feature-icon">
+					<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+						<polyline points="23 4 23 10 17 10"></polyline>
+						<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
+					</svg>
+				</div>
+				<h3>Persistent Campaigns</h3>
+				<p>Save your progress. Return days later. Pick up exactly where you left off — or start a new timeline from any moment.</p>
+			</div>
+			<div class="feature">
+				<div class="feature-icon">⚔️</div>
+				<h3>Real Consequences</h3>
+				<p>Actions have weight. Allies remember betrayals. Kingdoms fall because of one choice. The story is truly yours.</p>
+			</div>
+			<div class="feature">
+				<div class="feature-icon">💰</div>
+				<h3>Fair Usage Pricing</h3>
+				<p>Pay only for the tokens the model uses. No monthly fees. Most players spend less than a cup of coffee per week.</p>
+			</div>
+		</div>
+	</div>
+</section>
+
+<style>
+	/* Features specific */
+	.features-grid {
+		display: grid;
+		grid-template-columns: repeat(3, 1fr);
+		gap: 20px;
+	}
+	.feature {
+		background: #121218;
+		border: 1px solid #2a2a35;
+		border-radius: 16px;
+		padding: 26px 24px;
+	}
+	.feature-icon {
+		font-size: 26px;
+		margin-bottom: 14px;
+		color: #c084fc;
+		display: inline-flex;
+		align-items: center;
+		justify-content: center;
+	}
+	.feature-icon svg {
+		width: 26px;
+		height: 26px;
+		display: block;
+	}
+	.feature h3 {
+		font-size: 1.1rem;
+		margin-bottom: 10px;
+	}
+	.feature p {
+		font-size: 0.95rem;
+		color: #a1a1aa;
+		margin: 0;
+	}
+
+	@media (max-width: 900px) {
+		.features-grid {
+			grid-template-columns: 1fr 1fr;
+		}
+	}
+	@media (max-width: 700px) {
+		.features-grid {
+			grid-template-columns: 1fr;
+		}
+	}
+</style>

+ 80 - 0
src/routes/Footer.svelte

@@ -0,0 +1,80 @@
+<script>
+	import favicon from '$lib/assets/favicon.png';
+</script>
+
+<footer>
+	<div class="container footer-inner">
+		<div class="footer-left">
+			<div class="logo">
+				<div class="logo-icon">
+					<img src={favicon} alt="ChatRPG logo" width="20" height="20" />
+				</div>
+				<span>ChatRPG</span>
+			</div>
+			<p class="footer-tag">Text-based RPGs, reimagined with AI.</p>
+		</div>
+
+		<div class="footer-links">
+			<a href="#how">How it Works</a>
+			<a href="#features">Features</a>
+			<a href="#pricing">Pricing</a>
+			<a href="#faq">FAQ</a>
+		</div>
+
+		<div class="footer-right">
+			<p class="footer-small">Currently in active development.</p>
+		</div>
+	</div>
+</footer>
+
+<style>
+	/* Footer specific */
+	footer {
+		background: #08080c;
+		padding: 60px 0 40px;
+		border-top: 1px solid #1f1f27;
+	}
+	.footer-inner {
+		display: flex;
+		justify-content: space-between;
+		align-items: flex-start;
+		gap: 40px;
+	}
+	.footer-left .logo {
+		font-size: 18px;
+		margin-bottom: 8px;
+	}
+	.footer-left .logo-icon {
+		width: 20px;
+		height: 20px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+	.footer-tag {
+		color: #666;
+		font-size: 14px;
+		margin: 0;
+	}
+	.footer-links {
+		display: flex;
+		gap: 28px;
+		font-size: 14px;
+	}
+	.footer-links a {
+		color: #a1a1aa;
+		text-decoration: none;
+	}
+	.footer-links a:hover {
+		color: #c084fc;
+	}
+	.footer-right {
+		text-align: right;
+		font-size: 13px;
+		color: #666;
+	}
+	.footer-small {
+		margin-top: 2px;
+		font-size: 12px;
+	}
+</style>

+ 135 - 0
src/routes/Hero.svelte

@@ -0,0 +1,135 @@
+<script>
+	import image01 from '$lib/assets/image01.gif';
+
+	let { scrollTo } = $props();
+</script>
+
+<header class="hero">
+	<div class="hero-content">
+		<div class="badge">IN DEVELOPMENT • LAUNCHING SOON</div>
+		
+		<h1>
+			Your story.<br />
+			<span class="accent">Infinite possibilities.</span>
+		</h1>
+		
+		<p class="hero-subtitle">
+			An LLM-powered text RPG where you describe anything in natural language.
+			No commands. No limits. Just pure collaborative storytelling.
+		</p>
+
+		<div class="hero-ctas">
+			<button class="btn btn-primary large" onclick={() => scrollTo('pricing')}>
+				See Pricing
+			</button>
+			<button class="btn btn-secondary large" onclick={() => scrollTo('how')}>
+				See How It Works
+			</button>
+		</div>
+
+		<div class="hero-stats">
+			<div><strong>100% freeform</strong> input</div>
+			<div><strong>$1</strong> per million tokens</div>
+			<div><strong>Unlimited</strong> adventure length</div>
+		</div>
+	</div>
+
+	<!-- Visual preview -->
+	<div class="hero-visual">
+		<img src={image01} alt="Sample animated scene from a ChatRPG adventure" />
+	</div>
+</header>
+
+<style>
+	/* Hero specific styles */
+	.hero {
+		min-height: 100dvh;
+		display: grid;
+		grid-template-columns: 1fr 1fr;
+		align-items: center;
+		gap: 60px;
+		padding: 120px 24px 60px;
+		max-width: 1080px;
+		margin: 0 auto;
+	}
+	.hero-content {
+		max-width: 560px;
+	}
+	.hero .badge {
+		display: inline-block;
+		background: #1f1f27;
+		color: #c084fc;
+		font-size: 12px;
+		font-weight: 600;
+		padding: 4px 14px;
+		border-radius: 999px;
+		margin-bottom: 24px;
+		letter-spacing: 1px;
+	}
+	.hero h1 {
+		font-size: clamp(2.8rem, 7vw, 4.2rem);
+		margin-bottom: 24px;
+		letter-spacing: -2.5px;
+	}
+	.hero-subtitle {
+		font-size: 1.25rem;
+		color: #a1a1aa;
+		margin-bottom: 32px;
+		max-width: 460px;
+	}
+	.hero-ctas {
+		display: flex;
+		gap: 14px;
+		flex-wrap: wrap;
+		margin-bottom: 48px;
+	}
+	.hero-stats {
+		display: flex;
+		gap: 28px;
+		font-size: 14px;
+		color: #a1a1aa;
+	}
+	.hero-stats strong {
+		color: #e5e5ea;
+	}
+
+	/* Hero visual */
+	.hero-visual {
+		position: relative;
+	}
+	.hero-visual img {
+		background: #121218;
+		border: 1px solid #2a2a35;
+		border-radius: 16px;
+		box-shadow: 0 30px 70px -15px rgba(0, 0, 0, 0.6);
+		max-width: 380px;
+		margin-left: auto;
+		display: block;
+		width: 100%;
+		height: auto;
+	}
+
+	@media (max-width: 900px) {
+		.hero {
+			grid-template-columns: 1fr;
+			padding-top: 100px;
+			text-align: center;
+		}
+		.hero-content {
+			max-width: none;
+		}
+		.hero-subtitle {
+			margin-left: auto;
+			margin-right: auto;
+		}
+		.hero-ctas {
+			justify-content: center;
+		}
+		.hero-stats {
+			justify-content: center;
+		}
+		.hero-visual {
+			display: none;
+		}
+	}
+</style>

+ 74 - 0
src/routes/HowItWorks.svelte

@@ -0,0 +1,74 @@
+<section id="how" class="section">
+	<div class="container">
+		<div class="section-header">
+			<div class="eyebrow">FOUR SIMPLE STEPS</div>
+			<h2>How ChatRPG Works</h2>
+			<p>From idea to epic in seconds. Then the story never has to end.</p>
+		</div>
+
+		<div class="steps">
+			<div class="step">
+				<div class="step-num">01</div>
+				<h3>Define Your Adventure</h3>
+				<p>Choose genre, tone, setting, and your character. Or just start typing and let the AI surprise you. Want hard sci-fi with body horror and romance? Done.</p>
+			</div>
+			<div class="step">
+				<div class="step-num">02</div>
+				<h3>LLM Opens the World</h3>
+				<p>The model writes a rich, detailed opening scene that perfectly matches everything you described. No two games are ever the same.</p>
+			</div>
+			<div class="step">
+				<div class="step-num">03</div>
+				<h3>Respond Naturally</h3>
+				<p>Type anything you want: dialogue, internal thoughts, actions, questions, jokes, reckless ideas. The game understands plain English.</p>
+			</div>
+			<div class="step">
+				<div class="step-num">04</div>
+				<h3>The Story Continues</h3>
+				<p>The LLM narrates consequences, advances plots, introduces memorable characters, and keeps perfect continuity. Every decision matters forever.</p>
+			</div>
+		</div>
+	</div>
+</section>
+
+<style>
+	/* How it works / steps specific */
+	.steps {
+		display: grid;
+		grid-template-columns: repeat(4, 1fr);
+		gap: 24px;
+	}
+	.step {
+		background: #121218;
+		border: 1px solid #2a2a35;
+		border-radius: 16px;
+		padding: 28px 24px;
+	}
+	.step-num {
+		font-size: 13px;
+		font-weight: 700;
+		color: #c084fc;
+		margin-bottom: 16px;
+		letter-spacing: 1px;
+	}
+	.step h3 {
+		font-size: 1.15rem;
+		margin-bottom: 12px;
+	}
+	.step p {
+		color: #a1a1aa;
+		font-size: 0.97rem;
+		margin: 0;
+	}
+
+	@media (max-width: 900px) {
+		.steps {
+			grid-template-columns: 1fr 1fr;
+		}
+	}
+	@media (max-width: 700px) {
+		.steps {
+			grid-template-columns: 1fr;
+		}
+	}
+</style>

+ 218 - 0
src/routes/Navbar.svelte

@@ -0,0 +1,218 @@
+<script>
+	// Svelte 5 runes - navbar owns its mobile + transparency state
+	import favicon from '$lib/assets/favicon.png';
+
+	let scrollY = $state(0);
+	let mobileMenuOpen = $state(false);
+
+	// Receive scroll handler from parent
+	let { scrollTo } = $props();
+
+	function handleScroll() {
+		scrollY = window.scrollY;
+	}
+
+	function toggleMobileMenu() {
+		mobileMenuOpen = !mobileMenuOpen;
+	}
+
+	// Keyboard escape for mobile menu
+	function handleKeydown(e) {
+		if (e.key === 'Escape') {
+			if (mobileMenuOpen) mobileMenuOpen = false;
+		}
+	}
+
+	// Attach listeners (scoped to this component)
+	$effect(() => {
+		window.addEventListener('scroll', handleScroll, { passive: true });
+		window.addEventListener('keydown', handleKeydown);
+
+		return () => {
+			window.removeEventListener('scroll', handleScroll);
+			window.removeEventListener('keydown', handleKeydown);
+		};
+	});
+
+	const navSolid = $derived(scrollY > 80);
+
+	function handleNavClick(id) {
+		mobileMenuOpen = false;
+		scrollTo(id);
+	}
+</script>
+
+<nav class:nav-solid={navSolid}>
+	<div class="nav-inner">
+		<button 
+			class="logo logo-btn"
+			onclick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
+			aria-label="ChatRPG home"
+		>
+			<div class="logo-icon">
+				<img src={favicon} alt="ChatRPG logo" width="28" height="28" />
+			</div>
+			<span class="logo-text">ChatRPG</span>
+		</button>
+
+		<!-- Desktop Nav -->
+		<div class="nav-links">
+			<button onclick={() => handleNavClick('how')} class="nav-link">How it Works</button>
+			<button onclick={() => handleNavClick('features')} class="nav-link">Features</button>
+			<button onclick={() => handleNavClick('pricing')} class="nav-link">Pricing</button>
+			<button onclick={() => handleNavClick('faq')} class="nav-link">FAQ</button>
+		</div>
+
+		<div class="nav-actions">
+			<button class="btn btn-primary" onclick={() => handleNavClick('pricing')}>See Pricing</button>
+		</div>
+
+		<!-- Mobile hamburger -->
+		<button class="mobile-toggle" onclick={toggleMobileMenu} aria-label="Toggle menu">
+			<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
+				{#if mobileMenuOpen}
+					<path d="M18 6L6 18M6 6l12 12"/>
+				{:else}
+					<path d="M3 12h18M3 6h18M3 18h18"/>
+				{/if}
+			</svg>
+		</button>
+	</div>
+
+	<!-- Mobile Menu -->
+	{#if mobileMenuOpen}
+		<div class="mobile-menu">
+			<button onclick={() => handleNavClick('how')} class="mobile-link">How it Works</button>
+			<button onclick={() => handleNavClick('features')} class="mobile-link">Features</button>
+			<button onclick={() => handleNavClick('pricing')} class="mobile-link">Pricing</button>
+			<button onclick={() => handleNavClick('faq')} class="mobile-link">FAQ</button>
+			<div class="mobile-ctas">
+				<button class="btn btn-primary full" onclick={() => handleNavClick('pricing')}>See Pricing</button>
+			</div>
+		</div>
+	{/if}
+</nav>
+
+<style>
+	/* Navbar specific styles */
+	nav {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		z-index: 100;
+		background: rgba(11, 11, 16, 0.7);
+		backdrop-filter: blur(12px);
+		border-bottom: 1px solid transparent;
+		transition: all 0.2s ease;
+	}
+	.nav-solid {
+		background: #0b0b10;
+		border-bottom-color: #2a2a35;
+	}
+
+	.nav-inner {
+		max-width: 1080px;
+		margin: 0 auto;
+		padding: 0 24px;
+		height: 68px;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+	}
+
+	.logo {
+		font-size: 21px;
+	}
+	.logo-icon {
+		width: 28px;
+		height: 28px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+	.logo-btn {
+		background: none;
+		border: none;
+		padding: 0;
+		color: inherit;
+		font: inherit;
+	}
+
+	.nav-links {
+		display: flex;
+		gap: 6px;
+	}
+	.nav-link {
+		background: none;
+		border: none;
+		color: #a1a1aa;
+		padding: 8px 16px;
+		font-size: 14.5px;
+		font-weight: 500;
+		cursor: pointer;
+		border-radius: 8px;
+		transition: all 0.1s ease;
+	}
+	.nav-link:hover {
+		color: #e5e5ea;
+		background: #1f1f27;
+	}
+
+	.nav-actions {
+		display: flex;
+		gap: 10px;
+		align-items: center;
+	}
+
+	.mobile-toggle {
+		display: none;
+		background: none;
+		border: 1px solid #2a2a35;
+		color: #e5e5ea;
+		padding: 6px 10px;
+		border-radius: 8px;
+		cursor: pointer;
+	}
+
+	.mobile-menu {
+		display: none;
+		padding: 12px 24px 24px;
+		background: #0b0b10;
+		border-top: 1px solid #2a2a35;
+	}
+	.mobile-link {
+		display: block;
+		width: 100%;
+		text-align: left;
+		background: none;
+		border: none;
+		color: #e5e5ea;
+		padding: 14px 0;
+		font-size: 16px;
+		border-bottom: 1px solid #1f1f27;
+		cursor: pointer;
+	}
+	.mobile-link:last-of-type {
+		border-bottom: none;
+	}
+	.mobile-ctas {
+		display: flex;
+		flex-direction: column;
+		gap: 10px;
+		margin-top: 16px;
+	}
+
+	@media (max-width: 700px) {
+		.nav-links,
+		.nav-actions {
+			display: none;
+		}
+		.mobile-toggle {
+			display: block;
+		}
+		.mobile-menu {
+			display: block;
+		}
+	}
+</style>

+ 107 - 0
src/routes/Pricing.svelte

@@ -0,0 +1,107 @@
+<section id="pricing" class="section section-alt">
+	<div class="container">
+		<div class="section-header">
+			<div class="eyebrow">TRANSPARENT &amp; SIMPLE</div>
+			<h2>Pricing</h2>
+			<p>One rate. No subscriptions. Pay only for what you use.</p>
+		</div>
+
+		<div class="pricing-card">
+			<div class="pricing-badge">PAY AS YOU GO</div>
+			
+			<div class="pricing-main">
+				<div>
+					<span class="price">$1</span>
+					<span class="price-unit">per million tokens</span>
+				</div>
+				<p class="pricing-desc">The same low rate whether you're running a 30-minute one-shot or a 200-hour epic.</p>
+			</div>
+
+			<div class="pricing-breakdown">
+				<div class="breakdown-item">
+					<span>Typical hour of rich play</span>
+					<span class="breakdown-value">2–6¢</span>
+				</div>
+				<div class="breakdown-item">
+					<span>Full 8-hour campaign session</span>
+					<span class="breakdown-value">15–40¢</span>
+				</div>
+				<div class="breakdown-item">
+					<span>Very long detailed story (100k tokens)</span>
+					<span class="breakdown-value">$0.10</span>
+				</div>
+			</div>
+
+			<div class="pricing-note">
+				You control model choice in the future (faster/cheaper or more creative).
+			</div>
+		</div>
+	</div>
+</section>
+
+<style>
+	/* Pricing specific */
+	.pricing-card {
+		max-width: 620px;
+		margin: 0 auto;
+		background: #121218;
+		border: 1px solid #2a2a35;
+		border-radius: 20px;
+		padding: 44px 40px;
+		text-align: center;
+	}
+	.pricing-badge {
+		display: inline-block;
+		font-size: 12px;
+		background: #272733;
+		color: #c084fc;
+		padding: 3px 13px;
+		border-radius: 999px;
+		letter-spacing: 1px;
+		margin-bottom: 22px;
+	}
+	.pricing-main {
+		margin-bottom: 30px;
+	}
+	.price {
+		font-size: 4.2rem;
+		font-weight: 800;
+		line-height: 1;
+		color: #fff;
+	}
+	.price-unit {
+		display: block;
+		font-size: 1.1rem;
+		color: #a1a1aa;
+		font-weight: 500;
+		margin-top: 4px;
+	}
+	.pricing-desc {
+		max-width: 380px;
+		margin: 18px auto 0;
+		color: #a1a1aa;
+	}
+	.pricing-breakdown {
+		border-top: 1px solid #2a2a35;
+		border-bottom: 1px solid #2a2a35;
+		padding: 18px 0;
+		margin-bottom: 26px;
+	}
+	.breakdown-item {
+		display: flex;
+		justify-content: space-between;
+		padding: 8px 0;
+		font-size: 15px;
+		color: #d1d1d6;
+	}
+	.breakdown-value {
+		font-weight: 700;
+		color: #c084fc;
+	}
+	.pricing-note {
+		font-size: 13.5px;
+		color: #888;
+		margin-bottom: 26px;
+		line-height: 1.5;
+	}
+</style>

+ 240 - 0
src/routes/register/+page.svelte

@@ -0,0 +1,240 @@
+<script>
+	import favicon from '$lib/assets/favicon.png';
+	import { page } from '$app/stores';
+
+	let name = $state('');
+	let email = $state('');
+	let password = $state('');
+	let confirm_password = $state('');
+	let url = $derived($page.url.searchParams.get('url') ?? '');
+
+	let errors = $state({});
+	let submitting = $state(false);
+
+	function validate() {
+		const e = {};
+		if (!name.trim()) e.name = 'Name is required.';
+		if (!email.trim()) e.email = 'Email is required.';
+		else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) e.email = 'Enter a valid email.';
+		if (!password) e.password = 'Password is required.';
+		else if (password.length < 8) e.password = 'Password must be at least 8 characters.';
+		if (!confirm_password) e.confirm_password = 'Please confirm your password.';
+		else if (password !== confirm_password) e.confirm_password = 'Passwords do not match.';
+		return e;
+	}
+
+	function handleSubmit(e) {
+		errors = validate();
+		if (Object.keys(errors).length > 0) {
+			e.preventDefault();
+		} else {
+			submitting = true;
+		}
+	}
+</script>
+
+<svelte:head>
+	<title>Create Account — ChatRPG</title>
+</svelte:head>
+
+<div class="page">
+	<div class="card">
+		<a href="/" class="logo" aria-label="ChatRPG home">
+			<img src={favicon} alt="ChatRPG logo" width="28" height="28" />
+			<span class="logo-text">ChatRPG</span>
+		</a>
+
+		<h1>Create your account</h1>
+		<p class="subtitle">Start your adventure today.</p>
+
+		<form method="POST" action="/api/register" onsubmit={handleSubmit} novalidate>
+			<input type="hidden" name="url" value={url} />
+
+			<div class="field">
+				<label for="name">Name</label>
+				<input
+					id="name"
+					name="name"
+					type="text"
+					autocomplete="name"
+					placeholder="Your name"
+					bind:value={name}
+					class:error={errors.name}
+				/>
+				{#if errors.name}<span class="error-msg">{errors.name}</span>{/if}
+			</div>
+
+			<div class="field">
+				<label for="email">Email</label>
+				<input
+					id="email"
+					name="email"
+					type="email"
+					autocomplete="email"
+					placeholder="you@example.com"
+					bind:value={email}
+					class:error={errors.email}
+				/>
+				{#if errors.email}<span class="error-msg">{errors.email}</span>{/if}
+			</div>
+
+			<div class="field">
+				<label for="password">Password</label>
+				<input
+					id="password"
+					name="password"
+					type="password"
+					autocomplete="new-password"
+					placeholder="At least 8 characters"
+					bind:value={password}
+					class:error={errors.password}
+				/>
+				{#if errors.password}<span class="error-msg">{errors.password}</span>{/if}
+			</div>
+
+			<div class="field">
+				<label for="confirm_password">Confirm Password</label>
+				<input
+					id="confirm_password"
+					name="confirm_password"
+					type="password"
+					autocomplete="new-password"
+					placeholder="Repeat your password"
+					bind:value={confirm_password}
+					class:error={errors.confirm_password}
+				/>
+				{#if errors.confirm_password}<span class="error-msg">{errors.confirm_password}</span>{/if}
+			</div>
+
+			<button type="submit" class="btn btn-primary large full" disabled={submitting}>
+				{submitting ? 'Creating account…' : 'Create Account'}
+			</button>
+		</form>
+
+		<p class="signin">Already have an account? <a href="/login">Sign in</a></p>
+	</div>
+</div>
+
+<style>
+	.page {
+		min-height: 100vh;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 40px 18px;
+		background: #0b0b10;
+	}
+
+	.card {
+		width: 100%;
+		max-width: 420px;
+		background: #121218;
+		border: 1px solid #2a2a35;
+		border-radius: 16px;
+		padding: 40px 36px;
+	}
+
+	.logo {
+		display: flex;
+		align-items: center;
+		gap: 10px;
+		text-decoration: none;
+		color: #e5e5ea;
+		font-weight: 700;
+		font-size: 21px;
+		margin-bottom: 28px;
+	}
+	.logo-text {
+		letter-spacing: -0.5px;
+	}
+
+	h1 {
+		font-size: 1.6rem;
+		color: #e5e5ea;
+		margin-bottom: 6px;
+	}
+
+	.subtitle {
+		color: #a1a1aa;
+		font-size: 0.95rem;
+		margin: 0 0 28px;
+	}
+
+	form {
+		display: flex;
+		flex-direction: column;
+		gap: 18px;
+	}
+
+	.field {
+		display: flex;
+		flex-direction: column;
+		gap: 6px;
+	}
+
+	label {
+		font-size: 13.5px;
+		font-weight: 600;
+		color: #c8c8d0;
+	}
+
+	input[type="text"],
+	input[type="email"],
+	input[type="password"] {
+		background: #0b0b10;
+		border: 1px solid #3f3f4a;
+		border-radius: 9px;
+		color: #e5e5ea;
+		font-size: 15px;
+		padding: 11px 14px;
+		outline: none;
+		transition: border-color 0.15s ease;
+		width: 100%;
+	}
+
+	input::placeholder {
+		color: #52525b;
+	}
+
+	input:focus {
+		border-color: #c084fc;
+	}
+
+	input.error {
+		border-color: #f87171;
+	}
+
+	.error-msg {
+		font-size: 12.5px;
+		color: #f87171;
+	}
+
+	button[type="submit"]:disabled {
+		opacity: 0.6;
+		cursor: not-allowed;
+		transform: none;
+	}
+
+	.signin {
+		text-align: center;
+		font-size: 13.5px;
+		color: #a1a1aa;
+		margin: 22px 0 0;
+	}
+
+	.signin a {
+		color: #c084fc;
+		text-decoration: none;
+		font-weight: 600;
+	}
+
+	.signin a:hover {
+		text-decoration: underline;
+	}
+
+	@media (max-width: 480px) {
+		.card {
+			padding: 32px 22px;
+		}
+	}
+</style>

+ 13 - 0
svelte.config.js

@@ -0,0 +1,13 @@
+import adapter from '@sveltejs/adapter-node';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+	kit: {
+		// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
+		// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
+		// See https://svelte.dev/docs/kit/adapters for more information about adapters.
+		adapter: adapter()
+	}
+};
+
+export default config;